用一个文件实现迷你 Web 框架,技术细节一览无遗
ccwgpt 2024-09-26 07:39 23 浏览 0 评论
当下网络就如同空气一样在我们的周围,它以无数种方式改变着我们的生活,但要说网络的核心技术变化甚微。
随着开源文化的蓬勃发展,诞生了诸多优秀的开源 Web 框架,让我们的开发变得轻松。但同时也让我们不敢停下学习新框架的脚步,其实万变不离其宗,只要理解了 Web 框架的核心技术部分,当有一个新的框架出来的时候,基础部分大同小异只需要重点了解:它有哪些特点,用到了哪些技术解决了什么痛点?这样接受和理解起新技术来会更加得心应手,不至于疲于奔命。
还有那些只会用 Web 框架的同学,是否无数次打开框架的源码,想学习提高却无从下手?
今天我们就抽丝剥茧、去繁存简,用一个文件,实现一个迷你 Web 框架,从而把其核心技术部分清晰地讲解清楚,配套的源码均已开源。
GitHub 地址:github.com/521xueweihan/OneFile
如果你觉得我做的这件事对你有帮助,就请给我一个 ?Star,多多转发让更多人受益。
闲言少叙,下面就开始我们今天的提高之旅。
一、介绍原理
说到 Web 不得不提的就是网络协议,如果我们从 OSI 七层网络模型开始,我敢断定看完的绝对不超过三成!
所以今天我们就直接聊最上面的一层,也就是 Web 框架接触最多的 HTTP 应用层,至于 TCP/IP 部分会在聊 socket 的时候粗略带过。期间我会刻意打码非必要讲解技术的细枝末节,切断远离本期主题的技术话题,一个文件只讲一个技术点!绝不拖堂请大家放心阅读。
首先让我们先回忆下,平常浏览网站的流程。
如果我们把在网上冲浪,比做在一间教室听课,那么老师就是服务器(server),学生就是客户端(client)。当同学有问题的时候会先举手(请求建立 TCP),老师发现学生的提问请求,同意学生回答问题后,学生起立提出问题(发送请求),如果老师承诺会给提问的学生加课堂表现分,那么提问的时候就需要有个高效的提问方式(请求格式),即:
- 先报学号
- 再提问题
师接收到学生的提问后就可以立即回答问题,无需再问学号(返回响应),回答格式(响应格式)如下:
- 后回答问题
- 根据学号加分!
有了约定好的提问格式(协议),就可以省去老师每次询问学生的学号,即高效又严谨。最后,老师回答完问题让学生坐下(关闭连接)。
其实,我们在网络上通信流程也大致如此:
只不过机器执行起来更加严格,大家都是遵循某种协议来开发软件,这样就可以实现在某种协议下进行通信,而这种网络通信协议就叫做 HTTP(超文本传输协议)。
而我们要做的 Web 框架就是处理上面的流程:建立连接、接收请求、解析请求、处理请求、返回请求。
原理部分就聊这么多,目前你只需要记住网络上通信分为两大步:建立连接(用于通信)和处理请求。
所谓框架就是处理大多数情况下要处理的事情,所以我们要写的 Web 框架也就是处理两件事,即:
- 处理连接(socket)
- 处理请求(request)
一定要记住:连接和请求是两个东西,建立起连接才能发送请求。
而想要建立连接发起通信,就需要通过 socket 来实现(建立连接),socket 可以理解为两个虚拟的本子(文件句柄),通信的双方人手一个,它既可以读也可以写,只要把传输的内容写到本子上(处理请求),对方就可以看到了。
下面我把 Web 框架分成两部分进行讲解,所有代码将采用简单易懂的 Python3 进行实现。
二、编写 Web 框架
代码+注释一共 457 行,请放心绝对简单易懂。
2.1 处理连接(HTTPServer)
这里需要简单聊一下 socket 这个东西,在编程语言层面它就是一个类库,负责搞定连接建立网络通信。但本质上是系统级别提供通信的进程,而一台电脑可以建立多条通信线路,所以每一个端口号后面都是一个 socket 进程,它们相互独立、互不干涉,这也是为什么我们在启动服务的时候要指定端口号的原因。
最后,上面所说的服务器其实就是一台性能好一点、一直开着的电脑,而客户端就是浏览器、手机、电脑,它们都有 socket 这个东西(操作系统级别的一个进程)。
如果上面这段话没有看懂也不碍事,能看懂下面的图就行,得搞明白 socket 处理连接的步骤和流程,才能编写 Web 框架处理连接的部分。
下面分别展示基于 socket 编写的 server.py 和 client.py 代码。
# coding: utf-8
# 服务器端代码(server.py)
import socket
print('我是服务端!')
HOST = ''
PORT = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建 TCP socket 对象
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重启时释放端口
s.bind((HOST, PORT)) # 绑定地址
s.listen(1) # 监听TCP,1代表:操作系统可以挂起(未处理请求时等待状态)的最大连接数量。该值至少为1
print('监听端口:', PORT)
while 1:
conn, _ = s.accept() # 开始被动接受TCP客户端的连接。
data = conn.recv(1024) # 接收TCP数据,1024表示缓冲区的大小
print('接收到:', repr(data))
conn.sendall(b'Hi, '+data) # 给客户端发送数据
conn.close()
因为 HTTP 是建立在相对可靠的 TCP 协议上,所以这里创建的是 TCP socket 对象。
# coding: utf-8
# 客户端代码(client.py)
import socket
print('我是客户端!')
HOST = 'localhost' # 服务器的IP
PORT = 50007 # 需要连接的服务器的端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
print("发送'HelloGitHub'")
s.sendall(b'HelloGitHub') # 发送‘HelloGitHub’给服务器
data = s.recv(1024)
s.close()
print('接收到', repr(data)) # 打印从服务器接收回来的数据
运行效果如下:
结合上面的代码,可以更加容易理解 socket 建立通信的流程:
- socket:创建socket
- bind:绑定端口号
- listen:开始监听
- accept:接收请求
- recv:接收数据
- close:关闭连接
所以,Web 框架中处理连接的 HTTPServer 类要做的事情就呼之欲出了。即: 一开始在 __init__方法中创建 socket,接着绑定端口(server_bind)然后开始监听端口(server_activate)
# 处理连接进行数据通信
class HTTPServer(object):
def __init__(self, server_address, RequestHandlerClass):
self.server_address = server_address # 服务器地址
self.RequestHandlerClass = RequestHandlerClass # 处理请求的类
# 创建 TCP Socket
self.socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
# 绑定 socket 和端口
self.server_bind()
# 开始监听端口
self.server_activate()
通过传入的 RequestHandlerClass 参数可以看出,处理请求与建立连接是分开处理。
下面就要开始启动服务接收请求了,也就是 HTTPServer 的启动方法 serve_forever,这里包含了接收请求、接收数据、开始处理请求、结束请求的全过程。
def serve_forever(self):
while True:
ready = selector.select(poll_interval)
# 当客户端请求的数据到位,则执行下一步
if ready:
# 有准备好的可读文件句柄,则与客户端的链接建立完毕
request, client_address = self.socket.accept()
# 可以进行下面的处理请求了,通过 RequestHandlerClass 处理请求和连接独立
self.RequestHandlerClass(request, client_address, self)
# 关闭连接
self.socket.close()
如此循环下去,就是 HTTPServer 处理连接、建立起 HTTP 连接的全部代码,就这?对!是不是很简单?
代码中的 RequestHandlerClass 形参是处理请求的类,下面将深入讲解其对应的 HTTPRequestHandler 是如何处理 HTTP 请求。
2.2 处理请求(HTTPRequestHandler)
还记得上面介绍的 socket 如何实现两端通信吗?通过两个可读、可写的“虚拟本子”。
再加上还要保证通信的高效和严谨,就需要有对应的“通信格式”。
所以,处理请求只需要三步走:
- setup:初始化两个本子 读请求的文件句柄(rfile)写响应的文件句柄(wfile)
- handle:读取并解析请求、处理请求、构造响应并写入
- finish:返回响应,销毁两个本子释放资源,然后尘归尘土归土,等待下个请求
对应的代码:
# 处理请求
class HTTPRequestHandler(object):
def __init__(self, request, client_address, server):
self.request = request # 接收来的请求(socket)
# 1、初始化两个本子
self.setup()
try:
# 2、读取、解析、处理请求,构造响应
self.handle()
finally:
# 3、返回响应,释放资源
self.finish()
def setup(self):
self.rfile = self.request.makefile('rb', -1) # 读请求的本子
self.wfile = self.request.makefile('wb', 0) # 写响应的本子
def handle(self):
# 根据 HTTP 协议,解析请求
# 具体的处理逻辑,即业务逻辑
# 构造响应并写入本子
def finish(self):
# 返回响应
self.wfile.flush()
# 关闭请求和响应的句柄,释放资源
self.wfile.close()
self.rfile.close()
以上就是处理请求的整体流程,下面将详细介绍 handle 如何解析 HTTP 请求和构造 HTTP 响应,以及如何实现把框架和具体的业务代码(处理逻辑)分开。
在解析 HTTP 之前,需要先看一个实际的 HTTP 请求,当我打开 hellogithub.com 网站首页的时候,浏览器发送的 HTTP 请求如下:
整理归纳可得 HTTP 请求格式,如下:
{HTTP method} {PATH} {HTTP version}\r\n
{header field name}:{field value}\r\n
...
\r\n
{request body}
得到了请求格式,那么 handle 解析请求的方法也就有了。
def handle(self):
# --- 开始解析 --- #
self.raw_requestline = self.rfile.readline(65537) # 读取请求第一行数据,即请求头
requestline = str(self.raw_requestline, 'iso-8859-1') # 转码
requestline = requestline.rstrip('\r\n') # 去换行和空白行
# 就可以得到 "GET / HTTP/1.1" 请求头了,下面开始解析
self.command, self.path, self.request_version = requestline.split()
# 根据空格分割字符串,可得到("GET", "/", "HTTP/1.1")
# command 对应的是 HTTP method,path 对应的是请求路径
# request_version 对应 HTTP 版本,不同版本解析规则不一样这里不做展开讲解
self.headers = self.parse_headers() # 解析请求头也是处理字符串,但更为复杂标准库有工具函数这里略过
# --- 业务逻辑 --- #
# do_HTTP_method 对应到具体的处理函数
mname = ('do_' + self.command).lower()
method = getattr(self, mname)
# 调用对应的处理方法
method()
# --- 返回响应 --- #
self.wfile.flush()
def do_GET(self):
# 根据 path 区别处理
if self.path == '/':
self.send_response(200) # status code
# 加入响应 header
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.end_headers() # 结束头部分,即:'\r\n'
self.wfile.write(content.encode('utf-8')) # 写入响应 body,即:页面内容
def send_response(self, code, message=None):
# 响应体格式
"""
{HTTP version} {status code} {status phrase}\r\n
{header field name}:{field value}\r\n
...
\r\n
{response body}
"""
# 写响应头行
self.wfile.write("%s %d %s\r\n" % ("HTTP/1.1", code, message))
# 加入响应 header
self.send_header('Server', "HG/Python ")
self.send_header('Date', self.date_time_string())
以上就是 handle 处理请求和返回响应的核心代码片段了,至此 HTTPRequestHandler 全部内容均已讲解完毕,下面将演示运行效果。
2.3 运行
class RequestHandler(HTTPRequestHandler):
# 处理 GET 请求
def do_get(self):
# 根据 path 对应到具体的处理方法
if self.path == '/':
self.handle_index()
elif self.path.startswith('/favicon'):
self.handle_favicon()
else:
self.send_error(404)
if __name__ == '__main__':
server = HTTPServer(('', 8080), RequestHandler)
# 启动服务
server.serve_forever()
这里通过继承 Web 框架的 HTTPRequestHandler 实现的子类 RequestHandler 重写 do_get 方法,实现业务代码和框架的分离。这样保证了框架的灵活性和解耦。
接下来服务毫无意外地运行起来了,效果如下:
本文中涉及 Web 框架的代码,为方便阅读都经过了简化。如果想要获取完整可运行的代码,可前往 GitHub 地址获取:
github.com/521xueweihan/OneFile/blob/main/src/python/web-server.py
该框架并不包含 Web 框架应有的丰富功能,旨在通过最简单的代码,实现一个迷你 Web 框架,让不了解基本 Web 框架结构的同学,得以一探究竟。
如果本文的内容勾起了你对 Web 框架的兴趣,你还想更加深入的了解更加全面、适用于生产环境、代码和结构同样的简洁的 Web 框架。我建议的学习路径:
- Python3 的 HTTPServer、BaseHTTPRequestHandler
- bottle:单文件、无三方依赖、持续更新,可用于生产环境的开源 Web 框架: 地址:bottlepy/bottle
- werkzeug -> flask
- starlette -> uvicorn -> fastapi
有的时候阅读框架源码不是为了写一个新的框架,而是向前辈学习和靠拢。
最后
新的技术总是学不完的,掌握核心的技术原理,不仅可以在接受新的知识时快人一步,还可以在排查问题时一针见血。
不知道这种一个文件讲解一个技术点,力求通过简单的文字和精简的代码描述原理,期间抹去了细枝末节的技术专注于一门技术,最后给出完整可运行的开源代码的文章,是否符合你的胃口? 本文是我对新的系列一种尝试,接受任何指点和批评。
如果你喜欢此类文章,就请点赞给我一点鼓励,还可以留言提建议或者“点餐”。
OneFile 期待你的加入,贡献一份力量。
不要想你为开源做了什么,你只需要清楚你为自己做了什么。
相关推荐
- 详解DNFSB2毒王的各种改动以及大概的加点框架
-
首先附上改动部分,然后逐项分析第一个,毒攻掌握技能意思是力量智力差距超过15%的话差距会被强行缩小到15%,差距不到15%则无效。举例:2000力量,1650智力,2000*0.85=1700,则智力...
- 通篇干货!纵观 PolarDB-X 并行计算框架
-
作者:玄弟七锋PolarDB-X面向HTAP的混合执行器一文详细说明了PolarDB-X执行器设计的初衷,其初衷一直是致力于为PolarDB-X注入并行计算的能力,兼顾TP和AP场景,逐渐...
- 字节新推理模型逆袭DeepSeek,200B参数战胜671B,豆包史诗级加强
-
梦晨发自凹非寺量子位|公众号QbitAI字节最新深度思考模型,在数学、代码等多项推理任务中超过DeepSeek-R1了?而且参数规模更小。同样是MoE架构,字节新模型Seed-Thinkin...
- 阿里智能化研发起飞!RTP-LLM 实现 Cursor AI 1000 token/s 推理技术揭秘
-
作者|赵骁勇阿里巴巴智能引擎事业部审校|刘侃,KittyRTP-LLM是阿里巴巴大模型预测团队开发的高性能LLM推理加速引擎。它在阿里巴巴集团内广泛应用,支撑着淘宝、天猫、高德、饿...
- 多功能高校校园小程序/校园生活娱乐社交管理小程序/校园系统源码
-
校园系统通常是为学校、学生和教职工提供便捷的数字化管理工具。综合性社交大学校园小程序源码:同城校园小程序-大学校园圈子创业分享,校园趣事,同校跑腿交友综合性论坛。小程序系统基于TP6+Uni-app...
- 婚恋交友系统nuiAPP前端解决上传视频模糊的问题
-
婚恋交友系统-打造您的专属婚恋交友平台系统基于TP6+Uni-app框架开发;客户移动端采用uni-app开发,管理后台TH6开发支持微信公众号端、微信小程序端、H5端、PC端多端账号同步,可快速打包...
- 已节省数百万GPU小时!字节再砍MoE训练成本,核心代码全开源
-
COMET团队投稿量子位|公众号QbitAI字节对MoE模型训练成本再砍一刀,成本可节省40%!刚刚,豆包大模型团队在GitHub上开源了叫做COMET的MoE优化技术。COMET已应用于字节...
- 通用电气完成XA102发动机详细设计审查 将为第六代战斗机提供动力
-
2025年2月19日,美国通用电气航空航天公司(隶属于通用电气公司)宣布,已经完成了“下一代自适应推进系统”(NGAP)计划下提供的XA102自适应变循环发动机的详细设计审查阶段。XA102是通用电气...
- tpxm-19双相钢材质(双相钢f60材质)
-
TPXM-19双相钢是一种特殊的钢材,其独特的化学成分、机械性能以及广泛的应用场景使其在各行业中占有独特的地位。以下是对TPXM-19双相钢的详细介绍。**化学成分**TPXM-19双相钢的主要化学成...
- thinkphp6里怎么给layui数据表格输送数据接口
-
layui官网已经下架了,但是产品还是可以使用。今天一个朋友问我怎么给layui数据表格发送数据接口,当然他是学前端的,后端不怎么懂,自学了tp框架问我怎么调用。其实官方文档上就有相应的数据格式,js...
- 完美可用的全媒体广告精准营销服务平台PHP源码
-
今天测试了一套php开发的企业网站展示平台,还是非常不错的,下面来给大家说一下这套系统。1、系统架构这是一套基于ThinkPHP框架开发的HTML5响应式全媒体广告精准营销服务平台PHP源码。现在基于...
- 一对一源码开发,九大方面完善基础架构
-
以往的直播大多数都是一对多进行直播社交,弊端在于不能满足到每个用户的需求,会降低软件的体验感。伴随着用户需求量的增加,一对一直播源码开始出现。一个完整的一对一直播流程即主播发起直播→观看进入房间观看→...
- Int J Biol Macromol .|交联酶聚集体在分级共价有机骨架上的固定化:用于卤代醇不对称合成的高稳定酶纳米反应器
-
大家好,今天推送的文章发表在InternationalJournalofBiologicalMacromolecules上的“Immobilizationofcross-linkeden...
- 【推荐】一款开源免费的 ChatGPT 聊天管理系统,支持PC、H5等多端
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍GPTCMS是一款开源且免费(基于GPL-3.0协议开源)的ChatGPT聊天管理系统,它基于先进的GPT...
- 高性能计算(HPC)分布式训练:训练框架、混合精度、计算图优化
-
在深度学习模型愈发庞大的今天,分布式训练、高效计算和资源优化已成为AI开发者的必修课。本文将从数据并行vs模型并行、主流训练框架(如PyTorchDDP、DeepSpeed)、混合精度训练(...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 详解DNFSB2毒王的各种改动以及大概的加点框架
- 通篇干货!纵观 PolarDB-X 并行计算框架
- 字节新推理模型逆袭DeepSeek,200B参数战胜671B,豆包史诗级加强
- 阿里智能化研发起飞!RTP-LLM 实现 Cursor AI 1000 token/s 推理技术揭秘
- 多功能高校校园小程序/校园生活娱乐社交管理小程序/校园系统源码
- 婚恋交友系统nuiAPP前端解决上传视频模糊的问题
- 已节省数百万GPU小时!字节再砍MoE训练成本,核心代码全开源
- 通用电气完成XA102发动机详细设计审查 将为第六代战斗机提供动力
- tpxm-19双相钢材质(双相钢f60材质)
- thinkphp6里怎么给layui数据表格输送数据接口
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (52)
- java框架spring (43)
- grpc框架 (55)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)