关于send时产生WSAEWOULDBLOCK问题

声明:原文看着有点费力,稍微整理了下:
http://bbs.csdn.net/topics/70198557

1. 关于send时产生WSAEWOULDBLOCK时的处理办法,得出如下结论:

  1. 产生这个错误只是说明out buffer已经满了,不代表出错.
  2. 可以等待FD_WRITE消息,此时将没有发送完成的数据再次发送出去.
  3. WSAEWOULDBLOCK错误意味着请求的操作在调用期间没有时间完成。

2. 问:

  1. 我上面的两条结论正确吗?
  2. 关于在FD_WRITE里把没有发送完的数据发送完,这一点如何做到呢?因为
   int nSended = send(mysock, myBuf, myLen, 0);
  • 如果此时产生WSAEWOULDBLOCK,那么nSended肯定是SOCKET_ERROR,也就是-1了.此时nSended不能代表已经发送成功的字节数,那么在myBuf这个缓冲区里,究竟有多少字节是已经发送出去的,有多少数据是没有发送出去的呢?
  • 在我的程序时,我想在得到WSAEWOULDBLOCK时,将没有发送完的数据缓冲起来,等到FD_WRITE时,再将这些数据发送出去.所以还请大侠们指点一下,在这种情况下,如何知道有多少数据是被发送了的.

3.答:

  • 要确定一点:send函数中的这个myBuf是应用程序提供的一个const类型的字符串(就以字符串为例吧),并不是所谓的“缓冲区”。我觉得其实不是缓冲区已满,而是发送窗口已满,缓冲区是系统动态分配的,一般来说是足够用的,不大容易出现缓冲区已满的现象。
  • 我觉得过程是这样的:send函数先把数据传给Socket核心处理模块,核心处理模块负责管理这些需要发送出去的数据的内存堆栈,然后试着将这些数据包交付协议层的发送窗口,如果窗口大小足够,则核心驱动模块将数据包完全交付给窗口去发送,并删除暂存在自己内存堆栈上的数据;如果窗口大小不够(导致这种情况的原因有很多,比如接收端接收缓存已满;至于这个“窗口大小不够”这个消息到底是核心驱动模块经自己判断生成的,还是由协议层告知核心驱动模块的,这点我也不大清楚,也想搞明白),则将能交付的数据尽最大量交给窗口去发送,并将无法发送出去的那些数据暂存在核心处理模块的内存堆栈上,并同时向上给send函数发送一个发送窗口已满的消息,然后send函数便处于BLOCK状态。
  • 可以确定的是,send函数不会直接和协议层打交道,而是通过核心驱动模块来和协议层交互。另外,我觉得知道多少数据被发送是没有意义的,因为一旦将数据通过发送窗口发送出去,核心驱动模块就会将剩余的数据马上交付空的发送窗口,这个时间非常短,等你得到了发送数据的大小时说不定你要发送的所有数据已经发送出去了。

4.问:

不过还有一个问题,很简单的一个程序,如下:

   for(int i=0; i<=1000; i++)
   {
      int nSended = send(mySocket, myBuf, myLen, 0);
   }

按照你的说法,send函数把myBuf这个字节数组上的内容交给了核心处理模块,那么,如果i==500的时候,nSended变成了SOCKET_ERROR,且WSAGetLastError() == WSAEWOULDBLOCK,而此时这个XUN环并没有判断,还会进行接下来的500个循环,那是不是接下来的500个循环中,send()也都把数据交给核心处理模块了吗?那如果这个循环接下来还有50000个呢?”核心处理模块的内存堆栈”有这么大吗?
一旦WSAEWOULDBLOCK产生,那么接下来的send函数是如何动作的呢?是把待发送的数据放到”核心处理模块的内存堆栈”上了呢?还是其它什么动作呢?

