从零开始制作Web框架(4) – 生成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

Response 类的构建

上一篇文章中我们介绍了 WSGICGI 的支持,在结尾小结处的框架图中我们可以看到,在应用层处理完毕之后我们需要对应用层的结果进行包装,生成HTTP 响应,进而交予底层的 socket 连接处理数据发送。

回忆一下,在第一篇文章中,我们使用了一个固定的 Hello World 字符串来向客户端返回一个简单的问候;借此,我们来分析一下生成 HTTP 响应所需要的信息,以及思考如何构建我们的响应类。

1
2
3
4
5
6
7
8
RESPONSE = \
"""
HTTP/1.1 200 OK\r
Content-Type: text/html\r
Content-Length: 21\r
\r
<h1>Hello World!</h1>
""".encode()

可以看到,如同 HTTP 请求一样,响应遵循着大致相同的格式:

  1. 首行的第一个字段表示 HTTP 版本号
  2. 首行的第二个字段表示响应的状态,也就是我们常说的 HTTP 状态码
  3. 第三个字段用于表示返回状态的说明
  4. 下面的任意多行表示响应头
  5. 在一个 CR-LF 字符之后添加需要的相应数据

总结起来大致如此:

1
2
3
4
[HTTP Version] [Response Code] [Response Message]  
[Headers]

[Response data]

因为我们这里只做 HTTP/1.1 版本的支持,而响应消息由响应代码可以确定,所以我们的响应类应该具有如下的功能:

  • 接收一个响应代码 - int
  • 可选的接收响应头 - dict
  • 可选的接收响应数据 - bytes/str
  • 将上面的信息打包成为完整的响应 - bytes

响应头的补充规定

RFC2616 中已经详细的规定了 HTTP 响应头的相关规范,我们在 Flaks 项目的服务器部分不强制指定缓存与认证相关的功能,而是交由更上层的应用部分去处理;由此,根据标准,在头部必须指定的两个信息分别为:

  1. Content-Type - 指定响应内容类型
  2. Content-Length - 指定响应的长度

因此,这两个信息我们单独将处理:

  1. 内容类型指定一个默认值,当应用层没有给定时防止出现错误语义
  2. 长度由给定的数据进行计算,不需要应用层进行管理

结合上面所得到的功能,可以给出如下实现:

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
34
class Response:
"""
Wrap an HTTP Response packet based on
the return value generated by the Application layer.
Requires WSGI standard server to pass in an
Environ parameter for package generation.
"""

def __init__(self, code: int, data='', environ=None, headers=None,
content_type=settings.DEFAULT_RESPONSE_CONTENT_TYPE):
"""
Initialize a Response object.
Parameters:
code: int - HTTP Response code
data: str - HTTP Response data
environ: dict - WSGI Environ dict
headers: Optional[dict] - Extra header information
content_type: Optional[str] - Return type description
"""
self.code = code
self.data = data
self._extra_hedaers = headers
self._content_type = content_type

# Try to catch environ method
self._environ_method = None
if environ:
self._environ_method = environ.method

# Make sure there's no sth weird send to client
if not code in consts.HTTP_RESPONSE_DESCRIPTIONS.keys():
raise errors.InvalidHTTPResponseCode(code)

self._exception_data()

在这里,我们在程序内部的常量定义中实现了几乎所有可能的 HTTP 状态码及其说明消息,所以在类的构造函数中可以对传入的状态码进行检查;当状态码不正确/未知时,向应用层掷出一个 InvalidHTTPResponseCode 错误。

Response 类的构造函数允许仅仅传入单个的状态码,这是因为在任何非 2xx 的响应中,根据约定,不应该有任何的返回数据被传输。

在最后,我们的 Response 类提供了一个 done 方法,该方法的返回值是字节流 - 也就是打包好之后的响应,它将被交付给 socket 层进行客户端传输:

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
def done(self):
"""
Form a complete HTTP Response package:
HTTP/1.1 200 OK<CR>
Server: Simple-Python-HTTP-Server<CR>
Content-Type: text/plain<CR>
Content-Length: 37<CR>
<CR>
{body}...
if environ.method is HEAD - the {body} part
will not be addin.
"""
baseline = self._make_baseline()
headers = {
"Server": settings.SERVER_NAME,
"Content-Type": self._content_type,
"Content-Length": len(self.data)}

if self._extra_hedaers:
headers.update(self._extra_hedaers)

if self._environ_method == "HEAD":
headers.update({"Content-Length": 0})

response = baseline + self.header_maker(headers)
if not self._environ_method == "HEAD":
response += "\r\n" + self.data
return response.encode()

小结

到此为止我们这个简单 HTTP 服务器的所有基础架构都已经完成。现在,它可以实现以下功能:

  1. 接受来自用户的连接请求 - 使用 socket
  2. 对请求的数据进行解析 - 构建 Request 类实例
  3. 生成一个请求返回给用户 - 构建 Response 类实例
  4. CGIWSGI 应用支持

但是需要注意的是,除了第四点,剩下的三个部分是相互分离的的。这也就意味着,我们最重要的一个部分还没有完成 - Application 应用层,它将负责把以上的所有功能进行耦合,并提供给上层用户一个接口,用以实现“复杂的” Web 应用程序构建。

换句话说,我们已经实现了一个功能完备的 HTTP 伺服器。在下一篇文章中,我们将对前三篇文章的所有产出结果进行调用,并增加最重要的路由绑定与视图函数处理功能。

Author: 桂小方

Permalink: https://init.blog/1963/

文章许可协议:

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

Comments