hi,你好!欢迎访问本站!登录
本站由网站地图腾讯云宝塔系统阿里云强势驱动
当前位置:首页 - 教程 - 杂谈 - 正文 君子好学,自强不息!

150行代码搭建异步非壅塞Web框架

2019-11-18杂谈搜奇网64°c
A+ A-

近来看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数据

 1 class HttpRequest(object):
 2     def __init__(self, content: bytes):
 3         self.content = content.decode('utf-8')
 4         self.headers = {}
 5         self.GET = {}
 6         self.url = ''
 7         self.full_path = ''
 8         self.body = ''
 9         try:
10             header, self.body = self.content.split('\r\n\r\n')
11             temp = header.split('\r\n')
12             first_line = temp.pop(0)
13             self.method, self.url, self.protocol = first_line.split(' ')
14             self.full_path = self.url
15             for t in temp:
16                 k, v = t.split(': ', 1)
17                 self.headers[k] = v
18         except Exception as e:
19             print(e)
20         if len(self.url.split('?')) > 1: # 剖析GET参数
21             self.url = self.full_path.split('?')[0] # 把url中照顾的参数去掉
22             parms = self.full_path.split('?')[1].split('&')
23             for p in parms: # 将GET参数增添到self.GET字典
24                 k, v = p.split('=')
25                 self.GET[k] = v

在类中,我们完成剖析http要求的headers、method、url和GET参数,实在另有许多事变没有做,比方运用POST传输数据时,数据是在要求体中,针对这部分内容我并没有最先写,缘由在于本文主要目标照样异步非壅塞框架,现在的功用已足以支撑我们举行下一步试验了。

3.2 HttpResponse

HTTP相应也可以分为相应头和相应体,我们可以很简朴的完成一个response:

 1 class HttpResponse(object):
 2     def __init__(self, data: str):
 3         self.status_code = 200 # 默许相应状况 200
 4         self.headers = 'HTTP/1.1 %s OK\r\n'
 5         self.headers += 'Server:AsyncWeb'
 6         self.headers += '\r\n\r\n'
 7         self.data = data
 8 
 9     @property
10     def content(self):
11         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类:

1 class View(object):
2     # CBV应继续View类
3     def dispatch(self, request):
4         method = request.method.lower()
5         if hasattr(self, method):
6             return getattr(self, method)(request)
7         else:
8             return Http404

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

从用户角度来看,完成一个CBV只须要继续View类,然后经由过程定义get、post、delete等要领来完成差别的处置惩罚。

六、socket和多路复用

上面几个小节完成了web框架的大致实行途径,从这节最先我们完成web服务器的中心。

经由过程IO多路复用可以到达单线程完成高并发的效果,一个规范的IO多路复用写法:

 1 server = socket(AF_INET, SOCK_STREAM)
 2 server.bind(("127.0.0.1", 8000))
 3 server.setblocking(False) # 设置非壅塞
 4 server.listen(128)
 5 Future_Task_Wait = {}
 6 rlist = [server, ]
 7 while True:
 8     r, w, x = select.select(rlist, [], [], 0.1)
 9     for o in r:
10         if o == server:
11             '''推断o是server照样conn'''
12             conn, addr = o.accept()
13             conn.setblocking(False) # 设置非壅塞
14             rlist.append(conn) # 客户衔接 到场轮询列表
15         else:
16             data = b""
17             while True: # 吸收客户传输数据
18                 try:
19                     chunk = o.recv(1024)
20                     data = data + chunk
21                 except Exception as e:
22                     chunk = None
23                 if not chunk:
24                     break
25             dosomething(o, data, routers) # 拿到数据干点啥

 经由过程这段代码我们可以获得一切的要求了,下一步就是处置惩罚这些要求。

我们就定义一个dosomething函数

 1 import re
 2 import time
 3 from types import FunctionType
 4 
 5 def dosomething(o, data, routers):
 6     '''剖析http要求,寻觅映照函数并实行获得效果
7 :param o: socket衔接对象 8 :param data: socket吸收数据 9 :return: 相应效果 10 ''' 11 request = HttpRequest(data) 12 print(time.strftime("【%Y-%m-%d %X】",time.localtime()), o.getpeername()[0], 13 request.method, request.url) 14 flag = False 15 for router in routers: 16 if re.match(router[0], request.url): 17 target = router[1] 18 flag = True 19 break 20 if flag: 21 # 推断targe是函数照样类 22 if isinstance(target, FunctionType): 23 result = target(request) 24 elif issubclass(target, View): 25 result = target().dispatch(request) 26 else: 27 result = Http404 28 else: 29 result = Http404 30 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:

