python web框架_从tcp开始,用Python写一个web框架-TCP传输篇

bbec549b26b4e987ed5a8ffaf45b9f0a.png

想尝试写一个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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值