python的HttpServer出现socket.accept()阻塞卡死

来源:互联网 发布:司法考试培训网络班 编辑:程序博客网 时间:2024/05/18 06:27
最近想用python做一个我微信公众号的后台,结果发现,服务器刚启动的一个多小时微信发的消息是有回复,但过几个小时之后,所有给服务器发现的请求都没有回复了,找了两天问题,昨晚上还弄半夜3点。总算把问题给解决了。

服务器用的是下边这个类:

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):

和一个处理http各种do_请求的Handler:

class myHandler(BaseHTTPRequestHandler):

我这里只处理三个do_请求,do_GET,do_POST,do_HEAD.
作为http服务器,这部分所有的处理逻辑都是一样的。不多说了。下面我说一下问题出现的原因和处理办法。
出现问题的情况
先看下边服务器log输出,可以看到有不明身份的域名对服务器的433端口使用了get请求。
python的HttpServer出现socket.accept()阻塞卡死 - ♂苹果 - 眼睛想旅行
 这个域名会每过几个小时就对我的服务器进行一次get请求。当出现这个get请求之后,服务器就好像挂掉了一样,所有再向服务器发送的任何请求都不会有回应。感觉很奇怪,但因为对方使用的是三级域名请求,我又没有办法使用IP地址过滤的方法拒绝他,只有当服务器被请求挂掉之后,我才能有办法获取到对方的IP地址。
我就先把这个叫作请求攻击吧,因为每次请求之间都间隔几个小时,就折腾了两天时间,开始的时候以为是多线程对象调用引起的线程死锁,在这个处理思路上就花了一天时间,然后使用单线程启动服务器后看到上边的log,就知道问题所在了。
对问题情况模拟重现
作为程序员在解决bug的问题上,最重要的是要找到解决这个bug的钥匙,也就是让bug可以人为控制的重现。但凡不能重现的bug,都是让人揪心的bug.下边是bug重现方法。其实也很简单。
1.再次启动服务器,
2.用我电脑上的socket调试工具连接服务器的443端口
3.当socket成功连接上服务器的443端口,但不发送任何数据
4.给http服务器发送任意请求
5.服务器不会对任何请求进行回应
6.当从socket工具断开443端口连接时,服务器这时会回复第4步发送的http请求
找到问题了,也可以重现了,那接下来就是对问题的处理。
查看python库的httpServer的源码,找到出现问题的位置
 查看到python库代码之后,发现HttpServer在有客户端连接上后,HTTPServer的_handle_request_noblock()函数中会调用socket的一个socket.accept()方法,这个方法返回客户端IP地址端口以及回复消息的request连接。
当我只作443端口的连接,而不发http请求时,HTTPServer就是阻塞在这个socket.accept()方法调用上。而accept并没有超时参数设置。
mac os系统下,python的HTTPServer相关源码在下边路的两个文件中:

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/BaseHTTPServer.py

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/SocketServer.py

这两个文件一个是Socket服务器,一个是HTTP服务器,自然了,HTTP服务器类是继承自Socket类的了。
先是SocketServer得到客户端连接,会触发自已的_handle_request_noblock()函数,在这个函数中,连接和请求会转发给HTTPServer来处理。看下图代码部分:
python的HttpServer出现socket.accept()阻塞卡死 - ♂苹果 - 眼睛想旅行
 handle_request是服务器发出的一次客户端请求检测,超时没有发现客户端就调用超时handle_timeout()函数并返回。
当发现有客户端连接时,会调用_handle_request_nolock()方法,在这个方法中有一个获取客户端请求和客户端地址的get_request()方法。这个方法其实在HTTPServer中有实现,看下图代码:
python的HttpServer出现socket.accept()阻塞卡死 - ♂苹果 - 眼睛想旅行
 在这里,调用了socket.accept()函数用以返回客户端请求和客户端连接地址信息。当只作socket连接而不发送http请求时,这个accept就被阻塞了,python的HTTPServer是多线程连接,单线程处理消息,所以这个消息不接收到,就会阻塞后边的所有请求。
想办法解决问题 
找到问题所在了,那么接下来就是真正的解决问题,我试过了很多办法,包括在accept()之前查看客户端信息,设置socket设置方法setsocketopt()或者设置超时,但都解决不了。最后想到看python有没有调用函数超时的处理逻辑,一搜,还真有。下边是找到的处理办法:
python 使用 signal模块实现函数调用超时问题
我作的处理逻辑:
python的HttpServer出现socket.accept()阻塞卡死 - ♂苹果 - 眼睛想旅行
 到些问题理论上得到解决,我设置的accept()调用超时是2秒。接下来就是测试。
问题还是存在
以为找到了解决办法,但我的http服务器是在子线程里启动的,上边python调用函数超时装饰器办法还是解决不了问题。
python的HttpServer出现socket.accept()阻塞卡死 - ♂苹果 - 眼睛想旅行
 好吧,既然这样,有两种办法来解决这个问题,
第一种,找一个可以在子线程中使用的函数调用超时解决方案。
第二种,把http服务器放到主线程里,把任务处理逻辑放到子线程里。
以上两种方法理论上都是可以了。第二中方法要改很多代码,但实现起来技术难度要低一些。但我想试试第二种方法,于是到处找解决方案。到是让我找到一个。
https://my.oschina.net/leejun2005/blog/607741
用文中所说的方法,好像可以实现子线程调用函数超时,赶紧试一下。
经过测试,文中的方法是把accept()方法放在了另一个新建的子线程中去调用。这样httpserver的accept()将不会被阻塞,所有新的http请求都可以正常相应了,虽基socket连接攻击端看到socket还是保持连接着的,但实际上socket连接的accept进程已经被终止了,如果这时候socket连接攻击端发送消息时,就会显示socket已断开。
总结
在处理这个问题时,找到了一些相关的socket知识。在这里收藏一下,说不定以后会用到。

python socket 选项

python socket.setsocketopt()方法中参数的设置方法比较奇特,socket底层其实是C语言写的,在设置指针数据类型的参数时用到了struct库,
可以看一下例子:
python的HttpServer出现socket.accept()阻塞卡死 - ♂苹果 - 眼睛想旅行