现代互联网的很多流量都是承载在 HTTP 协议之上的,伟大的工程师前辈们制作了许多非常优秀的框架/协议,在我们的开发过程中帮助我们减轻了很多的工作,所以在业余时间,我想我们应该更加了解这些框架/协议的工作原理。
因此我构建了这个系列文章,以及 Flaks 项目(没错就是 Flaks – 高射炮),它模仿了一些 Flask 框架的特性(路由、可配置、…)并添加了一个简单的 并行/异步 HTTP 服务器与 CGI 支持;在这个系列文章中会较为详细的讲解该框架的构建流程以及思路,希望大家喜欢。
本人才疏学浅,如果在文章中有任何错误,还请大家不吝指正。
这个系列文章将会由以下几篇文章组成:
- 从
socket
到selectors
选择器 HTTP
请求解析WSGI
与CGI
支持- 生成
HTTP
响应 - 视图函数与路由
- 尝试
asyncio
的协程异步 I/O
Response 类的构建
上一篇文章中我们介绍了 WSGI
与 CGI
的支持,在结尾小结处的框架图中我们可以看到,在应用层处理完毕之后我们需要对应用层的结果进行包装,生成HTTP
响应,进而交予底层的 socket
连接处理数据发送。
回忆一下,在第一篇文章中,我们使用了一个固定的 Hello World 字符串来向客户端返回一个简单的问候;借此,我们来分析一下生成 HTTP
响应所需要的信息,以及思考如何构建我们的响应类。
RESPONSE = \
"""
HTTP/1.1 200 OK\r
Content-Type: text/html\r
Content-Length: 21\r
\r
<h1>Hello World!</h1>
""".encode()
可以看到,如同 HTTP
请求一样,响应遵循着大致相同的格式:
- 首行的第一个字段表示
HTTP
版本号 - 首行的第二个字段表示响应的状态,也就是我们常说的
HTTP
状态码 - 第三个字段用于表示返回状态的说明
- 下面的任意多行表示响应头
- 在一个
CR-LF
字符之后添加需要的相应数据
总结起来大致如此:
[HTTP Version] [Response Code] [Response Message]
[Headers]
[Response data]
因为我们这里只做 HTTP/1.1
版本的支持,而响应消息由响应代码可以确定,所以我们的响应类应该具有如下的功能:
- 接收一个响应代码 –
int
- 可选的接收响应头 –
dict
- 可选的接收响应数据 –
bytes/str
- 将上面的信息打包成为完整的响应 –
bytes
响应头的补充规定
在 RFC2616 中已经详细的规定了 HTTP
响应头的相关规范,我们在 Flaks
项目的服务器部分不强制指定缓存与认证相关的功能,而是交由更上层的应用部分去处理;由此,根据标准,在头部必须指定的两个信息分别为:
Content-Type
– 指定响应内容类型Content-Length
– 指定响应的长度
因此,这两个信息我们单独将处理:
- 内容类型指定一个默认值,当应用层没有给定时防止出现错误语义
- 长度由给定的数据进行计算,不需要应用层进行管理
结合上面所得到的功能,可以给出如下实现:
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
层进行客户端传输:
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
服务器的所有基础架构都已经完成。现在,它可以实现以下功能:
- 接受来自用户的连接请求 – 使用
socket
- 对请求的数据进行解析 – 构建
Request
类实例 - 生成一个请求返回给用户 – 构建
Response
类实例 CGI
与WSGI
应用支持
但是需要注意的是,除了第四点,剩下的三个部分是相互分离的的。这也就意味着,我们最重要的一个部分还没有完成 – Application
应用层,它将负责把以上的所有功能进行耦合,并提供给上层用户一个接口,用以实现“复杂的” Web
应用程序构建。
换句话说,我们已经实现了一个功能完备的 HTTP
伺服器。在下一篇文章中,我们将对前三篇文章的所有产出结果进行调用,并增加最重要的路由绑定与视图函数处理功能。