最近在学习《Linux高性能服务器编程》,仿着第五章书上的代码写了一个服务端和客户端的程序,其中谈到OOB字节会将recv函数截断的现象,因此产生了好奇,探究一下recv函数在什么情况下会返回。探究的结果不一定正确,但最后会尽量提出符合现象的结论,有错误欢迎指出。
以下是客户端发送数据部分代码:
//省略了前面连接建立的部分
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);
以下是服务端接收数据部分代码:
char buffer[BUF_SIZE];
memset(buffer, 0, BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE - 1, 0);//-1也许是为了添加字符串结束符
printf("得到了 %d bytes的正常数据 %s \n ", ret, buffer);
memset(buffer, 0, BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB);//接收紧急数据
printf("得到了 %d bytes的紧急数据 %s \n ", ret, buffer);
memset(buffer, 0, BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
printf("得到了 %d bytes的正常数据 %s \n ", ret, buffer);
以下是服务端的输出结果
得到了 5 bytes的正常数据 123ab
得到了 1 bytes的紧急数据 c
得到了 3 bytes的正常数据 123
可以看到在第二次调用recv函数时将读取的数据截断,仅接收了带外数据的一个字节。
带外数据先不谈,可以看到第一次调用recv函数时直接往用户定义的buffer区内写入了五个字节。
按照直觉来说,客户端分三次发送数据,服务端也"应该"分三次调用recv函数将输入缓冲区的数据读取。但是它没有,第一次返回recv函数时就读取了五个字节,因此让我产生了探究recv返回条件的好奇心。
接下来通过对代码进行简单的修改,来探究recv的返回条件。
将客户端和服务端的MSG_OOB全部改成0,即默认的读取数据的方式,不发送带外数据。
结果如下:
得到了 9 bytes的正常数据 123abc123
得到了 0 bytes的紧急数据
得到了 0 bytes的正常数据
(虽然第二行写的是紧急数据,实际上是正常将数据发送出去和接收的,这里提醒一下。)
即使客户端分了三次将数据发送出去,第一个recv函数还是直接将三次数据全部读取进来了。难道说,服务端会一直等待客户端将数据全部发送完,才会返回recv函数?
且慢,接下来尝试一下让服务端慢慢发送数据的情况。
将客户端的代码修改为如下所示,仅仅是添加了几个sleep函数,让每次发送数据后间隔一段时间。
const char* oob_data = "abc";
const char* normal_data = "123";
sleep(5);
send(sockfd, normal_data, strlen(normal_data), 0);
sleep(1);
send(sockfd, oob_data, strlen(oob_data),0);
sleep(1);
send(sockfd, normal_data, strlen(normal_data), 0);
输出如下:
得到了 3 bytes的正常数据 123
得到了 3 bytes的紧急数据 abc
得到了 3 bytes的正常数据 123
每次发送数据后等待1秒,这1秒足以让服务端的TCP模块将三个字节的数据从接收窗口中读出到用户定义的buffer中,并清空输入窗口的数据。那么就意味着,当接收窗口的数据全部读出后,recv函数就会直接返回。就这样,服务端反复读出了全部接收窗口的数据三次,recv函数也就返回了三次。
那么实验1的时候为什么会直接读入9个字节呢?做个不严谨的猜想,那是因为发送端源源不断地将数据发送过来,接收端的接收窗口并没有那么及时的将数据读出,因此直到9个字节都读完,它才发现接收窗口的数据已经空了,命令recv函数返回。
另外,在客户端第一次发送数据的前五秒sleep中,服务端的recv函数一直处于阻塞的状态中,因此那5s内是没有任何输出的,直到接收窗口有数据流入,recv函数才开始工作。
本来实验做到这里,再根据网上查阅到的资料,就可以做出阶段性的总结。但是我注意到,在实验1中,第二个和第三个recv函数即使没有接收到任何数据,他也仍然返回了,这是怎么回事呢?
这里提醒一下读者,在发送端发送完三次数据后,就迅速的关闭了连接,那么会不会是关闭连接的这个操作引起了接收端recv函数的返回呢?
将发送端代码改写如下:
const char* oob_data = "abc";
const char* normal_data = "123";
sleep(5);
send(sockfd, normal_data, strlen(normal_data), 0);
send(sockfd, oob_data, strlen(oob_data),0);
sleep(1);
send(sockfd, normal_data, strlen(normal_data), 0);
sleep(5);
第三次数据发送完,让程序暂停5秒再关闭连接。如果猜想正确的话,接收端的第三个recv函数会在5s后返回0个字节的数据。
输出结果
得到了 6 bytes的正常数据 123abc
得到了 3 bytes的紧急数据 123
得到了 0 bytes的正常数据
为了体现效果我应该在输出里标注时间的,但是我真的懒()
实验结果如我所预料,第三次输出在5s后姗姗来迟,虽然这里体现不出效果,但是作为笔者,我观察到的效果就是这样。那么得出结论,发送端关闭连接的时候,输出端的recv函数也会全部取消阻塞状态,返回0字节。
结合网上的资料,当服务端调用recv(阻塞模式)函数后,运行逻辑如下:
作为一个初学者,我一度认为当TCP报文头部出现PSH字段时,recv函数就会赶紧将数据读入缓冲区并返回。事实上,我的“认为”只猜对了一半,recv函数确实会赶紧将接收窗口的数据读取到用户定义的buffer中,但这并不意味着recv函数会直接返回,因为两者确实没有什么关系。PSH字段只是提醒TCP模块赶紧将接收窗口的数据读入缓存中,当接收窗口的数据读取后PSH就结束了它的使命,跟函数是否返回没有任何关系。
TCP recv(阻塞模式)函数到底时什么时候返回,结束阻塞的呢?原来是这样