5.答:

  • 后面的500个send都出一样的错。WSAGetLastError() == WSAEWOULDBLOCK
  • 这个问题就要搞清楚TIMEOUT和BLOCK的区别了。表面看上去这两个选项似乎没什么差别,都是暂停send函数的运行,其实里面有很大的差别。
  • 先说说TIMEOUT状态下的一种情形:首先,send函数将数据交付给核心驱动模块,后者将其暂存于它“掌管”的内存里,然后再将这些数据交付给发送窗口去发送。我们现在只考虑发送窗口大小不足的情况,一旦出现这种情况,send函数便处于TIMEOUT状态(仍然是依靠下层的核心驱动模块给它消息)。这种状态是这样的:如果设置了循环发送,每次send函数都会正常运行,也就是说它仍在不停地往核心驱动模块的缓存里写数据,此时所谓的TIMEOUT是指核心驱动模块的缓存在等待(因为发送窗口已满,所以核心驱动模块内存中的数据必须呆在那里等待发送),而不是send在等待,send的TIMEOUT状态只是映射了核心驱动模块内存的一个状态而已。关于这点,也是我测试时明白的,当接收端将断点设在recv函数上时,通过查看发送端的任务管理器,发现发送进程所占用的内存越来越大,这就说明send函数是在不停地往发送缓存里写数据。我发的一篇问题讨论贴里说了这种情况,不过我的问题重点不在这儿,就不详细说明了。另外,这种情况不丢包,等接收端的程序跳过断点后,send函数所要发送的数据全部到达了接收端,这就说明send函数没有发生错误。
    再说说BLOCK时的情况,我想说过TIMEOUT后,BLOCK的情况就很容易推断出来了吧?此时,send函数才是真正地停止了运行,just stop!它会停止继续向发送缓存里写数据, 以后每次调用它时(因为有循环)都会发出警告说“WSAEWOULDBLOCK”,正因为send函数不得不停下来,所以“错”才称其为“错”。相对的,上面那种情况下的TIMEOUT并不是错,它属于正常情况。

6.问:

虽然我没有从”发送窗口”和”out buffer”等这个层次去考虑send的执行原理,但是你讲的跟我的理解,还有我试验下来的结果一致,不像其它人讲的,一旦发生WSAEWOULDBLOCK时,数据是否发送出去已经处于不可预测的状态.
不过我发这个贴子是为了解决一个实际的问题,即是,如果我需要不停地发送数据(就像我上面的那个循环),当其中一个send返回SOCKET_ERROR,且GetLastError()==WSAEWOULDBLOCK时,我们采用什么方法比较合理.
当前可以像其它人讲的,每个人有每个人的做法,这种答案不能给任何人以任何帮助.
我的解决办法是这样的:
1.首次发生WSAEWOULDBLOCK时,应该记录下此时的状态,从此时起,就不应该再调用send函数了,而是将待发送的数据以自己的方式存储起来.
2.等待FD_WRITE消息产生时,依次发送自己缓存过的数据,如果所有的数据均发送完,则说明下次可以继续调用send来发送数据了.
3.首次发生WSAEWOULDBLOCK时的那次send调用,其要发送的数据已经全部被发送出去了,该次发送的数据不需要缓存.

7.答:

说明我的想法之前,我先纠正一下上面我犯的几个错误,也许说明这些错误对楼主也有帮助。
我的犯的最大错误是,没有搞清楚“模式”和“选项”,BLOCK是一种模式,而TIMEOUT只是一个在特定模式下的选项。
* 先说模式,一般SOCKET默认是BLOCK(阻断)模式,这种模式对send函数来说是同步的,而NONBLOCK(非阻断)模式对send函数来说则是异步的。

*楼主说send函数返回的错误是WSAEWOULDBLOCK,那么即是说,将SOCKET模式设置成了NONBLOCK,因为WSAEWOULDBLOCK在文档里是这么定义的:The socket is marked as nonblocking and the requested operation would block. 这句是说,socket被设置成了非阻断模式,然而程序员所要求的操作却将要导致阻断。这时,才会报WSAEWOULDBLOCK错误。

