参考文章:《UNIX网络编程》《UNIX环境高级编程》《TCP/IP编程》、socket阻塞模式

read

ssize_t 是有符号整型 long;size_t 是无符号整型 unsigned long

对于 read 和 write 这类 低速系统调用(即可能使进程永远阻塞下去的系统调用) ,当执行期间捕捉到信号,则该系统调用就会被中断不再执行,且返回 -1,并将 errno 设置为 -1。为什么呢?有这样一个常见的场景:网络中,由于不知道对端网络具体会发来多大的包,所以我们会让 read 尽可能多地接收数据,即把其第三个参数设置大些。那么问题来了,对端只发送了 1000 字节的数据,而你却指定本端的 read 读取 10000 字节,那怎么办?总不能一直阻塞下去吧?所以此时就需要用到信号,来中断 read 使其结束。

至于 read 在读取数据时是被什么信号中断的,笔者也没有查到相关资料,知晓的读者还请在评论区指点一二。

需要注意的是,TCP 套接字上的 IO 表现的行为不同于通常的磁盘 IO。 磁盘 IO 一般不会被信号打断而终止,因为磁盘 IO 与网络 IO 的区别在于:前者的 IO 量是可以确定的,虽然可能会暂时阻塞调用者,但只要把指定量的数据处理掉就 OK,任务很明确;而网络 IO 一般不知道具体的 IO 量,就像上面的场景一样,所以必须在某个时刻终止 IO(终端 IO 也是如此,计算机不知道用户可能会输入多少数据)。除非发生硬件错误,否则磁盘 IO 总会很快返回

对于被中断的 read,早期实现是:如果 read 已接收部分数据到应用缓冲区,但还未接收到应用程序请求的所有数据(即 read 的第三个参数),则操作系统会认为本次 read 调用失败,返回 -1,errno 被设置为 EINTR 。目前的 POSIX 标准采用的方式是:如果 read 只接收了部分数据就被信号中断,那么也算成功,且返回已接收的字节数,errno 不修改

这就是为什么在 Socket 编程中,往往将 read 函数放在循环中的原因! 大多数书籍将其归咎于 Socket 的缓冲机制,实际上这只是原因之一。也有许多人认为 read 只将接收缓冲区读一次就返回,当请求的数据量大于接收缓冲区时,read 返回值就当然会小于请求的字节数,这种说法更不准确。做个实验:

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
//server
int main()
{
int sock_server = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
server_addr.sin_family = AF_INET;
bind(sock_server, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sock_server, 20);
int sock_clnt = accept(sock_server,NULL,NULL);
char buf[100000] = {0};
int rcv_buf=1000; //系统会调整接收缓冲区大小,本例中被调整为2304
socklen_t len = sizeof(rcv_buf);
setsockopt(sock_clnt,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,sizeof(rcv_buf));
getsockopt(sock_clnt,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,&len);
printf("server rcv_buf : %d\n",rcv_buf);
ssize_t size = read(sock_clnt, buf, 100000);
printf("server received %ld bytes\n",size);
close(sock_server);
close(sock_clnt);
}
//=====================================
//client
int main()
{
int sock_clnt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock_clnt, (struct sockaddr*)&server_addr, sizeof(server_addr));
char out_buf[100000];
int snd_buf =100000;
socklen_t len = sizeof(snd_buf);
setsockopt(sock_clnt,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,sizeof(snd_buf));
getsockopt(sock_clnt,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,&len);
printf("client snd_buf : %d\n",snd_buf);
ssize_t size = write(sock_clnt, out_buf, 100000);
printf("client sent %ld bytes\n",size);
close(sock_clnt);
}

运行后 server 的结果如下:

你看,server 的接收缓冲区大小被修改为 2304 字节,但却 read 了 65482 字节,大于 2304,说明不止读取一次缓冲区;小于请求值 100000,说明 read 还没有读完对面发送的 10000 字节就被信号中断。

所以得出一个重要结论:如果 read 返回值小于请求值,说明有两种情况:

  1. 提前收到了 EOF。
  2. 被信号中断。

第一种情况说明数据已经接收完,第二种情况则还需要重新调用 read 才能将数据接收完毕。我们可以使用自己的包裹函数 readn 一次性将数据读完:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ssize_t readn(int fd, void* buf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char* tmp = buf;
while(nleft > 0)
{
if((nread = read(fd, tmp, nleft)) < 0)
{
if(nread == EINTR)
nread = 0; //被信号打断,则继续循环
else
return -1;
}
else if(nread == 0)
break;
nleft -= nread;
tmp += nread;
}
return n - nleft;
}

