痛入爽出 HTTP/2:代码实战1
这一期全是干货。干得你口渴想喝水。
环境搭建
- 安装 Python。你可以选择官网安装、Anaconda安装或者你已经有了 Python3.5 以上的版本。PyPy也可以的。
- 可选:创建一个 Python 虚拟环境(不知所云的直接忽略这一步)
- 创建我们的项目文件夹
# bash shell
mkdir gethy
cd gethy
复制代码
在 Windows 上的同学不用担心,本教程的一切操作都是可以在 Windows、Linux 和 Mac 上完成的。 4. 创建测试路径和源代码路径
mkdir gethy # Python 界的约定俗成是在项目根目录下创建一个同名的路径来放源代码
mkdir test
复制代码
- 安装依赖
pip install h2
复制代码
我们要用到 Lukasa 大神写的 hyper-h2 库:https://github.com/python-hyper/hyper-h2
这个库实现了 h2 协议的底层部分,包括:编码解码 TCP 层字节串(hpack),建立并管理 HTTP 连接。 但是,这个库并没有实现 HTTP 应用层的方法(GET、POST)和语义(Requset & Response),也没有实现 Flow Control(流量管理)和 Server Push(服务器推送)。这些也是我们要实现的部分(除了 Server Push)
我们可以看到,HTTP协议是5、6、7层的协议,但是 hyper-h2 只实现了5、6层的功能。Web 应用是没有办法直接使用 hyper-h2 。所以我们要在 hyper-h2 的基础上,实现完整的 h2 协议。
关于网络协议和架构,请参考 What’s The Difference Between The OSI Seven-Layer Network Model And TCP/IP?
开始编程
定义 API
我们遵循自上而下的设计。先设计API,再实现函数。
touch http2protocol.py event.py
复制代码
用你最喜欢的编辑器打开http2protocol.py
,加入以下代码
class HTTP2Protocol:"""A pure in-memory H2 implementation for application level development.It does not do IO."""def __init__(self):passdef receive(self, data: bytes):pass def send(self, stream: Stream):pass
复制代码
我们的库只有 2 个公开API,receive
和send
。
receive
用来从 TCP 层获取数据。send
将一个完整的 Stream
编码为 TCP 可以直接接收的数据。
值得强调的是,这个库不做任何 I/O。这种开发范式叫做 I/O 独立范式。库的使用者应该自己决定使用哪一种 IO 方式。这给予了开发者最大的灵活性。也符合 Clean Architecture 的原则。
hyper-h2 本身也是不做任何 IO 的,所以我们保留这个优良传统。
英文里叫 sans-IO model,请参考:http://sans-io.readthedocs.io
定义 Stream
除了HTTP2Protocol
类,Stream
类也是用户会直接使用的类。
class Stream:def __init__(self, stream_id: int, headers: iterable):self.stream_id = stream_idself.headers = headersself.stream_ended = Falseself.buffered_data = []self.data = None
复制代码
看到这里大家可能就会觉得很亲切了。一个 Stream 其实就代表了一个常规的 HTTP Request 或者 Response。我们有常规的 headers,常规的 data(有些人叫 body)。与 HTTP/1.x 时代唯一不同的是,多了一个 stream id。
写测试 TDD
Test Driven Development 与自上而下得到设计模式是密不可分的。现在我们有了API,写测试同时也交代了 API 的使用方法。
cd ../test
touch test_all.py
复制代码
我们的库很小,一个测试文件就够了。我们还需要一个帮助模组
wget https://raw.githubusercontent.com/CreatCodeBuild/gethy/master/test/helpers.py
复制代码
这个帮助模组是 Lusaka 大神在 hyper-h2 的测试中提供的。
我们现在来想象一下 gethy 的用法
# 伪代码
from gethy import HTTP2Protocol
import Socket
import SomeWebFrameworkprotocol = HTTP2Protocol()
socket = Socket()
while True:if socket.accept():while True:bytes = socket.receive()if bytes:requests = protocol.receive(bytes)for request in requests:response = SomeWebFramework.handle(request)bytes_to_send = protocol.send(response)socket.send(bytes_to_send)else:break
复制代码
大家可以看到,我在这里写了一个伪代码的单线程阻塞式同步服务器。我们的库是完全不做 IO 的。一切IO都直接交给 Server 去完成。gethy 仅仅是在内存里处理数据而已。上面的代码例子也清楚地展示了API的使用方式。
测试网络协议的实现的一大难点就在于 IO。如果类库没有 IO,那么测试其实变得简单了。那么,我们来看看具体的测试怎么写吧。
# test_all.py
def test_receive_headers_only():passdef test_receive_headers_and_data():passdef test_send_headers_only():passdef test_send_headers_and_data():passdef test_send_huge_data():passdef test_receive_huge_data():pass
复制代码
六个测试案例,测试了发送接收与回复请求。最后两个测试使用巨大数据量,是为了测试 Flow Control 的正确性。我们目前可以不管。
先实现第一个
# test_all.py
from gethy import HTTP2Protocol
from gethy.event import RequestEventfrom helpers import FrameFactory# 因为我们的测试很少,所以全局变量也OK
frame_factory = FrameFactory()
protocol = HTTP2Protocol()
protocol.receive(frame_factory.preamble()) # h2 建立连接时要有的字段
headers = [(':method', 'GET'),(':path', '/'),(':scheme', 'https'), # scheme 和 schema 在英文中是同一个词的不同写法# 不过,一般在 h2 中用 shceme,说到数据模型时用 schema(':authority', 'example.com'),
]def test_receive_headers_only():"""able to receive headers with no data"""# 客户端发起的 session 的 stream id 是单数# 服务器发起的 session 的 stream id 是双数# 一个 session 包含一对 request/response# id 0 代表整个 connectionstream_id = 1# 在这里手动生成一个 client 的 request frame,来模拟客户端请求frame_from_client = frame_factory.build_headers_frame(headers, stream_id=stream_id,flags=['END_STREAM'])# 将数据结构序列化为 TCP 可接受的 bytesdata = frame_from_client.serialize()# 服务器端接收请求,得到一些 gethy 定义的事件events = protocol.receive(data)# 因为请求只有一个请求,所以仅可能有一个事件,且为 RequestEvent 事件assert len(events) == 1assert isinstance(events[0], RequestEvent)event = events[0]assert event.stream.stream_id == stream_idassert event.stream.headers == headers # 验证 Headersassert event.stream.data == b'' # 验证没有任何数据assert event.stream.buffered_data is None # 验证没有任何数据assert event.stream.stream_ended is True # 验证请求完整(Stream 结束)
复制代码
阅读上面的测试,大家可以基本上知道 gethy 的用法和 http2 的基本语义。大家可以发现,http2 的语义和 http1 基本没有变化。唯一需要注意的就是 headers 里4个:xxx
字样的 header。:
冒号是协议使用的 header 符号。应用自定义的 header 不应该使用冒号。然后,虽然 http2 协议本身是允许大写字母,并且是大小写敏感的,但是 gethy 的依赖库 hyper-h2 只允许小写。
现在来实现
def test_receive_headers_and_data():stream_id = 3client_headers_frame = frame_factory.build_headers_frame(headers, stream_id=stream_id)headers_bytes = client_headers_frame.serialize()data = b'some amount of data'client_data_frame = frame_factory.build_data_frame(data, stream_id=stream_id, flags=['END_STREAM'])data_bytes = client_data_frame.serialize()events = protocol.receive(headers_bytes+data_bytes)assert len(events) == 1assert isinstance(events[0], RequestEvent)event = events[0]assert event.stream.stream_id == stream_idassert event.stream.headers == headers # 验证 Headersassert event.stream.data == data # 验证没有任何数据assert event.stream.buffered_data is None # 验证没有任何数据assert event.stream.stream_ended is True # 验证请求完整(Stream 结束)
复制代码
带数据的请求也很简单,加上DATA
frame即可。
好的,我们再来看看如何发送回复。
def test_send_headers_only():stream_id = 1response_headers = [(':status', '200')]stream = Stream(stream_id, response_headers)stream.stream_ended = Truestream.buffered_data = Nonestream.data = Noneevents = protocol.send(stream)assert len(events) == 2for event in events:assert isinstance(event, MoreDataToSendEvent)
复制代码
只发送 Headers 很简单,创建一个Stream
,然后发送就行了。目前大家可以忽略MoreDataToSendEvent
。我会在视频和后续文章中娓娓道来。
def test_send_headers_and_data():"""able to receive headers and small amount data.able to send headers and small amount of data"""stream_id = 3response_headers = [(':status', '400')]size = 1024 * 64 - 2 # default flow control window size per stream is 64 KB - 1 bytestream = Stream(stream_id, response_headers)stream.stream_ended = Truestream.buffered_data = Nonestream.data = bytes(size)events = protocol.send(stream)assert len(events) == size // protocol.block_size + 3for event in events:assert isinstance(event, MoreDataToSendEvent)assert not protocol.outbound_streamsassert not protocol.inbound_streams
复制代码
如果要发送数据,只需要将stream.data
赋值。注意,一定要是bytes
类型。以上测试也涉及到了 Flow Control(流量控制),我会在视频和后续文章中讲解。
结语
好啦,想必到这里你一定对 GetHy 有了大局观的认识,也熟悉了 API 及应用场景。接下来就是要实现它了。我们下一期再见!
资源
代码
GitHub
B站
痛入爽出 HTTP/2:代码实现1
油腻的管子
痛入爽出 HTTP/2:代码实现1
文章
上期
下期
痛入爽出 HTTP/2:代码实战1相关推荐
- Java开发中巧妙使用链表来实现模拟栈的入栈出栈操作
2019独角兽企业重金招聘Python工程师标准>>> 在Java开发中经常会碰到需要你用Java链表来实现入栈出栈的模拟操作,下文就这个操作做了一个详细的描述. 栈:后进先出:最后 ...
- css上滑事件,css transition 实现滑入滑出
transition是css最简单的动画. 通常当一个div属性变化时,我们会立即看的变化,从旧样式到新样式是一瞬间的,嗖嗖嗖!!! 但是,如果我希望是慢慢的从一种状态,转变成另外一种状态,怎么办? ...
- JS 从屏幕上下左右滑入滑出效果
从屏幕上下左右滑入滑出效果,代码比较粗糙,但是效果已实现 需要注意的是,从屏幕右边和下边滑入的时候,需要给滑动的容器外面再加一个容器,加样式 position: fixed; 让它 固定定位,否则页面 ...
- android底部滑出view,Android CoordinatorLayout与NestedScrollView基于Behavior几行代码实现底部View滑入滑出...
Android CoordinatorLayout与NestedScrollView基于Behavior几行代码实现底部View滑入滑出 在CoordinatorLayout的Behavior出现之前 ...
- Redis Sentinel-深入浅出原理和实战
本篇会简单的介绍Redis的Sentinel相关的原理,同时也会在最后的文章给出硬核的实战教程,让你在了解原理之后,能够实际上手的体验整个过程. 总的来说,为了满足Redis在真正复杂的生产环境的高可 ...
- Go语言_数据结构_栈(包括入栈和出栈,表达式的入栈出栈详细过程代码实现)
入栈和出栈代码实现如下: package main import ("fmt""errors" )//使用数组来模拟一个栈的使用 type Stack stru ...
- 栈的初始化,入栈,出栈,遍历操作(代码实现) [数据结构][Java]
栈的初始化,入栈,出栈,遍历操作(代码实现) 具体代码如下: package com.ffyc.stack;/*** 创建一个栈结构(使用数组实现)*/ public class ArrayStack ...
- 浅入浅出深度学习理论实践
全文共9284个字,40张图,预计阅读时间30分钟. 前言 之前在知乎上看到这么一个问题:在实际业务里,在工作中有什么用得到深度学习的例子么?用到 GPU 了么?,回头看了一下自己写了这么多东西一直围 ...
- 技术解析+代码实战,带你入门华为云政务区块链平台
摘要:政务区块链平台是行业区块链平台的初步实践,未来在区块链技术的发展下,还会打造面向其他领域的链管平台,构建多方协同的分布式账本,让区块链应用更便捷高效的为产业服务. 本文分享自华为云社区<技 ...
最新文章
- 论怎么写好一篇实验报告
- python快速编程入门课后程序题答案-Python 入门编程题:1~10(答案)
- 文科生如何理解深度学习?
- 对Docker常用命令的整理
- Flyweight设计模式
- [古诗]有关日本留学生的那些古诗
- 【开发工具】【i2c-tools】I2C总线调试工具(i2c-tools)的安装与使用
- 实现一个串口调试工具
- 2021高考成绩答题卡查询,【助力高考】2021届高考答题卡原来长这样,赶快看看...
- 思科服务器如何进入网站,思科路由器怎么进入设置网站
- 常用的字符编码:ASCII、Unicode、UTF-8
- 基于OLAP的时间维度设计
- 创易手机--真正DIY手机
- PDF如何导出成图片,操作教程
- 消息推送----微信小程序,公众号
- 机器学习入门(二) 准备工作
- Win95下的注册表文件(User.dat,System.dat)文件格式说明 (转)
- windows安装spacemacs
- 走向人生巅峰?这份自由程序员速成宝典你值得拥有
- 《般若波罗密多心经》拙解