零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

用户应该能够定义几个异步函数返回字符串或响应对象,然后用表示路由的字符串与这些函数配对,最后通过一个函数调用(start_server)开始处理请求。

有了这些设计后,我需要编码来实现这些抽象:

  • 一个可以接受TCP连接和进度的异步函数。
  • 将原始文本解析成某种抽象的容器。
  • 某种机制,可以确定每个请求,哪个函数应该被调用。
  • 将上面所有的集合在一起并提供一个简单的接口给开发者。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

模拟异步连接

为了满足限制(约束),每个HTTP请求是一个单独的 TCP 连接。这会导致请求处理变慢,因为建立多个TCP连接(DNS查询消耗,TCP三次握手消耗,慢启动等)会有相对高的消耗,但是这样更容易模拟。对于这个任务,我选择了asyncio传输协议之上的高级的asyncio-stream模块。我推荐从标准库(stdlib)中签出这段代码,因为阅读这段代码会有乐趣。

HTTPConnection的实例处理多个任务。它使用asyncio.StreamReader对象以增量方式从TCP连接中读取数据并将其存储在缓存中。每次读操作后,它试图解析数据(无论是否在缓存中)并生成一个请求(Request)对象。一旦它接收整个请求,它生成一个回复并通过asyncio.StreamWriter对象发送回客户端。它还处理两个更多的任务︰超时连接和错误处理。

你可以在这里查看这个类的完整代码。我将分开介绍代码的每个部分,为了简洁起见,我删除了描述部分:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

所有代码都是包在一个try-expect代码块中,在解析请求或响应期间抛出的任何异常都会被捕获到,并且一个错误响应会发送回客户端。

请求在一个while循环中会一直被读取,当遇到以下情况会停止:当解析器设置self.request.finished = True时,或客户端关闭了连接(self._reader.at_eof()方法返回True时)。代码尝试在每次循环迭代中从StreamReader中读取数据,并通过调用self.process_data(data)逐步扩展self.request。每次循环读取任何数据的时候,连接超时计时器会被重置。

代码中有一个错误,你能找到吗?我稍后会提到。我同样注意到这个循环有可能消耗掉所有CPU资源,因为没有东西可以读的话,self._reader.read返回b""对象,意味着会不断的循环,却什么也不做。一个可能的解决方案是以非阻塞方式等待一点时间:await asyncio.sleep(0.1)。我会在的确有必要的时候对其进行优化。

还记得我在上一段的开始提到的错误吗?self._reset_conn_timeout方法仅在数据从StreamReader读取时被调用。这种设置方式意味直接第一个字节到来时超时才会开始。如果一个客户端同服务器建立了连接但并不发送任何数据,那么就决不会超时。这可能会耗尽系统资源并引起服务的拒绝访问。解决方法是只需在 init方法中调用self._reset_conn_timeout。

在收到请求时或当连接断开时,代码流就会走到if-else代码块中。这部分代码块会判断是否已经收到的所有数据并完成解析请求,如果是?那么好,产生响应,并将其发送回客户端。如果否?请求有错误发生,抛出异常。最后,调用self.close_connection做一些清理工作。

然析请求的代码是在self.process_data方法中。这段代码非常短且简单,易于测试。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

每次调用将数据积累到self._buffer中并且使用self.http_parser尝试解析任何已存放在缓冲区中的数据。值得在这里指出这段代码展示了一种“依赖注入”的模式。如果你记得init这个初始化函数,你应该知道我传进来一个http_server对象。在这种情况下,http_parser对象是diy_framework包中的一个模块,它有一个parse_into函数,这个函数接受一个Request对象和一个bytearray作为参数。这很有用,原因有两个。首先,这段代码很容易扩展。比如某人想通过一个不同的解析器来使用HTTPConnection,没问题,只要将这个解析器作为参数传递就可以了。其次,它使测试更加容易,因为http_parser不是硬编码的,因此我们可以用假数据来代替会变得非常容易。

下一个有趣部分是响应方法:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

在这里,HTTPConnection实例使用一个来自HTTPServer的路由(router)对象来获取一个生成响应的对象。一个路由可以是任何一个具有get_handler方法的对象,这个方法接受一个字符串参数,并且返回一个可调用的对象或抛出NotFoundException异常。可调用对象是用来处理请求和生成响应。处理程序由使用这个框架的用户来写,就像上面用例中概括的那样,应返回字符串或响应对象。响应对象提供给我们一个友好的接口,因此简单的if代码块确保,无论处理程序返回什么,这段代码最终返回一个统一的响应对象。

