0%

随笔-关于WinSock,折腾了两天做点总结

先说前因后果,之前用#include <WinSock2.h>写了个发送HTTP请求的功能,服务器用的是koa.js,大致的流程就是建立socket连接之后使用send()发送请求,然后用recv()获取响应,然后发送和接收的时候也没考虑那么多,就想着发送一次那也就接收一次好了,所以就简单在recv()的缓冲区里分配了1024个字节,然后收到多少是多少。

一直用了好几个月,用的人也不少,总之就是一点问题也没有。可是,由于有新的研发任务,要用到python作为服务端后端,就顺其自然地用上了以前用过的工具Flask。费了一番功夫搭建好服务器开始测试的时候,发现发送数据后,只能收到HTTP报文的前几个字段,就像这样:

1
HTTP/1.1 200 OK

一开始还以为是Flask的问题,以为它不能填充数据,就花了大半天时间去找怎么在Flask的POST请求和GET请求中填充数据,照着官方的非官方的教程调用各种函数填充后都没办法在客户端看到填充的数据,几乎把Python和Flask的问题排查完毕还是无法解决以至于百思不得其解之后,突然想到客户端会不会有问题,然后才去客户端检查WinSock2。又是费了好大功夫,发现收到的数据一直只有十几个字节,也就是HTTP报文的第一行,然后就在recv()函数上面打了个断点,结果发现,运行到断点截停之后,再执行recv()函数,好家伙,该有的数据全都来了(内心:?????),又尝试了几次发现,如果把断点设置在recv()函数的后面,那么还是只能收到第一行。此时得出了一个结论:Flask发送了两次数据,recv()需要循环接收才能接收完整。

好了, 第一个需要解决的问题出现了:把以前接收一次的recv()函数改成循环接收。但是这里面又遇到了一堆的问题,首先是循环的终止条件如何确定?当服务端是Flask时,循环接收就只需要判断recv()的返回值是不是0就ok了,因为接收完了前两次,第三次调用recv()的返回值是0,说明服务端那边把socket关掉了,好,解决!解决了吗?事情远远没有这么简单,当服务端换成koa.js时,第一次调用recv()就已经把数据收完了,按理说接下来服务端应该也把socket关闭,完成数据传输对吧,可是,实际情况是,第二次调用的recv()它进入了阻塞。也就是说,koa.js,即便服务端把数据发完了,它也不关闭socket。

其实一开始没想到问题出在了socket不关闭上面,而是往另一个方向去了:既然recv()阻塞,那么我找个办法在它阻塞之前判断它是不是阻塞不就行了吗?然后就去搜“winsock2 recv 阻塞”之类的,结果得到两个答案:setsockopt()select()。具体怎么用就不详细说了,大致意思就是说,你可以通过setsockopt()去设置一些超时参数,让它阻塞之后自己退出,可以用select()去看看是否阻塞之类的。好家伙看来看去得到的结论就是:第一个函数好像能用但是是通过超时来结束阻塞的,第二个函数看好多人说可以实现多请求响应,也可以判断缓冲区内是否有可读取的内容,就用它了!事实证明那些人说的也确实没错,只是我理解错了,select()是可以判断缓冲区内是否有可读取的内容,但是这里的缓冲区指的是FD_SET设置的集合,在socket里面也就是一个socket连接的列表,是列表!然后判断是否可读取的意思是,判断这些个socket连接是否已关闭!好家伙等我全都试明白了才理解到这一点,难怪为什么select()的参数里面有一个timeout,这下全说通了,说到底你这个多请求也就是加了个列表罢了,本质上还得看socket连接是否关闭,看样子要从WinSock2.h这边判断数据是否传完已经没希望了。还是得看socket是否关闭。

行吧,第三个问题:怎么样让koa.js服务器的socket在传输完数据后自动关闭?这时候距离发现问题已经过去了一天半,开始着手研究,通过查阅koa的官网发现,其上下文ctx保留了一个Node.js原生的res响应实例,通过这个实例可以获取当前连接的socket,在这里或许可以关闭,于是就把原来的通过上下文设置响应数据改成了通过res实例响应数据,就像这样:

1
2
3
4
5
6
7
8
// 修改之前
ctx.body = 'Response';

// 修改之后
ctx.respond = false; // 关闭自动响应功能
ctx.res.status = 200;
ctx.res.end('Response');
ctx.res.socket.end(); // 手动关闭socket

虽然koa官网建议你千万别这么用否则后果自负,但是为了解决这该死的问题我还是不得不这么用了。好了,这回Flaskkoa.js都可以自动关闭socket,在本地也全都测试通过了,那是该部署到服务器上看看了。事实证明,我果然高兴得太早了,部署上去之后发现,奇怪,为什么还是发生了阻塞?所有的请求在服务端已经关闭socket之后还是进入了等待直到超时???原本以为是代码没部署上去,反复拷来拷去也没发现什么毛病,难道是操作系统出错了?那换个Windows的服务器试试?再把代码部署到Windows的服务器上去发现,好家伙,还是阻塞。。。冷静分析一波,服务器的共同点,本地服务器和远程服务器的差别,哦豁,找到了——Nginx!二话不说,立刻,本地,配置Nginx,启动,发送本地请求,好!本地也阻塞了!罪魁祸首就是Nginx,靠它关闭socket就意味着得去改底层代码,基本不可能,虽然还是有点不服,但是查了资料发现有点没啥必要,额外工作量太大了。

其实吧,都到这里了,也没必要继续搞了,总不会要去改Nginx底层代码吧,Nginx不关闭socket我能怎么办啊呜呜呜。后来又瞎搞了几次,发现Flask的服务器用Nginx代理一下,竟然只需要接收一次就把所有数据接受全了?!好家伙,天无绝人之路,那也就是说,不用搞循环接收,也能用了!好,如果没办法解决循环接收的问题,那每次传少量数据,然后用Nginx做个代理,也能把Flask用起来。

虽然这两天白干了,但至少问题的原因基本找到了,也可以有一个不错的能用的初步解决方案了,同时对WinSock2有了更深的理解(其局限性),总结如下:

  1. recv()本质上是阻塞的,除非客户端关闭连接,或者超时,否则不会退出,也没办法提前知道是否阻塞;
  2. http服务很喜欢搞长连接,也不喜欢关闭socket,用一般的socket方式容易陷入阻塞。不过在Python和Node.js中对于什么时候接收完毕似乎有一套不错的解决方案,比如Python的requestspost()方法,Node.js的request,用它们向http服务器发送GET/POST请求就很正常顺利;
  3. 如果要在C++中使用WinSock2实现一个HTTP请求发送的功能,首先需要解决的就是阻塞问题,需要参考些用来发送http请求的库的实现方式,看看它们是如何处理这类阻塞问题的;
  4. 初步分析导致阻塞的原因是没有关闭socket,但也不排除是WinSock2recv()函数实现的问题,可以尝试找找有没有别的些用来建立连接发送请求接受请求的基础类库或工具;
  5. 目前来说,发送少量数据分配固定内存是完全够用了,但是等需要用到传输大量数据的时候就得考虑循环终止条件了;
  6. Nginx做个转发代理也可以解决数据分次发送的尴尬,但是使用Python-Flask时如果再开一个Nginx或许就多此一举了,特别是原本就没有Nginx,所以这两天的研发成果也不完全没有用,当服务端就是单纯的Python-Flask时,其发送完数据自动关闭socket的特性正是需要靠循环recv()来接收,没有问题。