![bbec549b26b4e987ed5a8ffaf45b9f0a.png](https://i-blog.csdnimg.cn/blog_migrate/5640dd84281e916927b6d63875f7fe36.jpeg)
想尝试写一个web框架,不是因为Django, Flask, Sanic, tornado等web框架不香, 而是尝试造一个轮子会对框架的认识更深,为了认识更深自然不应该依赖第三方库(仅使用内置库)。
大多数写web框架的文章专注于应用层的实现,比如在wsgi接口的基础上实现web框架,这样当然是没有问题的,就是少了更底层一点的东西,比如不知道request到底怎么来的,但是我也理解如此做法,因为解析http请求实在不是太有意思的内容。
本文主要会从tcp传输开始讲起,依次介绍tcp传输,http协议的解析,路由解析,框架的实现。
而其中框架的实现会分为三个阶段:单线程,多线程,异步IO。
最终的目标就是一个使用上大概类似flask, sanic的框架。
因为http的内容比较多,本文自然也不会实现http协议的所有内容。而且本文也不会实现模板引擎, 因为这个可以单独说一篇文章。
文章目录结构如下:
- TCP传输
- HTTP解析
- 路由
- WEB框架
环境说明
Python: 3.6.8 不依赖任何第三方库
高于此版本应该都可以
HTTP协议
HTTP应该是受众最广的应用层协议了,没有之一。
HTTP协议一般分为两个部分,客户端,服务端。其中客户端一般指浏览器。客户端发送HTTP请求给服务端,服务端根据客户端的请求作出响应。
那么这些请求和响应是什么呢?下面在tcp层面模拟http请求及响应。
TCP传输
HTTP是应用层的协议,而所谓协议自然是一堆约定,比如第一行内容应该怎么写,怎么组织内容的格式。
TCP作为传输层承载着这些内容的传输任务,自然可以在不使用任何http库的情况下,用tcp模拟http请求,或者说发送http请求。所谓传输无非发送(send)接收(recv)。
#socket_http_client.pyimport socketclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)CRLF = b""req = b"GET / HTTP/1.1" + (CRLF * 3)client.connect(("www.baidu.com", 80))client.send(req)resp = b""while True: data = client.recv(1024) if data: resp += data else: breakclient.close()# 查看未解码的前1024的bytesprint(resp[:1024])# 查看解码后的前1024个字符print()print(resp.decode("utf8")[:1024])
输出如下:
b'HTTP/1.1 200 OKAccept-Ranges: bytesCache-Control: no-cacheConnection: keep-aliveContent-Length: 14615Content-Type: text/htmlDate: Wed, 10 Jun 2020 10:14:37 GMTP3p: CP=" OTI DSP COR IVA OUR IND COM "P3p: CP=" OTI DSP COR IVA OUR IND COM "Pragma: no-cacheServer: BWS/1.1Set-Cookie: BAIDUID=32C6E7B012F4DBAAB40756844698B7DF:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BIDUPSID=32C6E7B012F4DBAAB40756844698B7DF; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: PSTM=1591784077; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BAIDUID=32C6E7B012F4DBAA3C9883ABA2DD201E:FG=1; max-age=31536000; expires=Thu, 10-Jun-21 10:14:37 GMT; domain=.baidu.com; path=/; version=1; comment=bdTraceid: 159178407703725358186803341565479700940Vary: Accept-EncodingX-Ua-Compatible: IE=Edge,chrome=1
既然通过tcp就能完成http的客户端的请求,那么完成服务端的实现不也是理所当然么?
#socket_http_server.pyimport socketserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 防止socket关闭之后,系统保留socket一段时间,以致于无法重新绑定同一个端口server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)CRLF = b""host = "127.0.0.1"port = 6666server.bind((host, port))server.listen()print("启动服务器: http://{}:{}".format(host, port))resp = b"HTTP/1.1 200 OK" + (CRLF * 2) + b"Hello world"while True: peer, addr = server.accept() print("客户端来自于: {}".format(str(addr))) data = peer.recv(1024) print("收到请求如下:") print("字节码格式数据") print(data) print() print("字符串格式数据") print(data.decode("utf8")) peer.send(resp) peer.close() # 因为windows没办法ctrl+c取消, 所以这里直接退出了 break
在启动之后,我们可以通过requests进行测试
In [1]: import requestsIn [2]: resp = requests.get("http://127.0.0.1:6666")In [3]: resp.okOut[3]: TrueIn [4]: resp.textOut[4]: 'Hello world'
然后服务端会输出一些信息然后退出。
收到请求如下:字节码格式数据b'GET / HTTP/1.1Host: 127.0.0.1:6666User-Agent: python-requests/2.18.4Accept-Encoding: gzip, deflateAccept: */*Connection: keep-alive'字符串格式数据GET / HTTP/1.1Host: 127.0.0.1:6666User-Agent: python-requests/2.18.4Accept-Encoding: gzip, deflateAccept: */*Connection: keep-alive
这里之所孜孜不倦的既输出bytes也输出str类型的数据, 主要是为了让大家注意到其中的****, 这两个不可见字符很重要。
谁说不可见字符不可见,我在字节码格式数据格式数据中不看到了么?这是一个很有意思的问题呢。
至此,我们知道http(超文本传输协议)就如它的名字一样, 它定义的客户端端应该使用怎样格式的文本发送请求,服务端应该使用怎样格式的文本回应请求。
上面完成了http客户端,服务端的模拟,这里可以进一步将服务端的响应内容做封装,抽象出Response类来
为什么不也抽象出客户端的Request类呢? 因为本文打算写的是web服务端的框架它 : )。
# response.pyfrom collections import namedtupleRESP_STATUS = namedtuple("RESP_STATUS", ["code", "phrase"])CRLF = ""status_ok = RESP_STATUS(200, "ok")status_bad_request = RESP_STATUS(400, "Bad Request")statue_server_error = RESP_STATUS(500, "Internal Server Error")default_header = {"Server": "youerning", "Content-Type": "text/html"}class Response(object): http_version = "HTTP/1.1" def __init__(self, resp_status=status_ok, headers=None, body=None): self.resp_status = resp_status if not headers: headers = default_header if not body: body = "hello world" self.headers = headers self.body = body def to_bytes(self): status_line = "{} {} {}".format(self.http_version, self.resp_status.code, self.resp_status.phrase) header_lines = ["{}: {}".format(k, v) for k,v in self.headers.items()] headers_text = CRLF.join(header_lines) if self.body: headers_text += CRLF message_body = self.body data = CRLF.join([status_line, headers_text, message_body]) return data.encode("utf8")
所以前面的响应可以这么写。
# socket_http_server2.pyimport socketfrom response import Responseserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 防止socket关闭之后,系统保留socket一段时间,以致于无法重新绑定同一个端口server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)CRLF = b""host = "127.0.0.1"port = 6666server.bind((host, port))server.listen()print("启动服务器: http://{}:{}".format(host, port))resp = Response()while True: peer, addr = server.accept() print("客户端来自于: {}".format(str(addr))) data = peer.recv(1024) print("收到请求如下:") print("二进制数据") print(data) print() print("字符串") print(data.decode("utf8")) peer.send(resp.to_bytes()) peer.close() # 因为windows没办法ctrl+c取消, 所以这里直接退出了 break
最终的结果大同小异,唯一的不同是后者的响应中还有http头信息。
关于HTTP请求(Request)及响应(Response)的具体定义可以参考下面链接:
https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5
https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6