5. Linux网络编程基础API
第5章 Linux网络编程基础API
1. socket地址API
1.1 主机字节序和网络字节序
字节序分为大端字节序(big endian)和小端字节序(little endian)。
- 大端字节序是指一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7 bit)存储在内存的高地址处。
- 小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
代码清单5-1 判断机器字节序
1 |
|
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
需要指出的是,即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
Linux提供了如下4个函数来完成主机字节序和网络字节之间的转换:
1 |
|
它们的含义很明确,比如htonl表示“host to network long”,即将长整型(32 bit)的主机字节序数据转化为网络字节序数据。这4个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号(当然不限于此。任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)。
1.2 通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
1 |
|
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain,见后文)和对应的地址族如表5-1所示。
宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如表5-2所示。
由表5-2可见,14字节的sa_data根本无法完全容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体:
1 |
|
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
1.3 专用socket地址
上面这两个通用socket地址结构体显然很不好用,比如设置与获取IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提供了专门的socket地址结构体。
UNIX本地域协议族使用如下专用socket地址结构体:
1 |
|
TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6:
1 |
|
所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。
1.4 IP地址转换函数
用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
1 |
|
inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败则返回0。
inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。代码清单5-2揭示了其不可重入性。
1 |
|
运行这段代码,得到的结果是:
1 |
|
下面这对更新的函数也能完成和前面3个函数同样的功能,并且它们同时适用于IPv4地址和IPv6地址:
1 |
|
- inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。
- 其中,af参数指定地址族,可以是AF_INET或者AF_INET6。inet_pton成功时返回1,失败则返回0并设置errno[1]。
- inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定目标存储单元的大小。下面的两个宏能帮助我们指定这个大小(分别用于IPv4和IPv6):
1 |
|
inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno。
2. 创建socket
UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的socket系统调用可创建一个socket:
1 |
|
- domain参数告诉系统使用哪个底层协议族。
- 对TCP/IP协议族而言,该参数应该设置为PF_INET(Protocol Family of Internet,用于IPv4)或PF_INET6(用于IPv6);
- 对于UNIX本地域协议族而言,该参数应该设置为PF_UNIX。
- 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并设置errno。
3. 命名socket
将一个socket与socket地址绑定称为给socket命名。
在服务器程序中,我们通常要命名socket,因为只有命名后客户端才能知道该如何连接它。
客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。
命名socket的系统调用是bind,其定义如下:
1 |
|
- bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,
- addrlen参数指出该socket地址的长度。
- bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errno是EACCES和EADDRINUSE,它们的含义分别是:
- EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。
- EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。
4. 监听socket
socket被命名之后,还不能马上接受客户连接,创建一个监听队列以存放待处理的客户连接:
1 |
|
- sockfd参数指定被监听的socket。
- 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并设置errno。 下面我们编写一个服务器程序,如代码清单5-3所示,以研究backlog参数对listen系统调用的实际影响。
1 |
|
该服务器程序(命名为testlisten)接收3个参数:IP地址、端口号和backlog值。我们在Kongming20上运行该服务器程序,并在ernest-laptop上多次执行telnet命令来连接该服务器程序。同时,每使用telnet命令建立一个连接,就执行一次netstat命令来查看服务器上连接的状态。具体操作过程如下:
1 |
|
代码清单5-4是netstat命令某次输出的内容,它显示了这一时刻listen监听队列的内容。
1 |
|
可见,在监听队列中,处于ESTABLISHED状态的连接只有6个(backlog值加1),其他的连接都处于SYN_RCVD状态。我们改变服务器程序的第3个参数并重新运行之,能发现同样的规律,即完整连接最多有(backlog+1)个。在不同的系统上,运行结果会有些差别,不过监听队列中完整连接的上限通常比backlog值略大。
5. 接受连接
下面的系统调用从listen队列中接受一个连接:
1 |
|
- sockfd参数是执行过listen系统调用的监听socket[1]。
- addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。
- accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。
- accept失败时返回-1并设置errno。
现在考虑如下情况:如果监听队列中处于ESTABLISHED状态的连接对应的客户端出现网络异常(比如掉线),或者提前退出,那么服务器对这个连接执行的accept调用是否成功?我们编写一个简单的服务器程序来测试之,如代码清单5-5所示。
1 |
|
我们在Kongming20上运行该服务器程序(名为testaccept),并在ernest-laptop上执行telnet命令来连接该服务器程序。具体操作过程如下:
1
2./testaccept 192.168.1.109 54321#监听54321端口
telnet 192.168.1.109 543211
connected with ip:192.168.1.108 and port:38545
1
2netstat-nt|grep 54321
tcp 0 0 192.168.1.109:54321 192.168.1.108:38545 ESTABLISHED1
connected with ip:192.168.1.108 and port:52070
1
2netstat-nt|grep 54321
tcp 1 0 192.168.1.109:54321 192.168.1.108:52070 CLOSE_WAIT
[1] 我们把执行过listen调用、处于LISTEN状态的socket称为监听socket,而所有处于ESTABLISHED状态的socket则称为连接socket。
6. 发起连接
如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
1 |
|
- sockfd参数由socket系统调用返回一个socket。
- serv_addr参数是服务器监听的socket地址,
- addrlen参数则指定这个地址的长度。
- connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。
- connect失败则返回-1并设置errno。
- 其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:
- ECONNREFUSED,目标端口不存在,连接被拒绝。
- ETIMEDOUT,连接超时。
7. 关闭连接
关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:
1 |
|
- fd参数是待关闭的socket。
- 不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。
- 多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
- 如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门针对网络编程设计的):
1 |
|
- sockfd参数是待关闭的socket。
- howto参数决定了shutdown的行为,它可取表5-3中的某个值。
- 由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。shutdown成功时返回0,失败则返回-1并设置errno。
8. 数据读写
8.1 TCP数据读写
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
1 |
|
recv读取sockfd上的数据,
buf和len参数分别指定读缓冲区的位置和大小,
flags参数的含义见后文,通常设置为0即可。
recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。
flags参数为数据收发提供了额外的控制,它可以取表5-4所示选项中的一个或几个的逻辑或。[1]
- 我们举例来说明如何使用这些选项。MSG_OOB选项给应用程序提供了发送和接收带外数据的方法,如代码清单5-6和代码清单5-7所示。
代码清单5-6 发送带外数据
1 |
|
代码清单5-7 接收带外数据
1 |
|
我们先在Kongming20上启动代码清单5-7所示的服务器程序(名为testoobrecv),然后从ernest-laptop上执行代码清单5-6所示的客户端程序(名为testoobsend)来向服务器发送带外数据。同时用tcpdump抓取这一过程中客户端和服务器交换的TCP报文段。具体操作如下:
1 |
|
服务器程序的输出如下:
1 |
|
由此可见,客户端发送给服务器的3字节的带外数据“abc”中,仅有最后一个字符“c”被服务器当成真正的带外数据接收。并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能被一个recv调用全部读出的。
tcpdump的输出内容中,和带外数据相关的是代码清单5-8所示的TCP报文段。
1 |
|
- 这里我们第一次看到tcpdump输出标志U,这表示该TCP报文段的头部被设置了紧急标志。“urg 3”是紧急偏移值,它指出带外数据在字节流中的位置的下一字节位置是7(3+4,其中4是该TCP报文段的序号值相对初始序号值的偏移)。因此,带外数据是字节流中的第6字节,即字符“c”。
- 值得一提的是,flags参数只对send和recv的当前调用生效,而后面我们将看到如何通过setsockopt系统调用永久性地修改socket的某些属性。
8.2 UDP数据读写
socket编程接口中用于UDP数据报读写的系统调用是:
1 |
|
- recvfrom读取sockfd上的数据,
- buf和len参数分别指定读缓冲区的位置和大小。
- 因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度。
- sendto往sockfd上写入数据,
- buf和len参数分别指定写缓冲区的位置和大小。
- dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的长度。
- 这两个系统调用的flags参数以及返回值的含义均与send/recv系统调用的flags参数及返回值相同。
- 值得一提的是,recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。
8.3 通用数据读写函数
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:
1 |
|
sockfd参数指定被操作的目标socket。msg参数是msghdr结构体类型的指针,msghdr结构体的定义如下:
1
2
3
4
5
6
7
8
9struct msghdr {
void*msg_name;/*socket地址*/
socklen_t msg_namelen;/*socket地址的长度*/
struct iovec*msg_iov;/*分散的内存块,见后文*/
int msg_iovlen;/*分散内存块的数量*/
void*msg_control;/*指向辅助数据的起始位置*/
socklen_t msg_controllen;/*辅助数据的大小*/
int msg_flags;/*复制函数中的flags参数,并在调用过程中更新*/
};
1 |
|
由上可见,iovec结构体封装了一块内存的起始位置和长度。
- msg_iovlen指定这样的iovec结构对象有多少个。
- 对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读(scatter read);
- 对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。
msg_control和msg_controllen成员用于辅助数据的传送。
- msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。
- recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
- recvmsg/sendmsg的flags参数以及返回值的含义均与send/recv的flags参数及返回值相同。
[1]由于socket连接是全双工的,这里的“读端”是针对通信对方而言的。
9. 带外标记
代码清单5-7演示了TCP带外数据的接收方法。但在实际应用中,我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:
1 |
|
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。
如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。
如果不是,则sockatmark返回0。
10. 地址信息函数
在某些情况下,我们想知道一个连接socket的本端socket地址,以及远端的socket地址。下面这两个函数正是用于解决这个问题:
1 |
|
- getsockname
- 获取sockfd对应的本端socket地址,
- 并将其存储于address参数指定的内存中,
- 该socket地址的长度则存储于address_len参数指向的变量中。如果实际socket地址的长度大于address所指内存区的大小,那么该socket地址将被截断。
- getsockname成功时返回0,失败返回-1并设置errno。
- getpeername
- 获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同。
11. socket选项
如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调用则是专门用来读取和设置socket文件描述符属性的方法:
1 |
|
sockfd参数指定被操作的目标socket。
level参数指定要操作哪个协议的选项(即属性),比如IPv4、IPv6、TCP等。
option_name参数则指定选项的名字。
我们在表5-5中列举了socket通信中几个比较常用的socket选项。option_value和option_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值,如表5-5中“数据类型”一列所示。
- getsockopt和setsockopt这两个函数成功时返回0,失败时返回-1并设置errno。
- 值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket[1]设置才有效。这是因为连接socket只能由accept调用返回,
- 而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态,参见图3-8和代码清单5-4),这说明服务器已经往被接受连接上发送出了TCP同步报文段。
- 但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项(回忆3.2.2小节,该选项只能由同步报文段来发送)。
- 对这种情况,Linux给开发人员提供的解决方案是:对监听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三次握手已完成。
11.1 SO_REUSEADDR选项
我们在3.4.2小节讨论过TCP连接的TIME_WAIT状态,并提到服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。具体实现方法如代码清单5-9所示。
代码清单5-9 重用本地地址 1
2
3
4
5
6
7
8
9
10int 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));
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章讨论这两个内核参数。
下面我们编写一对客户端和服务器程序,如代码清单5-10和代码清单5-11所示,它们分别修改TCP发送缓冲区和接收缓冲区的大小。
代码清单5-10 修改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#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<=2) {
printf("usage:%s ip_address port_numbersend_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);
/*先设置TCP发送缓冲区的大小,然后立即读取之*/
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;
}
代码清单5-11 修改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#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<=2) {
printf("usage:%s ip_address port_number recv_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);
/*先设置TCP接收缓冲区的大小,然后立即读取之*/
setsockopt(sock,SOL_SOCKET,SO_RCVBUF,&recvbuf,sizeof(recvbuf));
getsockopt(sock,SOL_SOCKET,SO_RCVBUF,&recvbuf,(socklen_t*)&len);
printf("the tcp 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;
}1
2
3
4
5./set_recv_buffer 192.168.1.108 12345 50#将TCP接收缓冲区的大小设置为50字节
the tcp receive buffer size after settting is 256
./set_send_buffer 192.168.1.108 12345 2000#将TCP发送缓冲区的大小设置为2 000字节
the tcp send buffer size after setting is 4000
tcpdump-nt-i eth0 port 12345
1 |
|
首先注意第2个TCP报文段,它指出服务器的接收通告窗口大小为192字节。该值小于256字节,显然是在情理之中。同时,该同步报文段还指出服务器采用的窗口扩大因子是6。所以服务器后续发送的大部分TCP报文段(6、8、10和12)的实际接收通告窗口大小都是3×26字节,即192字节。因此客户端每次最多给服务器发送192字节的数据。客户端一共给服务器发送了512字节的数据,这些数据必须至少被分为3个TCP报文段(4、7和9)来发送。
有意思的是TCP报文段5和6。当服务器收到客户端发送过来的第一批数据(TCP报文段4)时,它立即用TCP报文段5给予了确认,但该确认报文段的接收通告窗口的大小为0。这说明TCP模块发送该确认报文段时,应用程序还没来得及将数据从TCP接收缓冲中读出。所以此时客户端是不能发送数据给服务器的,直到服务器发送一个重复的确认报文段(TCP报文段6)来扩大其接收通告窗口。
11.3 SO_RCVLOWAT和SO_SNDLOWAT选项
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。
它们一般被I/O复用系统调用(见第9章)用来判断socket是否可读或可写。
当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;
当TCP发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。
默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
11.4 SO_LINGER选项
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。
如表5-5所示,设置(获取)SO_LINGER选项的值时,我们需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,其定义如下:
1
2
3
4
5#include<sys/socket.h>
struct linger {
int l_onoff;/*开启(非0)还是关闭(0)该选项*/
int l_linger;/*滞留时间*/
};
❑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将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。关于阻塞和非阻塞,我们将在第8章讨论。
[1]确切地说,socket在执行listen调用前是不能称为监听socket的,此处是指将执行listen调用的socket。
12. 网络信息API
socket地址的两个要素,即IP地址和端口号,都是用数值表示的。这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。因此在前面的章节中,我们用主机名来访问一台机器,而避免直接使用其IP地址。同样,我们用服务名称来代替端口号。比如,下面两条telnet命令具有完全相同的作用:
1 |
|
上面的例子中,telnet客户端程序是通过调用某些网络信息API来实现主机名到IP地址的转换,以及服务名称到端口号的转换的。下面我们将讨论网络信息API中比较重要的几个。
12.1 gethostbyname和gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。这些在前面章节中都讨论过。这两个函数的定义如下:
1 |
|
- name参数指定目标主机的主机名,
- addr参数指定目标主机的IP地址,
- len参数指定addr所指IP地址的长度,
- type参数指定addr所指IP地址的类型,
- 其合法取值包括AF_INET(用于IPv4地址)和AF_INET6(用于IPv6地址)。
这两个函数返回的都是hostent结构体类型的指针,hostent结构体的定义如下:
1 |
|
12.2 getservbyname和getservbyport
getservbyname函数根据名称获取某个服务的完整信息,getservbyport函数根据端口号获取某个服务的完整信息。它们实际上是通过读取/etc/services文件来获取服务的信息的。这两个函数的定义如下:
1 |
|
- name参数指定目标服务的名字,
- port参数指定目标服务对应的端口号。
- proto参数指定服务类型,给它传递“tcp”表示获取流服务,给它传递“udp”表示获取数据报服务,给它传递NULL则表示获取所有类型的服务。
这两个函数返回的都是servent结构体类型的指针,结构体servent的定义如下:
1 |
|
下面我们通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系统时间,如代码清单5-12所示。
代码清单5-12 访问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#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);
/*获取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;
}
12.3 getaddrinfo
getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义如下:
1
2#include<netdb.h>
int getaddrinfo(const char*hostname,const char*service,const struct addrinfo*hints,struct addrinfo**result);
同样,service参数可以接收服务名,也可以接收字符串表示的十进制端口号。
hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。
hints参数可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果。result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:
1 |
|
该结构体中,ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,它通常被设置为0。ai_flags成员可以取表5-6中的标志的按位或。
当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
例如,代码清单5-13利用了hints参数获取主机ernest-laptop上的“daytime”流服务信息。
代码清单5-13 使用getaddrinfo函数 1
2
3
4
5struct addrinfo hints;
struct addrinfo*res;
bzero(&hints,sizeof(hints));
hints.ai_socktype=SOCK_STREAM;
getaddrinfo("ernest-laptop","daytime",&hints,&res);1
2#include<netdb.h>
void freeaddrinfo(struct addrinfo*res);
12.4 getnameinfo
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它们的可重入版本。该函数的定义如下:
1 |
|
getnameinfo将返回的主机名存储在host参数指向的缓存中,
将服务名存储在serv参数指向的缓存中,
hostlen和servlen参数分别指定这两块缓存的长度。
flags参数控制getnameinfo的行为,它可以接收表5-7中的选项。
getaddrinfo和getnameinfo函数成功时返回0,失败则返回错误码,可能的错误码如表5-8所示。
Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可将表5-8中的错误码转换成其字符串形式:
1 |
|