从头开始写一个简单的Python框架
从头开始写一个简单的Python框架,你为什么想搭建一个Web框架?我想有下面几个原因:
有一个新奇的想法,将会取代其他框架。
获得一些疯狂的街头信誉。
你的问题比较独特,现有的框架不适合。
你想成为一位更好的Web开发者,你对Web框架是如何运行的感到好奇。
我将集中精力在最后一点上。这篇文章旨在描述我从写一个小型的服务框架中学到了什么,我将解释框架的设计,以及如何一步一步,一个函数一个函数的实现这个框架的。关于此项目完整的代码可以点击此链接。
我希望我的行为可以鼓励大家去尝试因为真的非常有趣,我们可以从中学到很多关于web应用程序是如何工作的知识,而且比我想象的要容易的多。
范围
框架的功能有:请求-响应周期、身份验证、数据库访问、模板的生成等。Web开发者使用框架,因为大多数Web应用程序共享大量的相同功能,并且没必要为每个项目都重新实现这些功能。
像Rails或Django这些较大的框架做了高层次的抽象并且功能完备。这些框架经历了很长时间来完成所有这些特性,因此,我们重点完成一个微型框架。开始写代码前,我先列一下这个微型框架的功能及一些限制。
功能:
可以处理GET和POST的HTTP请求。从该WIKI中你可以了解获得关于HTTP简介。
异步的(我喜欢Python3asyncio这个模块)。
包含简单的路由逻辑,以及参数捕获。
像其他酷的微框架一样,提供简单的用户级API。
可以处理身份验证,因为学会会非常的酷(在第2部分介绍)。
限制:
仅完成HTTP/1.1协议的一小部分:不实现transfer-encoding,http-auth,content-encoding(gzip),persistantconnections(持久连接)这些功能。
响应信息中无MIME-guessing,用户将不得不手动设置。
无WSGI-只是简单的TCP连接处理。
不支持数据库。
用户应该能够定义几个异步函数返回字符串或响应对象,然后用表示路由的字符串与这些函数配对,最后通过一个函数调用(start_server)开始处理请求。
有了这些设计后,我需要编码来实现这些抽象:
一个可以接受TCP连接和进度的异步函数。
将原始文本解析成某种抽象的容器。
某种机制,可以确定每个请求,哪个函数应该被调用。
将上面所有的集合在一起并提供一个简单的接口给开发者。
我针对每个功能点开始写一些必要的抽象描述。在几次重构后,页面布局被分成几个部分。每部分是相对独立的,这种情况下特别好,因为每一部分都可以自行研究学习。这些是我上面列出的抽象的具体体现:
一个HTTPServer对象,需要一个Router对象和一个http_parser模块,并使用他们来初始化。
HTTPConnection对象,每个表示一个单独的客户端HTTP连接并且处理请求-响应周期:使用http_parser模块解析进来的字节流到一个Request对象;使用一个Router实例找到正确的函数调用产生一个响应;最后将这个响应发送回客户端。
一对Request和Response对象提供给用户一种友好的方式来处理本质上是字节流的字符串形式。用户不必了解正确的信息格式或分隔符。
一个Router对象,包含路由功能:函数对。它提供一种增加这些函数对的方法,并且提供了一种给定URL路径,找到对应函数的方法。
最后,一个包含配置文件的App对象,并使用它来实例化一个HTTPServer实例。
让我们仔细分析每一部分,从HTTPConnection开始。
模拟异步连接
为了满足限制(约束),每个HTTP请求是一个单独的TCP连接。这会导致请求处理变慢,因为建立多个TCP连接(DNS查询消耗,TCP三次握手消耗,慢启动等)会有相对高的消耗,但是这样更容易模拟。对于这个任务,我选择了asyncio传输协议之上的高级的asyncio-stream模块。我推荐从标准库(stdlib)中签出这段代码,因为阅读这段代码会有乐趣。
HTTPConnection的实例处理多个任务。它使用asyncio.StreamReader对象以增量方式从TCP连接中读取数据并将其存储在缓存中。每次读操作后,它试图解析数据(无论是否在缓存中)并生成一个请求(Request)对象。一旦它接收整个请求,它生成一个回复并通过asyncio.StreamWriter对象发送回客户端。它还处理两个更多的任务︰超时连接和错误处理。
你可以在这里查看这个类的完整代码。我将分开介绍代码的每个部分,为了简洁起见,我删除了描述部分:
这个init方法比较简单,它仅收集了些对象,后面会用到。存储了router,http_parser和loop对象,分别用作生成响应,解析请求和在事件循环中安排进度。
下一步,存储了reader-writer,一起代表一个TCP连接;一个空的bytearray,充当原始字节的缓冲区。_conn_timeout存储了asyncio.Handle的实例,用来管理超时逻辑。最后,它还存储了一个单独的请求(Request)实例。
下面的代码处理接收和发送数据的核心功能:
所有代码都是包在一个try-expect代码块中,在解析请求或响应期间抛出的任何异常都会被捕获到,并且一个错误响应会发送回客户端。
请求在一个while循环中会一直被读取,当遇到以下情况会停止:当解析器设置self.request.finished=True时,或客户端关闭了连接(self._reader.at_eof()方法返回True时)。代码尝试在每次循环迭代中从StreamReader中读取数据,并通过调用self.process_data(data)逐步扩展self.request。每次循环读取任何数据的时候,连接超时计时器会被重置。
代码中有一个错误,你能找到吗?我稍后会提到。我同样注意到这个循环有可能消耗掉所有CPU资源,因为没有东西可以读的话,self._reader.read()返回b''对象,意味着会不断的循环,却什么也不做。一个可能的解决方案是以非阻塞方式等待一点时间:awaitasyncio.sleep(0.1)。我会在的确有必要的时候对其进行优化。
还记得我在上一段的开始提到的错误吗?self._reset_conn_timeout()方法仅在数据从StreamReader读取时被调用。这种设置方式意味直接第一个字节到来时超时才会开始。如果一个客户端同服务器建立了连接但并不发送任何数据,那么就决不会超时。这可能会耗尽系统资源并引起服务的拒绝访问。解决方法是只需在init方法中调用self._reset_conn_timeout()。
在收到请求时或当连接断开时,代码流就会走到if-else代码块中。这部分代码块会判断是否已经收到的所有数据并完成解析请求,如果是?那么好,产生响应,并将其发送回客户端。如果否?请求有错误发生,抛出异常。最后,调用self.close_connection做一些清理工作。
在这里,HTTPConnection实例使用一个来自HTTPServer的路由(router)对象来获取一个生成响应的对象。一个路由可以是任何一个具有get_handler方法的对象,这个方法接受一个字符串参数,并且返回一个可调用的对象或抛出NotFoundException异常。可调用对象是用来处理请求和生成响应。处理程序由使用这个框架的用户来写,就像上面用例中概括的那样,应返回字符串或响应对象。响应对象提供给我们一个友好的接口,因此简单的if代码块确保,无论处理程序返回什么,这段代码最终返回一个统一的响应对象。
接下来,赋值给self._writer的StreamWriter实例被调用,将字节流字符串发送回客户端。在函数返回前,它等待awaitself._writer.drain(),这就保证了所有数据已被发送到客户端。这将确保当仍有未发送的数据在缓冲区中时,对self._writer.close的调用不会发生。
每次_reset_conn_timeout被调用,它首先取消任何以前赋值给self._conn_timeout设置的asyncio.Handle对象。然后,使用BaseEventLoop.call_later函数,计划在超时数秒后运行_conn_timeout_close函数。如果你记得handle_request函数的内容,你就会知道每次接收任何数据时,就会调用此函数。在将来,这将取消任何现有的超时并且重新设置_conn_timeout_close函数的超时秒数。只要有数据到来,这个循环就不断重置超时回调。如果在超时秒数内没接收到任何数据,_conn_timeout_close最终将被调用。
创建连接
有些事情不得不创建HTTPConnection对象并且要处理好这个对象。这项任务委托给HTTPServer类,该类是一个非常简单的容器,可以帮助存储一些配置(解析器、路由器和事件循环实例),然后使用该配置来创建。
Python培训、Python培训班、Python培训机构,就选光环大数据!
还不够过瘾?想学习更多?点击 http://hadoop.aura.cn/python/ 进行Python学习!
大数据培训、人工智能培训、Python培训、大数据培训机构、大数据培训班、数据分析培训、大数据可视化培训,就选光环大数据!光环大数据,聘请专业的大数据领域知名讲师,确保教学的整体质量与教学水准。讲师团及时掌握时代潮流技术,将前沿技能融入教学中,确保学生所学知识顺应时代所需。通过深入浅出、通俗易懂的教学方式,指导学生更快的掌握技能知识,成就上万个高薪就业学子。 更多问题咨询,欢迎点击------>>>>在线客服!