参考:
TCP之深入浅出send和recv
linux下非阻塞的tcp研究
题外话
今天在看epoll的ET模式时,说ET模式时,套接字描述符必须设置成非堵塞模式,为什么 IO 多路复用要搭配非阻塞 IO?
于是想看看堵塞和非堵塞recv/send的区别,网上鱼龙混杂的博文,错误百出,查了好久,在此做个总结,如有错误的地方,希望大家指出来。
关于阻塞和非阻塞read/write,可以参考阻塞和非阻塞read/write
-------------------------------------这部分转载于TCP之深入浅出send和recv
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
接收窗口是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负载》。
实例功能说明:接收端129作为客户端去连接发送端130,连接上之后并不调用recv()接收,而是sleep(1000),把进程暂停下来,不让进程接收数据。内核会缓存数据至接收缓冲区。发送端作为服务器接收TCP请求之后,立即用ret = send(sock,buf,70k,0);这个C语句,向接收端发送70k数据。
我们现在来观察这个过程。看看究竟发生了些什么事。wireshark抓包截图如下图4
图4说明,包序号等同于时序
图4和send()的关系说明完毕。
那什么时候send返回呢?有3种返回场景
send()返回场景
随着进程不断的用"recv(fd,buf,2048,0);"将数据从内核的接收缓冲区拷贝至应用层的buf,在使用win=0关闭接收窗口之后,现在接收缓冲区又逐渐恢复了缓存的能力,这个条件下,收端会主动发送携带"win=n(n>0)"这样的ACK包去通告发送端接收窗口已打开;
send()发送结论
send()只是负责拷贝,拷贝完立即返回,不会等待发送和发送之后的ACK。如果socket出现问题,RST包被反馈回来。在RST包返回之时,如果send()还没有把数据全部放入内核或者发送出去,那么send()返回-1,errno被置错误值;如果RST包返回之时,send()已经返回,那么RST导致的错误会在下一次send()或者recv()调用的时候被立即返回。
概念上容易疑惑的地方
实际上理解了阻塞式的,就能理解非阻塞的。
参考linux下非阻塞的tcp研究
在阻塞模式下,send函数的过程是将应用程序请求发送的数据拷贝到内核发送缓存中,待发送数据完全被拷贝到内核发送缓存区中才返回,当然如果内核发送缓存区一直没有空间能容纳待发送的数据,则一直阻塞;
在非阻塞模式下,send函数的过程也是将应用程序请求发送的数据拷贝内核发送缓存中,区别在于非堵塞模式下,send函数不需要等到待发送数据完全被拷贝到内核发送区中才返回。
如果内核缓存区可用空间不够容纳所有待发送数据,则尽能力的拷贝,返回成功拷贝的大小;
如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.
-------------------------------------------------以下部分为个人思考总结所得
参考linux下非阻塞的tcp研究
注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是底层TCP/IP协议栈传的,send仅仅是把用户buf中的数据copy到s的发送缓冲区的剩余空间里
在阻塞模式下,send函数的过程是将应用程序请求发送的数据拷贝到内核发送缓存中,待发送数据完全被拷贝到内核发送缓存区中才返回,当然如果内核发送缓存区一直没有空间能容纳待发送的数据,则一直阻塞;
在非阻塞模式下,send函数的过程也是将应用程序请求发送的数据拷贝内核发送缓存中,区别在于非堵塞模式下,send函数不需要等到待发送数据完全被拷贝到内核发送区中才返回。
如果内核缓存区可用空间不够容纳所有待发送数据,则尽能力的拷贝,返回成功拷贝的大小;
如果缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.
看看官方手册中的描述https://linux.die.net/man/2/send,摘抄如下
If the message is too long to pass atomically through the underlying protocol, the error EMSGSIZE is returned, and the message is not transmitted.
No indication of failure to deliver is implicit in a send(). Locally detected errors are indicated by a >return value of -1.
When the message does not fit into the send buffer of the socket, send() normally blocks, unless the socket has been placed in nonblocking I/O mode. In nonblocking mode it would fail with the error EAGAIN or EWOULDBLOCK in this case. The select(2) call may be used to determine when it is possible to send more data.
在看看《UNIX网络编程卷1》第 16 章 非阻塞式 I/O中的描述,如下
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内核发送缓冲区的长度的数据了,可以看看socket内核发送缓冲区的默认大小
cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4161536
表明socket内核发送缓冲区默认大小为16kB,那发送大于16kB的数据怎么办呢?所以这里明显有问题
官方手册的原话是
If the message is too long to pass atomically through the underlying protocol, the error EMSGSIZE is returned, and the message is not transmitted.可以看看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函数把内核接收缓冲中的数据copy到buf
int recv( SOCKET s,char FAR *buf,int len, int flags);
具体的看看https://linux.die.net/man/2/recv
有一段话摘录在此
If no messages are available at the socket, the receive calls wait for a message to arrive, unless the socket is nonblocking (see fcntl(2)), in which case the value -1 is returned and the external variable errno is set to EAGAIN or EWOULDBLOCK.
如果内核接收缓冲区内没有数据可读,则recv会堵塞,直到有数据到达,然后返回;
如果内核接收缓冲区内没有数据可读,但是recv设置为非堵塞,那么recv会返回-1,同时将errno置为EAGAIN or EWOULDBLOCK.
The receive calls normally return any data available, up to the requested amount, rather than waiting for receipt of the full amount requested.
recv返回值为读取到的字节数(这个数是内核接收缓冲区中可读取的字节数,可能是1个字节或者某个字节),最大可为buf区的大小len。recv并不是要等到读取完len个字节才返回。
posix 系统上,
在阻塞模式下
如果内核缓冲区没有数据可读, recv ()会阻塞,直到有一些数据存在可以读取为止。然后,它将返回读取到的数据(可能少于请求的数量len) ,返回的数最大为len。
在非阻塞模式下
如果内核缓冲区没有数据可读,recv 将立即返回 -1,设置 errno 为 EAGAIN 或 ewoudblock。
所以,通常在循环中调用 recv,直到得到所需的量,同时检查返回码是0(另一端断开)还是 -1(一些错误)。
在看看《UNIX网络编程卷1》第 16 章 非阻塞式 I/O中的描述,如下
int recv( SOCKET s,char FAR *buf,int len, int flags);
该函数的:
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
堵塞socket的recv函数的执行流程:
来自https://baike.baidu.com/item/recv%28%29
下面的描述正确性还不确定,反正网上都是这么说的,暂且看看吧
当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,**
recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。**个人质疑:直到协议把数据接收完毕是什么意思?TCP是数据流,那什么叫做完毕呢?
假如,接收端套接字的接收缓冲区为16kB大小,发送端套接字一次发送8kB数据过来,肯定需要一点时间后,这8KB的数据才能完全到达接收端套接字的接收缓冲区,那接收端recv是一看到接收缓冲区有一点数据就返回?还是等接收缓冲区有8KB数据之后才返回?那如果发送端套接字发送完8kB数据后,立马又发送8kB数据呢?
所以接收端套接字的recv根本不知道发送端什么时候才把数据发送完毕,因为发送端可以想发多少发多少
看看《UNIX网络编程卷1》第 16 章 非阻塞式 I/O中的描述中的原话
可以知道正确的描述应该是:调用recv时,若该套接字的内核接收缓冲区没有数据可读,recv会堵塞(注意,这里前提是在堵塞模式下),直到有一些数据到达,这一些数据可能是单个字节,也可能是一个完整的TCP分节中的数据,recv就会被唤醒,然后返回copy到的字节数。(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)
recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
注意:在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
特别地:返回值<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索引的不是套接字