不过需要注意,readn 可能会一直阻塞,因为正常情况下它只会在接收到 EOF 或者读满 n 字节才会返回;而 read 不同,read 收到 EOF 、读满 n 字节或者被信号中断(没读满)时就能返回 。所以当发送的字节数小于请求的字节数时,readn 就会被阻塞,如下:

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
//server
int main()
{
int sock_server = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
server_addr.sin_family = AF_INET;
bind(sock_server, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sock_server, 20);
int sock_clnt = accept(sock_server,NULL,NULL);
char buf[1000];
ssize_t size = readn(sock_clnt, buf, 1000);
printf("received %lu bytes\n",size);
printf("%s",buf);
while(1);
}
//==============================================
//client
int main()
{
int sock_clnt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(sock_clnt, (struct sockaddr*)&server_addr, sizeof(server_addr));
char buf[100];
fgets(buf,100,stdin);
writen(sock_clnt,buf,strlen(buf));
while(1);//while停住,以免关闭套接字发送EOF
}

运行以上程序时,服务器不会显示客户端发来的消息,因为 readn 要求读满 1000 或者读到 EOF。当把 readn 改成 read,就能成功显示消息了。解决这个问题的办法之一是设计应用层协议,比如在包头说明本包的大小,这方面内容后续补充。

recv 的第四个参数可以选 MSG_WAITALL 标志来阻止这种行为,当 flags 为 MSG_WAITALL 时,recv 会阻塞直到所指定的长度 nbytes 字节的数据全部返回,recv 才会返回。

write

不同于 read,作用于字节流套接字时,输出字节比请求的字节少这种情况仅在非阻塞前提下才会发生

调用 write 时,如果套接字的发送缓冲区容不下请求量,那么 write 被阻塞,直到缓冲区中的数据被发送到对端,腾出足够的空间,才唤醒 write 函数继续写入数据。有以下几个要点需要注意:

  1. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
  2. 发送缓冲区中的数据必须保留,直到对端回复相应的 ACK(这是由内核自动完成的)。
  3. 接收缓冲区的数据将一直保留,直到应用层读取(read)。
  4. write 函数的返回只说明应用层的数据已经全部转移到输出缓冲区,这不代表对方已经收到所有数据。

让我们做几个实验来验证上面的结论。

实验一

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
//server
int main()
{
int sock_server = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
server_addr.sin_family = AF_INET;
Bind(sock_server, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sock_server, 20);
int sock_clnt = accept(sock_server,NULL,NULL);

int buf_size = 100000; //设置接收缓冲区,系统会调节该值,可能为200000
socklen_t len = sizeof(buf_size);
setsockopt(sock_clnt,SOL_SOCKET,SO_RCVBUF,(void*)&buf_size,len);
char buf[100000];
ssize_t size = readn(sock_clnt, buf, 100000);
printf("received %lu bytes\n",size);
printf("%s",buf);
close(sock_server);
close(sock_clnt);
}
//================
//client
int main()
{
int sock_clnt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
Connect(sock_clnt, (struct sockaddr*)&server_addr, sizeof(server_addr));

int buf_size = 1000; //系统会调节该值,可能为3000左右
socklen_t len = sizeof(buf_size);
setsockopt(sock_clnt,SOL_SOCKET,SO_SNDBUF,(void*)&buf_size,len);
char buf[100000] = {0};
size_t size = write(sock_clnt,buf,sizeof(buf));
printf("%lu\n",size);
close(sock_clnt);
}

运行后,立刻 输出结果,client 发送 100000 字节,server 接收 100000 字节。这说明即使我们将 client 的发送缓冲区设置为 1000,数据也会分批发送。结论一得证。

实验二

将上面代码 server 的接收缓冲区改为 1000,运行程序,等待几秒后 输出结果,两方仍是 100000 字节。这说明接收缓存区虽小,但由于应用层一直在读取数据(readn),所以接收缓冲区可以不断地丢弃旧数据、接收新数据,直到收到所有数据。

继续,修改 14~22 行代码,如下:

1
2
3
4
int buf_size = 1000;
socklen_t len = sizeof(buf_size);
setsockopt(sock_clnt,SOL_SOCKET,SO_RCVBUF,(void*)&buf_size,len);
while(1); //循环,不close套接字

即,不从接收缓冲区读取数据,同时不 close 套接字。close 套接字可能导致缓冲区的一系列动作,不利于我们进行实验。

运行程序,两端都陷入阻塞 。这说明由于接收缓冲区没有被 read,数据一直积压,因此无法接收 client 发来的数据,也就不能回复 ACK;而 client 因为没有收到 server 回复的 ACK ,所以发送缓冲区不能接收应用层的新数据,以至于在 write 中阻塞。结论 2、3 得证。

实验三

在实验二的基础上,把 client 的发送缓冲区改为 100000 字节,运行程序。client 立刻输出 100000 。表明 client 成功向发送缓冲区写入 100000 字节,并立刻从 write 返回。但 server 的接收缓冲区只有 1000 字节的大小,不可能接收完这 100000 字节。因此,结论 4 得证。



sendfile