tornado是一个异步非阻塞的web框架,由Facebook开源,源代码用的python语言。之前也用tornado做过几个项目,实习的时候公司也是用的tornado来做工业物联网的后端。空余时间研究一下tornado的源码,毕竟是Facebook的代码,还是有学习的价值。以下为学习的一点笔记。
一.socket编程
现代计算机网络中使用TCP/IP协议架构:物理链路层,ip层,TCP/UDP层,应用层。
![6662fa53da024f3d6c882c72afa5b598.png](https://i-blog.csdnimg.cn/blog_migrate/46bc9c004c37f7b719b994c5bf76b47a.jpeg)
socket层则是对TCP/UDP层的一层简要封装,能够方便开发者定义自己的高层应用协议。在python中,使用内置的socket模块能够进行socket编程。该模块提供的api有:
![29bcb16eab152e181e9bd5715ac32780.png](https://i-blog.csdnimg.cn/blog_migrate/b89cbc058e343cc766ecd9c11e949373.jpeg)
![5345e5e73583570e0cfab64ef4661753.png](https://i-blog.csdnimg.cn/blog_migrate/e378de35450c73eaf980448426f5e344.jpeg)
利用提供的这些基础api,能够很方便的自行编写高层协议,比如,你可以自己实现一个HTTP server。 HTTP协议基于TCP协议,是面向连接的,构造一条虚电路,保证数据包的顺序,不丢包,可靠的发送。基于TCP的socket逻辑架构如下:
![34328642240f3206b9342dc51dc6df69.png](https://i-blog.csdnimg.cn/blog_migrate/8f812e66b49db1911d36b578afb0b956.jpeg)
socket在类unix系统中均可以看成是一个特殊文件,
sock=socket.socket()
返回一个句柄sock,sock.fileno()即为该句柄的编号,为int类型。考虑一下在tornado中tcpserver如何在ioloop注册事件。在ioloop中提供了一个方法:
def add_handler( # noqa: F811
self, fd: Union[int, _Selectable], handler: Callable[..., None], events: int
) -> None
add_handler函数用来绑定socket对应的回调函数,具体来说:当服务器端socket对应的’读事件‘(IOOP.READ,即为参数event)被触发时,将调用handler函数进行处理。以上理解比较抽象,下面写个例子,理解下:
首先写个server:
from tornado import httpserver, ioloop, tcpserver
import socket
def a(A, B):
print("====开始测试=====")
print(A)
print(B)
print("我是服务器socket的回调函数")
if __name__ == '__main__':
loop = ioloop.IOLoop.current()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8881))
loop.add_handler(server.fileno(), a, ioloop.IOLoop.READ)
server.listen()
#conn, addr = server.accept()
#a = conn.recv(1024)
#print("服务器收到的数据", repr(a))
#loop.spawn_callback(c, conn)
loop.start()
以上server用socket编程来实现,功能为:首先定义一个socket,变量名为server,该socket绑定在8881端口上。然后我们利用add_handler函数将该socket注册到ioloop上,注册的这一步在平时的开发中的写法为:
server = TCPServer()
server.bind(8881)
server.start(0) # Forks multiple sub-processes
IOLoop.current().start()
在注册该socket之后,ioloop.start()开始事件循环。
再写一个client:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 8881))
sock.sendall(b"hello,worldfwefewfwefwefwefwefwefwefw")
print(sock.fileno())
# a = sock.recv(1024)
# print(repr(a))
sock.close()
该client,依然是创建一个socket,然后请求连接8881端口的服务(模拟发送HTTP请求)。当该数据被发送之后,可以看见server端的输出为:
![99e4906581394e6d6f56ff33728dd889.png](https://i-blog.csdnimg.cn/blog_migrate/f45c3f42753b39664ddb6918702a0a71.png)
可以看见ioloop监听到了client对server发送的请求,进行了回调。具体来说:client向server发送了一个请求,server端的socket在收到该请求后,会触发READ事件,此时ioloop会触发该socket对应的回调函数。
二.tornado中的多进程
对于tornado这种单线程的web框架,如果遇到cpu bound类型的任务,虽然及时能够通过coroutine模式保证不会丢失服务,但是由于CPU被占用,所以响应时间一点也不会减少。此时可以考虑使用多进程tornado,或者多tornado实例,然后再用nginx做负载均衡(这个还没试过)。
我们通过time.sleep(20)来模拟CPU的计算过程,原因是在这20s内,server不能响应其他用户的请求(cpu一直在计算)。
代码如下:
class MainHandler(tornado.web.RequestHandler):
async def get(self):
print("睡眠10s")
time.sleep(20)
print("pid", os.getpid())
print("睡眠结束")
self.write(str(os.getpid()))
class WebHandler(tornado.web.RequestHandler):
async def get(self):
print("hello,world123")
def make_app():
return tornado.web.Application([
tornado.web.url(r"/123", MainHandler, name="hello"),
tornado.web.url(r"/456", WebHandler, name="web"),
])
if __name__ == "__main__":
app = make_app()
# server = tornado.httpserver.HTTPServer(app)
# server.bind(8888, '127.0.0.1')
# server.start(0)
sockets = tornado.netutil.bind_sockets(8888)
tornado.process.fork_processes(2)
server = tornado.httpserver.HTTPServer(app)
server.add_sockets(sockets)
loop1 = tornado.ioloop.IOLoop()
loop = loop1.current()
# loop.add_handler(1, a, 0x001)
# loop.add_handler(2, b, 0x004)
# loop.add_callback(d)
# print(loop1._ioloop_for_asyncio)
loop.start()
由于我的机器有四个核,所以输出如下:
![9b8f403b91da2eeb9405211bd7435c26.png](https://i-blog.csdnimg.cn/blog_migrate/eea86a9f3c1f3f0feec190fabf901aff.jpeg)
在tornado中,多进程是通过os.fork函数来实现(windows无法实现)。父进程在fork子进程之后,子进程会和父进程同时执行剩下的代码,需要注意os.fork有两个返回值,如果返回值为0,则表明当前这个进程是子进程,如果返回值为int,则int是子进程的pid号。那么通过这种方式在执行cpubound的任务时,可以充分利用多核。在tornado中的内部实现为tornado.netutil.add_accept_handler函数:
def add_accept_handler(
sock: socket.socket, callback: Callable[[socket.socket, Any], None]
) -> Callable[[], None]:
io_loop = IOLoop.current()
removed = [False]
def accept_handler(fd: socket.socket, events: int) -> None:
for i in range(_DEFAULT_BACKLOG):
if removed[0]:
# The socket was probably closed
return
try:
connection, address = sock.accept()
except socket.error as e:
if errno_from_exception(e) in _ERRNO_WOULDBLOCK:
return
if errno_from_exception(e) == errno.ECONNABORTED:
continue
raise
set_close_exec(connection.fileno())
callback(connection, address)
def remove_handler() -> None:
io_loop.remove_handler(sock)
removed[0] = True
io_loop.add_handler(sock, accept_handler, IOLoop.READ)
return remove_handler
关键在于内部函数accept_handler,当使用多进程时会产生惊群效应,那么tornado是如何防止这种情况的呢?关键在:
if errno_from_exception(e) in _ERRNO_WOULDBLOCK:
在异步函数中(errno.EWOULDBLOCK,errno.EAGAIN)可以不被当做错误,用来避免客户端的socket被重复处理。
欢迎关注微信公众号:生物信息与python,及时更新与分享~