« 上一篇下一篇 »

阻塞与非阻塞socket在数据往返间的区别,在缓冲区如何运转

     最近在网络上看到一些帖子以及回复,同时又搜索了一些网络上关于阻塞非阻塞区别的描述,发现很多人在描述两者的发送接收时操作返回以及缓冲区处理的区别时有不同程度的误解。所以我想写一篇文章来纠正错误,并作为记录方便查阅。

  首先socket在默认情况下是阻塞状态的(未指异步操作以及其它一些特殊用途下,直接默认为非阻塞),这就使得发送以及接收操作处于阻塞的状态,即调用不会立即返回,而是进入睡眠等待操作完成。下面把讨论点分为发送以及接收。

  一.发送选用send(这里特指TCP)以及sendto(这里特指UDP)来描述

    首先需要说明的是,不管阻塞还是非阻塞,在发送时都会将数据从应用缓冲区拷贝到内核缓冲区(SO_RCVBUF选项声明,除非缓冲区大小为0)。我在网络上看到某些人说,阻塞就是将数据真正发送给对方,并且阻塞是发生在需要把前面的所有数据全部发送出去,然后再发送本次的,而非阻塞则是拷贝到发送缓冲区。我不得不说,上面的这种说法是错误的。

    在阻塞模式下send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。

    如果当前发送缓冲总大小为8192,已经拷贝到缓冲的数据为8000,那剩余的大小为192,现在需要发送2000字节数据,那阻塞发送就会等待缓冲区足够把所有2000字节数据拷贝进去,如第一次拷贝进192字节,当缓冲区成功发送出1808字节后,再把应用缓冲区剩余的1808字节拷贝到内核缓冲,而后send操作返回成功发送字节数。

    从上面的过程不难看出,阻塞的send操作返回的发送大小,必然是你参数中的发送长度的大小。

man 一下 send,发现man里有描述容易误导,send() shall fail.应该是需要自己判断的,会返回实际发送成功的字节,想全部成功发送,需要自己实现一遍阻塞循环发送的逻辑,当多端口*大包发送的时候,可以非阻塞拆包发送(循环每个端口send一次小包,避免单个端口阻塞很久),效果会比阻塞的好很多。

关于拆分大包的一些实施方法,(一般是图片和视频),有人在应用层分片,针对50K的默认发送缓冲区和4k以上的滑动窗口来做文章,动态调整每次send的size在4K到50k间浮动,来减少发包的分片,通过减少分片重组的次数来优化网络服务的总延时。至于MSS一般是1460字节,为何选4k,而不是1.4k起,是因为这会造成多次RTT消耗以及用户态和内核态切换,得不偿失。

If space is not available at the sending socket to hold the message to be transmitted, and the socket file descriptor does have O_NONBLOCK set, send() shall fail.

    在阻塞模式下的sendto操作不会阻塞。

    关于这一点的原因在于:UDP并没有真正的发送缓冲区,它所做的只是将应用缓冲区拷贝给下层协议栈,在此过程中加上UDP头,IP头,所以实际不存在阻塞。

    在非阻塞模式下send操作调用会立即返回。

    关于立即返回大家都不会有异议。还是拿阻塞send的那个例子来看,当缓冲区只有192字节,但是却需要发送2000字节时,此时调用立即返回,并得到返回值为192。从中可以看到,非阻塞send仅仅是尽自己的能力向缓冲区拷贝尽可能多的数据,因此在非阻塞下send才有可能返回比你参数中的发送长度小的值。

    如果缓冲区没有任何空间时呢?这时肯定也是立即返回,但是你会得到WSAEWOULDBLOCK/E WOULDBLOCK 的错误,此时表示你无法拷贝任何数据到缓冲区,你最好休息一下再尝试发送。

    在非阻塞模式下sendto操作 不会阻塞(与阻塞一致,不作说明)。

 

  二.接收选用recv(这里特指TCP)以及recvfrom(这里特指UDP)来描述

    在阻塞模式下recv,recvfrom操作将会阻塞 到缓冲区里有至少一个字节(TCP)或者一个完整UDP数据报才返回。

    在没有数据到来时,对它们的调用都将处于睡眠状态,不会返回。

    在非阻塞模式下recv,recvfrom操作将会立即返回。

    如果缓冲区 有任何一个字节数据(TCP)或者一个完整UDP数据报,它们将会返回接收到的数据大小。而如果没有任何数据则返回错误 WSAEWOULDBLOCK/E WOULDBLOCK。

 

  以上是关于阻塞非阻塞发送接收的区别以及在缓冲区处理上的差别,希望给看到这篇文章的人一些帮助。同时也想纠正网络上的某些错误观点,文章中表述如有错误,望大家指正,谢谢。

 

三、Socket 的模式大概分为这么几种:
1、阻塞式的,Socket操作都需要将线程挂起,等待内核完成后才能返回。
如: 调用connect=>进入内核=>Syn包=〉服务器返回SYN ACK 包=〉connect返回。
=〉ACK包发往服务器。
但一般来说,阻塞和非阻塞对于recv来说意义更大。
当在阻塞式的Socket上调用recv时,如果这时网络栈上没有数据给你接收,那么这时线程将
会挂起,直到有报文给你接收才返回。
这样就造成你的应用程序在企图接收数据时候,而网络栈上没有数据的时候就会被锁住。
有什么办法解决这个问题呢? 我们来介绍IO
2、 IO复用, 就是在企图读写数据的时候先询问下是否可读写,如果不能,可以去干别的事情,不会造成死锁。
但是假如我们有大量的连接需要去频繁的查询可读写状态,每次查询都会和内核交互。这样会造成
效率低下。再介绍一种
3、 重叠IO. 就是一次查询多个Socket的状态。不用去来来回回的遍历。
另外,
  在windows socket api 中还有一种消息机制,就是把Socket状态通知到窗口。然后用消息去处理。
  对于重叠IO, 在windows上还有完成端口模型,他和重叠端口相比,不但能捕捉到IO事件, 而且内核已经替你完成了Socket IO, 比如read事件, 在内核通知你的时候,他已经帮你读好数据了,并放在你指定的缓存中(这里是指在用户态下,事先为每个Socket分配的内存)。
为什么有这么多socket模式呢? 哪个更好呢?
为什么有,我不知道,可能是出于需求吧,
说说哪个更好?
  孤立的来说,其实没有哪个更好? 只有哪个更适合你的应用应用环境。
如: 阻塞式的比较简单,方便,稳定。适合比较简单的客户端程序。
IO复用我认为它适合SocketIO操作比较少的情况。
重叠IO就适合高性能的服务器的开发,另外完成端口是windows上比较公认的高性能服务器的网络开发模型。当然, windows 的IOCP也有个坏处,就是需要大量的内存,应为前面说了他需要事先指定缓存。不过高性能的 服务器,一般都不用windows平台。
windows的消息模型就比较适合有UI的应用程序。
当然, 有些模型的选择上可能还有个人爱好的因素,
如, 我可能不喜欢用消息模型,
我不喜欢被动的被通知, 而喜欢主动的去查询。