从零开始制作Web框架(2) – HTTP请求解析

tech

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 请求的解析过程。

本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。

这个系列文章将会由以下几篇文章组成:

  1. socketselectors 选择器
  2. HTTP 请求解析
  3. WSGICGI 支持
  4. 生成 HTTP 响应
  5. 视图函数与路由
  6. 尝试 asyncio 的协程异步 I/O

上一篇文章我们已经构建了一个简单的 socket 服务器,通过 selectorsthreading 的使用使他能够异步+并行处理请求。而从服务器中获取到的活动连接以及客户端信息作为参数传递给了 _handler 函数,并由它向应用层传递。

从这篇文章开始,就进行到框架的设计过程。今天,就来实现应用层的重要功能之一 —— HTTP 包解析,并将其封装到我们的 Request 类中。

除此之外,今天将会介绍 WSGICGI 标准,并在 Request 类中实现对它们的支持。

HTTP 请求

首先我们来关注标准的 HTTP 请求格式:

1
2
3
4
[Method] [URL] [HTTP Version]  
[Headers]

[Request data]

可以看到,HTTP 请求分为三个部分:

  1. 首行的 Request Info 信息
  2. 不定行数的 Headers 信息
  3. 空行之后的 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
2
3
4
5
ACCEPT_METHODS = {
"GET", "HEAD", "POST", "PUT",
"DELETE", "CONNECT", "OPTIONS",
"TRACE", "PATCH"
}

多说一句,为什么要设置为集合呢?因为集合的本质是 Hash Table 拥有 O(1) 的访问时间。

对于第二点,我们既然是从零开始制作框架,想要在尽量减少依赖的情况下进行设计,所以决定自己构造一个解码函数(不使用 urllib 和正则表达式)。

根据 HTTP 标准,默认的编码字符集是 UTF-8,所有的非ASCII字符都会被编码成 %xx 的形式,所以问题的本质相当于在字符串中寻找以 % 开头的最大连续子串,并从相应的位置进行替换。

而再在这里只需要从左至右单向的遍历,因此可以用一个栈来保存已经解码完成/不需要解码的字符串,使用 (bytes.fromhex()).decode() 函数解码十六进制字符串。这里给出在 request.py 中的 unquote 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@staticmethod
def unquote(encoded: str, encoding: str = "utf-8") -> str:
"""
Unquto url encoded string like:
"Hello%20world" -> "Hello world"

Parameters:
encoded: str - url encoded string
Usage:
unquote(encoded: str) -> decoded: str
"""

index, decoded = 0, list()
while index < len(encoded):
hexchars = list()

# Find for normal char
if encoded[index] != '%':
decoded.append(encoded[index])
index += 1
continue

# When find '%' - try to maximum match
while index < len(encoded) and encoded[index] == '%':
hexchars.append(encoded[index + 1])
hexchars.append(encoded[index + 2])
index += 3

# Then decode from hex
word = bytes.fromhex(''.join(hexchars)).decode(encoding)
decoded.append(word)

return ''.join(decoded)

在这之后,我们可以将请求的首行 parse 成好几段,对于上面这个例子来说:

  • 请求路径: /user
  • 请求参数: {"name": "桂小方", "age": "13"}
  • 请求方法: GET
  • 协议版本: HTTP/1.1

将他们装载到我们的 Request 类中,就可以通过实例化的 request.args 等去查看关于请求的信息了。怎么样,是不是感觉和 Flask 越来越像了呢?

HTTP Headers

接下来是对HTTP头信息的处理。其实头部是一个简单的键值对而已,如果你用 curl 请求本博客,就会看到请求的头部信息如下:

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 301 Moved Permanently
Date: Tue, 14 Apr 2020 03:41:36 GMT
Connection: keep-alive
Cache-Control: max-age=3600
Expires: Tue, 14 Apr 2020 04:41:36 GMT
Location: https://init.blog/
X-Content-Type-Options: nosniff
Server: cloudflare
CF-RAY: 583a697fa8c5c867-AMS
alt-svc: h3-27=":443"; ma=86400, h3-25=":443"; ma=86400, h3-24=":443"; ma=86400, h3-23=":443"; ma=86400

所以在这里我们仅仅需要以 : 对头部信息进行分割,之后存入headers 字典即可:

1
2
3
4
5
6
7
8
9
10
11
try:
headers = list()
line = rawreq.pop(0).strip()
while line:
headers.append(tuple(line.split(": ")))
if not rawreq:
break
line = rawreq.pop(0).strip()
self.headers.update(dict(headers))
except ValueError as _error:
raise errors.InvalidRequest(rawreq)

请求主体

当请求的方法为 POST UPDATE 等可以上传资源的方法时,在以上两者信息结束之后会产生一个空行(CR-LF),并在空行之后给定请求的主体。对于服务器来说,请求主题仅仅是一段无意义的字符串,但是之后要做的事情是根据上文中获得的 Headers 信息对这段信息进行处理,例如:

Headers 中的 Content-Type 指定为 text/plain 时,不需要对请求主体做任何事情,但如果是 application/x-www-form-urlencoded 时,则需要进行解码(依照上文的 url_unquote 方法)。

在这里可以建立一个类似向量表的东西,存储在类的静态变量中,将给定的值与对应的操作函数进行绑定,这样就可以扩展性的实现请求主体的处理,例如:

1
2
3
4
5
6
__body_handler = dict({
"text/html": lambda body: body,
"text/plain": lambda body: body,
"text/css": lambda body: body,
"text/javascript": lambda body: body
})

而对于使用者来说,可以轻松的添加一个针对 POST 信息的扩展:

1
2
3
Request.register_body_handler(
"application/x-www-form-urlencoded",
Request.url_decode)

将上述处理过后的信息存入request.body中,后面就可以通过这个接口访问请求的 body 信息了。

至此,我们将 HTTP 的请求头部信息处理完毕了,具体的全部实现代码大家可以去 Flaks 的 request.py 中查看,下面一篇文章我们将介绍 WSGICGI,并使用已经解析过后的头部信息实现对这两个标准的支持。

Author: 桂小方

Permalink: https://init.blog/1920/

文章许可协议:

如果你觉得文章对你有帮助,可以 支持我

Comments