关于网络编程中recv函数在什么情况下会返回的一点心得。

问题的提出

最近在学习《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的返回条件。

实验1

将客户端和服务端的MSG_OOB全部改成0,即默认的读取数据的方式,不发送带外数据。
结果如下:

得到了 9 bytes的正常数据 123abc123 
 得到了 0 bytes的紧急数据  
 得到了 0 bytes的正常数据 

(虽然第二行写的是紧急数据,实际上是正常将数据发送出去和接收的,这里提醒一下。)
即使客户端分了三次将数据发送出去,第一个recv函数还是直接将三次数据全部读取进来了。难道说,服务端会一直等待客户端将数据全部发送完,才会返回recv函数?
且慢,接下来尝试一下让服务端慢慢发送数据的情况。

实验2

将客户端的代码修改为如下所示,仅仅是添加了几个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函数才开始工作。

实验3

本来实验做到这里,再根据网上查阅到的资料,就可以做出阶段性的总结。但是我注意到,在实验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(阻塞模式)函数后,运行逻辑如下:

  1. 当检测到接收缓冲队列中没有数据时候,一直循环阻塞
  2. 当检测到接收缓冲队列有数据时,将数据读出直到用户定义的缓存满,返回缓存的大小。
  3. 如果接收缓冲队列数据读完了,用户定义的缓存还没有满,recv函数依然返回,返回值为读取数据的字节数。(实验2)
  4. 如果TCP模块检测到紧急报文字段,则recv函数会将带外字节前面的数据全部读取并返回,由下一个能够读取带外数据的recv函数来处理带外字节。
  5. 发送端如果关闭连接,接收端处于阻塞状态的recv函数会直接全部返回0字节。(实验3)

(补充)关于PSH字段和缓冲区的关系

作为一个初学者,我一度认为当TCP报文头部出现PSH字段时,recv函数就会赶紧将数据读入缓冲区并返回。事实上,我的“认为”只猜对了一半,recv函数确实会赶紧将接收窗口的数据读取到用户定义的buffer中,但这并不意味着recv函数会直接返回,因为两者确实没有什么关系。PSH字段只是提醒TCP模块赶紧将接收窗口的数据读入缓存中,当接收窗口的数据读取后PSH就结束了它的使命,跟函数是否返回没有任何关系。


参考资料

TCP recv(阻塞模式)函数到底时什么时候返回,结束阻塞的呢?原来是这样

猜你喜欢

转载自blog.csdn.net/weixin_45685193/article/details/123601930