Sans I/O 系ライブラリのお作法まとめ
Conclusion
スクラップにまとめてから半年したらサッパリ忘れていたので、一目瞭然な表にまとめました (拡大推奨)。
HTML 表バージョン (改行で見づらい、コピペ用)
Package | イベントを入力する | バイトを出力する | バイトを入力する | イベントを出力する |
---|---|---|---|---|
h11 |
send(event) -> bytes |
send(event) -> bytes Same as left |
receive_data(data) -> None |
next_event() -> Event |
h2 |
send_headers(stream_id, headers) -> None send_data(stream_id, data) -> None etc |
data_to_send() -> bytes |
receive_data(data) -> list[Event] |
receive_data(data) -> list[Event] Same as left |
wsproto |
send(event) -> bytes |
send(event) -> bytes Same as left |
receive_data(data) -> None |
events() -> Generator[Event] |
websockets |
send_request(request) -> None send_text(data) -> None etc |
data_to_send() -> list[bytes] |
receive_data(data) -> None |
events_received() -> list[Event] |
それぞれプロトコルのセマンティクスが違うと言えど、Sans I/O としてのシンタックスが地味に異なっており理解するのが難しいです (websockets
は別にしても python-hyper
系の間でも API が統一されていません)。
API に関しては h11
が最も直感的で分かりやすいと思いました。 wsproto
もほぼ同じですが、受信したイベントがない (ネットワークからのバイトが必要な) 時に関して h11
の next_event()
は h11.NEED_EVENT
が出力されます。 wsproto
の events()
は StopIteration
が挙がるだけです。 ネットワークコード側としては前者の方が制御し易いです。
h2
は先駆者なパッケージでこの中では最も古いのもあるか、または HTTP/2 プロトコルの自体が複雑であるからなのか、 API 的にも他よりフレンドリーに感じませんでした。
websockets
の Sans I/O layer は最後発ながらも難しいと感じていたのですが、表にしてまとめると綺麗な API に整えれていることが分かります。 イベントとオブジェクトの入出力メソッドそれぞれの役割が被っていません。
Reference
Sans I/O とは
ネットワーク I/O を抜いたプロトコルの実装。 HTTP/1.1 プロトコルを例にすると、h11
という Sans I/O ライブラリはバイトデータをを HTTP メッセージとしてパースしてヘッダーやボディのバイトを作ってくれる。 なので HTTP クライアント開発者であれば Sans I/O ライブラリのお作法だけ知っていれば、HTTP プロトコルを任意のネットワーク I/O、例えば同期 I/O socket
や非同期 I/O の asyncio
や trio
などに結合することができる。
お作法まとめ
-
h11
(HTTP/1.1) -
h2
(HTTP/2) -
wsproto
(WebSocket) -
websockets
Sans I/O layer (WebSocket)
上記の Sans I/O 系ライブラリのお作法を調べてまとめてみる。 実際にコードを書いて、バイトをパースする為の手順や、どのような名称のメソッドを利用するかを知りたい。
HTTP クライアントの httpx
はオプションで HTTP/1.1 と HTTP/2 を切り替えることができるが、内部で利用している h11
h2
の利用方法の違いを理解したい。 両者は python-hyper
からリリースされているが元の開発者が違うらしい [1] のでどのような違いがあるのか気になっている。
また WebSocket プロトコルの Sans I/O ライブラリとして wsproto
は以前からあるみたいだが、asyncio
に実装を結合していたwebsockets
が最近 Sans I/O 機能のあるモジュールを追加した。 この両者についてもどんな違いがあるか気になるので調べたい。
ネットワーク I/O はとりあえず socket
を利用してシンプル HTTP / WebSocket リクエストをする最小のスニペットを書いてみる。
h11
import socket
import ssl
import h11
hostname = "httpbin.org"
context = ssl.create_default_context()
with (
socket.create_connection((hostname, 443)) as sock,
context.wrap_socket(sock, server_hostname=hostname) as sock,
):
# ----------------------------------------------------------------------------------
# Request
# ----------------------------------------------------------------------------------
# h11 Connection
h11_connection = h11.Connection(our_role=h11.CLIENT)
# h11 Request
h11_request = h11.Request(
method="GET", target="/get", headers=[("Host", "httpbin.org")]
)
h11_eof = h11.EndOfMessage()
# h11 objects to bytes
bytes_request = h11_connection.send(h11_request)
bytes_eof = h11_connection.send(h11_eof)
# bytes to I/O
sock.sendall(bytes_request)
sock.sendall(bytes_eof)
# ----------------------------------------------------------------------------------
# Response
# ----------------------------------------------------------------------------------
response: h11.Response | None = None
body = b""
while True:
# bytes to h11 objects
event = h11_connection.next_event()
match event:
case h11.NEED_DATA():
# bytes from I/O
bytes_received = sock.recv(8192)
# Buffering bytes
h11_connection.receive_data(bytes_received)
case h11.Response():
# Response object
response = event
case h11.Data():
# Data object
body += event.data
case h11.EndOfMessage():
# End object
break
print(response.headers)
print(body.decode())
リクエスト
-
h11.Connection
: コネクションクラス -
h11.Request
: リクエストクラス -
h11.EndOfMessage
: 終了メッセージクラス -
コネクションクラスインスタンスの
send
メソッドの引数に、リクエストクラスや終了メッセージクラスのインスタンスを与えるとインスタンスの内容をbytes
で出力する。 -
socket
でbytes
を送信する
レスポンス
- コネクションクラスインスタンスの
next_event
メソッドでイベントを受け取る - イベントが
h11.NEED_DATA
だったらsocket
からbytes
受信して、コネクションクラスインスタンスのreceive_data
メソッドでbytse
をバッファリングする -
h11.Response
やh11.Data
クラスインスタンスを受け取る -
h11.EndOfMessage
クラスインスタンスだったら HTTP 通信を終了する
h2
import socket
import ssl
import h2.connection
import h2.events
hostname = "httpbin.org"
context = ssl.create_default_context()
context.set_alpn_protocols(["h2"])
with (
socket.create_connection((hostname, 443)) as sock,
context.wrap_socket(sock, server_hostname=hostname) as sock,
):
# ----------------------------------------------------------------------------------
# Request
# ----------------------------------------------------------------------------------
# h2 Connection
h2_connection = h2.connection.H2Connection()
h2_connection.initiate_connection()
# h2 Request
h2_connection.send_headers(
1,
[
(":method", "GET"),
(":path", "/get"),
(":authority", hostname),
(":scheme", "https"),
],
end_stream=True,
)
# h2 objects to bytes
bytes_request = h2_connection.data_to_send()
# bytes to I/O
sock.sendall(bytes_request)
# ----------------------------------------------------------------------------------
# Response
# ----------------------------------------------------------------------------------
response: h2.events.ResponseReceived | None = None
body = b""
response_stream_ended = False
while not response_stream_ended:
# bytes from I/O
bytes_received = sock.recv(8192)
# bytes to h2 objects
events = h2_connection.receive_data(bytes_received)
for event in events:
match event:
case h2.events.ResponseReceived():
# Response object
response = event
case h2.events.DataReceived():
# Data objct
h2_connection.acknowledge_received_data(
event.flow_controlled_length, event.stream_id
)
body += event.data
case h2.events.StreamEnded():
# End object
response_stream_ended = True
break
# send any pending data to the server
sock.sendall(h2_connection.data_to_send())
print(response.headers)
print(body.decode())
リクエスト
- コネクションクラス
h2.connection.H2Connection
のインスタンスを生成する - コネクションクラスの
initiate_connection
メソッドを呼び出す - コネクションクラスの
send_headers
メソッドでヘッダーを送信する - コネクションクラスの
data_to_send
メソッドでバイトを生成する - ネットワークにバイトを送信する
レスポンス
- ネットワークからバイトを取得する
- コネクションクラスの
receive_data
メソッドでバイトを読み込んでイベントリストを取得する - イベントリストをイテレートしてイベントを区別する
- レスポンスイベント
- データイベント
- コネクションクラスの
acknowledge_received_data
メソッドを呼び出してサーバー側に通知する
- コネクションクラスの
- エンドストリームイベント
- コネクションクラスの
data_to_send
メソッドでバイトを生成して、何か双方向的に送信するデータがあればネットワークで送信する
h11 / h2 お作法まとめ
リクエスト
-
h11
- リクエストクラスやエンドメッセージクラスのインスタンスを生成する
- コネクションクラスの
send
メソッドでリクエストクラスやエンドメッセージクラスをバイトに変換する - バイトをネットワークに送信する
-
h2
- コネクションクラスの
initiate_connection
メソッドで初期化する - コネクションクラスの
send_headers
メソッドで送信するヘッダーを指定する - コネクションクラスの
data_to_send
メソッドでバイトを生成する - バイトをネットワークに送信する
- コネクションクラスの
リクエストは、h11 はリクエストクラスなどを作成して send
メソッドでバイトを出力するのに対して、h2 は send_headers
メソッドでなどでコネクションクラスに指示を溜めてから data_to_send
メソッドでバイトを出力している。
レスポンス
-
h11
- コネクションクラスの
next_event
メソッドでイベントを取得する - レスポンスを判定する
-
h11.NEED_DATA
なら、ネットワークからバイトを受信してコネクションクラスのreceive_data
でバイトをバッファリングする
-
- コネクションクラスの
-
h2
- ネットワークからバイトを受信する
- コネクションクラスの
receive_data
メソッドでイベントリストを取得する - イベントリストのイベントを判定する
- もし必要な送信データがあれば送信する
h11 は next_event
メソッドで簡易的なイテレーションができる設計になっている。 receive_data
メソッドの名前が共通しているが、h11 はバイトをバッファリングするのに対して、h2 はバイトをイベントリストに変換している。
wsproto
import socket
import ssl
from collections import deque
from wsproto import ConnectionType, WSConnection
from wsproto.events import (
AcceptConnection,
CloseConnection,
Message,
Request,
)
hostname = "echo.websocket.events"
context = ssl.create_default_context()
with (
socket.create_connection((hostname, 443)) as sock,
context.wrap_socket(sock, server_hostname=hostname) as sock,
):
# ----------------------------------------------------------------------------------
# Handshake
# ----------------------------------------------------------------------------------
ws = WSConnection(ConnectionType.CLIENT)
events = deque()
request = Request(host=hostname, target="/")
data = ws.send(request)
sock.send(data)
print("handshake", len(events))
handshake_finished = False
while not handshake_finished:
if not len(events):
data = sock.recv(8192)
print(data)
ws.receive_data(data)
events.extend(ws.events())
while len(events):
event = events.popleft()
print(event)
match event:
case AcceptConnection():
handshake_finished = True
break
# ----------------------------------------------------------------------------------
# Receive message
# ----------------------------------------------------------------------------------
print("receive first message", len(events))
message_finished = False
while not message_finished:
if not len(events):
data = sock.recv(8192)
print(data)
ws.receive_data(data)
events.extend(ws.events())
while len(events):
event = events.popleft()
print(event)
match event:
case Message(message_finished=True):
message_finished = True
break
# ----------------------------------------------------------------------------------
# Send message
# ----------------------------------------------------------------------------------
print("sending message", len(events))
data = ws.send(Message(data="hello wsproto 123456789123456789123456789123456789"))
sock.send(data)
# ----------------------------------------------------------------------------------
# Receive message
# ----------------------------------------------------------------------------------
print("receive echo message", len(events))
message_finished = False
while not message_finished:
if not len(events):
data = sock.recv(8192)
print(data)
ws.receive_data(data)
events.extend(ws.events())
while len(events):
event = events.popleft()
print(event)
match event:
case Message(message_finished=True):
message_finished = True
break
# ----------------------------------------------------------------------------------
# Close connection
# ----------------------------------------------------------------------------------
sock.send(ws.send(CloseConnection(code=1000)))
リクエスト
- コネクションクラス
wsproto.WSConnection
のインスタンスを生成する - リクエストクラス
wsproto.events.Request
のインスタンスを生成する - コネクションクラスの
send
メソッドで、リクエストクラスからバイトを出力する - ネットワークにバイトを送信する
レスポンス
- ネットワークからバイトを受信する
- コネクションクラスの
receive_data
メソッドでバイトを読み込む - コネクションクラスの
events
メソッドで WebSocket イベントを生成する - コネクションアクセプトクラス
wsproto.events.AcceptConnection
が生成される
WebSocket メッセージ - 受信
- ネットワークからバイトを受信する
- コネクションクラスの
receive_data
メソッドでバイトを読み込む - コネクションクラスの
events
メソッドで WebSocket イベントを生成する - メッセージクラス
wsproto.events.Message
が生成される-
message_finished
属性がFalse
場合、ネットワークから受信したバイトままの途切れたメッセージとなっているので注意が必要
-
WebSocket メッセージ - 送信
- メッセージクラス
wsproto.events.Message
のインスタンスを生成する - コネクションクラスの
send
メソッドで、メッセージクラスからバイトを出力する - ネットワークにバイトを送信する
websockets Sans I/O layer
import socket
import ssl
from collections import deque
from websockets.client import ClientProtocol
from websockets.frames import Frame
from websockets.http11 import Response
from websockets.uri import parse_uri
wsuri = parse_uri("wss://echo.websocket.events")
context = ssl.create_default_context()
with (
socket.create_connection((wsuri.host, wsuri.port)) as sock,
context.wrap_socket(sock, server_hostname=wsuri.host) as sock,
):
# ----------------------------------------------------------------------------------
# Handshake
# ----------------------------------------------------------------------------------
protocol = ClientProtocol(wsuri)
request = protocol.connect()
protocol.send_request(request)
for data in protocol.data_to_send():
print(">", data)
sock.sendall(data)
events = deque()
print("handshake", len(events))
handshake_finished = False
while not handshake_finished:
if not len(events):
data = sock.recv(8192)
print("<", data)
if data:
protocol.receive_data(data)
else:
protocol.receive_eof()
events.extend(protocol.events_received())
while len(events):
event = events.popleft()
print(event)
match event:
case Response():
handshake_finished = True
break
# ----------------------------------------------------------------------------------
# Receive message
# ----------------------------------------------------------------------------------
print("receive first message", len(events))
message_finished = False
while not message_finished:
if not len(events):
data = sock.recv(32)
print("<", data)
if data:
protocol.receive_data(data)
else:
protocol.receive_eof()
events.extend(protocol.events_received())
while len(events):
event = events.popleft()
print(repr(event))
match event:
case Frame():
message_finished = True
break
# ----------------------------------------------------------------------------------
# Send message
# ----------------------------------------------------------------------------------
print("sending message", len(events))
protocol.send_text(b"hello wsproto 123456789123456789123456789123456789")
for data in protocol.data_to_send():
print(">", data)
sock.sendall(data)
# ----------------------------------------------------------------------------------
# Receive message
# ----------------------------------------------------------------------------------
print("receive first message", len(events))
message_finished = False
while not message_finished:
if not len(events):
data = sock.recv(32)
print("<", data)
if data:
protocol.receive_data(data)
else:
protocol.receive_eof()
events.extend(protocol.events_received())
while len(events):
event = events.popleft()
print(repr(event))
match event:
case Frame():
message_finished = True
break
リクエスト
- プロトコルクラス
websockets.client.ClientProtocol
のインスタンスを生成する - プロトコルクラスの
connect
メソッドでリクエストクラスのインスタンスを生成する - プロトコルクラスの
send_request
メソッドでリクエストクラスを内部にバッファする - プロトコルクラスの
data_to_send
メソッドでバイトを出力する - ネットワークにバイトを送信する
レスポンス
- ネットワークからバイトを受信する
- プロトコルクラスの
receive_data
メソッドでバイトを内部にバッファする - プロトコルクラスの
events_received
メソッドでイベントを生成する - レスポンスクラス
websockets.http11.Response
が生成される
WebSocket メッセージ - 受信
- ネットワークからバイトを受信する
- プロトコルクラスの
receive_data
メソッドでバイトを内部にバッファする - プロトコルクラスの
events_received
メソッドでイベントを生成する - レスポンスクラス
websockets.frames.Frame
が生成される
WebSocket メッセージ - 送信
- プロトコルクラスの
send_text
メソッドでバイトを内部にバッファする - プロトコルクラスの
data_to_send
メソッドでバイトを出力する - ネットワークにバイトを送信する
wsproto / websockets Sans I/O layer お作法まとめ
リクエスト、メッセージ送信
-
wsproto
- リクエストオブジェクトまたはメッセージオブジェクトを、コネクションオブジェクトの
send
メソッドに渡してバイトを出力する
- リクエストオブジェクトまたはメッセージオブジェクトを、コネクションオブジェクトの
-
websockts
Sans I/O layer- リクエストオブジェクトはプロトコルオブジェクトの
send_request
メソッドで、メッセージのバイトはプロトコルオブジェクトのsend_text
メソッドで内部にバッファする - プロトコルオブジェクトの
data_to_send
メソッドで内部のバッファからバイトを出力する
- リクエストオブジェクトはプロトコルオブジェクトの
レスポンス、メッセージ受信
-
wsproto
- コネクションオブジェクトの
receive_data
メソッドでバイトを内部にバッファする - コネクションオブジェクトの
events
メソッドで内部のバッファからイベントを出力する
- コネクションオブジェクトの
-
websockets
Sans I/O layer- プロトコルオブジェクトの
receive_data
メソッドでバイトを内部にバッファする - プロトコルオブジェクトの
events_received
メソッドで内部のバッファからイベントを出力する
- プロトコルオブジェクトの