痛入爽出 HTTP/2:代码实战1

这一期全是干货。干得你口渴想喝水。


环境搭建

  1. 安装 Python。你可以选择官网安装、Anaconda安装或者你已经有了 Python3.5 以上的版本。PyPy也可以的。
  2. 可选:创建一个 Python 虚拟环境(不知所云的直接忽略这一步)
  3. 创建我们的项目文件夹
# bash shell
mkdir gethy
cd gethy
复制代码

在 Windows 上的同学不用担心,本教程的一切操作都是可以在 Windows、Linux 和 Mac 上完成的。 4. 创建测试路径和源代码路径

mkdir gethy  # Python 界的约定俗成是在项目根目录下创建一个同名的路径来放源代码
mkdir test
复制代码
  1. 安装依赖
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):
        pass

    def receive(self, data: bytes):
        pass    

    def send(self, stream: Stream):
        pass
复制代码

我们的库只有 2 个公开API,receivesend

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_id
		self.headers = headers
		self.stream_ended = False
		self.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 SomeWebFramework

protocol = 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():
    pass
    
def test_receive_headers_and_data():
    pass

def test_send_headers_only():
    pass
    
def test_send_headers_and_data():
    pass
    
def test_send_huge_data():
    pass
    
def test_receive_huge_data():
    pass
复制代码

六个测试案例,测试了发送接收与回复请求。最后两个测试使用巨大数据量,是为了测试 Flow Control 的正确性。我们目前可以不管。

先实现第一个

# test_all.py
from gethy import HTTP2Protocol
from gethy.event import RequestEvent

from 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 代表整个 connection
	stream_id = 1
	
    # 在这里手动生成一个 client 的 request frame,来模拟客户端请求
	frame_from_client = frame_factory.build_headers_frame(headers, 
	                                                      stream_id=stream_id,
	                                                      flags=['END_STREAM'])
	# 将数据结构序列化为 TCP 可接受的 bytes
	data = frame_from_client.serialize()
	# 服务器端接收请求,得到一些 gethy 定义的事件
	events = protocol.receive(data)
    
    # 因为请求只有一个请求,所以仅可能有一个事件,且为 RequestEvent 事件
	assert len(events) == 1
	assert isinstance(events[0], RequestEvent)

	event = events[0]
	assert event.stream.stream_id == stream_id
	assert event.stream.headers == headers      #  验证 Headers
	assert 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 = 3

	client_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) == 1
	assert isinstance(events[0], RequestEvent)

	event = events[0]
	assert event.stream.stream_id == stream_id
	assert event.stream.headers == headers     # 验证 Headers
	assert event.stream.data == data           # 验证没有任何数据
	assert event.stream.buffered_data is None  # 验证没有任何数据
	assert event.stream.stream_ended is True   # 验证请求完整(Stream 结束)
复制代码

带数据的请求也很简单,加上DATAframe即可。

好的,我们再来看看如何发送回复。

def test_send_headers_only():
	stream_id = 1
	response_headers = [(':status', '200')]

	stream = Stream(stream_id, response_headers)
	stream.stream_ended = True
	stream.buffered_data = None
	stream.data = None

	events = protocol.send(stream)
	assert len(events) == 2
	for 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 = 3
	response_headers = [(':status', '400')]
	size = 1024 * 64 - 2  # default flow control window size per stream is 64 KB - 1 byte

	stream = Stream(stream_id, response_headers)
	stream.stream_ended = True
	stream.buffered_data = None
	stream.data = bytes(size)

	events = protocol.send(stream)
	assert len(events) == size // protocol.block_size + 3
	for event in events:
		assert isinstance(event, MoreDataToSendEvent)

	assert not protocol.outbound_streams
	assert not protocol.inbound_streams
复制代码

如果要发送数据,只需要将stream.data赋值。注意,一定要是bytes类型。以上测试也涉及到了 Flow Control(流量控制),我会在视频和后续文章中讲解。

结语

好啦,想必到这里你一定对 GetHy 有了大局观的认识,也熟悉了 API 及应用场景。接下来就是要实现它了。我们下一期再见!


资源

代码

GitHub

B站

痛入爽出 HTTP/2:代码实现1

油腻的管子

痛入爽出 HTTP/2:代码实现1

文章

上期
下期

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值