代码执行send成功后,数据就发出去了吗?
回答这个问题之前,需要了解什么是Socket 缓冲区。
Socket 缓冲区
什么是 socket 缓冲区
编程的时候,如果要跟某个IP建立连接,我们需要调用操作系统提供的 socket API。
socket 在操作系统层面,可以理解为一个文件。
我们可以对这个文件进行一些方法操作。
用listen方法,可以让程序作为服务器监听其他客户端的连接。
用connect,可以作为客户端连接服务器。
用send或write可以发送数据,recv或read可以接收数据。
在建立好连接之后,这个 socket 文件就像是远端机器的 "代理人" 一样。比如,如果我们想给远端服务发点什么东西,那就只需要对这个文件执行写操作就行了。
那写到了这个文件之后,剩下的发送工作自然就是由操作系统内核来完成了。
既然是写给操作系统,那操作系统就需要提供一个地方给用户写。同理,接收消息也是一样。
这个地方就是 socket 缓冲区。
用户发送消息的时候写给 send buffer(发送缓冲区)
用户接收消息的时候写给 recv buffer(接收缓冲区)
也就是说一个socket ,会带有两个缓冲区,一个用于发送,一个用于接收。因为这是个先进先出的结构,有时候也叫它们发送、接收队列。
一个socket有两个缓冲区
怎么观察 socket 缓冲区
如果想要查看 socket 缓冲区,可以在linux环境下执行 netstat -nt 命令。
# netstat -nt
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 60 172.22.66.69:22 122.14.220.252:59889 ESTABLISHED
这上面表明了,这里有一个协议(Proto)类型为 TCP 的连接,同时还有本地(Local Address)和远端(Foreign Address)的IP信息,状态(State)是已连接。
还有Send-Q 是发送缓冲区,下面的数字60是指,当前还有60 Byte在发送缓冲区中未发送。而 Recv-Q 代表接收缓冲区, 此时是空的,数据都被应用进程接收干净了。
TCP部分
我们在使用TCP建立连接之后,一般会使用 send 发送数据。
执行 send 发送的字节,会立马发送吗?
答案是不确定!执行 send 之后,数据只是拷贝到了socket 缓冲区。至 什么时候会发数据,发多少数据,全听操作系统安排。
tcp_sendmsg 逻辑
在用户进程中,程序通过操作 socket 会从用户态进入内核态,而 send方法会将数据一路传到传输层。在识别到是 TCP协议后,会调用 tcp_sendmsg 方法。
在 tcp_sendmsg 中, 核心工作就是将待发送的数据组织按照先后顺序放入到发送缓冲区中, 然后根据实际情况(比如拥塞窗口等)判断是否要发数据。如果不发送数据,那么此时直接返回。
如果缓冲区满了会怎么办
前面提到的情况里是,发送缓冲区有足够的空间,可以用于拷贝待发送数据。
如果发送缓冲区空间不足,或者满了,执行发送,会怎么样?
这里分两种情况。
首先,socket在创建的时候,是可以设置是阻塞的还是非阻塞的。
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
比如通过上面的代码,就可以将 socket 设置为非阻塞 (SOCK_NONBLOCK)。
当发送缓冲区满了,如果还向socket执行send
- 如果此时 socket 是阻塞的,那么程序会在那干等、死等,直到释放出新的缓存空间,就继续把数据拷进去,然后返回。
- 如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息,意思是 Try again , 现在缓冲区满了,你也别等了,待会再试一次。
socket 是阻塞的
socket 是非阻塞的
接收缓冲区
如果接收缓冲区为空,执行 recv 会怎么样?
接收缓冲区也是类似的情况。
当接收缓冲区为空,如果还向socket执行 recv
- 如果此时 socket 是阻塞的,那么程序会在那干等,直到接收缓冲区有数据,就会把数据从接收缓冲区拷贝到用户缓冲区,然后返回。
- 如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息。
recv阻塞
recv非阻塞
socket读写缓冲区满了的情况汇总
如果socket缓冲区还有数据,执行close了,会怎么样?
首先我们要知道,一般正常情况下,发送缓冲区和接收缓冲区 都应该是空的。
如果发送、接收缓冲区长时间非空,说明有数据堆积,这往往是由于一些网络问题或用户应用层问题,导致数据没有正常处理。
那么正常情况下,如果 socket 缓冲区为空,执行 close。就会触发四次挥手。
recvbuf非空
如果接收缓冲区有数据时,执行close了,会怎么样?
先说结论,关闭过程主要有两种情况:
- 如果接收缓冲区还有数据未读,会先把接收缓冲区的数据清空,然后给对端发一个RST。
- 如果接收缓冲区是空的,那么就调用 tcp_send_fin() 开始进行四次挥手过程的第一次挥手。
如果发送缓冲区有数据时,执行close了,会怎么样?
此时,还有些数据没发出去,内核会把发送缓冲区最后一个数据块拿出来。然后置为 FIN。
socket 缓冲区是个先进先出的队列,这种情况是指内核会等待TCP层安静把发送缓冲区数据都发完,最后再执行 四次挥手的第一次挥手(FIN包)。
有一点需要注意的是,只有在接收缓冲区为空的前提下,我们才有可能走到 tcp_send_fin() 。而只有在进入了这个方法之后,我们才有可能考虑发送缓冲区是否为空的场景。
sendbuf 非空
UDP部分
UDP也有缓冲区吗
UDP socket 也是 socket,一个socket 就是会有收和发两个缓冲区。跟用什么协议关系不大。
有没有是一回事,用不用又是一回事。
UDP不用发送缓冲区?
事实上,UDP不仅有发送缓冲区,也用发送缓冲区。
一般正常情况下,会把数据直接拷到发送缓冲区后直接发送。
还有一种情况,是在发送数据的时候,设置一个 MSG_MORE 的标记。
ssize_t send(int sock, const void *buf, size_t len, int flags); // flag 置为 MSG_MORE
大概的意思是告诉内核,待会还有其他更多消息要一起发,先别着急发出去。此时内核就会把这份数据先用发送缓冲区缓存起来,待会应用层说ok了,再一起发。
因此,不管是不是 MSG_MORE, IP都会先把数据放到发送队列中,然后根据实际情况再考虑是不是立刻发送。
而我们大部分情况下,都不会用 MSG_MORE,也就是来一个数据包就直接发一个数据包。从这个行为上来说,虽然UDP用上了发送缓冲区,但实际上并没有起到"缓冲"的作用。