什么是tornado?
Tornado是一种 Web 服务器软件的开源版本。Tornado 和主流Web 服务器框架(包括大多数 Python
的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。得利于其非阻塞的方式和对epoll的运用,Tornado 每秒可以处理数以千计的连接,因此 Tornado 是实时 Web
服务的一个 理想框架。——百度百科
关键词:非阻塞式服务器、速度快、epoll,为便于理解,接下来对这几点进行展开解释
要理解非阻塞式服务器,首先我们要理解什么是阻塞式服务器,我们可以借以socket来实现一个简单的阻塞式服务器
# file: block_server.py
import socket
# 创建一个 tcp socket, ip地址版本为ipv4
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 为服务段绑定端口,在通信过程中,客户端和服务端都需要端口和地址
sock.bind(('127.0.0.1', 8080))
sock.listen(128) # 监听队列,128为容量,最多能够接收的请求
print('Listening client connect...')
new_sock, addr = sock.accept() # 等待连接,返回一个sock连接对象和一个连接地址addr
print('A new connect:', addr)
data = new_sock.recv(1024) # 等待接受数据,1024为设置每次接受数据的大小(字节)
print(data)
new_sock.send(b'Wellcome tcp server') # 回执响应给客户端,只能传输bytes-like对象
# 关闭连接
new_sock.close()
sock.close()
运行上面的代码,可以看到服务一直在sock.accept()
这里等待连接
PS D:\Fire\PycharmProject\tornado_test\servs> python .\block_server.py
Listening client connect...
下面我们写一个简单的客户端,来访问一下这个服务
# file: cli.py
import socket
import time
# 创建一个 tcp socket, ip地址版本为ipv4
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器,在通信过程中,客户端和服务端都需要端口和地址
sock.connect(('127.0.0.1', 8080))
time.sleep(5) # 暂停5秒,这是一个伏笔
sock.send(b'hello server!') # 传输bytes-like对象给服务端
data = sock.recv(1024) # 接收服务端回执的响应
print(data)
sock.close() # 关闭连接
在服务端代码运行时,运行上面的客户端代码,5秒后打印出了b'Wellcome tcp server'
PS D:\Fire\PycharmProject\tornado_test\servs> python .\cli.py
b'Wellcome tcp server'
PS D:\Fire\PycharmProject\tornado_test\servs>
服务端这边,不再卡在sock.accept()
,直接打印了A new connect...
,并在5秒后打印出了客户端传输的数据
PS D:\Fire\PycharmProject\tornado_test\servs> python .\01block_server.py
Listening client connect...
A new connect: ('127.0.0.1', 62400)
b'hello server!'
PS D:\Fire\PycharmProject\tornado_test\servs>
上述情况说明,服务端分别会在sock.accept()
和new_sock.recv(1024)
这两个地方等待,直到客户端发送相应的请求。而客户端也会在服务端接收到请求后,接收到服务端的回执响应
以上,这种等待连接请求的服务端形式被称作阻塞式服务器,基本原理如图,这里的recvfrom可以理解为一个需要等待的进程,例如accept()
和 recv()
使用sock.setblocking(False)
可以将服务器设置为非阻塞式的,但在那之前需要解决程序在上文所述的两个地方等待的问题,原因是如果设置成非阻塞式的,程序在没有接收到连接时继续执行后续代码就会报错。
因此我们可以用异常处理来解决这个问题。
与此同时,因为是非阻塞的,所以程序会很快的执行完,为了不让客户端的请求能够被接收到,需要给accept()
这里加上循环,让它一直处于能够接收的状态
# file: non_block_server.py
import socket
# 创建一个 tcp socket, ip地址版本为ipv4
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 端口复用, 当程序结束时立刻回收端口,避免端口被一直占用
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 为服务段绑定端口,在通信过程中,客户端和服务端都需要端口和地址
sock.bind(('127.0.0.1', 8080))
sock.listen(128) # 监听队列,128为容量,最多能够接收的请求
sock.setblocking(False) # 设置为非阻塞
sock_list = [] # 定义一个存放sock连接的容器
def handle_data():
for new_sock in sock_list:
while True:
try:
data = new_sock[0].recv(1024) # recv接受数据,()设置每次接受数据的大小
print(data)
except Exception as e:
# print(e, '---recv wait') # 如果没有接收连接这里会一直打印错误信息
pass
else:
new_sock[0].send(b'this is tcp server')
new_sock[0].close()
sock_list.remove(new_sock)
print('Listening client connect...')
while True:
try:
new_sock = sock.accept() # 等待连接,返回一个sock连接对象和一个连接地址addr
except Exception as e:
# print(e, '---accept wait') # 如果没有接收连接这里会一直打印错误信息
pass
else:
print('A new connect:', new_sock[1]) # addr
new_sock[0].setblocking(False) #
sock_list.append(new_sock)
handle_data() # 处理新连接发送的数据
同样我们运行一下服务端和客户端来看看结果
PS D:\Fire\PycharmProject\tornado_test\servs> python .\non_block_server.py
Listening client connect...
A new connect: ('127.0.0.1', 57489)
b'hello server!'
得到的效果和阻塞式服务端相同,但我们设置了非阻塞之后,可以同时执行很多其它功能,相较于阻塞式服务端对cpu的利用效率更高,基本原理如图
上面实现的非阻塞式服务器有一个弊端,就是在循环等待连接请求时,cpu会一直空转,导致大量资源被占用。那么有没有什么方法解决这个问题呢?
我们可以设置一个监视着,帮我们去监控指定的一个socket,当它就位时我们就执行对应的方法,从而减少资源的占用率
我们可以用python的内置库selectors
来实现对socket的事件监控
# file: epoll_server.py
import socket
import selectors
# 创建一个 tcp socket, ip地址版本为ipv4
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 端口复用, 当程序结束时立刻回收端口,避免端口被一直占用
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 为服务段绑定端口,在通信过程中,客户端和服务端都需要端口和地址
sock.bind(('127.0.0.1', 8080))
sock.listen(128) # 容量,最多能够接收的请求
sock.setblocking(False) # 设置为非阻塞
epoll = selectors.DefaultSelector() # 创建一个epoll 监视者
def accept(sock):
new_sock, addr = sock.accept()
print('A new connect:', addr)
# 当new_sock触发 读 事件时,就会执行handle方法
epoll.register(new_sock, selectors.EVENT_READ, handle)
def handle(conn):
data = conn.recv(1024) # 接受大小为1024字节的数据
if data:
print('echoing:', data)
conn.send(b'this is tcp server') # 向客户端发送提示信息
else:
print('closing:', conn)
epoll.unregister(conn) # 执行完取消注册该连接对象的事件
conn.close() # 关闭连接
epoll.register(sock, selectors.EVENT_READ, accept) # 注册一个事件监控器,只要有新的连接接入就会执行accept
while True:
print('Server in running')
events = epoll.select() # 轮询系统的事件
# print(events) # 打印事件
for key, mask in events:
# print(key, mask) # 打印事件中的key对象和mask标识
callback = key.data # data代表我们绑定的方法
callback(key.fileobj) # 回调函数并传入请求连接对象
运行服务端和客户端后,可以实现同样的功能
PS D:\Fire\PycharmProject\tornado_test\servs> python .\epoll_server.py
Server in running
A new connect: ('127.0.0.1', 56316)
Server in running
echoing: b'hello server!'
Server in running
closing: .socket fd=480, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 56316)>
Server in running
基本原理如下图所示,select监听事件,并在触发时执行对应的函数,其实可以看出select监听事件的时候也会有资源被占用,但相对于上面使用异常处理的实现方式,占用的资源会少很多
这一点是因为tornado使用了协程,官方文档中对协程的解释比较详细
详情参考:协程与任务
之后的文章我们会对协程的一些常见用法进行讲解
如果你喜欢本文,麻烦点赞支持一下,如果对文中内容有意见,请在评论区给我留言,我们一起对文章进行改进,谢谢~