接下来,赋值给self._writer的StreamWriter实例被调用,将字节流字符串发送回客户端。在函数返回前,它等待await self._writer.drain,这就保证了所有数据已被发送到客户端。这将确保当仍有未发送的数据在缓冲区中时,对self._writer.close的调用不会发生。

HTTPConnection类有两个有趣的地方:一个关闭连接的方法和一组处理超时机制的方法。首先,关闭一个连接是通过下面这个小函数来完成的:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

任何时候一个连接将被关闭时,代码首先要做的就是取消超时,将它从事件循环中清除掉。

超时机制是一套三个相关的函数︰一个函数,发送错误信息给客户端并且关闭连接的超时器;一个函数,取消当前的超时器;一个函数,调度超时功能。前两个是简单的,出于完整性考虑我添加了它们,我将着重解释下第三个:_reset_conn_timeout。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

每次_reset_conn_timeout被调用,它首先取消任何以前赋值给self._conn_timeout设置的asyncio.Handle对象。然后,使用BaseEventLoop.call_later函数,计划在超时数秒后运行_conn_timeout_close函数。如果你记得handle_request函数的内容,你就会知道每次接收任何数据时,就会调用此函数。在将来,这将取消任何现有的超时并且重新设置_conn_timeout_close函数的超时秒数。只要有数据到来,这个循环就不断重置超时回调。如果在超时秒数内没接收到任何数据,_conn_timeout_close最终将被调用。

创建连接

有些事情不得不创建HTTPConnection对象并且要处理好这个对象。这项任务委托给HTTPServer类,该类是一个非常简单的容器,可以帮助存储一些配置(解析器、路由器和事件循环实例),然后使用该配置来创建HTTPConnection的实例:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

HTTPServer 的每个实例监听在一个端口上。它有一个异步的handle_connection方法,这个方法用来创建HTTPConnection实例并且在事件循环中调试执行它们。这个方法传递给asyncio.start_server作为一个回调函数:每次TCP连接开始的时候被调用(StreamReader和StreamWriter作为参数)。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

这构成了应用程序工作原理的核心:asyncio.start_server接受TCP连接,并在预配置的 HTTPServer 对象上调用一个方法。此方法处理单个连接的所有逻辑:获取请求、解析、生成响应并发送回客户端,最后关闭连接。它侧重于IO逻辑、解析和产生响应。

非核心IO东西我们先不管。

解析请求

这个微型框架的用户被宠坏了,他们不想使用字节流。他们想要一个更高层次的抽象——一种处理请求(requests)更方便的方式。这个微型框架包含了一个简单的HTTP解析器将字节流转换为请求对象。

这些请求对象是看起来像这样的容器:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

它拥有开发者需要的所有,接收来自客户端很容易理解的包中的数据。那么,除了cookies外,身份验证是至关重要的,我将这部分留在第2部分。

每个HTTP请求包含某些所需的部分——比如请求的路径或请求的方法。它还包含某些可选的部分,比如body体,header头部,或URL参数。此外,由于REST的流行,URL,省略URL参数,还可能包含部分信息。比如,"/users/1/edit"包含了用户的id。

每个请求的各个部分必须被识别,解析并被赋值给一个请求(Request)对象的各个属性。HTTP/1.1是文本协议,简化了过程(HTTP/2是二进制协议-相当有趣)。

http_parser模块中是一组函数,因为解析器不需要跟踪状态。相反,调用代码需要管理一个请求(Request)对象并将其和bytearray(包含请求的原始字节)一起传递到parse_into函数中。为此,解析器修改请求对象以及bytearray缓冲区内容。请求对象获取越来越多数据,而ytearray缓冲区逐渐变空。

Http_parser模块的核心功能是在parse_into函数中:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

正如你在上面代码中看到的那样,我将解析过程划分为三个部分:解析请求行(请求行有这样的格式:GET /resource HTTP/1.1),解析头部和解析请求body体。

请求行包含的 HTTP 方法和 URL。URL按序还包含更多的信息:路径、url参数和开发人员自定义的url参数。解析出方法和URL很容易——重要的是适当地分割字符串。urlparse.parse函数用于解析URL参数。开发人员定义的url参数使用正则表达式提取。

