Болезненный и веселый HTTP/2: кодовый бой 1

HTTP

В этом эпизоде ​​все сухо. Вы испытываете жажду и хотите пить воду.


Строительство окружающей среды

  1. Установите Питон. Вы можете выбрать установку с официального сайта, установку Anaconda или у вас уже есть версия Python 3.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), а также не реализует управление потоком (управление трафиком) и Server Push (проталкивание сервера). Это также те части, которые мы хотим реализовать (кроме Server Push).

Мы видим, что протокол HTTP является протоколом уровней 5, 6 и 7, а hyper-h2 реализует только функции уровней 5 и 6. Веб-приложения не могут напрямую использовать гипер-h2. Итак, мы хотим реализовать полный протокол h2 на основе hyper-h2.

Сетевые протоколы и архитектуры см.В чем разница между семиуровневой сетевой моделью OSI и 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,receiveиsend.

receiveИспользуется для получения данных с уровня TCP.sendполныйStreamКодируется как данные, которые TCP может получать напрямую.

Стоит подчеркнуть, что эта библиотека не выполняет никаких операций ввода-вывода. Эта парадигма разработки называется независимой парадигмой ввода-вывода. Пользователи библиотеки должны сами решить, какой метод ввода-вывода использовать. Это дает разработчику максимальную гибкость. также встретитьсяClean Architectureправила.

Hyper-h2 сам по себе не занимается вводом-выводом, поэтому мы сохраняем эту добрую традицию.

На английском языке это называется моделью sans-IO, см.: http://sans-io.readthedocs.io

Определить поток

Кроме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

Когда вы видите это, вы можете почувствовать себя очень дружелюбным. Поток фактически представляет собой обычный HTTP-запрос или ответ. У нас есть обычные заголовки, обычные данные (некоторые люди называют это телом). Единственным отличием от эпохи HTTP/1.x является наличие дополнительного идентификатора потока.

Написать тестовый TDD

Разработка через тестирование неотделима от шаблонов проектирования сверху вниз. Теперь, когда у нас есть 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

Как видите, здесь я написал псевдокод для однопоточного блокирующего сервера синхронизации. Наша библиотека вообще не выполняет ввод-вывод. Все операции ввода-вывода напрямую передаются серверу для завершения. gethy просто обрабатывает данные в памяти. Приведенный выше пример кода также ясно показывает, как используется API.

Одним из самых сложных аспектов тестирования реализаций сетевых протоколов является ввод-вывод. Если в библиотеке классов нет 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

Шесть тестовых случаев, которые проверяют отправку, получение и ответ на запросы. Последние два теста используют огромные объемы данных для проверки правильности управления потоком. Мы можем пока игнорировать это.

реализовать первый

# 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. Единственное, на что стоит обратить внимание, это цифра 4 в заголовках.:xxxзаголовок слова.:Двоеточие — это символ заголовка, используемый протоколом. В настраиваемых заголовках приложений не должны использоваться двоеточия. Затем, хотя сам протокол 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 结束)

Запросы с данными также просты, плюсDATAрамка может быть.

Хорошо, давайте посмотрим, как отправить ответ.

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)

Отправка только заголовков проста, создайте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

Станция Б

Мучительный и приятный HTTP/2: реализация кода 1

жирная трубка

Мучительный и приятный HTTP/2: реализация кода 1

статья

Предыдущий период
в следующий раз