Linux网络API:
socket地址API。一个ip地址和端口对(ip, port)。唯一表示使用TCP通信的一端
socket基础API。头文件<sys/socket.h>,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
网络信息API。Linux提供的网络信息API,实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。头文件<netdb.h>中。
1.socket地址API 主机字节序和网络字节序
大端字节序:一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7bit)存储在内存的高地址处
小端字节序:整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。 Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:
1 2 3 4 5 6 #include <netinet/in.h> unsigned long int htonl (unsigned long int hostlong) ; unsigned short int htons (unsigned short int hostshort) ; unsigned long int ntohl (unsigned long int netlong) ; unsigned short int ntohs (unsigned short int netshort) ;
长整型函数通常用来转换IP地址,短整型函数用来转换端口号。
通用socket地址 结构体sockaddr,定义如下:
1 2 3 4 5 #include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; }
常见协议族与对应的地址族的关系:
协议族
地址族
描述
PF_UNIX
AF_UNIX
UNIX本地域协议族
PF_INET
AF_INET
TCP/IPv4协议族
PF_INET6
AF_INET6
TCP/IPv6协议族
宏PF_*
和AF_*
都定义在bits/socket.h
头文件中,且后者与前者有完全相同的值,所以二者通常混用。
不同的协议族的地址值具有不同的含义和长度。14字节的sa_data根本无法完全容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体:
1 2 3 4 5 6 #include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int__ss_align; char__ss_padding[128 -sizeof (__ss_align)]; }
不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align
成员的作用)
专用socket地址 Linux为各个协议族提供了专门的socket地址结构体。
UNIX本地域协议族的专用socket地址sockaddr_un
:
1 2 3 4 5 #include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; char sun_path[108 ]; };
TCP/IP协议族:sockaddr_in
(IPV4)和sockaddr_in6
(IPV6)地址结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct sockaddr_in { sa_family_t sin_family; u_int16_t sin_port; struct in_addr sin_addr ; }; struct in_addr { u_int32_t s_addr; }; struct sockaddr_in6 { sa_family_t sin6_family; u_int16_t sin6_port; u_int32_t sin6_flowinfo; struct in6_addr sin6_addr ; u_int32_t sin6_scope_id; }; struct in6_addr { unsigned char sa_addr[16 ]; };
所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。
IP地址转换函数 IPV4地址:点分十进制字符串 IPV6地址:十六进制字符串 编程中需要将他们转化为整数(二进制数)使用,记录日志则相反,需要由整数转化为可读的字符串。
用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <arpa/inet.h> in_addr_t inet_addr (const char *strptr) ; int inet_aton (const char *cp,struct in_addr*inp) ; char *inet_ntoa (struct in_addr in) ;char * szValue1 = inet_ntoa (“1.2 .3 .4 ”); char * szValue2 = inet_ntoa (“10.194 .71 .60 ”); printf (“address 1 :%s\n”, szValue1); printf (“address 2 :%s\n”, szValue2);
更新函数适用于IPV4和IPV6:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <arpa/inet.h> int inet_pton (int af, const char * src, void * dst) ;const char * inet_ntop (int af, const void * src, char * dst,socklen_t cnt) ;#include <netinet/in.h> #define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
创建socket UNIX/Linux系统中:所有东西都是文件。 socket,可读可写、可控制、可关闭的文件描述符。 socket系统调用创建一个socket:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <sys/types.h> #include <sys/socket.h> int socket (int domain,int type,int protocol) ;
命名socket 命名socket:将一个socket与socket地址绑定。 服务器程序中,命名后客户端才知道如何连接它。 命名socket的系统调用:bind函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <sys/types.h> #include <sys/socket.h> int bind (int sockfd, const struct sockaddr* my_addr, socklen_t addrlen) ;
监听socket 创建一个监听队列以存放待处理的客户连接,listen函数:
1 2 3 4 5 6 7 8 9 #include <sys/socket.h> int listen (int sockfd, int backlog) ;
backlog函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <stdio.h> #include <string.h> static bool stop = false ;static void handle_term ( int sig ) { stop = true ; } int main ( int argc, char * argv[] ) { signal ( SIGTERM, handle_term ); if ( argc <= 3 ) { printf ( "usage: %s ip_address port_number backlog\n" , basename ( argv[0 ] ) ); return 1 ; } const char * ip = argv[1 ]; int port = atoi ( argv[2 ] ); int backlog = atoi ( argv[3 ] ); int sock = socket ( PF_INET, SOCK_STREAM, 0 ); assert ( sock >= 0 ); 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 ) ); assert ( ret != -1 ); ret = listen ( sock, backlog ); assert ( ret != -1 ); while ( ! stop ) { sleep ( 1 ); } close ( sock ); return 0 ; }
服务器程序testlisten,接受3个参数:IP地址,端口号,backlog值。 服务器运行该程序,客户端多次执行telnet命令连接该服务器程序。使用telnet建立连接,执行netstat命令查看服务器上连接的状态。
1 2 3 $ ./testlisten 192.168.1.109 12345 5 $ telnet 192.168.1.109 12345 $ netstat-nt|grep 12345
在监听队列中,处于ESTABLISHED状态的连接只有6个(backlog值加1),其他的连接都处于SYN_RCVD状态。即完整连接最多有(backlog+1)个。在不同的系统上,运行结果会有些差别,不过监听队列中完整连接的上限通常比backlog值略大。
接受连接 从listen监听队列中接受一个连接,accept函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <sys/types.h> #include <sys/socket.h> int accept (int sockfd, struct sockaddr*addr, socklen_t * addrlen) ;
接受一个异常的连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> int 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 ); 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 ); assert ( ret != -1 ); 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 { 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 ; }
服务器运行testaccept程序,在客户端执行telnet命令连接该服务器的程序: (服务器ip地址:192.168.1.109)
1 2 $ ./testaccept 192.168.1.109 54321 $ telnet 192.168.1.109 54321
启动telnet客户端程序,立即断开该客户端的网络连接(建立和断开连接的过程要在服务器启动后20秒内完成)。结果发现accept调用能够正常返回,服务器输出如下:
1 connected with ip:192.168.1.108 and port:38545
服务器运行netstat命令查看accept返回socket连接的状态:
1 2 $ netstat-nt|grep 54321 tcp 0 0 192.168.1.109:54321 192.168.1.108:38545 ESTABLISHED
accept调用对于客户端网络断开毫不知情。重新执行上述过程,不过这次不断开客户端网络连接,而是在建立连接后立即退出客户端程序。这次accept调用同样正常返回。服务器运行netstat命令查看:
1 2 $ netstat-nt|grep 54321 tcp 1 0 192.168.1.109:54321 192.168.1.108:52070 CLOSE_WAIT
由此可见,accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。
发起连接 服务器通过listen调用,被动接受连接;客户端通过connect调用,主动与服务器建立连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <sys/types.h> #include <sys/socket.h> int connect (int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen) ;
关闭连接 关闭该连接所对应的socket,通过关闭普通文件描述符的系统调用完成:
1 2 3 4 5 6 #include <unistd.h> int close (int fd) ;
多进程程序中,一次fork系统调用默认使父进程中打开的socket的引用计数加1,因此,必须在父进程和子进程中都对该socket执行close调用,才能将连接关闭。
如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的):
1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/socket.h> int shutdown (int sockfd,int howto) ;
shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。
数据读写 TCP数据读写 对文件的读写操作read和write同样适用于socket。用于TCP流数据读写的系统调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <sys/types.h> #include <sys/socket.h> ssize_t recv (int sockfd, void * buf, size_t len, int flags) ; ssize_t send (int sockfd, const void * buf, size_t len, int flags) ;
发送带外数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int 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 server_address ; bzero ( &server_address, sizeof ( server_address ) ); server_address.sin_family = AF_INET; inet_pton ( AF_INET, ip, &server_address.sin_addr ); server_address.sin_port = htons ( port ); int sockfd = socket ( PF_INET, SOCK_STREAM, 0 ); assert ( sockfd >= 0 ); if ( connect ( sockfd, ( struct sockaddr* )&server_address, sizeof ( server_address ) ) < 0 ) { printf ( "connection failed\n" ); } else { printf ( "send oob data out\n" ); const char * oob_data = "abc" ; const char * normal_data = "123" ; send ( sockfd, normal_data, strlen ( normal_data ), 0 ); send ( sockfd, oob_data, strlen ( oob_data ), MSG_OOB ); send ( sockfd, normal_data, strlen ( normal_data ), 0 ); } close ( sockfd ); return 0 ; }
接受带外数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> #define BUF_SIZE 1024 int 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 ); 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 ); assert ( ret != -1 ); 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 { 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 ); return 0 ; }
现在服务器上启动5-7oobrecv.cpp服务器程序testoobrecv,客户端执行5-6oobsend.cpp客户端程序testoobsend,向服务器发送带外数据。
1 2 $ ./testoobrecv 192.168.1.109 54321 $ sudo tcpdump-ntx-i eth0 port 54321
服务器的输出如下:
1 2 3 got 5 bytes of normal data'123ab' got 1 bytes of oob data'c' got 3 bytes of normal data'123'
客户端发送给服务器的3字节的带外数据“abc”中,仅有最后一个字符“c”被服务器当成真正的带外数据接收。并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能被一个recv调用全部读出的。
UDP数据读写 用于UDP数据报读写的系统调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom (int sockfd, void * buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t * addrlen) ; ssize_t sendto (int sockfd, const void * buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen) ;
recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。
通用数据读写函数 不仅适用于TCP流数据,也能用于UDP数据报:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <sys/socket.h> ssize_t recvmsg (int sockfd, struct msghdr* msg, int flags) ; ssize_t sendmsg (int sockfd, struct msghdr *msg, int flags) ;struct msghdr { void * msg_name; socklen_t msg_namelen; struct iovec * msg_iov ; int msg_iovlen; void * msg_control; socklen_t msg_controllen; int msg_flags; }; struct iovec { void * iov_base; size_t iov_len; };
msghdr结构体中,msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。
iovec结构体封装了一块内存的起始位置和长度。
分散读(scatter read):recvmsg调用,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定。
集中写(gather write):sendmsg调用,msg_iovlen块分散内存中的数据将被一并发送。
msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
带外标记 内核通知应用进程带外数据抵达的两种方式:
即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。使用sockatmark调用:
1 2 3 4 5 6 7 #include <sys/socket.h> int sockatmark (int sockfd) ;
地址信息函数 获取一个连接socket的本端socket地址,以及远端的socket地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <sys/socket.h> int getsockname (int sockfd, struct sockaddr* address, socklen_t * address_len) ; int getpeername (int sockfd, struct sockaddr* address, socklen_t * address_len) ;
socket选项 读取和设置socket文件描述符属性的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <sys/socket.h> int getsockopt (int sockfd, int level, int option_name, void * option_value, socklen_t * restrict option_len) ; int setsockopt (int sockfd, int level, int option_name, const void * option_value, socklen_t option_len) ;
对于服务器,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤,这说明服务器已经往被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项。
解决方法:对监听socket设置socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。
对于客户端,socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。
SO_REUSEADDR选项 服务器程序可以通过设置socket选项SO_REUSEADDR,来强制使用被处于TIME_WAIT状态的连接占用的socket地址。重用本地地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> int 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 ] ); 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 ) ); assert ( ret != -1 ); ret = listen ( sock, 5 ); assert ( ret != -1 ); 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 { 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 ; }
SO_RCVBUF和SO_SNDBUF选项 SO_RCVBUF选项:TCP接收缓冲区的大小,最小值256字节 SO_SNDBUF选项:TCP发送缓冲区的大小,最小值2048字节 用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞。
修改TCP发送缓冲区的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <sys/socket.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #define BUFFER_SIZE 512 int main ( int argc, char * argv[] ) { if ( argc <= 3 ) { printf ( "usage: %s ip_address port_number send_bufer_size\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 ); server_address.sin_port = htons ( port ); int sock = socket ( PF_INET, SOCK_STREAM, 0 ); assert ( sock >= 0 ); int sendbuf = atoi ( argv[3 ] ); int len = sizeof ( sendbuf ); setsockopt ( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof ( sendbuf ) ); getsockopt ( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, ( socklen_t * )&len ); printf ( "the tcp send buffer size after setting is %d\n" , sendbuf ); if ( connect ( sock, ( struct sockaddr* )&server_address, sizeof ( server_address ) ) != -1 ) { char buffer[ BUFFER_SIZE ]; memset ( buffer, 'a' , BUFFER_SIZE ); send ( sock, buffer, BUFFER_SIZE, 0 ); } close ( sock ); return 0 ; }
修改TCP接收缓存区的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <string.h> #define BUFFER_SIZE 1024 int main ( int argc, char * argv[] ) { if ( argc <= 3 ) { printf ( "usage: %s ip_address port_number receive_buffer_size\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 ); address.sin_port = htons ( port ); int sock = socket ( PF_INET, SOCK_STREAM, 0 ); assert ( sock >= 0 ); int recvbuf = atoi ( argv[3 ] ); int len = sizeof ( recvbuf ); setsockopt ( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof ( recvbuf ) ); getsockopt ( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, ( socklen_t * )&len ); printf ( "the receive buffer size after settting is %d\n" , recvbuf ); int ret = bind ( sock, ( struct sockaddr* )&address, sizeof ( address ) ); assert ( ret != -1 ); ret = listen ( sock, 5 ); assert ( ret != -1 ); 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 { char buffer[ BUFFER_SIZE ]; memset ( buffer, '\0' , BUFFER_SIZE ); while ( recv ( connfd, buffer, BUFFER_SIZE-1 , 0 ) > 0 ){} close ( connfd ); } close ( sock ); return 0 ; }
服务器端运行5-11set_recv_buffer.cpp(set_recv_buffer程序),在客户端上运行5-10set_send_buffer.cpp(set_send_buffer程序),客户端向服务器发送512字节的数据。
1 2 3 4 5 $ ./set_recv_buffer 192.168.1.108 12345 50 the tcp receive buffer size after settting is 256 $ ./set_send_buffer 192.168.1.108 12345 2000 the tcp send buffer size after setting is 4000
从服务器的输出来看,系统允许的TCP接收缓冲区最小为256字节。当我们设置TCP接收缓冲区的大小为50字节时,系统将忽略我们的设置。从客户端的输出来看,我们设置的TCP发送缓冲区的大小被系统增加了一倍。
SO_RCVLOWAT和SO_SNDLOWAT选项 SO_RCVLOWAT选项:TCP接收缓冲区的低水位标记,可读数据总数大于其低水位标记,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据。 SO_SNDLOWAT选项:TCP发送缓冲区的低水位标记,缓冲区的空闲空间(可写入数据的空间)大于其低水位标记,I/O复用系统调用将通知应用程序可以往对应的socke上写入数据。 一般被I/O复用系统调用,用来判断socket是否可读或可写
默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
SO_LINGER选项 SO_LINGER选项:用于控制close系统调用在关闭TCP连接时的行为。
默认情况下,使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。
设置(获取)SO_LINGER选项的值时,需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,其定义如下:
1 2 3 4 5 #include <sys/socket.h> struct linger { int l_onoff; int l_linger; };
close系统调用可能产生的3种行为:
l_onoff等于0。此时SO_LINGER选项不起作用,close用默认行为关闭socket。
l_onoff不为0,l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此,这种情况给服务器提供了异常终止一个连接的方法。
l_onoff不为0,l_linger大于0。close的行为取决于两个条件:
被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据。
该socket是阻塞的,还是非阻塞的。 对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。 如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。
网络信息API socket地址的两个要素,即IP地址和端口号,都是用数值表示的。这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。可以用主机名来访问一台机器,而避免直接使用其IP地址,用服务名称来代替端口号。比如,下面两条telnet命令具有完全相同的作用:
1 2 telnet 127.0.0.1 80 telnet localhost www
telnet客户端程序,通过调用某些网络信息API,来实现主机名到IP地址的转换,以及服务名称到端口号的转换。
gethostbyname和gethostbyaddr gethostbyname函数:根据主机名称获取主机的完整信息。通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。 gethostbyaddr函数:根据IP地址获取主机的完整信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <netdb.h> struct hostent* gethostbyname (const char * name) ; struct hostent* gethostbyaddr (const void * addr, size_t len, int type) ;#include <netdb.h> struct hostent { char * h_name; char ** h_aliases; int h_addrtype; int h_length; char ** h_addr_list };
getservbyname和getservbyport getservbyname函数:根据名称获取某个服务的完整信息。 getservbyport函数:根据端口号获取某个服务的完整信息。 它们实际上都是通过读取/etc/services文件来获取服务的信息的。这两个函数的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <netdb.h> struct servent* getservbyname (const char * name, const char * proto) ; struct servent* getservbyport (int port, const char * proto) ;#include <netdb.h> struct servent { char * s_name; char ** s_aliases; int s_port; char * s_proto; };
通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系统时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <unistd.h> #include <assert.h> int main ( int argc, char *argv[] ) { assert ( argc == 2 ); char *host = argv[1 ]; struct hostent * hostinfo = gethostbyname ( host ); assert ( hostinfo ); 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; 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 item is: %s" , buffer ); close ( sockfd ); return 0 ; }
注:以上讨论的4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本。这些函数的函数名是在原函数名尾部加上_r(re-entrant)。
getaddrinfo getaddrinfo函数:既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。 是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <netdb.h> int getaddrinfo (const char * hostname, const char * service, const struct addrinfo* hints, struct addrinfo** result) ;struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; char * ai_canonname; struct sockaddr * ai_addr ; struct addrinfo * ai_next ; };
使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
利用hints参数获取主机ernest-laptop上的“daytime”流服务信息:
1 2 3 4 5 struct addrinfo hints ;struct addrinfo * res ;bzero (&hints, sizeof (hints)); hints.ai_socktype = SOCK_STREAM; getaddrinfo ("ernest-laptop" , "daytime" , &hints, &res);
getaddrinfo将隐式地分配堆内存(可以通过valgrind等工具查看),因为res指针原本是没有指向一块合法内存的,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:
1 2 #include <netdb.h> void freeaddrinfo (struct addrinfo* res) ;
getnameinfo getnameinfo函数:通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。函数定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <netdb.h> int getnameinfo (const struct sockaddr* sockaddr, socklen_t addrlen, char * host, socklen_t hostlen, char * serv, socklen_t servlen, int flags) ;
Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可将表5-8中的错误码转换成其字符串形式:
1 2 #include <netdb.h> const char * gai_strerror (int error) ;