4.6 socket 的阻塞模式和非阻塞模式
对 socket 在阻塞和非阻塞模式下的各个函数的行为差别深入的理解是掌握网络编程的基本要求之一,是重点也是难点。
阻塞和非阻塞模式下,我们常讨论的具有不同行为表现的 socket 函数一般有如下几个:
- connect
- accept
- send (Linux 平台上对 socket 进行操作时也包括 write 函数,下文中对 send 函数的讨论也适用于 write 函数)
- recv (Linux 平台上对 socket 进行操作时也包括 read 函数,下文中对 recv 函数的讨论也适用于 read 函数)
在正式讨论以上四个函数之前,我们先解释一下阻塞模式和非阻塞模式的概念。所谓阻塞模式,是当某个函数“执行成功的条件”当前不能满足时,该函数会阻塞当前执行线程,程序执行流在超时时间到达或“执行成功的条件”满足后恢复继续执行。而非阻塞模式恰恰相反,即使某个函数的“执行成功的条件”不当前不能满足,该函数也不会阻塞当前执行线程,而是立即返回,继续运行执行程序流。如果读者不太明白这两个定义也没关系,后面我们会以具体的示例来讲解这两种模式的区别。
# 4.6.1 如何将 socket 设置成非阻塞模式
无论是 Windows 还是 Linux 平台,默认创建的 socket 都是阻塞模式的。
在 Linux 平台上,我们可以使用 fcntl() 函数或 ioctl() 函数给创建的 socket 增加 O_NONBLOCK 标志来将 socket 设置成非阻塞模式。示例代码如下:
int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);
2
3
ioctl() 函数 与 fcntl() 函数使用方式基本一致,这里就不再给出示例代码了。
当然,Linux 下的 socket() 创建函数也可以直接在创建时将 socket 设置为非阻塞模式,socket() 函数的签名如下:
int socket(int domain, int type, int protocol);
给 type 参数增加一个 SOCK_NONBLOCK 标志即可,例如:
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
不仅如此,Linux 系统下利用 accept() 函数返回的代表与客户端通信的 socket 也提供了一个扩展函数 accept4(),直接将 accept 函数返回的 socket 设置成非阻塞的。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
2
只要将 accept4() 函数最后一个参数 flags 设置成 SOCK_NONBLOCK 即可。也就是说以下代码是等价的:
socklen_t addrlen = sizeof(clientaddr);
int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK);
2
socklen_t addrlen = sizeof(clientaddr);
int clientfd = accept(listenfd, &clientaddr, &addrlen);
if (clientfd != -1)
{
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(clientfd, F_SETFL, newSocketFlag);
}
2
3
4
5
6
7
8
在 Windows 平台上,可以调用 ioctlsocket() 函数 将 socket 设置成非阻塞模式,ioctlsocket() 签名如下:
int ioctlsocket(SOCKET s, long cmd, u_long *argp);
将 cmd 参数设置为 FIONBIO,*argp 设置为 0 即可将 socket 设置成阻塞模式,而将 *argp 设置成非 0 即可设置成非阻塞模式。示例如下:
//将 socket 设置成非阻塞模式
u_long argp = 1;
ioctlsocket(s, FIONBIO, &argp);
//将 socket 设置成阻塞模式
u_long argp = 0;
ioctlsocket(s, FIONBIO, &argp);
2
3
4
5
6
7
Windows 平台需要注意一个地方,如果对一个 socket 调用了 WSAAsyncSelect() 或 WSAEventSelect() 函数后,再调用 ioctlsocket() 函数将该 socket 设置为非阻塞模式会失败,读者必须先调用 WSAAsyncSelect() 将 lEvent 参数设置为 0 或调用 WSAEventSelect() 将 lNetworkEvents 参数设置为 0 来,这样会清除已经设置的 socket 相关标志位,接着调用 ioctlsocket() 将该 socket 设置成阻塞模式才会成功。因为调用 WSAAsyncSelect() 或WSAEventSelect() 函数会自动将 socket 设置成非阻塞模式。MSDN 上原文(https://docs.microsoft.com/en-us/windows/desktop/api/winsock/nf-winsock-ioctlsocket)如下:
The WSAAsyncSelect and WSAEventSelect functions automatically set a socket to nonblocking mode. If WSAAsyncSelect or WSAEventSelect has been issued on a socket, then any attempt to use ioctlsocket to set the socket back to blocking mode will fail with WSAEINVAL.
To set the socket back to blocking mode, an application must first disable WSAAsyncSelect by calling WSAAsyncSelect with the lEvent parameter equal to zero, or disable WSAEventSelect by calling WSAEventSelect with the lNetworkEvents parameter equal to zero.
2
3
关于 WSAAsyncSelect() 和 WSAEventSelect() 这两个函数,后文中会详细讲解。
注意事项:无论是 Linux 的 fcntl 函数,还是 Windows 的 ioctlsocket,建议读者在实际编码中判断一下函数返回值以确定是否调用成功。
# 4.6.2 send 和 recv 函数在阻塞和非阻塞模式下的行为
send 和 recv 函数其实名不符实。
send 函数本质上并不是往网络上发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区中去,至于什么时候数据会从网卡缓冲区中真正地发到网络中去要根据 TCP/IP 协议栈的行为来确定。如果 socket 设置了 TCP_NODELAY 的选项(即禁用 nagel 算法),存放到内核缓冲区的数据会被立即发出去;反之,一次放入内核缓冲区的数据包如果太小,系统会在多个小的数据包凑成一个足够大的数据包后才会将数据发出去。
recv 函数本质上也并不是从网络上收取数据,而只是将内核缓冲区中的数据拷贝到应用程序的缓冲区中,当然拷贝完成以后会将内核缓冲区中该部分数据移除。
可以用下面一张图来描述上述事实:
通过上图我们知道,不同的程序进行网络通信时,发送的一方会将内核缓冲区的数据通过网络传输给接收方的内核缓冲区。在应用程序 A 与 应用程序 B 建立了 TCP 连接之后,假设应用程序 A 不断调用 send 函数,这样数据会不断拷贝至对应的内核缓冲区中,如果 B 那一端一直不调用 recv 函数,那么 B 的内核缓冲区被填满以后,A 的内核缓冲区也会被填满,此时 A 继续调用 send 函数会是什么结果呢? 具体的结果取决于该 socket 是否是阻塞模式。我们这里先给出结论:
- 当 socket 是阻塞模式的,继续调用 send/recv 函数会导致程序阻塞在 send/recv 调用处。
- 当 socket 是非阻塞模式,继续调用 send/recv 函数,send/recv 函数不会阻塞程序执行流,而是会立即出错返回,我们会得到一个相关的错误码,Linux 平台上该错误码为 EWOULDBLOCK 或 EAGAIN(这两个错误码值相同),Windows 平台上错误码为 WSAEWOULDBLOCK。
我们实际编写一下代码来验证一下以上说的两种情况。
# 1. socket 阻塞模式下的 send 行为
服务端代码(blocking_server.cpp)如下:
/**
* 验证阻塞模式下send函数的行为,server端
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
int main(int argc, char* argv[])
{
//1.创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}
//2.初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
//3.启动侦听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
while (true)
{
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//4. 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd != -1)
{
//只接受连接,不调用recv收取任何数据
std:: cout << "accept a client connection." << std::endl;
}
}
//7.关闭侦听socket
close(listenfd);
return 0;
}
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
客户端代码(blocking_client.cpp)如下:
/**
* 验证阻塞模式下send函数的行为,client端
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"
int main(int argc, char* argv[])
{
//1.创建一个socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}
//2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
std::cout << "connect socket error." << std::endl;
close(clientfd);
return -1;
}
//3. 不断向服务器发送数据,或者出错退出
int count = 0;
while (true)
{
int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
if (ret != strlen(SEND_DATA))
{
std::cout << "send data error." << std::endl;
break;
}
else
{
count ++;
std::cout << "send data successfully, count = " << count << std::endl;
}
}
//5. 关闭socket
close(clientfd);
return 0;
}
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
在 shell 中分别编译这两个 cpp 文件得到两个可执行程序 blocking_server 和 blocking_client:
g++ -g -o blocking_server blocking_server.cpp
g++ -g -o blocking_client blocking_client.cpp
2
我们先启动 blocking_server,然后用 gdb 启动 blocking_client,输入 run 命令让 blocking_client跑起来,blocking_client 会不断地向 blocking_server 发送"helloworld"字符串,每次 send 成功后,会将计数器 count 的值打印出来,计数器会不断增加,程序运行一段时间后,计数器 count 值不再增加且程序不再有输出。操作过程及输出结果如下:
blocking_server 端:
[root@localhost testsocket]# ./blocking_server
accept a client connection.
2
[root@localhost testsocket]# gdb blocking_client
Reading symbols from /root/testsocket/blocking_client...done.
(gdb) run
//输出结果太多,省略部分...
send data successfully, count = 355384
send data successfully, count = 355385
send data successfully, count = 355386
send data successfully, count = 355387
send data successfully, count = 355388
send data successfully, count = 355389
send data successfully, count = 355390
2
3
4
5
6
7
8
9
10
11
此时程序不再有输出,说明我们的程序应该“卡在”某个地方,继续按 Ctrl + C 让 gdb 中断下来,输入 bt 命令查看此时的调用堆栈,我们发现我们的程序确实阻塞在 send 函数调用处:
^C
Program received signal SIGINT, Interrupt.
0x00007ffff72f130d in send () from /lib64/libc.so.6
(gdb) bt
#0 0x00007ffff72f130d in send () from /lib64/libc.so.6
#1 0x0000000000400b46 in main (argc=1, argv=0x7fffffffe598) at blocking_client.cpp:41
(gdb)
2
3
4
5
6
7
上面的示例验证了如果一端一直发数据,而对端应用层一直不取数据(或收取数据的速度慢于发送速度),则两端的内核缓冲区很快就会被填满,导致发送端调用 send 函数被阻塞。这里说的“内核缓冲区” 其实有个专门的名字,即 TCP 窗口。也就是说 socket 阻塞模式下, send 函数在 TCP 窗口太小时的行为是阻塞当前程序执行流(即阻塞 send 函数所在的线程的执行)。
说点题外话,上面的例子,我们每次发送一个“helloworld”(10个字节),一共发了 355390 次(每次测试的结果略有不同),我们可以粗略地算出 TCP 窗口的大小大约等于 1.7 M左右 (10 * 355390 / 2)。
让我们再深入一点,我们利用 Linux tcpdump 工具来动态看一下这种情形下 TCP 窗口大小的动态变化。需要注意的是,Linux 下使用 tcpdump 这个命令需要有 root 权限。
我们开启三个 shell 窗口,在第一个窗口先启动 blocking_server 进程,在第二个窗口用 tcpdump 抓取经过 TCP 端口 3000 上的数据包:
[root@localhost testsocket]# tcpdump -i any -nn -S 'tcp port 3000'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
2
3
接着在第三个 shell 窗口,启动 blocking_client。当 blocking_client 进程不再输出时,我们抓包的结果如下:
13:58:54.370662 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [S], seq 317054582, win 43690, options [mss 65495,sackOK,TS val 2890857589 ecr 0,nop,wscale 7], length 0
13:58:54.370673 IP 127.0.0.1.3000 > 127.0.0.1.39102: Flags [S.], seq 1396473902, ack 317054583, win 43690, options [mss 65495,sackOK,TS val 2890857589 ecr 2890857589,nop,wscale 7], length 0
13:58:54.370684 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [.], ack 1396473903, win 342, options [nop,nop,TS val 2890857589 ecr 2890857589], length 0
13:58:54.370699 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [P.], seq 317054583:317054593, ack 1396473903, win 342, options [nop,nop,TS val 2890857589 ecr 2890857589], length 10
...省略部分输出...
13:59:37.020155 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [.], ack 1396473903, win 342, options [nop,nop,TS val 2890900238 ecr 2890885390], length 0
13:59:37.020183 IP 127.0.0.1.3000 > 127.0.0.1.39102: Flags [.], ack 317972685, win 0, options [nop,nop,TS val 2890900238 ecr 2890870646], length 0
14:00:06.204170 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [.], ack 1396473903, win 342, options [nop,nop,TS val 2890929421 ecr 2890900238], length 0
14:00:06.204214 IP 127.0.0.1.3000 > 127.0.0.1.39102: Flags [.], ack 317972685, win 0, options [nop,nop,TS val 2890929421 ecr 2890870646], length 0
2
3
4
5
6
7
8
9
抓取到的前三个数据包是 blocking_client 与 blocking_server 建立三次握手的过程。
13:58:54.370662 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [S], seq 317054582, win 43690, options [mss 65495,sackOK,TS val 2890857589 ecr 0,nop,wscale 7], length 0
13:58:54.370673 IP 127.0.0.1.3000 > 127.0.0.1.39102: Flags [S.], seq 1396473902, ack 317054583, win 43690, options [mss 65495,sackOK,TS val 2890857589 ecr 2890857589,nop,wscale 7], length 0
13:58:54.370684 IP 127.0.0.1.39102 > 127.0.0.1.3000: Flags [.], ack 1396473903, win 342, options [nop,nop,TS val 2890857589 ecr 2890857589], length 0
示意图如下:
当每次 blocking_client 给 blocking_server 发数据以后,blocking_server 会应答 blocking_client,在每次应答的数据包中会带上自己的当前可用 TCP 窗口大小(看上文中结果从 127.0.0.1.3000 > 127.0.0.1.39102 方向的数据包的 win 字段大小变化),由于 TCP 流量控制和拥赛控制机制的存在,blocking_server 端的 TCP 窗口大小短期内会慢慢增加,后面随着接收缓冲区中数据积压越来越多, TCP 窗口会慢慢变小,最终变为 0。
另外,细心的读者如果实际去做一下这个实验会发现一个现象,即当 tcpdump 已经显示对端的 TCP 窗口是 0 时, blocking_client 仍然可以继续发送一段时间的数据,此时的数据已经不是在发往对端,而是逐渐填满到本端的内核发送缓冲区中去了,这也验证了 send 函数实际上是往内核缓冲区中拷贝数据这一行为。
# 2. socket 非阻塞模式下的 send 行为
我们再来验证一下非阻塞 socket 的 send 行为,server 端的代码不变,我们将 blocking_client.cpp 中 socket 设置成非阻塞的,修改后的代码如下:
/**
* 验证非阻塞模式下send函数的行为,client端,nonblocking_client.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"
int main(int argc, char* argv[])
{
//1.创建一个socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}
//2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
std::cout << "connect socket error." << std::endl;
close(clientfd);
return -1;
}
//连接成功以后,我们再将 clientfd 设置成非阻塞模式,
//不能在创建时就设置,这样会影响到 connect 函数的行为
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set socket to nonblock error." << std::endl;
return -1;
}
//3. 不断向服务器发送数据,或者出错退出
int count = 0;
while (true)
{
int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
if (ret == -1)
{
//非阻塞模式下send函数由于TCP窗口太小发不出去数据,错误码是EWOULDBLOCK
if (errno == EWOULDBLOCK)
{
std::cout << "send data error as TCP Window size is too small." << std::endl;
continue;
}
else if (errno == EINTR)
{
//如果被信号中断,我们继续重试
std::cout << "sending data interrupted by signal." << std::endl;
continue;
}
else
{
std::cout << "send data error." << std::endl;
break;
}
}
if (ret == 0)
{
//对端关闭了连接,我们也关闭
std::cout << "send data error." << std::endl;
close(clientfd);
break;
}
else
{
count ++;
std::cout << "send data successfully, count = " << count << std::endl;
}
}
//5. 关闭socket
close(clientfd);
return 0;
}
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
编译 nonblocking_client.cpp 得到可执行程序 nonblocking_client:
g++ -g -o nonblocking_client nonblocking_client.cpp
运行 nonblocking_client,运行一段时间后,由于对端和本端的 TCP 窗口已满,数据发不出去了,但是 send 函数不会阻塞,而是立即返回,返回值是 -1(Windows 系统上 返回 SOCKET_ERROR,这个宏的值也是 -1),此时得到错误码是 EWOULDBLOCK。执行结果如下:
# 3. socket 阻塞模式下的 recv 行为
在了解了 send 函数的行为,我们再来看一下阻塞模式下的 recv 函数行为。服务器端代码不需要修改,我们修改一下客户端代码,如果服务器端不给客户端发数据,此时客户端调用 recv 函数执行流会阻塞在 recv 函数调用处。修改后的客户端代码如下:
/**
* 验证阻塞模式下recv函数的行为,client端,blocking_client_recv.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"
int main(int argc, char* argv[])
{
//1.创建一个socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}
//2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
std::cout << "connect socket error." << std::endl;
close(clientfd);
return -1;
}
//直接调用recv函数,程序会阻塞在recv函数调用处
char recvbuf[32] = {0};
int ret = recv(clientfd, recvbuf, 32, 0);
if (ret > 0)
{
std::cout << "recv successfully." << std::endl;
}
else
{
std::cout << "recv data error." << std::endl;
}
//5. 关闭socket
close(clientfd);
return 0;
}
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
编译 blocking_client_recv.cpp 并启动,我们发现程序既没有打印 recv 调用成功的信息也没有调用失败的信息,将程序中断下来,使用 bt 命令查看此时的调用堆栈,发现程序确实阻塞在 recv 函数调用处。
[root@localhost testsocket]# g++ -g -o blocking_client_recv blocking_client_recv.cpp
[root@localhost testsocket]# gdb blocking_client_recv
Reading symbols from /root/testsocket/blocking_client_recv...done.
(gdb) r
Starting program: /root/testsocket/blocking_client_recv
^C
Program received signal SIGINT, Interrupt.
0x00007ffff72f119d in recv () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-16.el7_4.2.x86_64 libstdc++-4.8.5-16.el7_4.2.x86_64
(gdb) bt
#0 0x00007ffff72f119d in recv () from /lib64/libc.so.6
#1 0x0000000000400b18 in main (argc=1, argv=0x7fffffffe588) at blocking_client_recv.cpp:40
2
3
4
5
6
7
8
9
10
11
12
# 4. socket 非阻塞模式下的 recv 行为
非阻塞模式下如果当前无数据可读,recv 函数将立即返回,返回值为 -1,错误码为 EWOULDBLOCK。将客户端代码修改一下:
/**
* 验证阻塞模式下recv函数的行为,client端,blocking_client_recv.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"
int main(int argc, char* argv[])
{
//1.创建一个socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}
//2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
std::cout << "connect socket error." << std::endl;
close(clientfd);
return -1;
}
//连接成功以后,我们再将 clientfd 设置成非阻塞模式,
//不能在创建时就设置,这样会影响到 connect 函数的行为
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set socket to nonblock error." << std::endl;
return -1;
}
//直接调用recv函数,程序会阻塞在recv函数调用处
while (true)
{
char recvbuf[32] = {0};
int ret = recv(clientfd, recvbuf, 32, 0);
if (ret > 0)
{
//收到了数据
std::cout << "recv successfully." << std::endl;
}
else if (ret == 0)
{
//对端关闭了连接
std::cout << "peer close the socket." << std::endl;
break;
}
else if (ret == -1)
{
if (errno == EWOULDBLOCK)
{
std::cout << "There is no data available now." << std::endl;
}
else if (errno == EINTR)
{
//如果被信号中断了,则继续重试recv函数
std::cout << "recv data interrupted by signal." << std::endl;
} else
{
//真的出错了
break;
}
}
}
//5. 关闭socket
close(clientfd);
return 0;
}
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
执行结果与我们预期的一模一样, recv 函数在无数据可读的情况下并不会阻塞程序执行流,所以会一直有“There is no data available now.”相关的输出。
# 4.6.3 非阻塞模式下 send 和 recv 函数的返回值总结
我们来根据前面的讨论总结一下 send 和 recv 函数的各种返回值意义:
返回值 n | 返回值含义 |
---|---|
大于 0 | 成功发送(send)或收取(recv) n 个字节 |
0 | 对端关闭连接 |
小于 0( -1) | 出错或者被信号中断或者对端 TCP 窗口太小数据发不出去(send)或者当前网卡缓冲区已无数据可收(recv) |
我们来逐一介绍下这三种情况:
返回值大于 0
对于 send 和 recv 函数返回值大于 0,表示发送或接收多少字节,需要注意的是,在这种情形下,我们一定要判断下 send 函数的返回值是不是我们期望发送的字节数长度,而不是简单判断其返回值大于 0。举个例子:
int n = send(socket, buf, buf_length, 0);
if (n > 0)
{
printf("send data successfully\n");
}
2
3
4
5
很多新手会写出上述代码,虽然返回值 n 大于 0,但是实际情形下,由于对端的 TCP 窗口可能因为缺少一部分字节就满了,所以返回值 n 的值可能在 (0, buf_length] 之间,当 0 < n < buf_length 时,虽然此时 send 函数是调用成功了,但是业务上并不算正确,因为有部分数据并没发出去。你可能在一次测试中测不出 n 小于 buf_length 的情况,但是不代表实际中不存在。所以,建议要么返回值 n 等于 buf_length 才认为正确,要么在一个循环中调用 send 函数,如果数据一次性发不完,记录偏移量,下一次从偏移量处接着发,直到全部发送完为止。
//不推荐的方式
int n = send(socket, buf, buf_length, 0);
if (n == buf_length)
{
printf("send data successfully\n");
}
2
3
4
5
6
//推荐的方式:在一个循环里面根据偏移量发送数据
bool SendData(const char* buf, int buf_length)
{
//已发送的字节数目
int sent_bytes = 0;
int ret = 0;
while (true)
{
ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0);
if (ret == -1)
{
if (errno == EWOULDBLOCK)
{
//严谨的做法,这里如果发不出去,应该缓存尚未发出去的数据,后面介绍
break;
}
else if (errno == EINTR)
continue;
else
return false;
}
else if (ret == 0)
{
return false;
}
sent_bytes += ret;
if (sent_bytes == buf_length)
break;
//稍稍降低 CPU 的使用率
usleep(1);
}
return true;
}
返回值等于 0
通常情况下,如果 send 或者 recv 函数返回 0,我们就认为对端关闭了连接,我们这端也关闭连接即可,这是实际开发时最常见的处理逻辑。send 函数主动发送 0 字节时也会返回 0,这是一种特例,我们下一节会介绍。
返回值小于 0
对于 send 或者 recv 函数返回值小于 0 的情况(即返回 -1),根据前文的讨论,此时并不表示 send 或者 recv 函数一定调用出错。这里列一个表格说明:
返回值和错误码 send 函数 recv 函数 操作系统说明 1 返回 -1,错误码 EWOUDBLOCK 或 EAGAIN TCP 窗口太小,数据暂时发不出去 当前内核缓冲区中无可读数据 Linux 2 返回 -1,错误码 EINTR 被信号中断,需要重试 被信号中断,需要重试 Linux 3 返回 -1,错误码不是上述 1 和 2 出错 出错 Linux 4 返回 -1,错误码 WSAEWOUDBLOCK TCP 窗口太小,数据暂时发不出去 当前内核缓冲区中无可读数据 Windows 5 返回 -1,错误码不是上述 4 出错 出错 Windows 注意:这里是针对非阻塞模式下 socket 的 send 和 recv 返回值,如果是阻塞模式下 socket,如果返回值是 -1(Windows 上即 SOCKET_ERROR),则一定表示出错。
# 4.6.4 阻塞与非阻塞 socket 各自适用场景
阻塞的 socket 函数在调用 send、recv、connect、accept 等函数时,如果特定的条件不满足,就会阻塞其调用线程直至超时,非阻塞的 socket 恰恰相反。这并不意味着非阻塞模式比阻塞模式模式好,二者各有优缺点。
非阻塞模式一般用于需要支持高并发多 QPS 的场景下(如服务器程序),但是正如前文所述,这种模式让程序执行流和控制逻辑变得复杂;相反,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊的场景。非阻塞模式使用非常普遍,这里不再举例;这里举两个可以使用阻塞模式的应用的场景:
示例一
某程序需要临时发送一个文件,文件分段发送,每发送一段,对端都会给与一个响应,该程序可以单独开一个任务线程,在这个任务线程函数里面,使用先 send 后 recv 再 send 再 recv 的模式,每次 send 和 recv 都是阻塞式的。
示例二
A 端与 B 端之间的通信只有问答模式,即 A 每发送给 B 一个请求,B 必定会给 A 一个响应,除此以外,B 不会给 A 推送任何数据,此时 A 端就可以采用阻塞模式,A 端每次 send 完请求后,就可以直接使用阻塞式的 recv 函数去接收一定要有的应答包。