*文档里还有一句话:If no buffer space is available within the transport system to hold the data to be transmitted, send will block unless the socket has been placed in a nonblocking mode. 这句是说,如果传输系统已经没有缓冲区来存储发送数据(我仍然理解为发送窗口已满),那么,除非将SOCKET设置成了NONBLOCK(非阻断)模式,否则send函数就会BLOCK(阻断)。这句话是什么意思呢?我的理解是,NONBLOCK(非阻断)模式下永远不会发生BLOCK(阻断)的情况。这就明确了我上面一篇贴子里的错误,那时我说,“此时,send函数才是真正地停止了运行,just stop!它会停止继续向发送缓存里写数据, 以后每次调用它时(因为有循环)都会发出警告说“WSAEWOULDBLOCK””,我说这句话是以SOCKET为阻断模式下为前提的,确实,send函数这个时候是停止了运行,也停止了继续向缓存里写数据,但是,程序将永远停留在这个send调用上,send函数也不会返回,于是后面的循环也不可能发生,直到发送窗口有了空间并将核心驱动模块缓存里的所有数据(注1)填入其中,程序才会跳过刚才的send调用而接着执行循环。导致这种一直阻断的原因大多是由于接收端的问题,所以我在我的程序里设置了TIMEOUT的选项,让send等待一段时间,等超过这段时间后如果还是阻断,那么就报TIMEOUT错误,好跳过这个send调用,继续执行循环。
* [注1] 注意,是所有数据,在阻断模式下,如果要求发送的数据不能一次全部填入发送窗口,那么send就会阻断。文档里有这么两句话:
On nonblocking stream oriented sockets, the number of bytes written can be between 1 and the requested length.
If no error occurs, send returns the total number of bytes sent, which can be less than the number indicated by len for nonblocking sockets.
这两句的意思是,只有在面向流的非阻断模式下,send才可能会返回1到要求发送数据大小之间的一个数。言外之意,阻断模式下要不就成功返回要求发送的所有数据的大小,要不就一个字节也不发送并且永远停留在这次send调用上,再没有其他的可能性了。这才是BLOCK(阻断)的真正含义,所谓的“同步”也正是这个意思——执行完才能过去,否则没门儿!

现在就可以分析一下楼主的解决方案了。
* 先分析第一条:“首次发生WSAEWOULDBLOCK时,应该记录下此时的状态,从此时起,就不应该再调用send函数了,而是将待发送的数据以自己的方式存储起来.”既然发生了WSAEWOULDBLOCK错误,那么就说明楼主将SOCKET设置成了非阻断模式,这种模式有这个好处:哪怕发送窗口还有1byte的空间,那么send就将1byte的数据填入发送窗口,从而正常返回1,未发送出去的数据就交给核心驱动模块去发送(和本次send调用就没什么关系了),后面的循环也得以继续执行,这就是NONBLOCK模式下所谓的“异步”!其实是看起来像异步。而楼主所说的“将待发送的数据以自己的方式存储起来”是没有必要的,因为系统会自动存储这些未发送的数据,等待发送窗口有了空间再将其发送出去。
后面的两条方案是在SOCKET为阻断模式下才有必要实施的,但是,阻断模式下系统也会自动把本次要发送的所有数据暂存下来,等待发送窗口有了空间就能将其发送出去,然后send就会返回指定发送的数据大小,一切也都是自动的,不需要人为控制。而且,等待FD_WRITE消息是个消极的措施(因为如果导致BLOCK的因素只是暂时的话,系统迟早会将所有数据发送出去,那时候自然就会得到FD_WRITE的消息),我认为这个时候最好将选项设为TIMEOUT,不会因为本次send的阻断而影响了后续send的执行[注2],这就相当于人为“制造”了一个NONBLOCK(非阻断)模式下的发送机制,
[注2]当然,这样是要消耗内存的。因为,如果上次的阻断一直没有得到解决(这个解决过程是系统自动完成的),后续的send调用理所当然仍然还是阻断,而我们后续的千千万万个send函数仍然在向本地缓存里写数据,占用内存就是在自然不过的事情了。(这时只好祈祷接收端赶快处理好它的问题,好让我们的“士兵”不要老挤在“护城河”里出不去了,呵呵)。
* NONBLOCK模式下仍然可能发生阻断(虽然可能性非常非常小),比如调用send函数时发送窗口完全满了,连填入一个字节的空间都没了,这时,即使是在非阻断模式也会发生阻断,当然,这时的阻断是作为一个错误来告诉send调用者的,即是WSAEWOULDBLOCK。这个时候(非阻断模式下报出WSAEWOULDBLOCK错误时)非阻断模式特有的“异步”行为就玩不转了,从而等待发送窗口有了新的空间后再继续玩它的“异步”游戏。

