Open4

MiniSock: UART用ネットワークプロトコルの設計

okuokuokuoku

前回: https://zenn.dev/okuoku/scraps/426d07c242123b

とりあえず、あまり高ビットレートでの効率は狙わずにシリアルポート想定でシリアライズプロトコルを実装することにした。

プロトコルは、信頼できる、順序保証されたバイトストリーム、要するにTCP/IPのようなストリームを前提に設計する。

UARTのようなシリアルポートであればビット化けが発生するかもしれないし、Bluetooth LEではパケット順序の入れ替えや脱落が発生するかもしれない。これらに対する対処は別途トランスポート毎に専用の対処を入れることにした。

okuokuokuoku

UART版のフレーム化プロトコル

UARTの性質上パケットを直接やりとりすることはできない。バイトの並びのみでパケットを表現するために、データの始端や終端がわかるように追加のデータを送信する必要がある。

例えば SLIP では特定のバイト(エスケープ文字)を直接送信しないように約束して始端と終端を追加の文字としてエンコードできるようにしている。しかし、これは通信データ自体にエスケープ文字が含まれていないかどうかの検閲が必要になり、データの書き換えが必要になってしまう可能性がある。

(データの書き換えが必要になると、ユーザが与えたデータを直接DMAする方法がなくなるので都合がわるい)

代わりに、MiniSockではヘッダと長さを送るように約束する。

バイト 長さ(bytes) データ
M 1 ヘッダ(ASCIIの M = 0x4D)
S 1 シーケンス番号
T 1 パケットの長さタイプ(A = 8bit長)
L N パケットの長さ(リトルエンディアン)
(パケット) M パケット本体
C 2 16bitsチェックサム(リトルエンディアン)

... TCP/IPでは本来チェックサムは不要だが、将来チェックサムの無いバリアントも企画することにして今のところは付けておく(実装のバグが心配なため)。

チェックサムはまぁ何でも良いので CRC-16-CCITT を使う。コードは例えば IARのtechnote に有る。

一般に、UARTはデータを下位ビットから送信するため、連続して送信した多bit長データを8bit単位に纏めた場合はリトルエンディアンに見える。

シーケンス番号はフロー制御に使用する。つまり、コントローラーやワーカーが処理しきれないデータを受信した場合はACKを送らないことによって上位のアプリケーションの動作を止められるようにする。

okuokuokuoku

プロトコル要素

MiniSockのプロトコルはリクエスト、イベント、データの3要素で構成される。データはイベントの一種として実装されることになるため、リクエストとイベントの2つの要素が存在するとも言える。

リクエストにはレスポンスが存在し、イベントにはレスポンスが存在しない。ただし、後述のフロー制御のために受信したイベントに対してACKを通知する可能性はある。

コントローラー、ワーカー

通常のネットワークにおけるサーバーとクライアントとの混同を避けるため、MiniSockではプロトコルの両端を "コントローラー" および "ワーカー" と称する。

コントローラー は、実際のTCP/UDPアプリケーションであり、ワーカーに対してインターネットへの接続を要求する。

ワーカー は、TCP/IPプロトコルスタックを搭載し実際の通信を行う。

通信の制約については原則的にワーカーが提示したものに従う必要がある。

フロー制御

ワーカーは2種類のフロー制御を行うことができる。両者とも最大で255個のパケットを平行して処理することができ、フロー制御はコントローラーとワーカーの両端で、かつ、 独立して 実施される。

フレームキュー は ↑ で定義したフレーム自体のキューで、定期的に正常に処理したフレームのシーケンス番号を通知することで、他端にフレームの正常処理通知を行う。

リクエストキュー は、UDPパケット送受信等、フレームに収まらない可能性がある可能性があるリクエスト/イベントを未処理のまま他端に保持できる限界数を示す。

フレームキューにはプロトコル上の上限として最大255の深さを持つが、単なるフロー制御以上の意味は持たない。非常にメモリの少い環境であれば、フレームキューの深さを1にして、毎フレームACKを返却する実装もあり得るだろう。

リクエストキューは両端が確保できるメモリサイズに上界され、かつ、両端で異なるサイズになる可能性がある。リクエストキューの長さが、コントローラーやワーカーのあつかえるUDPパケットのMTUを規定することになる。

okuokuokuoku

リクエスト、イベントの基本構造

リクエストやイベントは以下の基本構造に従う。とくにセッションIDは16bitsしか無いため、UDPやTCPが持つポート番号空間をフルに埋めることはできない。また、 ワーカーはセッションIDに上限を設定できる 。マイコンではTCP接続を同時に数本しか扱えないのも珍しくない。

フィールド 長さ 内容
session_id 2 セッションID、ゼロでID不要のイベント
op 1 opコード
data N データ

終端はパケット終端で表現されるため、データの長さフィールドは存在しない。

リクエスト

リクエストは以下がある。

  • create_session -- acceptの機能を兼ねる
  • shutdown_session

TBD: エラーは未定だがSocks5のエラーコードから持ってこようと思っている。

セッション/名前空間タイプ

セッション/名前空間タイプとして以下を持つ。Socks5と異なり、"バーチャル"空間を設けることで、管理用プロトコル(Wi-Fiのパスワード入力とか)との接続を可能にしている。また、Socks5と異なりIPv4のみ/IPv6のみの接続を可能にしている。

Datagram は "対話" 型のUDPセッションを前提にしている。つまり、 ワーカーが送出したUDPメッセージに何かレスポンスが返ってくると期待することになる。

  • セッションタイプ
    • Stream (= TCP)
    • StreamServer (= TCPの待ち受けモード)
    • Datagram (= UDP) -- フレーミングプロトコルは現状順序保証モードしか持たないため必然的にOrdered処理となる(パケットの送信順が保証される)
  • 名前空間タイプ
    • IPv4 numeric
    • IPv6 numeric
    • DNS string IPv4+IPv6
    • DNS string IPv4
    • DNS string IPv6
    • Virtual

ポート番号を名前で引くことはできない。現代的には最早意味がない。

イベント

TBD