This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.
现代互联网的很多流量都是承载在 HTTP 协议之上的,伟大的工程师前辈们制作了许多非常优秀的框架/协议,在我们的开发过程中帮助我们减轻了很多的工作,所以在业余时间,我想我们应该更加了解这些框架/协议的工作原理。
因此我构建了这个系列文章,以及 Flaks 项目(没错就是 Flaks – 高射炮),它模仿了一些 Flask 框架的特性(路由、可配置、…)并添加了一个简单的 并行/异步 HTTP 服务器与 CGI 支持;在这个系列文章中会较为详细的讲解该框架的构建流程以及思路,希望大家喜欢。
这是这个系列的第二篇文章,本篇文章我们使用探讨 HTTP 请求的解析过程。
本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。
这个系列文章将会由以下几篇文章组成:
- 从
socket
到selectors
选择器 HTTP
请求解析WSGI
与CGI
支持- 生成
HTTP
响应 - 视图函数与路由
- 尝试
asyncio
的协程异步 I/O
上一篇文章我们已经构建了一个简单的 socket
服务器,通过 selectors
和 threading
的使用使他能够异步+并行处理请求。而从服务器中获取到的活动连接以及客户端信息作为参数传递给了 _handler
函数,并由它向应用层传递。
从这篇文章开始,就进行到框架的设计过程。今天,就来实现应用层的重要功能之一 —— HTTP
包解析,并将其封装到我们的 Request
类中。
除此之外,今天将会介绍 WSGI
与 CGI
标准,并在 Request
类中实现对它们的支持。
HTTP 请求
首先我们来关注标准的 HTTP
请求格式:
1 | [Method] [URL] [HTTP Version] |
可以看到,HTTP 请求分为三个部分:
- 首行的
Request Info
信息 - 不定行数的
Headers
信息 - 空行之后的
Request Data
需要注意的是,这里所有的换行 实质上是两个字符:CR-LF
,也就是\r\n
请求信息
第一行的请求信息组成很简单,在这里给出一个例子:
1 | GET /user?name=%e6%a1%82%e5%b0%8f%e6%96%b9&age=21 HTTP/1.1 |
可以看到,以空格分隔的第一个信息位请求方法,第二个而请求路径,第三个指定协议版本。在这里设计 Request
类的时候需要只注意两个点:
- 检查请求的
Method
是否在全部允许的HTTP Methods
里 -
URL
中含有非ASCII
字符时,需要进行URLDecode
对于第一点,我们可以简单的在框架中加入一个字面量集合,我在 const.py
中加入了这组常量:
1 | ACCEPT_METHODS = { |
多说一句,为什么要设置为集合呢?因为集合的本质是 Hash Table
拥有 O(1)
的访问时间。
对于第二点,我们既然是从零开始制作框架,想要在尽量减少依赖的情况下进行设计,所以决定自己构造一个解码函数(不使用 urllib
和正则表达式)。
根据 HTTP
标准,默认的编码字符集是 UTF-8
,所有的非ASCII
字符都会被编码成 %xx
的形式,所以问题的本质相当于在字符串中寻找以 %
开头的最大连续子串,并从相应的位置进行替换。
而再在这里只需要从左至右单向的遍历,因此可以用一个栈来保存已经解码完成/不需要解码的字符串,使用 (bytes.fromhex()).decode()
函数解码十六进制字符串。这里给出在 request.py
中的 unquote
函数:
1 | @staticmethod |
在这之后,我们可以将请求的首行 parse
成好几段,对于上面这个例子来说:
- 请求路径:
/user
- 请求参数:
{"name": "桂小方", "age": "13"}
- 请求方法:
GET
- 协议版本:
HTTP/1.1
将他们装载到我们的 Request
类中,就可以通过实例化的 request.args
等去查看关于请求的信息了。怎么样,是不是感觉和 Flask
越来越像了呢?
HTTP Headers
接下来是对HTTP
头信息的处理。其实头部是一个简单的键值对而已,如果你用 curl
请求本博客,就会看到请求的头部信息如下:
1 | HTTP/1.1 301 Moved Permanently |
所以在这里我们仅仅需要以 :
对头部信息进行分割,之后存入headers
字典即可:
1 | try: |
请求主体
当请求的方法为 POST
UPDATE
等可以上传资源的方法时,在以上两者信息结束之后会产生一个空行(CR-LF
),并在空行之后给定请求的主体。对于服务器来说,请求主题仅仅是一段无意义的字符串,但是之后要做的事情是根据上文中获得的 Headers
信息对这段信息进行处理,例如:
当 Headers
中的 Content-Type
指定为 text/plain
时,不需要对请求主体做任何事情,但如果是 application/x-www-form-urlencoded
时,则需要进行解码(依照上文的 url_unquote
方法)。
在这里可以建立一个类似向量表的东西,存储在类的静态变量中,将给定的值与对应的操作函数进行绑定,这样就可以扩展性的实现请求主体的处理,例如:
1 | __body_handler = dict({ |
而对于使用者来说,可以轻松的添加一个针对 POST
信息的扩展:
1 | Request.register_body_handler( |
将上述处理过后的信息存入request.body
中,后面就可以通过这个接口访问请求的 body
信息了。
至此,我们将 HTTP
的请求头部信息处理完毕了,具体的全部实现代码大家可以去 Flaks 的 request.py 中查看,下面一篇文章我们将介绍 WSGI
与 CGI
,并使用已经解析过后的头部信息实现对这两个标准的支持。
Comments