下面使用 echo.cc、echo_client.cc 演示一个阻塞IO实验。
运行
- 启动服务器
./echo
- 启动客户端
./echo_client localhost 1024
阻塞现象
-
当运行
./echo_client localhost 20240000
,阻塞掉 -
在执行 netstat 命令后可以看到它们各自的收发队列的情况
netstat -tpn | grep '3007\|^[AP]'
服务器的接收队列上有 6103028 字节的数据,发送队列上 2605368 字节的数据,存在缓冲区里;
客户端同理
阻塞原因
-
当接收端(echo服务端)不接收数据,或者处理速率比发送方的发送速率低导致其接收缓冲区已满(接收窗口win=0),进而导致数据发送方的发送缓冲区的数据不断堆积进而缓冲区满,此时调用send()将阻塞等待(send等待发送缓冲区有空间,系统在等待对端接收缓冲区有空间以便将发送缓冲区的数据通过网卡发到网络)
TCP协议的原理 引用:TCP Send函数的阻塞和非阻塞,以及TCP发送数据的异常情况
- 每个Tcp socket连接在内核(kernal space)中都有一个发送缓冲区和接收缓冲区)
阻塞模式与非阻塞模式:
- 在阻塞模式下, send函数的过程是将应用程序请求发送的数据拷贝到发送缓存中发送就返回.但由于发送缓存的存在,表现为:如果发送缓存大小比请求发送的大小要大,那么send函数立即返回,同时向网络中发送数据;否则,send会等待接收端对之前发送数据的确认,以便腾出缓存空间容纳新的待发送数据,再返回(接收端协议栈只要将数据收到接收缓存中,就会确认,并不一定要等待应用程序调用recv),如果一直没有空间能容纳待发送的数据,则一直阻塞;
- 在非阻塞模式下,send函数的过程仅仅是将数据拷贝到协议栈的缓存区而已,如果缓存区可用空间不够,则尽能力的拷贝,立即返回成功拷贝的大小;如缓存区可用空间为0,则返回-1,同时设置errno为EAGAIN.
socket缓冲区 引用:谈谈socket缓冲区
- socket缓冲区在每个套接字中单独存在;
- socket缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送发送缓冲区中遗留的数据;
- 关闭套接字将丢失接收缓冲区中的数据
- 每个Tcp socket连接在内核(kernal space)中都有一个发送缓冲区和接收缓冲区)
-
在阻塞IO中,send()调用时,如果发送缓冲区没有足够的空间容纳数据,就会发生阻塞。而恰恰服务段(echo)与客户端(echo_client)都处于这样一种发送阻塞的状态。
-
而以上过程发生阻塞的具体过程为:
C(客户端):客户端send() 大量数据堆积在发送缓冲区,而对端没有及时的读取导致数据堆积,致使发送缓冲区可用空间不足,send() 发生阻塞。S(服务端):服务端recv() 之后立刻发送数据到对端,但由于对端流程处于send()阻塞状态 ,无法及时recv(),导致服务端也阻塞在 send() 处。
-
这里tcp默认的接收缓冲区为 6M 左右,因此在 echo 的接收缓冲区接近这个阈值时,再来数据就不收了。而由于服务端没有及时处理接收缓冲区的消息,使得客户端的发送缓冲区也满了。因此,客户端就阻塞在send()不再发送数据了
解决方法
- 在echo程序的设计上,服务端每次读4K数据,就向客户端发送一次消息,而客户端是一次发送完所有消息后,才接收服务端的消息。导致服务端向客户端发送的消息,没有及时的读取。
- 从程序的设计考虑,客户端发送一个20MB的数据是合法的,而服务端预先不知客户端请求数据的大小,采用了读取4K就响应一次的方式,最后导致的阻塞。因此我们在设计应用层协议时,应采取客户端先发送一个 header,告诉客户端请求的大小,然后服务端这边收到 header 后,准备一个符合请求大小的buffer,准备接收请求。然后客户端再发送请求至服务端,服务端这边接收到请求后,计算出一个响应,再按照相同的方式发送回客户端。