接下来是HTTP头部。这些都是键/值对形式的简单文本。问题在于,有可能多个头部有相同名称但值不同。一个需要注意的header头部是Content-Length,它指定了body体的长度(不是整个请求,仅仅是body体),对于决定是否解析body体来说是重要的。

最后,解析器根据HTTP方法和头部来决定是否解析请求的body体。

路由

在某种意义上,路由是框架和用户间连接的桥,用户用合适的方法创建一个路由(Router)对象,这个对象由路径/函数对组成,然后将路由对象赋值给App对象。这个App对象按顺序调用get_handler函数生成一个响应。总之,路由负责两件事情——存储路径/函数对和返回所要求的一对。

在路由(Router)类中有两个方法允许最终开发人员增加路由:add_routes和add_route。由于add_routes是在add_route上的一层封装,我将略过它,将注意力集中在 add_route上。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

使用Router.build_route_regexp类方法首先将一个路由(一个字符串,类似"/cars/{id}")编译成一个已编译的正则表达式对象。这些已编译正则表达式对象匹配请求路径和提取指定路由的开发人员定义的URL参数。下一步如果有相同的路由存在,则会抛出异常,最终,路由/处理函数对被加入到一个简单的字典中——self.routes。

这里展示了Router如何编译路由(routes)的:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

该方法使用正则表达式来替换命名的正则表达式组"(?P<variable>...)"中"{variable}"出现的位置。然后在结果字符串的开头和结尾加上^和$标记,最后编译正则表达式对象。

存储一条路由仅仅成功了一半,这里是如何获得一条路由:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

一旦App对象获得一个请求对象,就会获知URL的路径部分(比如,/users/15/edit)。然后需要一个匹配函数生成响应或404错误。get_handler将路径作为参数,循环遍历路由,在每个路由上调用Router.match_path类方法来检测是否有已编译的正则对象匹配请求的路径。如果存在,则调用HandleWrapper来包装这个路由函数。path_params字典包含了路径变量(比如,/users/15/edit中的"15")或如果路由不指定任何变量的话则为空。最后返回包装过的路由函数给App对象。

如果代码迭代遍历整个路由,没有找到匹配路径的,函数抛出NotFoundException异常。

这个Route.match类方法简单:

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

使用正则对象的匹配方法检测路由与路径是否匹配。如果不匹配则返回None。

最后,我们使用HandleWrapper类。它唯一的工作就是包装一个异步函数,存储path_params字典,通过handle方法对外提供一个统一的接口。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

整合所有

框架的最后部分是将所有部分集合在一起——App类。

App类旨在收集所有的配置详细信息。App对象使用它的单个方法start_server,使用一些配置数据创建HTTPServer实例,然后将其传递给函数asyncio.start_server,这里查看。每一个进来的TCP连接,asyncio.start_server函数调用HTTPServer对象的handle_connection方法。

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

吸取的经验教训

如果你看了整个源码,你会注意到如果不算上测试代码的话,整个框架大致320行代码(算上测试代码,大约540行代码)。确实令我感到惊讶,如此少的代码满足如此多的功能。当然,这个框架还没提供一些有用的功能比如模板,身份验证或数据库访问,不过,这些工作会非常有趣:)。这同样给了我一些想法关于其他框架,比如Django或Tornado是如何工作在一个一般水平的并且我能够快速调试它们。

这也是我以TDD方式(测试驱动开发(Test-Driven Development))做的第一个项目,过程是愉快和高效的。编写测试首先迫使我思考设计和架构而不仅仅是将能工作的代码粘合起来。不要误会我的意思,有很多情况,后者的方法是首选的,但是,如果你对不可维护的代码很重视,你和其他人在未来的数周或数月可以很好的工作,那么TDD确实是你需要的。

我研究一些东西像清晰架构和依赖注入,很明显,路由(Router)类是一个更高级别的接近“核心”的抽象(实体?),然而,像http_parser或App是处在边缘上的,它们要么处理极小的字符串,要么是字节流,要么是中级IO。然而,TDD迫使我去单独思考每个小部分,这让我问自己这样的问题:是否这些方法调用的组合是可理解的?是否类名准确的反映了我正在解决的问题?是否容易区分我代码中不同级别的抽象?

来吧,写一个小的框架,充满乐趣 !

最后,想学习Python的小伙伴们!

请关注+私信回复:“学习”就可以拿到一份我为大家准备的Python学习资料!

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

pytyhon学习资料

零基础搭建一个Web框架 python菜鸟必看(文末附赠教程分享)

python学习资料

相关推荐