关于TCP阻塞和非阻塞方式的总结
1、引言
在网络编程时,我们需要注意创建的socket是阻塞状态还是非阻塞状态的,这两种状态在编程时,对于每一个socket系统调用函数(connect、send、sendto、recv、recvfrom)都有一定的影响,socket的阻塞和非阻塞状态可以在每一个socket系统调用调用之前设置,对应着每一个socket系统调用是否是阻塞和非阻塞的;
2、发送操作:
(1)对于TCP的send系统调用发送数据,如果socket是阻塞的,我们需要这样理解:send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。
—如果发送缓冲区可用大小为0或者比要发送的数据长度要小,则会阻塞,直到发送缓冲区里的数据被系统发送出去后,可用缓冲区大小比要发送的数据长度大时,send返回成功,否则一直阻塞等待。因此,send返回的发送大小,一定是参数中发送长度的大小。
—例如:如果当前发送缓冲区总大小为8192,个字节,通过send已经拷贝到缓冲区的数据为8000字节,那缓冲区剩余大小为192字节,而现在上层应用需要发送2000字节的数据,那么send就会等待缓冲区足够把所有2000字节数据拷贝进去,如第一次拷贝进去192字节,当缓冲区成功发送出1808字节后,再把上层应用buf中剩余的1808字节拷贝到发送缓冲区,最后send返回成功拷贝到发送缓冲区的字节数。
(2)对于UDP的sendto系统调用发送数据,如果socket是阻塞的,sendto操作不会阻塞,UDP没有真正意义上的发送缓冲区,它只是把应用层的缓冲区数据拷贝到下层的协议栈,在此过程中加UDP头,IP头,所以不存在阻塞。
—UDP是不可靠连接,不必保存应用进程的数据拷贝,应用进程中的数据在沿协议栈向下传递时,以某种形式拷贝到内核缓冲区,当数据链路层把数据传出后就把内核缓冲区中数据拷贝删除。因此它不需要一个发送缓冲区。写UDP套接口的sendto返回表示应用程序的数据或者数据分片已经进入链路层的输出队列,如果输出队列没有足够的空间存放数据,将返回错误EBOBUFS。
—UDP套接口有发送缓冲区大小(SO_SNDBUF修改),不过它仅仅是写到套接口的UDP数据报的大小上限,如果应用程序写一个大于套接口发送缓冲区大小的数据报,内核将返回一个EMSGSIZE错误。
(3)对于TCP的send系统调用,如果socket是非阻塞的,send会立即返回。
—例如,当发送缓冲区中有192字节,但是需要发送2000字节,此时send调用立即返回,并且返回值为192。因此,非阻塞send仅仅是尽自己的能力向发送缓冲区拷贝尽可能多的数据。如果发送缓冲区剩余空间为0,这时send立即返回,send返回值为EAGAIN,这是应用层最好休息一下再尝试发送。
(4)对于UDP的sendto系统调用发送数据,如果socket是非阻塞的,sendto操作不会阻塞,跟阻塞方式一致的。
3、接收操作:
(1)在阻塞socket模式下,recv/recvfrom会一直阻塞到接收缓冲区里有一个字节或者一个完整的UDP数据报为止,然后再返回。
—如果socket发送缓冲区中有数据,或者接收缓冲区中无数据,或者协议正在接收数据,socket都阻塞等待,直到有数据拷贝到用户程序中,然后返回拷贝的字节数。(协议接收到的数据长度可能大于buf的长度(recvbuf长度),需要调用几次recv函数才能把socket接收缓冲区中的数据拷贝完)。
(2)在非阻塞socket模式下,recv/recvfrom会立即返回。
—如果接收缓冲区有任何一个字节数据(TCP)或者一个完整的UDP数据报,它们将会返回接收到的数据大小;如果缓冲区没有数据,则直接返回错误EWOULDBLOCK或者EGAIN,表示没有数据,休息一会儿再次接收操作。
4、连接操作:
(1)TCP的连接需要进行一个三路握手,调用tcp connect函数一直要等到调用方收到对于自己的SYN(同步TCP请求)的ACK为止才返回,因此,如果当前socket是阻塞方式的话,TCP的connect总会阻塞其调用至少一个到服务器的RTT的时间才返回。
(2)如果对一个非阻塞的socket,调用TCP connect建立连接,并且连接不能立即建立,这时connect会返回一个EINPROGRESS的错误,表示连接正在建立中,这种情况,后续需要使用select函数在检查连接建立成功。(这里套接字是非阻塞的,如果连接的服务器在同一个主机上,我们的connect调用可能会使连接立即建立成功)