【Tornado框架】什么是非阻塞式服务器?

什么是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()

【Tornado框架】什么是非阻塞式服务器?_第1张图片

非阻塞式服务器

使用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的利用效率更高,基本原理如图

【Tornado框架】什么是非阻塞式服务器?_第2张图片

I/O多路复用

上面实现的非阻塞式服务器有一个弊端,就是在循环等待连接请求时,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框架】什么是非阻塞式服务器?_第3张图片

速度快

这一点是因为tornado使用了协程,官方文档中对协程的解释比较详细
详情参考:协程与任务
之后的文章我们会对协程的一些常见用法进行讲解


如果你喜欢本文,麻烦点赞支持一下,如果对文中内容有意见,请在评论区给我留言,我们一起对文章进行改进,谢谢~

你可能感兴趣的:(tornado笔记,python,socket,epoll)