1 class Future(object):
2     def __init__(self):
3         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)

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

  1 import re
  2 import time
  3 import select
  4 import asyncio
  5 from socket import *
  6 from threading import Thread
  7 from types import FunctionType
  8 from http.response import Http404, HttpResponse
  9 from http.request import HttpRequest
 10 from views import View
 11 from core.future import Future
 12 
 13 class App(object):
 14     # web应用程序
 15     coroutine_loop = None
 16 
 17     def __new__(cls, *args, **kwargs):
 18         # 运用单例形式
 19         if not hasattr(cls, '_instance'):
 20             App._instance = super().__new__(cls)
 21         return App._instance
 22 
 23     def listen(self, host, port, routers):
 24         # IO多路复用监听衔接
 25         server = socket(AF_INET, SOCK_STREAM)
 26         server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
 27         server.bind((host, port))
 28         server.setblocking(False)
 29         server.listen(128)
 30         Future_Task_Wait = {}
 31         rlist = [server, ]
 32         while True:
 33             r, w, x = select.select(rlist, [], [], 0.01)
 34             for o in r:
 35                 if o == server:
 36                     '''推断o是server照样conn'''
 37                     conn, addr = o.accept()
 38                     conn.setblocking(False)
 39                     rlist.append(conn)
 40                 else:
 41                     data = b""
 42                     while True:
 43                         try:
 44                             chunk = o.recv(1024)
 45                             data = data + chunk
 46                         except Exception as e:
 47                             chunk = None
 48                         if not chunk:
 49                             break
 50                     try:
 51                         request = HttpRequest(data, o)
 52                         print(time.strftime("【%Y-%m-%d %X】",time.localtime()), o.getpeername()[0],
 53                               request.method, request.url)
 54                         flag = False
 55                         for router in routers:
 56                             if re.match(router[0], request.url):
 57                                 target = router[1]
 58                                 flag = True
 59                                 break
 60                         if flag:
 61                             # 推断targe是函数照样类
 62                             if isinstance(target, FunctionType):
 63                                 result = target(request)
 64                             elif issubclass(target, View):
 65                                 result = target().dispatch(request)
 66                             else:
 67                                 result = Http404
 68                         else:
 69                             result = Http404
 70                         # 推断result是不是是future
 71                         if isinstance(result, Future):
 72                             Future_Task_Wait[o] = result
 73                         else:
 74                             o.sendall(result.content)
 75                             rlist.remove(o)
 76                             o.close()
 77                     except Exception as e:
 78                         print(e)
 79             rm_conn = []
 80             for conn, future in Future_Task_Wait.items():
 81                 if future.result:
 82                     try:
 83                         conn.sendall(HttpResponse(data=future.result).content)
 84                     finally:
 85                         rlist.remove(conn)
 86                         conn.close()
 87                         rm_conn.append(conn)
 88             for conn in rm_conn:
 89                 del Future_Task_Wait[conn]
 90 
 91     def run(self, host='127.0.0.1', port=8000, routers=()):
 92         # 主线程select多路复用,处置惩罚http请乞降相应
 93         # 给协程零丁建立一个子线程,负责处置惩罚View函数提交的协程
 94         def start_loop(loop):
 95             asyncio.set_event_loop(loop)
 96             loop.run_forever()
 97         self.coroutine_loop = asyncio.new_event_loop()  # 建立协程事宜轮回
 98         run_loop_thread = Thread(target=start_loop, args=(self.coroutine_loop,))  # 新起线程运转事宜轮回, 防备壅塞主线程
 99         run_loop_thread.start()  # 运转线程,即运转协程事宜轮回
100         self.listen(host, port, routers)

八、框架测试

如今,可以测试我们的web框架了。

 1 import asyncio
 2 from core.server import App
 3 from views import View
 4 from http.response import *
 5 from core.future import Future
 6 
 7 
 8 class IndexView(View):
 9     def get(self, request):
10         return HttpResponse('迎接来到首页')
11 
12     def post(self, request):
13         return HttpResponse('post')
14 
15 def asy(request):
16     future = Future()
17     print('异步挪用')
18     wait = request.url.split('/')[-1]
19     try:
20         wait = int(wait)
21     except:
22         wait = 5
23     asyncio.run_coroutine_threadsafe(dosomething(future, wait), app.coroutine_loop)
24     print('返回Future')
25     return future
26 
27 async def dosomething(future, wait):
28     # 异步函数
29     await asyncio.sleep(wait)# 模仿异步操纵
30     future.result = '守候了%s秒' % wait
31 
32 routers = [
33     ('/$', IndexView),
34     ('/home', asy)
35 ]
36 
37 # 从用户角度只需运用run()
38 app = App()
39 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

  选择打赏方式
微信赞助

打赏

QQ钱包

打赏

支付宝赞助

打赏

  移步手机端
150行代码搭建异步非壅塞Web框架

1、打开你手机的二维码扫描APP
2、扫描左则的二维码
3、点击扫描获得的网址
4、可以在手机端阅读此文章
未定义标签

本文来源:搜奇网

本文地址:https://www.sou7.cn/281966.html

关注我们:微信搜索“搜奇网”添加我为好友

版权声明: 本文仅代表作者个人观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。请记住本站网址https://www.sou7.cn/搜奇网。

发表评论

选填

必填

必填

选填

请拖动滑块解锁
>>