最近看Tornado源码给了我不少启发,心血来潮决定自己试着只用python标准库来实现一个异步非阻塞web框架。花了点时间感觉还可以,一百多行的代码已经可以撑起一个极简框架了。

一、准备工作

需要的相关知识点:

  • HTTP协议的请求和响应
  • IO多路复用
  • asyncio

掌握上面三个点的知识就完全没有问题,不是很清楚的同学我也推荐几篇参考文章

  HTTP协议详细介绍(https://www.cnblogs.com/haiyan123/p/7777924.html

  Python篇-IO多路复用详解(https://www.jianshu.com/p/818f27379a5e

  Python异步IO之协程(一):从yield from到async的使用(https://blog.csdn.net/SL_World/article/details/86597738

实验环境:

python 3.7.3

由于在框架中会使用到async/await关键字,所以只要确保python版本在3.5以上即可。

二、框架功能目标

我们的框架要实现最基本的几个功能:

  • 封装HTTP请求响应
  • 路由映射
  • 类视图和函数视图
  • 协程支持

当然一个完善的web框架需要实现的远远不止这些,这里我们现在只需要它能跑起来就足够了。

三、封装HTTP协议

HTTP是基于TCP/IP通信协议来实现数据传输,与一般的C/S相比,它的特点在于当客户端(浏览器)向服务端发起HTTP请求,服务端响应数据后双方立马断开连接,服务端无法主动向客户端发送数据。HTTP协议数据传输内容分为请求头和请求体,请求头和请求体之间使用"\r\n\r\n"进行分隔。在请求头中,第一行包含了请求方式,请求路径和HTTP协议,此后每一行以key: value的形式传输数据。

对于我们的web服务端来说,需要的就是解析http请求和处理http响应。

我们通过写两个类,HttpRequest和HttpResponse来实现。

3.1 HttpRequest

HttpRequest设计目标是解析从socket接收request数据

 class HttpRequest(object):
def __init__(self, content: bytes):
self.content = content.decode('utf-8')
self.headers = {}
self.GET = {}
self.url = ''
self.full_path = ''
self.body = ''
try:
header, self.body = self.content.split('\r\n\r\n')
temp = header.split('\r\n')
first_line = temp.pop(0)
self.method, self.url, self.protocol = first_line.split(' ')
self.full_path = self.url
for t in temp:
k, v = t.split(': ', 1)
self.headers[k] = v
except Exception as e:
print(e)
if len(self.url.split('?')) > 1: # 解析GET参数
self.url = self.full_path.split('?')[0] # 把url中携带的参数去掉
parms = self.full_path.split('?')[1].split('&')
for p in parms: # 将GET参数添加到self.GET字典
k, v = p.split('=')
self.GET[k] = v

在类中,我们实现解析http请求的headers、method、url和GET参数,其实还有很多事情没有做,比如使用POST传输数据时,数据是在请求体中,针对这部分内容我并没有开始写,原因在于本文主要目的还是异步非阻塞框架,目前的功能已经足以支持我们进行下一步实验了。

3.2 HttpResponse

HTTP响应也可以分为响应头和响应体,我们可以很简单的实现一个response:

 class HttpResponse(object):
def __init__(self, data: str):
self.status_code = 200 # 默认响应状态 200
self.headers = 'HTTP/1.1 %s OK\r\n'
self.headers += 'Server:AsyncWeb'
self.headers += '\r\n\r\n'
self.data = data @property
def content(self):
return bytes((self.headers + self.data) % self.status_code, encoding='utf8')

HttpResponse中并没有做太多的事情,接受一个字符串,并使用content返回一个满足HTTP响应格式的bytes。

从用户调用角度,可以使用return HttpResponse("欢迎来到AsynicWeb")来返回数据。

我们也可以简单的定义一个404页面:

Http404 = HttpResponse('<html><h1>404</h1></html>')
Http404.status_code = 404

四、路由映射

路由映射简单理解就是从一个URL地址找到对应的逻辑函数。举个例子,我们访问http://127.0.0.1:8000这个页面,在http请求中它的url是"/",在web服务器中有一个函数index,web服务器能够由url地址"/"找到函数index,这就是一个路由映射。

其实路由映射实现起来非常简单。我们只要定义一个映射列表,列表中的每个元素包含url和逻辑处理(视图函数)两部分,当一个http请求到达的时候,遍历映射列表,使用正则匹配每一个url,如果请求的url和映射表中的相同,我们就可以取出对应的视图函数。

路由映射表是完全由用户来定义映射关系的,它应该使用一个我们定义的标准结构,比如:

routers = [
('/$', IndexView),
('/home', asy)
]

五、类视图和函数视图

视图是指能够根据一个请求,执行某些逻辑运算,最终返回响应的模块。说到这里,一个web框架的运行流程就出来了:

    http请求——路由映射表——视图——执行视图获取返回值——http响应

在我们的框架中,借鉴Django的设计,我们让它支持类视图(CBV)和函数视图(FBV)两种模式。

对于函数视图,完全由用户自己定义,只要至少能够接受一个request参数即可

对于类视图,我们需要做一些预处理,确保用户按我们的规则来实现类视图。

定义一个View类:

 class View(object):
# CBV应继承View类
def dispatch(self, request):
method = request.method.lower()
if hasattr(self, method):
return getattr(self, method)(request)
else:
return Http404

在View类中,我们只写了一个dispatch方法,其实就做了一件事:反射。当我们在路由映射表中找对应的视图时,如果判断视图属于类,我们就调用dispatch方法。

从用户角度来看,实现一个CBV只需要继承View类,然后通过定义get、post、delete等方法来实现不同的处理。

六、socket和多路复用

上面几个小节实现了web框架的大体执行路径,从这节开始我们实现web服务器的核心。

通过IO多路复用可以达到单线程实现高并发的效果,一个标准的IO多路复用写法:

 server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8000))
server.setblocking(False) # 设置非阻塞
server.listen(128)
Future_Task_Wait = {}
rlist = [server, ]
while True:
r, w, x = select.select(rlist, [], [], 0.1)
for o in r:
if o == server:
'''判断o是server还是conn'''
conn, addr = o.accept()
conn.setblocking(False) # 设置非阻塞
rlist.append(conn) # 客户连接 加入轮询列表
else:
data = b""
while True: # 接收客户传输数据
try:
chunk = o.recv(1024)
data = data + chunk
except Exception as e:
chunk = None
if not chunk:
break
dosomething(o, data, routers) # 拿到数据干点啥

通过这段代码我们可以获得所有的请求了,下一步就是处理这些请求。

我们就定义一个dosomething函数

 import re
import time
from types import FunctionType def dosomething(o, data, routers):
'''解析http请求,寻找映射函数并执行得到结果
:param o: socket连接对象
:param data: socket接收数据
:return: 响应结果
'''
request = HttpRequest(data)
print(time.strftime("【%Y-%m-%d %X】",time.localtime()), o.getpeername()[0],
request.method, request.url)
flag = False
for router in routers:
if re.match(router[0], request.url):
target = router[1]
flag = True
break
if flag:
# 判断targe是函数还是类
if isinstance(target, FunctionType):
result = target(request)
elif issubclass(target, View):
result = target().dispatch(request)
else:
result = Http404
else:
result = Http404
return result

这段代码做了这么几件事。1.实例化HttpRequest;2.使用正则遍历路由映射表;3.将request传入视图函数或类视图的dispatch方法;4.拿到result结果

我们通过result = dosomething(o, data, routers)可以拿到结果,接下来我们只需要把结果发回给客户端并断开连接就可以了

o.sendall(result.content)  # 由于result是一个HttpResponse对象 我们使用content属性
rlist.remove(o) # 从轮询中删除连接
o.close() # 关闭连接

至此,我们的web框架已经搭建好了。

但它还是一个同步的框架,在我们的服务端中,其实一直通过while循环在监听select是否变化,假如我们在视图函数中添加IO操作,其他连接依然会阻塞等待,接下来让我们的框架实现对协程的支持。

七、协程支持

在实现协程之前,我们先聊聊Tornado的Future对象。可以说Tornado异步非阻塞的实现核心就是Future。

Future对象内部维护了一个重要属性_result,这是一个标记位,一个刚实例化的Future内部的_result=None,我们可以通过其他操作来更改_result的状态。另一方面,我们可以一直监听每个Future对象的_result状态,如果发生变化就执行某些特定的操作。

我们在第六节定义的dosomething函数中拿到了一个result,它应当是一个HttpResponse对象,那么能不能返回一个Future对象呢。

假如result是一个Future对象,我们的服务端不立马返回结果,而是把Future放进另一个轮询列表中,当Future内的_result改变时再返回结果,就达到了异步的效果。

我们也可以定义一个Future类,这个类维护只一个变量result:

 class Future(object):
def __init__(self):
self.result = None

对于框架使用者来说,在视图函数要么返回一个HttpResponse对象代表立即返回,要么返回一个Future对象说你先别管我,我把事情干完了再通知你返回结果。

既然视图函数返回的可能不只是HttpResponse对象,那么我们就需要对第六步的代码增加额外的处理:

Future_Task_Wait = {} # 定义一个异步Future字典
result = dosomething() # 拿到结果后执行下面判断
if isinstance(result, Future):
Future_Task_Wait[o] = result # Futre对象则加入字典
else:
o.sendall(result.content) # 非Future对象直接返回结果并断开连接
rlist.remove(o)
o.close()

在while True轮询内再增加一段代码,遍历Future_Task_Wait字典:

rm_conn = [] # 需要移除列表的conn
for conn, future in Future_Task_Wait.items():
if future.result:
try:
conn.sendall(HttpResponse(data=future.result).content) # 返回result
finally:
rlist.remove(conn)
conn.close()
rm_conn.append(conn)
for conn in rm_conn: # 在字典中删除conn
del Future_Task_Wait[conn]

这样,我们就可以返回一个Future来告诉服务器这是将来才返回的对象。

那回归正题,我们到底该如何使用协程?这里我用的方法是创建一个子线程来执行协程事件循环,主线程永远在监听socket。

from threading import Thread
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
coroutine_loop = asyncio.new_event_loop() # 创建协程事件循环
run_loop_thread = Thread(target=start_loop, args=(coroutine_loop,)) # 新起线程运行事件循环, 防止阻塞主线程
run_loop_thread.start() # 运行线程,即运行协程事件循环

当我们要把asyncdo方法添加作为协程任务时

asyncio.run_coroutine_threadsafe(asyncdo(), coroutine_loop)

好了,异步非阻塞的核心代码分析的差不多了,将六七节的代码整合写成一个类

 import re
import time
import select
import asyncio
from socket import *
from threading import Thread
from types import FunctionType
from http.response import Http404, HttpResponse
from http.request import HttpRequest
from views import View
from core.future import Future class App(object):
# web应用程序
coroutine_loop = None def __new__(cls, *args, **kwargs):
# 使用单例模式
if not hasattr(cls, '_instance'):
App._instance = super().__new__(cls)
return App._instance def listen(self, host, port, routers):
# IO多路复用监听连接
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind((host, port))
server.setblocking(False)
server.listen(128)
Future_Task_Wait = {}
rlist = [server, ]
while True:
r, w, x = select.select(rlist, [], [], 0.01)
for o in r:
if o == server:
'''判断o是server还是conn'''
conn, addr = o.accept()
conn.setblocking(False)
rlist.append(conn)
else:
data = b""
while True:
try:
chunk = o.recv(1024)
data = data + chunk
except Exception as e:
chunk = None
if not chunk:
break
try:
request = HttpRequest(data, o)
print(time.strftime("【%Y-%m-%d %X】",time.localtime()), o.getpeername()[0],
request.method, request.url)
flag = False
for router in routers:
if re.match(router[0], request.url):
target = router[1]
flag = True
break
if flag:
# 判断targe是函数还是类
if isinstance(target, FunctionType):
result = target(request)
elif issubclass(target, View):
result = target().dispatch(request)
else:
result = Http404
else:
result = Http404
# 判断result是不是future
if isinstance(result, Future):
Future_Task_Wait[o] = result
else:
o.sendall(result.content)
rlist.remove(o)
o.close()
except Exception as e:
print(e)
rm_conn = []
for conn, future in Future_Task_Wait.items():
if future.result:
try:
conn.sendall(HttpResponse(data=future.result).content)
finally:
rlist.remove(conn)
conn.close()
rm_conn.append(conn)
for conn in rm_conn:
del Future_Task_Wait[conn] def run(self, host='127.0.0.1', port=8000, routers=()):
# 主线程select多路复用,处理http请求和响应
# 给协程单独创建一个子线程,负责处理View函数提交的协程
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
self.coroutine_loop = asyncio.new_event_loop() # 创建协程事件循环
run_loop_thread = Thread(target=start_loop, args=(self.coroutine_loop,)) # 新起线程运行事件循环, 防止阻塞主线程
run_loop_thread.start() # 运行线程,即运行协程事件循环
self.listen(host, port, routers)

八、框架测试

现在,可以测试我们的web框架了。

 import asyncio
from core.server import App
from views import View
from http.response import *
from core.future import Future class IndexView(View):
def get(self, request):
return HttpResponse('欢迎来到首页') def post(self, request):
return HttpResponse('post') def asy(request):
future = Future()
print('异步调用')
wait = request.url.split('/')[-1]
try:
wait = int(wait)
except:
wait = 5
asyncio.run_coroutine_threadsafe(dosomething(future, wait), app.coroutine_loop)
print('返回Future')
return future async def dosomething(future, wait):
# 异步函数
await asyncio.sleep(wait)# 模拟异步操作
future.result = '等待了%s秒' % wait routers = [
('/$', IndexView),
('/home', asy)
] # 从用户角度只需使用run()
app = App()
app.run('127.0.0.1', 8080, routers=routers)

浏览器访问http://127.0.0.1:8080,返回没有问题,如果有同学使用Chrome可能会乱码,那是因为我们的HttpResponse没有返回指定编码,添加一个响应头即可。

浏览器访问http://127.0.0.1:8080/home,这时候会执行协程,默认等待5s后返回结果,你可以在多个标签页访问这个地址,通过等待时间来验证我们的异步框架是否正常工作。

九、其他

至此,我们要实现的异步非阻塞web框架已经完成了。当然这个框架说到底还是太简陋,后续完全可以优化HttpRequest和HttpResponse、增加对数据库、模板语言等等组件的扩展。

完整源码已经上传至https://github.com/sswest/AsyncWeb

150行代码搭建异步非阻塞Web框架的更多相关文章

  1. 200行自定义异步非阻塞Web框架

    Python的Web框架中Tornado以异步非阻塞而闻名.本篇将使用200行代码完成一个微型异步非阻塞Web框架:Snow. 一.源码 本文基于非阻塞的Socket以及IO多路复用从而实现异步非阻塞 ...

  2. Tornado----自定义异步非阻塞Web框架:Snow

    Python的Web框架中Tornado以异步非阻塞而闻名.本篇将使用200行代码完成一个微型异步非阻塞Web框架:Snow. 一.源码 本文基于非阻塞的Socket以及IO多路复用从而实现异步非阻塞 ...

  3. 03: 自定义异步非阻塞tornado框架

    目录:Tornado其他篇 01: tornado基础篇 02: tornado进阶篇 03: 自定义异步非阻塞tornado框架 04: 打开tornado源码剖析处理过程 目录: 1.1 源码 1 ...

  4. python-自定义异步非阻塞爬虫框架

    api import socket import select class MySock: def __init__(self, sock, data): self.sock = sock self. ...

  5. Tornado的异步非阻塞

    阻塞和非阻塞Web框架 只有Tornado和Node.js是异步非阻塞的,其他所有的web框架都是阻塞式的. Tornado阻塞和非阻塞两种模式都支持. 阻塞式: 代表:Django.Flask.To ...

  6. Python web框架 Tornado(二)异步非阻塞

    异步非阻塞 阻塞式:(适用于所有框架,Django,Flask,Tornado,Bottle) 一个请求到来未处理完成,后续一直等待 解决方案:多线程,多进程 异步非阻塞(存在IO请求): Torna ...

  7. 异步非阻塞IO的Python Web框架--Tornado

    Tornado的全称是Torado Web Server,从名字上就可知它可用作Web服务器,但同时它也是一个Python Web的开发框架.最初是在FriendFeed公司的网站上使用,FaceBo ...

  8. Python web框架 Tornado异步非阻塞

    Python web框架 Tornado异步非阻塞   异步非阻塞 阻塞式:(适用于所有框架,Django,Flask,Tornado,Bottle) 一个请求到来未处理完成,后续一直等待 解决方案: ...

  9. 在nginx启动后,如果我们要操作nginx,要怎么做呢 别增加无谓的上下文切换 异步非阻塞的方式来处理请求 worker的个数为cpu的核数 红黑树

    nginx平台初探(100%) — Nginx开发从入门到精通 http://ten 众所周知,nginx性能高,而nginx的高性能与其架构是分不开的.那么nginx究竟是怎么样的呢?这一节我们先来 ...

随机推荐

  1. 用Python构造ARP请求、扫描、欺骗

    目录 0. ARP介绍 1. Scapy简述 2. Scapy简单演示 2.1 安装 2.2 构造包演示 2.2.1 进入kamene交互界面 2.2.2 查看以太网头部 2.2.3 查看 ICMP ...

  2. MapReduce之Job提交流程源码和切片源码分析

    hadoop2.7.2 MapReduce Job提交源码及切片源码分析 首先从waitForCompletion函数进入 boolean result = job.waitForCompletion ...

  3. Axure实现banner功能

    1.添加一个动态面板,添加上一张.下一张及当前banner对应的序号圆圈,如图所示: 当添加好元素后,实现自动轮播:点击[轮播图面板]页面:选中动态面板:右边添加事件编辑栏——属性——载入时——添加动 ...

  4. linux mint 19.2与Windows 10 双系统硬盘安装与卸载

    安装linux mint 和win10双系统: 1.win10系统下如果没有空闲分区,请从容量较大的分区用partition manager在选中的较大的分区下,调整大小.此步骤最好在pe下的part ...

  5. 【集群监控】Docker上部署Prometheus+Alertmanager+Grafana实现集群监控

    Docker部署 下载 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.re ...

  6. 一个企图用来进行前端交流的qq群

    我建了一个企图用来进行前端交流的qq群! 希望各位前端开发攻城狮们加入! 大佬.小白都欢迎! 禁广告党! 只是想有一个纯净的环境去讨论一下大家遇到的问题和行业前景之类的话题. 661270378 期待 ...

  7. 【Java】 读取Txt文件 处理数据

    @RequestMapping("/importTxt") public String readTxtPoints(@RequestParam("file") ...

  8. CentOS 8 网卡设置

    本次测试环境是在虚拟机上测试 网卡配置文件路径:/etc/sysconfig/network-scripts/ifcfg-ens33 [root@localhost ~]# cd /etc/sysco ...

  9. Vue入门教程 第一篇 (概念及初始化)

    注:为了本教程的准确性,部分描述引用了官网及网络内容. 安装Vue 1.使用npm安装vue: npm install vue 2.下载使用js文件: https://vuejs.org/js/vue ...

  10. 跑的比谁都快 51Nod - 1789

    香港记者跑的比谁都快是众所周知的常识.   现在,香港记者站在一颗有  nn 个点的树的根结点上(即1号点),编号为  ii 的点拥有权值  a[i]a[i] ,数据保证每个点的编号都小于它任意孩子结 ...