8.问:

“哪怕发送窗口还有1byte的空间,那么send就将1byte的数据填入发送窗口,从而正常返回1,未发送出去的数据就交给核心驱动模块去发送(和本次send调用就没什么关系了),后面的循环也得以继续执行,这就是NONBLOCK模式下所谓的“异步”!”
我对这段话中的”后面的循环也得以继续执行”理解不是很清楚,后面的循环继续执行的话,会引发什么动作呢?是send同样也把数据交给核心驱动模块,然后自己返回吗?
按道理,前面一个send返回值小于待发送的字节数,对后面那个send而言,发送窗口是满的这种可能性很大,那么它要发送的数据没有一个字节是放到发送窗口上的,因而我要问了,后面那个send为何不返回0而是返回SOCKET_ERROR,即是-1呢?这样做有何意义吗?不都是把数据交给核心驱动模块了吗?只不过一个是放了1个字节到发送窗口上,一个没有放数据到发送窗口上而已.这里写代码片
不知道我的问题清不清楚,我的假设情况是这样的,程序如下:

   char *myBuf;                            //指向100M字节的存储区
   int myLen = 100*1024*1024;              //要发送的数据长度为100M
   int nRet = 0;
   nRet = send(mySock, myBuf, myLen, 0);   //此时nRet=1,虽然nRet<myLen 
                                           //但系统会保证myLen个字节都发送出去
   nRet = send(mySock, myBuf, myLen, 0);   //此时nRet=SOCKET_ERROR, 而WSAGetLastError()
                                           //是WSAEWOULDBLOCK

如果我的myBuf中有100M的数据要发送,那么第一个send返回1而第二个send引发WSAEWOULDBLOCK的可能性是有的,因为毕竟是100M的数据啊…:)这里写代码片
问题是:
1.那对第二个send来说,它要求发送的100M数据发送了多少呢?第二个send也是把数据交给了核心驱动模块就返回了呢,还是说它根本就没有做任何动作就返回SOCKET_ERROR了?
2.如果第二个send并没有把数据交给核心驱动模块去发送(毕竟它返回的是SOCKET_ERROR嘛),也就是说它什么动作都没有做,那我们肯定要把第二个send要发送的数据保存起来,等之后再发送了,对不?

9.答:

非阻断模式下还是经常会碰到阻断的情况的。这个时候唯一的解决方法就是楼主所说的必须把要发送的数据缓存起来,在程序中调用send函数,直到成功后再继续执行循环,但是这种处理方法带来的结果是效率低下。仔细想想这种方式,其实这就和阻断模式下的情况没多大差别了,只是非阻断模式下更不容易遇到阻断罢了——因为它会“肢解”数据。
非阻断模式下发生WSAEWOULDBLOCK时,呆在缓存区里要发送的整块数据都会被free掉,在程序中,就只能采取这种措施了:

while(nRet == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
{
    nRet = send(...);
}

如果不采取这种措施,那么本次要发送的数据就不会发出去,因为在发生错误时本地缓冲区就已经将这些数据清空了。而且如果一直阻塞的话,接下去正常循环中的send调用也都会发生错误。

一般来讲,最好不要采取这种方式,而选择另一种I/O模型——select模型。这种模型的效率更高些。
在非阻断模式下,如果不采取上述的措施,那么会导致这样一个结果:假设要发送的数据分别是1、2、3、4、5、6、7、8、9,又假设在发送3时发生第一次阻断,如果网络本身良好的话,那么接收端接收到的可能是1、2、5、6、8、9,当然这也是一种假设的情况。从接收端接收数据的情况来看,发生过3次阻断,第一次是3,接下来的4也发生了阻断,从5开始由于系统解决了阻断问题,所以发送正常了,但发送7的时候,又产生了阻断,所以7也没发送出去,而这之后阻断又被解决了……
这就是非阻断模式存在的一个问题,当然这个例子有点过于理想化了,但比较容易说清楚非阻断模式的运行机制。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值