当前位置: 代码迷 >> 综合 >> recv/send堵塞和非堵塞
  详细解决方案

recv/send堵塞和非堵塞

热度:15   发布时间:2024-01-29 18:08:42.0

recv/send堵塞和非堵塞理解

  • TCP之深入浅出send和recv
    • 需要理解的3个概念
    • 实例详解send()
  • send函数
  • recv函数

参考:
TCP之深入浅出send和recv
linux下非阻塞的tcp研究

题外话

今天在看epoll的ET模式时,说ET模式时,套接字描述符必须设置成非堵塞模式,于是想看看堵塞和非堵塞recv/send的区别,网上鱼龙混杂的博文,错误百出,查了好久,在此做个总结,感觉不看源码实现的话,还是理解得不彻底。

TCP之深入浅出send和recv

需要理解的3个概念

1.TCP socket的buffer
每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是依赖于这两个独立的buffer以及buffer的填充状态。

接收缓冲区把数据缓存入内核,应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内

再啰嗦一点,不管进程是否调用recv()读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中

recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回,仅此而已。进程调用send()发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send()返回之时,数据不一定会发送到对端去(和write写文件有点类似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。

对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。

查看测试机的socket发送缓冲区大小,

cat /proc/sys/net/ipv4/tcp_wmem
4096	16384	4194304

第一个值是一个限制值,socket发送缓存区的最少字节数;
第二个值是默认值;
第三个值是一个限制值,socket发送缓存区的最大字节数;
根据实际测试,发送缓冲区的尺寸在默认情况下的全局设置是16384字节,即16k。
在测试系统上,发送缓存默认值是16k。
proc文件系统下的值和sysctl中的值都是全局值,应用程序可根据需要在程序中使用setsockopt()对某个socket的发送缓冲区尺寸进行单独修改,详见文章《TCP选项之SO_RCVBUF和SO_SNDBUF》,不过这都是题外话。

2.接收窗口(滑动窗口)
TCP连接建立之时的收端的初始接受窗口大小是14600,细节如图2所示(129是收端,130是发端)
图2
图2
接收窗口是TCP中的滑动窗口,TCP的收端用这个接受窗口----win=14600,通知发端,我目前的接收能力是14600字节。
后续发送过程中,收端会不断的用ACK ( ACK的全部作用请参照博文《TCP之ACK发送情景 )。通知发端自己的接收窗口的大小状态,如图3,而发端发送数据的量,就根据这个接收窗口的大小来确定,发端不会发送超过收端接收能力的数据量。这样就起到了一个流量控制的的作用。
在这里插入图片描述
图3

图3说明
21,22两个包都是收端发给发端的ACK包

第21个包,收端确认收到的前7240个字节数据,7241的意思是期望收到的包从7241号开始,序号加了1.同时,接收窗口从最初的14656(如图2)经过慢启动阶段增加到了现在的29120。用来表明现在收端可以接收29120个字节的数据,而发端看到这个窗口通告,在没有收到新的ACK的时候,发端可以向收端发送29120字节这么多数据。

第22个包,收端确认收到的前8688个字节数据,并通告自己的接收窗口继续增长为32000这么大。

3.单个TCP的负载量和MSS的关系
MSS在以太网上通常大小是1460字节,而我们在后续发送过程中的单个TCP包的最大数据承载量是1448字节,这二者的关系可以参考博文《TCP之1460MSS和1448负载》。

实例详解send()

实例功能说明:接收端129作为客户端去连接发送端130,连接上之后并不调用recv()接收,而是sleep(1000),把进程暂停下来,不让进程接收数据。内核会缓存数据至接收缓冲区。发送端作为服务器接收TCP请求之后,立即用ret = send(sock,buf,70k,0);这个C语句,向接收端发送70k数据。
我们现在来观察这个过程。看看究竟发生了些什么事。wireshark抓包截图如下图4
在这里插入图片描述
图4说明,包序号等同于时序

  1. 客户端sleep在recv()之前,目的是为了把数据压入接收缓冲区。服务端调用"ret = send(sock,buf,70k,0);"这个C语句,向接收端发送70k数据。由于发送缓冲区大小16k,send()无法将70k数据全部拷贝进发送缓冲区,故先拷贝16k进入发送缓冲区,下层发送缓冲区中有数据要发送,内核开始发送。上层send()在应用层处于阻塞状态;
  2. 11号TCP包,发端从这儿开始向收端发送1448个字节的数据;
  3. 12号TCP包,发端没有收到之前发送的1448个数据的ACK包,仍然继续向收端发送1448个字节的数据;
  4. 13号TCP包,收端向发端发送1448字节的确认包,表明收端成功接收总共1448个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入1448字节。由于处于慢启动状态,win接收窗口持续增大,表明接受能力在增加,吞吐量持续上升;
  5. 14号TCP包,收端向发端发送2896字节的确认包,表明收端成功接收总共2896个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入2896字节。由于处于慢启动状态,win接收窗口持续增大,表明接受能力在增加,吞吐量持续上升;
  6. 15号TCP包,发端继续向收端发送1448个字节的数据;
  7. 16号TCP包,收端向发端发送4344字节的确认包,表明收端成功接收总共4344个字节。此时收端并未调用recv()读取,目前发送缓冲区中被压入4344字节。由于处于慢启动状态,win接收窗口持续增大,表明接受能力在增加,吞吐量持续上升;
  8. 从这儿开始,我略去很多包,过程类似上面过程。同时,由于不断的发送出去的数据被收端用ACK确认,发送缓冲区的空间被逐渐腾出空地,send()内部不断的把应用层buf中的数据向发送缓冲区拷贝,从而不断的发送,过程重复。70k数据并没有被完全送入内核,send()不管是否发送出去,send不管发送出去的是否被确认,send()只关心buf中的数据有没有被全部送往发送缓冲区。如果buf中的数据没有被全部送往发送缓冲区,send()在应用层阻塞,负责等待发送缓冲区中有空余空间的时候,逐步拷贝buf中的数据;如果buf中的数据被全部拷入发送缓冲区,send()立即返回。
  9. 经过慢启动阶段接收窗口增大到稳定阶段,TCP吞吐量升高到稳定阶段,收端一直处于sleep状态,没有调用recv()把内核中接收缓冲区中的数据拷贝到应用层去,此时收端的接收缓冲区中被压入大量数据;
  10. 66号、67号TCP数据包,发端继续向收端发送数据;
  11. 68号TCP数据包,收端发送ACK包确认接收到的数据,ACK=62265表明收端已经收到62264字节的数据,这些数据目前被压在收端的接收缓冲区中。win=3456,比较之前的16号TCP包的win=23296,表明收端的窗口已经处于收缩状态,收端的接收缓冲区中的数据迟迟未被应用层读走,导致接收缓冲区空间吃紧,故收缩窗口,控制发送端的发送量,进行流量控制;
  12. 69号、70号TCP数据包,发端在接收窗口允许的数据量的范围内,继续向收端发送2段1448字节长度的数据;
  13. 71号TCP数据包,至此,收端已经成功接收65160字节的数据,全部被压在接收缓冲区之中,接收窗口继续收缩,尺寸为1600字节;
  14. 72号TCP数据包,发端在接收窗口允许的数据量的范围内,继续向收端发送1448字节长度的数据;
  15. 73号TCP数据包,至此,收端已经成功接收66608字节的数据,全部被压在接收缓冲区之中,接收窗口继续收缩,尺寸为192字节。
  16. 74号TCP数据包,和我们这个例子没有关系,是别的应用发送的包;
  17. 75号TCP数据包,发端在接收窗口允许的数据量的范围内,向收端发送192字节长度的数据;
  18. 76号TCP数据包,至此,收端已经成功接收66800字节的数据,全部被压在接收缓冲区之中,win=0接收窗口关闭,接收缓冲区满,无法再接收任何数据;
  19. 77号、78号、79号TCP数据包,由keepalive触发的数据包,响应的ACK持有接收窗口的状态win=0,另外,ACK=66801表明接收端的接收缓冲区中积压了66800字节的数据。
  20. 从以上过程,我们应该熟悉了滑动窗口通告字段win所说明的问题,以及ACK确认数据等等。现在可得出一个结论,接收端的接收缓存尺寸应该是66800字节(此结论并非本篇主题)。
    send()要发送的数据是70k,现在发出去了66800字节,发送缓存中还有16k,应用层剩余要拷贝进内核的数据量是N=70k-66800-16k。接收端仍处于sleep状态,无法recv()数据,这将导致接收缓冲区一直处于积压满的状态,窗口会一直通告0(win=0)。发送端在这样的状态下彻底无法发送数据了,send()的剩余数据无法继续拷贝进内核的发送缓冲区,最终导致send()被阻塞在应用层;
  21. send()一直阻塞中。。。

图4和send()的关系说明完毕。

那什么时候send返回呢?有3种返回场景

send()返回场景

  • 场景1,我们继续图4这个例子,不过这儿开始我们就跳出图4所示的过程了
  1. 接收端sleep(1000)到时间了,进程被唤醒,代码片段如图5
    在这里插入图片描述
    图5

随着进程不断的用"recv(fd,buf,2048,0);"将数据从内核的接收缓冲区拷贝至应用层的buf,在使用win=0关闭接收窗口之后,现在接收缓冲区又逐渐恢复了缓存的能力,这个条件下,收端会主动发送携带"win=n(n>0)"这样的ACK包去通告发送端接收窗口已打开;

  1. 发端收到携带"win=n(n>0)"这样的ACK包之后,开始继续在窗口运行的数据量范围内发送数据。发送缓冲区的数据被发出;
  2. 收端继续接收数据,并用ACK确认这些数据;
  3. 发端收到ACK,可以清理出一些发送缓冲区空间,应用层send()的剩余数据又可以被不断的拷贝进内核的发送缓冲区;
  4. 不断重复以上发送过程;
  5. send()的70k数据全部进入内核,send()成功返回。
  • 场景2,我们继续图4这个例子,不过这儿开始我们就跳出图4所示的过程了
  1. 收端进程或者socket出现问题,给发端发送一个RST;
  2. 内核收到RST,send返回-1。
  • 场景3,和以上例子没关系
    连接上之后,马上send(1k),这样,发送的数据肯定可以一次拷贝进入发送缓冲区,send()拷贝完数据立即成功返回。

send()发送结论

send()只是负责拷贝,拷贝完立即返回,不会等待发送和发送之后的ACK。如果socket出现问题,RST包被反馈回来。在RST包返回之时,如果send()还没有把数据全部放入内核或者发送出去,那么send()返回-1,errno被置错误值;如果RST包返回之时,send()已经返回,那么RST导致的错误会在下一次send()或者recv()调用的时候被立即返回。

概念上容易疑惑的地方

  1. TCP协议本身是为了保证可靠传输,并不等于应用程序用tcp发送数据就一定是可靠的,必须要容错;
  2. send()和recv()没有固定的对应关系,不定数目的send()可以触发不定数目的recv(),这话不专业,但是还是必须说一下,初学者容易疑惑;
  3. 关键点,send()只负责拷贝,拷贝到内核就返回,我通篇在说拷贝完返回,很多文章中说send()在成功发送数据后返回,成功发送是说发出去的东西被ACK确认过send()只拷贝,不会等ACK
  4. 此次send()调用所触发的程序错误,可能会在本次返回,也可能在下次调用网络IO函数的时候被返回。

实际上理解了阻塞式的,就能理解非阻塞的。

参考linux下非阻塞的tcp研究

在阻塞模式下,send函数的过程是将应用程序请求发送的数据拷贝到内核发送缓存中,待发送数据完全被拷贝到内核发送缓存区中才返回,当然如果内核发送缓存区一直没有空间能容纳待发送的数据,则一直阻塞;

在非阻塞模式下,send函数的过程也是将应用程序请求发送的数据拷贝内核发送缓存中,区别在于非堵塞模式下,send函数不需要等到待发送数据完全被拷贝到内核发送区中才返回。
如果内核缓存区可用空间不够容纳所有待发送数据,则尽能力的拷贝,返回成功拷贝的大小;
如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.

send函数

参考linux下非阻塞的tcp研究

注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是底层TCP/IP协议栈传的,send仅仅是把用户buf中的数据copy到s的发送缓冲区的剩余空间里

在阻塞模式下,send函数的过程是将应用程序请求发送的数据拷贝到内核发送缓存中,待发送数据完全被拷贝到内核发送缓存区中才返回,当然如果内核发送缓存区一直没有空间能容纳待发送的数据,则一直阻塞;

在非阻塞模式下,send函数的过程也是将应用程序请求发送的数据拷贝内核发送缓存中,区别在于非堵塞模式下,send函数不需要等到待发送数据完全被拷贝到内核发送区中才返回。
如果内核缓存区可用空间不够容纳所有待发送数据,则尽能力的拷贝,返回成功拷贝的大小;
如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.

int send( SOCKET s, const char FAR *buf, int len, int flags );  

该函数的:
第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。

堵塞模式下socket的send函数的执行流程:
1.如果内核发送缓冲区可用大小为0,send()直接堵塞。。。,直到内核发送缓冲区里的数据被系统发送后,腾出空间后,send()再将剩余的待发送数据拷贝到内核发送缓冲区中去;

2.如果内核发送缓冲区可用空间小于待发送的数据长度len,则send()函数会先把部分数据拷贝到内核发送缓冲区中,然后会阻塞。。。,直到内核发送缓冲区里的数据被系统发送后,腾出空间后,send()再将剩余的待发送数据拷贝到内核发送缓冲区中去;

不知道为啥百度百科上send()是 “send先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;”
明显感觉描述不对,按这样说,那不就无法发送大于socket内核发送缓冲区的长度的数据了

可以看看TCP之深入浅出send和recv中的抓包实验,可知待发送数据大于socket的内核缓冲区大小时,也是可以发送的,没有返回SOCKET_ERROR。
也可以看看这里socket之send与发送缓冲区大小的关系

可知,待发送数据的长度大于s的内核发送缓冲区的长度时,会先将s(发送端)的内核发送缓冲区填满,然后发送端会将内核发送缓冲区的数据发送到接收端socket的内核接收缓冲区,所以s(发送端)的内核发送缓冲区又会慢慢腾出空间,send又会将待发送数据往s(发送端)的内核发送缓冲区中copy,

极端情况就是s(发送端)的内核发送缓冲区填满,接收端socket的内核接收缓冲区也被填满,但是send待发送的数据还是没发完,此时会堵塞。。。,等待s(发送端)的内核发送缓冲区产生空闲内存,

3.如果内核发送缓冲区可用空间大于待发送的数据长度len,send()函数直接将待发送数据完全拷贝到内核的发送缓冲区,然后成功返回。

要注意send()函数把待发送数据完全拷贝到s的内核发送缓冲区中之后,它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。

注意:在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

Send函数的返回值有三类:
(1)返回值=0:
(2)返回值<0:发送失败,错误原因存于全局变量errno中
(3)返回值>0:表示发送的字节数(实际上是拷贝到发送缓冲中的字节数)

错误代码:
EBADF 参数s 非合法的socket处理代码。
EFAULT 参数中有一指针指向无法存取的内存空间
ENOTSOCK 参数s为一文件描述词,非socket。
EINTR 被信号所中断。
EAGAIN 此操作会令进程阻断,但参数s的socket为不可阻断。
ENOBUFS 系统的缓冲内存不足
ENOMEM 核心内存不足
EINVAL 传给系统调用的参数不正确。

recv函数

recv函数把内核接收接收缓冲中的数据copy到buf

在阻塞模式下,send函数的过程是将应用程序请求发送的数据拷贝到内核发送缓存中,待发送数据完全被拷贝到内核发送缓存区中才返回,当然如果内核发送缓存区一直没有空间能容纳待发送的数据,则一直阻塞;

在非阻塞模式下,send函数的过程也是将应用程序请求发送的数据拷贝内核发送缓存中,区别在于非堵塞模式下,send函数不需要等到待发送数据完全被拷贝到内核发送区中才返回。
如果内核缓存区可用空间不够容纳所有待发送数据,则尽能力的拷贝,返回成功拷贝的大小;
如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.

int recv( SOCKET s,char FAR *buf,int len, int flags);   

该函数的:
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。

堵塞socket的recv函数的执行流程:

下面的描述正确性无法保证,反正网上都是这个说的,暂且看看吧

当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,

如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。

当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的

recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

注意:在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

  • 默认情况下socket是阻塞的。
    阻塞与非阻塞recv返回值没有区别,都是:
    <0 出错
    =0 对方调用了close API来关闭连接
    > 0 接收到的数据大小,

特别地:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。

只是阻塞模式下recv会一直阻塞直到接收到数据,非阻塞模式下如果没有数据就会返回,不会阻塞着读,因此需要循环读取)。

返回说明:
(1)成功执行时,返回接收到的字节数。
(2)若另一端已关闭连接则返回0,这种关闭是对方主动且正常的关闭
(3)失败返回-1,errno被设为以下的某个值

EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字

  相关解决方案