用什么软件做网站好,做彩票网站捉怎么处理,查询企业信息的官方网站,佛山网页网站制作5.1 socket 地址 API
现代CPU的累加器一次都能装载(至少)4 字节(这里考虑32位机#xff0c;下同)#xff0c;即一个整 数。那么这4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序 问题。字节序分为大端字节序(big endian)和小端字节序(little endi…5.1 socket 地址 API
现代CPU的累加器一次都能装载(至少)4 字节(这里考虑32位机下同)即一个整 数。那么这4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序 问题。字节序分为大端字节序(big endian)和小端字节序(little endian)o 大端字节序是指 一个整数的高位字节(23〜 31 b it)存储在内存的低地址处低位字节(0 〜 7 b it)存储在 内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处而低位字节则 存储在内存的低地址处。现代PC大多采用小端字节序因此小端字节序又被称为主机字节序。当格式化的数据(比如32 bit整型数和16 bit短整型数)在两台使用不同字节序的主机 之间直接传递时接收端必然错误地解释之。解决问题的方法是:发送端总是把要发送的 数据转化成大端字节序数据后再发送而接收端知道对方传送过来的数据总是采用大端字 节序所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转 换大端机不转换)。因此大端字节序也称为网络字节序它给所有接收数据的主机提供了 一个正确解释收到的格式化数据的保证
5.1.2 通用socket地址 TCP/IP协议族有sockaddr_in和 sockaddr_in6两个专用socket地址结构体它们分别用 于 IPv4 和 IPv6所有专用socket地 址 (以 及 sockaddr storage)类型的变量在实际使用时都需要转化为 通用socket地址类型sockaddr (强制转换即可)因为所有socket编程接口使用的地址参数 的类型都是sockaddrinet_ntop成功时返回目标存储单元的地址失败则返回NULL并设置ermo
5.2 创建 socket domain参数告诉系统使用哪个底层协议族。对 TCP/IP协议族而言该参数应该设置为PF_INET Protocol Family of In tern et,用于 IPv4 或 PF_INET6 用于 IPv6 对于 UNIX本地域协议族而言该参数应该设置为PF_UNIX。关于socket系统调用支持的所有协议族 请读者自己参考其man手册。 type参数指定服务类型。服务类型主要有SOCK_STREAM服 务 流服务和 SOCK_ UGRAM 数据报服务。对 TCP/IP协议族而言其值取SOCK_STREAM表示传输层使用TCP协议取 SOCK_DGRAM表示传输层使用UDP协议。值得指出的是自Linux内核版本2.6.17起type参数可以接受上述服务类型与下面两个重要的标志相与的值SOCK_NONBLOCK和 SOCK_CLOEXEC它们分别表示将新创建的socket设为非阻塞的以及用fork调用创建子进程时在子进程中关闭该socket。在内核版本2.6.17之前的Linux中文件描述符的这两个属性都需要使用额外的系统调用比如fcntl 来设置。protocol参数是在前两个参数构成的协议集合下再选择一个具体的协议。不过这个值 通常都是唯一的前两个参数已经完全决定了它的值。几乎在所有情况下我们都应该把它设置为0 ,表示使用默认协议。socket系统调用成功时返回一个socket文件描述符失败则返回-1并设置ermo
5.3 命名 socket
创建socket时我们给它指定了地址族但是并未指定使用该地址族中的哪个具体 socket地址。将一个socket与 socket地址绑定称为给socket命名.在服务器程序中我们通 常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名 socket,而是采用匿名方式即使用操作系统自动分配的socket地址。命名socket的系统调 用是bind,其定义如下bind将 my_addr所指的socket地址分配给未命名的sockfd文件描述符addrlen参数指出该socket地址的长度。bind成功时返回0 , 失败则返回-1并设置ermo其中两种常见的ermo是 EACCES和EADDRINUSE,它们的含义分别是EACCES,被绑定的地址是受保护的地址仅超级用户能够访问。比如普通用户将 socket绑定到知名服务端口端口号为0〜1023 上时bind将返回EACCES错误EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_ WAIT状态的socket地址。
5.4 监听 socket
socket被命名之后还不能马上接受客户连接我们需要使用如下系统调用来创建一个 监听队列以存放待处理的客户连接: sockfd参数指定被监听的socketo backlog参数提示内核监听队列的最大长度。监 听队列的长度如果超过backlog,服务器将不受理新的客户连接客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux中backlog参数是指所有处于半 连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的 socket的上限。但自内核版本 2.2之后它只表示处于完全连接状态的socket的上限处于半连接状态的socket的上限则由 /proc/sys/net/ipv4/tcp_max_syn_backlog 内核参数定义。backlog 参数的典型值是 5。 listen成功时返回0 , 失败则返回-1并设置ermo
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include csignal
#include cassert
#include unistd.h#include include/algorithm_text.hstatic bool stop false;
static void handle_term(int sig){stop true;
}
int main(int argc,char* argv[]) {//参考链接 https://blog.51cto.com/u_15284125/2988992//signal函数 第一参数是指需要进行处理的信号第二个参数是指 处理的方式(系统默认/忽略/捕获)//SIGTERM 请求中止进程kill命令缺省发送 交给handle_term函数进行处理signal(SIGTERM,handle_term);if (argc 3){//basename 参考链接 https://blog.csdn.net/Draven_Liu/article/details/38235585//假设路径为 nihao/nihao/jhhh/txt.c//basename函数并不会关心路径是否正确文件是否存在只不过是把路径上除了最后的txt.c 这个文件名字其他的东西都删除了然后返回txt.c而已std::cout usage: basename(argv[0]) ip_address port_number backlog\nstd::endl;}//argv[1] ip地址//argv[2] 端口号//argv[3] 日志级别const char* ip argv[1];//atoi 把字符串转换成一个整数//参考链接 https://www.runoob.com/cprogramming/c-function-atoi.htmlint port atoi(argv[2]);int backlog atoi(argv[3]);//socket编程 第一个参数表示使用哪个底层协议族对 TCP/IP协议族而言该参数应该设置为PF_INET//对 TCP/IP协议族而言其值取SOCK_STREAM表示传输层使用TCP协议//第三个参数是在前两个参数构成的协议集合下再选择一个具体的协议 设置为0 ,表示使用默认协议int sock socket(PF_INET,SOCK_STREAM,0);//断言 如果不正确 不会往下继续执行assert(sock0);//创建一个IPv4 socket地址//TCP/IP 协议族sockaddr_in 表示IPv4专用socket地址结构体struct sockaddr_in address;// bzero() 会将内存块字符串的前n个字节清零;// s为内存字符串指针n 为需要清零的字节数。// 在网络编程中会经常用到。bzero(address,sizeof (address));address.sin_family AF_INET;//int inet_pton(int af,const char* src,void* dst)//af 指定地址族 AF_INET 或者 AF_INET6//inet_pton函数成功返回1 失败返回0,并且设置errno//errno 表示各种错误// inet_pton 将字符串表示的IP地址src(使用点分十进制表示的IPv4地址和使用十六进制表示的IPv6)转换成网络字节序整数表示的IP地址,并把转换的结果存储在dst指向的内存中inet_pton(AF_INET,ip,address.sin_addr);//const char* inet_ntop(int af,const char* src,void* dst,socklen_t cnt)//inet_tpon函数和inet_pton进行相反的转换前三个参数的含义与其相同最后一个参数cnt指定目标存储单元的大小//成功 返回目标单元的地址 失败返回NULL 并且设置errnoaddress.sin_port htons(port);//bind将 my_addr所指的socket地址(2)分配给未命名的sockfd(1)文件描述符addrlen参数(3)指出该socket地址的长度。int ret bind(sock,(struct sockaddr*)address,sizeof (address));assert(ret ! -1);ret listen(sock,backlog);//循环等待连接 直到有SIGTERM信号将其中断while(!stop){sleep(1);}//关闭 socketclose(sock);return 0;
} 我们改变服务器程序的第3个参数并重新运行之能发现同样的规律即完整连接最多有(backlog1)个。在不同的系统上运行结果会有些差 别不过监听队列中完整连接的上限通常比backlog值略大。
5 . 5 接受连接 sockfd参数是执行过listen系统调用的监听socketaddr参数用来获取被接受连接的远 端 socket地址该 socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接 socket,该 socket唯一地标识了被接受的这个连接服务器可通过读写该socket来与被接受 连接对应的客户端通信accept失败时返回-1并设置ermo们把执行过listen调用、处于LISTEN状态的socket称为监听socket,而所有处于ESTABLISHED状态的 socket则称为连接socket
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include csignal
#include cassert
#include unistd.h#include include/algorithm_text.hint main(int argc,char* argv[]) {if (argc 2){printf(usage:%s ip_address port_number\n, basename(argv[0]));return 1;}const char* ip argv[1];int port atoi(argv[2]);struct sockaddr_in address{};bzero(address,sizeof (address));address.sin_family AF_INET;inet_pton(AF_INET,ip,address.sin_addr);//htons 将一个无符号短整型数值转换为网络字节序即大端模式(big-endian)address.sin_port htons(port);int sock socket(PF_INET,SOCK_STREAM,0);assert(sock 0);int ret bind(sock,(struct sockaddr*)address,sizeof (address));assert(ret ! -1);ret listen(sock,5);//等待20秒 等待客户端连接和相关操作 (掉线/退出)完成sleep(20);struct sockaddr_in client;socklen_t client_addrlength sizeof (client);int connfd accept(sock,(struct sockaddr*)client,client_addrlength);if (connfd 0){printf(errno is:%d\n,errno);} else{//接受连接成功 则打印客户端的IP地址和端口号char remote[INET_ADDRSTRLEN];printf(connected with ip:%s and port:%d\n,inet_ntop(AF_INET,client.sin_addr,remote,INET_ADDRSTRLEN),// 将一个无符号短整形数从网络字节顺序转换为主机字节顺序ntohs(client.sin_port));close(connfd);}close(sock);return 0;
}
accept是从监听队列中取出连接而不论连接处于何种状态(如上面的 ESTABLISHED状态和CLOSE_WAIT状态)更不关心任何网络状况的变化
5 .6 发起连接
如果说服务器通过listen调用来被动接受连接那么客户端需要通过如下系统调用来主 动与服务器建立连接sockfd参数由socket系统调用返回一个socket。serv addr参数是服务器监听的socket地 址addrlen参数则指定这个地址的长度。 connect成功时返回0。一旦成功建立连接sockfd就唯一地标识了这个连接客户端就 可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置e rm s 其中两种常见的 ermo是 ECONNREFUSED和 ETIMEDOUT,它们的含义如下: ECONNREFUSED,目标端口不存在连接被拒绝ETIMEDOUT,连接超时
5 .7 关闭连接
关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述 符的系统调用来完成fd参数是待关闭的socketo不过close系统调用并非总是立即关闭一个连接而是将fd 的引用计数减1。只有当fd的引用计数为。时才真正关闭连接。多进程程序中一次fork系统调用默认将使父进程中打开的socket的引用计数加1 ,因此我们必须在父进程和子进程 中都对该socket执行close调用才能将连接关闭。 如果无论如何都要立即终止连接(而不是将socket的引用计数减1 ) ,可以使用如下的 shutdown系统调用(相对于close来说它是专门为网络编程设计的)sockfd参数是待关闭的socketo howto参数决定了 shutdown的行为它可取表5・ 3中的 某个值。由此可见shutdown能够分别关闭socket上的读或写或者都关闭°而 close在关闭连 接时只能将socket上的读和写同时关闭oshutdown成功时返回0 , 失败则返回-1并设置ermoo
5 . 8 数据读写 5.8.1 TCP数据读写
对文件的读写操作read和 write同样适用于sockets但是socket编程接口提供了几个专 门用于socket数据读写的系统调用它们增加了对数据读写的控制。其中用于TCP流数据读 写的系统调用是recv读取sockfd上的数据buf和 Ien 参数分别指定读缓冲区的位置和大小flags参数的含义见后文通常设置为0 即可。即recv成功时返回实际读取到的数据的长度它可能小于 我们期望的长度len。因此我们可能要多次调用recv ,才能读取到完整的数据。recv可能返回 0 , 这意味着通信对方已经关闭连接了。recv出错时返回-1并设置ermosend往 sockfd上写入数据buf和 len参数分别指定写缓冲区的位置和大小。send成功 时返回实际写入的数据的长度失败则返回并设置errnoflags参数为数据收发提供了额外的控制它可以取表所示选项中的一个或几个的逻辑或发送端代码
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include cassert
#include unistd.h
#include cstdio
#include cstdlib#include include/algorithm_text.hint main(int argc,char* argv[]) {if (argc 2){printf(usage:%s ip_server_address port_number\n, basename(argv[0]));return 1;}const char* ip argv[1];int port atoi(argv[2]);struct sockaddr_in server_address{};bzero(server_address,sizeof (server_address));server_address.sin_family AF_INET;inet_pton(AF_INET,ip,server_address.sin_addr);//htons 将一个无符号短整型数值转换为网络字节序即大端模式(big-endian)server_address.sin_port htons(port);int sock_fd socket(PF_INET,SOCK_STREAM,0);assert(sock_fd 0);if (connect(sock_fd,(struct sockaddr*)server_address,sizeof (server_address))0){printf(connected failed.\n);} else{const char* oob_data abc;const char* normal_data 123;send(sock_fd,normal_data, strlen(normal_data),0);send(sock_fd,oob_data, strlen(oob_data),MSG_OOB);send(sock_fd,normal_data, strlen(normal_data),0);}close(sock_fd);return 0;
}
接收端代码
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include cassert
#include unistd.h
#include cstdio
#include cstdlib#include include/algorithm_text.h
#define BUF_SIZE 1024int main(int argc,char* argv[]) {if (argc 2){printf(usage:%s ip_server_address port_number\n, basename(argv[0]));return 1;}const char* ip argv[1];int port atoi(argv[2]);struct sockaddr_in server_address{};bzero(server_address,sizeof (server_address));server_address.sin_family AF_INET;inet_pton(AF_INET,ip,server_address.sin_addr);//htons 将一个无符号短整型数值转换为网络字节序即大端模式(big-endian)server_address.sin_port htons(port);int sock_fd socket(PF_INET,SOCK_STREAM,0);assert(sock_fd 0);int ret bind(sock_fd,(struct sockaddr*)server_address,sizeof (server_address));assert(ret ! -1);ret listen(sock_fd,5);assert(ret ! -1);struct sockaddr_in client{};socklen_t client_addrlength sizeof (client);int connfd accept(sock_fd,(struct sockaddr*)client,client_addrlength);if (connfd 0){printf(errno is %d\n,errno);} else{char buffer[BUF_SIZE];memset(buffer,\0,BUF_SIZE);ret recv(connfd,buffer,BUF_SIZE-1,0);printf(got %d bytes of normal data %s \n,ret,buffer);memset(buffer,\0,BUF_SIZE);ret recv(connfd,buffer,BUF_SIZE-1,MSG_OOB);printf(got %d bytes of oob data %s \n,ret,buffer);memset(buffer,\0,BUF_SIZE);ret recv(connfd,buffer,BUF_SIZE-1,0);printf(got %d bytes of normal data %s \n,ret,buffer);close(connfd);}close(sock_fd);return 0;
} 5.8.2 UDP数据读写 recvfrom读取sockfd上的数据buf和 len参数分别指定读缓冲区的位置和大小。因为 UDP通信没有连接的概念所以我们每次读取数据都需要获取发送端的socket地址即参数 src_addr所指的内容addrlen参数则指定该地址的长度。sendto往 sockfd上写入数据buf和 len参数分别指定写缓冲区的位置和大小。dest_addr 参数指定接收端的socket地址addrlen参数则指定该地址的长度。这两个系统调用的flags参数以及返回值的含义均与send/recv系统调用的flags参数及返回值相同。值得一提的是recvGom/sendto系统调用也可以用于面向连接(STREAM)的 socket的 数据读写只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因 为我们已经和对方建立了连接所以已经知道其socket地址了)。
5 .8 .3 通用数据读写函数
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据, 也能用于UDP数据报sockfd参数指定被操作的目标socketmsg参数是msghdr结构体类型的指针msghdr结构体的定义如下所示msg name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议该成员没有意义必须被设置为 NULL 这是因为对数据流socket而言对方的地址已经知道。msg namelen成员则指定了 msg_name所 指 socket地址的长度。msg iov成员是iovec结构体类型的指针iovec结构体的定义如下由上可见iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的 iovec结构对象有多少个。对于recvmsg而言数据将被读取并存放在msg_iovlen块分散 的内存中这些内存的位置和长度则由msgiov指向的数组指定这称为分散读(scatter read)对于sendmsg而言msg iovlen块分散内存中的数据将被一并发送这称为集中写msg contro 1和 msg_controllen成员用于辅助数据的传送。我们不详细讨论它们仅在第 13章介绍如何使用它们来实现在进程间传递文件描述符。msg_flags成员无须设定它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前将某些更新后的标志设置到msg flags中。recvmsg/sendmsg的 flags参数以及返回值的含义均与send/recv的 flags参数及返回值 相同。
5 .9 带外标记
实际应用中我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时将通知应用程序有带外数据 需要接收。内核通知应用程序带外数据到达的两种常见方式是I/O复用产生的异常事件和SIGURG信号。但是即使应用程序得到了有带外数据需要接收的通知还需要知道带外数据在数据流中的具体位置才能准确接收带外数据。这一点可通过如下系统调用实现sockatmark判断sockfd是否处于带外标记即下一个被读取到的数据是否是带外数据。 如果是sockatmark返回1 , 此时我们就可以利用带MSGJDOB标志的recv调用来接收带外 数据。如果不是则sockatmark返回0。
5 .1 0 地址信息函数
在某些情况下我们想知道一个连接socket的本端socket地址以及远端的socket地 址。下面这两个函数正是用于解决这个问题getsockname获 取 sockfd对应的本端socket地址并将其存储于address参数指定的内 存中该 socket地址的长度则存储于addressjen参数指向的变量中。如果实际socket地址 的长度大于address所指内存区的大小那么该socket地址将被截断。getsockname成功时返 回0 , 失败返回-1并设置errnogetpeername获取sockfd对应的远端socket地址其参数及返回值的含义与getsockname 的参数及返回值相同
5.11 socket 选项
如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法那么下面两个系统调 用则是专门用来读取和设置socket文件描述符属性的方法sockfd参数指定被操作的目标socket。level参数指定要操作哪个协议的选项(即属性), 比如IPv4、IPv6、TCP等。option_iiame参数则指定选项的名字。我们在表5-5中列举了socket通信中几个比较常用的socket选项。option value和 option len参数分别是被操作选项 的值和长度。不同的选项具有不同类型的值如表中 “数据类型” 一列所示。getsockopt和 setsockopt这两个函数成功时返回0 , 失败时返回-1并设置ermo值得指出的是对服务器而言有部分socket选项只能在调用listen系统调用前针对监 听 socket设置才有效。这是因为连接socket只能由accept调用返回而 accept从 listen监 听队列中接受的连接至少已经完成了 TCP三次握手的前两个步骤因为listen监听队列中 的连接至少已进入SYN_RCVD状态 , 这说明服务器已经往被接受连接上发送出了 TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设 置比如TCP最大报文段选项(该选项只能由同步报文段来发送)。对这种情况Linux给开发人员提供的解决方案是:对监听socket设置这些socket选项那么accept返回的连接socket将自动继承这些选项。这 些 socket选项包括SO_DEBUG、SO_ DONTROUTE、SO_KEEPALIVE、SO_LINGER、SOJDOBINLINE、SO_RCVBUF SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT TCP_MAXSEG 和 TCP_N0DELAYo 而 对客户端而言这些socket选项则应该在调用connect函数之前设置因为connect调用成功返回之后TCP三次握手已完成。
5.11.1 SO_REUSEADDR 选项
TCP连接的TIME_WAIT状态,并提到服务器程序可以通过设置 socket选项 SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。具体实现方法如代码所示。重用本地IP地址int sock socket(PF_INET,SOCK_STREAM,0);assert(sock 0);int reuse 1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,reuse,sizeof(reuse));struct sockaddr_in address{};bzero(address,sizeof(address));address.sin_family AF_INET;inet_pton(AF_INET,ip,address.sin_addr);address.sin_port htons(port);int ret bind(sock,(struct sockaddr*)address,sizeof (address));
经过setsockopt的设置之后即使sock处于TIME_WAIT状态与之绑定的socket地址 也可以立即被重用。此外我们也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT状态进而允许应 用程序立即重用本地的socket地址。
5.11.2 SO_RCVBUF 和 SO_SNDBUF 选项
SO_RCVBUF和 SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。不过当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时系统都会将其值 加倍并且不得小于某个最小值。TCP接收缓冲区的最小值是256字节而发送缓冲区的最小值是2048字节(不过,不同的系统可能有不同的默认最小值)。系统这样做的目的主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段。此外我们可以直接修改内核参数 /proc/sys/net/ipv4/tcp_rmem 和 /proc/sys/net/ipv4/tcp_wmem 来强制 TCP 接收缓冲区和发送缓冲区的大小没有最小值限制。我们将在第16章讨论这两个内核参数。
修改TCP发送缓冲区的客户端程序
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include cassert
#include unistd.h
#include cstdio
#include cstdlib#include include/algorithm_text.h
#define BUF_SIZE 1024int main(int argc,char* argv[]) {if (argc 2){printf(usage:%s ip_server_address port_number\n, basename(argv[0]));return 1;}const char* ip argv[1];int port atoi(argv[2]);struct sockaddr_in server_address{};bzero(server_address,sizeof (server_address));server_address.sin_family AF_INET;inet_pton(AF_INET,ip,server_address.sin_addr);//htons 将一个无符号短整型数值转换为网络字节序即大端模式(big-endian)server_address.sin_port htons(port);int sock_fd socket(PF_INET,SOCK_STREAM,0);assert(sock_fd 0);int send_buf atoi(argv[3]);int len sizeof (send_buf);//先设置TCP发送缓冲区的大小然后立即读取数据setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,send_buf,len);getsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,send_buf,(socklen_t*)len);printf(the tcp send buffer size after setting is %d\n, send_buf);if (connect(sock_fd,(struct sockaddr*)server_address,sizeof (server_address))!-1){char buffer[BUF_SIZE];memset(buffer, a, BUF_SIZE);send(sock_fd, buffer, BUF_SIZE, 0);}close(sock_fd);return 0;
} 修改TCP接收缓冲区的服务器程序
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include cassert
#include unistd.h
#include cstdio
#include cstdlib#include include/algorithm_text.h
#define BUF_SIZE 1024int main(int argc,char* argv[]) {if (argc 2){printf(usage:%s ip_server_address port_number\n, basename(argv[0]));return 1;}const char* ip argv[1];int port atoi(argv[2]);struct sockaddr_in server_address{};bzero(server_address,sizeof (server_address));server_address.sin_family AF_INET;inet_pton(AF_INET,ip,server_address.sin_addr);//htons 将一个无符号短整型数值转换为网络字节序即大端模式(big-endian)server_address.sin_port htons(port);int sock_fd socket(PF_INET,SOCK_STREAM,0);assert(sock_fd 0);int recv_buf atoi(argv[3]);int len sizeof (recv_buf);//先设置TCP接收缓冲区的大小然后立即读取数据setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,recv_buf,len);getsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,recv_buf,(socklen_t*)len);printf(the tcp receive buffer size after setting is %d\n, recv_buf);int ret bind(sock_fd,(struct sockaddr*)server_address,sizeof (server_address));assert(ret ! -1);struct sockaddr_in client{};socklen_t client_addrlength sizeof (client);int connfd accept(sock_fd,(struct sockaddr*)client,client_addrlength);if (connfd 0){printf(errno is:%d\n,errno);} else{char buffer[BUF_SIZE];memset(buffer,\0,BUF_SIZE);while (recv(connfd,buffer,BUF_SIZE-1,0)0){}close(connfd);}close(sock_fd);return 0;
} 从服务器的输出来看系统允许的TCP接收缓冲区最小为256字节。当我们设置TCP 接收缓冲区的大小为50字节时系统将忽略我们的设置。从客户端的输出来看我们设置 的TCP发送缓冲区的大小被系统增加了一倍。这两种情况和我们前面讨论的一致。
5.11.3 SO_RCVLOWAT 和 SO_SNDLOWAT 选项
SO_RCVLOWAT和 SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用见第9 章用来判断socket是否可读或可写。当 TCP接收缓冲区中可读数据的总数大于其低水位标记时I/O复用系统调用将通知应用程序可以从对应的socket上读取数据当TCP发送缓冲区中的空闲空间可以写入数据的空间 大于其低水位标记时I/O复用系统调用将通知应用程序可以往对应的socke上写入数据。默认情况下TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
5.11.4 SO_LINGER 选项
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下当 我们使用close系统调用来关闭一个socket时close将立即返回TCP模块负责把该socket 对应的TCP发送缓冲区中残留的数据发送给对方。如 表 5・5 所 示 设 置 获 取 SO_LINGER选 项 的 值 时 我 们 需 要 给 setsockopt getsockopt 系统调用传递一个linger类型的结构体其定义如下根据linger结构体中两个成员变量的不同值close系统调用可能产生如下3 种行为之一:
l_onoff等于0 此时SO_LINGER选项不起作用close用默认行为来关闭socket□l_onoff不为0, l_linger等于0。此时close系统调用立即返回TCP模块将丢弃被关 闭的socket对应的TCP发送缓冲区中残留的数据同时给对方发送一个复位报文段 见 3.5.2小节。因此这种情况给服务器提供了异常终止一个连接的方法。l onoff不 为 0, l_linger大 于 0。此 时 close的行为取决于两个条件一是被关闭 的 socket对应的TCP发送缓冲区中是否还有残留的数据二是该socket是阻塞 的还是非阻塞的。对于阻塞的socket, close将等待一段长为Linger的时间直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认那么close系统调用将返回-1并设置ermo为 EWOULDBLOCK如果socket是非阻塞的close将立即返回此时我们需要根据其 返回值和ermo来判断残留数据是否已经发送完毕。关于阻塞和非阻塞我们将在第 8章讨论。
5 .1 2 网络信息API
socket地址的两个要素即IP地址和端口号都是用数值表示的。这不便于记忆也 不便于扩展比如从IPv4转移到IPv6。因此在前面的章节中我们用主机名来访问一台 机器而避免直接使用其IP地址。同样我们用服务名称来代替端口号。比如下面两条telnet命令具有完全相同的作用telnet 127.0.0.1 80telnet localhost www上面的例子中telnet客户端程序是通过调用某些网络信息API来实现主机名到IP地 址的转换以及服务名称到端口号的转换的。下面我们将讨论网络信息API中比较重要的几个
5.12.1 gethostbyname 和 gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查 找主机如果没有找到再去访问DNS服务器。这些在前面章节中都讨论过。这两个函数的定义如下name参数指定目标主机的主机名addr参数指定目标主机的IP地址len参数指定addr 所指IP地址的长度type参数指定addi•所指IP地址的类型其合法取值包括AF_INET (用于 IPv4地 址 )和 AF INET6 (用于IPv6地址)。这两个函数返回的都是hostent结构体类型的指针hostent结构体的定义如下5.12.2 getservbyname 和 getservbyport
getservbyname函数根据名称获取某个服务的完整信息getservbyport函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取/etc/services文件来获取服务的信息的。 这两个函数的定义如下name参数指定目标服务的名字port参数指定目标服务对应的端口号。proto参数指定 服务类型给它传递“tcp” 表示获取流服务给它传递“udp” 表示获取数据报服务给它 传递NULL则表示获取所有类型的服务。这两个函数返回的都是servent结构体类型的指针结构体servent的定义如下下面我们通过主机名和服务名来访问目标服务器上的daytime服务以获取该机器的系 统时间如代码清单5-12所示。
访问daytime服务
ntohs等相关的参数的含义
#include iostream
#include sys/socket.h
#include arpa/inet.h
#include cstring
#include cassert
#include unistd.h
#include cstdio
#include cstdlib
#include netdb.h#include include/algorithm_text.h
#define BUF_SIZE 1024int main(int argc,char* argv[]) {assert(argc 2);char* host argv[1];//获取目标主机地址信息struct hostent* hostinfo gethostbyname(host);assert(hostinfo);//获取daytime服务信息struct servent* servinfo getservbyname(daytime,tcp);assert(servinfo);printf(daytime port is %d\n, ntohs(servinfo-s_port));struct sockaddr_in address{};address.sin_family AF_INET;address.sin_port servinfo-s_port;/* 注意下面示码因为h_addr_list本身是使用网络字节序的地址列表所以使用其中的IP地址时* 无须对目标IP地址转换字节序*/address.sin_addr *(struct in_addr*)*hostinfo-h_addr_list;int sockfd socket(AF_INET,SOCK_STREAM,0);int result connect(sockfd,(struct sockaddr*)address,sizeof (address));assert(result ! -1);char buffer[128];result read(sockfd,buffer,sizeof (buffer));assert(result 0);buffer[result] \0;printf(the day tiem is: %s,buffer);close(sockfd);return 0;
} 需要指出的是上面讨论的4个函数都是不可重入的即非线程安全的。不过netdb.h 头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重入版本的命名规则那样 这些函数的函数名是在原函数名尾部加上_r re-entrant 。
5.12.3 getaddrinfo
getaddrinfo函数既能通过主机名获得IP 地 址 内部使用的是gethostbyname函数也能通过服务名获得端口号内部使用的是getservbyname函数。它是否可重入取决于其内部调用的gethostbyname和 getservbyname函数是否是它们的可重入版本。该函数的定义如下#include netdb.hhostname参数可以接收主机名也可以接收字符串表示的IP地 址 IPv4采用点分十 进制字符串IPv6则采用十六进制字符串。同样service参数可以接收服务名也可以 接收字符串表表示的十进制端口号。hints参数是应用程序给getaddrinfo的一个提示以对getaddrinfo的输出进行更精确的控制。hints参数可以被设置为N U L L ,表示允许getaddrinfo反馈任何可用的结果。result参数指向一个链表该链表用于存储getaddrinfo反馈的结果。getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象结构体addrinfo的定义如下该结构体中ai_protocol成员是指具体的网络协议其含义和socket系统调用的第三个 参数相同它通常被设置为0。ai_flags成员可以取表5-6中的标志的按位或。当我们使用hints参数的时候 ,可以设置其ai_flags, ai_family, ai_socktype和 ai_protocol四个字段其他字段则必须被设置为NULL。例如代码清单 13利用了 hints参数获取主机emest-laptop 上的 daytime” 流服务信息5.12.4 getnameinfo
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名内部使用的是 gethostbyaddr函数和 服 务 名 内部使用的是getservbyport函数。它是否可重入取决于 其内部调用的gethostbyaddr和 getservbyport函数是否是它们的可重入版本。该函数的定义 如下getnameinfo将返回的主机名存储在host参数指向的缓存中将服务名存储在serv 参数指向的缓存中hostlen和 servlen参数分别指定这两块缓存的长度。Hags参数控制 getnameinfo的行为它可以接收表5-7中的选项。getaddrinfo和 getnameinfo函数成功时返回0 , 失败则返回错误码可能的错误码如表 所示Linux下 strerror函数能将数值错误码ermo转换成易读的字符串形式。同样下面的函 数可将表5-8中的错误码转换成其字符串形式
参考链接
socket编程中用到的头文件ntohs, ntohl, htons,htonl的比较和详解详解C语言的htons函数C memset()函数和bzero()函数