UDPの仕組みと信頼性の確保
データグラムの検査と、上位層で必要になる配送規則を確認します。
説明
UDPはデータグラム単位でアプリケーション間のデータを運びます。TCPのような累積ACKや再送をUDP自体が提供するわけではないため、必要な信頼性は上位の設計で補います。
8バイトのUDPヘッダーと4バイトのデータを組み立て、checksumで破損を検出します。続いてメッセージの欠落、再送、重複を扱う小さなモデルを作り、再送だけでは二重処理を防げないことを確認します。
1UDPの四つの欄
送信元ポート50000、宛先9000、データPINGの4バイトを使います。Lengthはヘッダー8バイトを含みます。
import struct,socketバイト列とIPの変換; payload=4バイトのデータb"PING"; udp=四つの16ビット欄struct.pack("!HHHH",50000,9000,8+len(payload),0)+payload; udp.hex16進数表示(" ")c3 50が50000、23 28が9000、00 0cが12バイトです。checksum欄はまず0にして計算し、後で値を入れます。UDPにはこの8バイトの中にsequenceやACKの欄はありません。
21の補数の和を計算する
16ビット単位で加算し、桁あふれを下位へ加え直してから全ビットを反転します。
def checksum(data):検査値の計算
data+=奇数長を計算用に補うbytes(len(data)%2)
total=sum16ビットごとに加算(struct.unpack("!"+str(len(data)//2)+"H",data))
while total>>16:桁あふれを加える
total=(total&65535)+(total>>16)
return total^655351の補数奇数バイトのデータは計算時だけ0で補います。UDPのchecksumはヘッダーとデータに加え、IPの送受信先等から作る疑似ヘッダーも対象です。単にデータPINGだけを計算するものではありません。
3疑似ヘッダーを含めて検査値を入れる
IPv4の送信元192.0.2.10、宛先198.51.100.20、プロトコル17、UDP長12を使います。
pseudo=IP情報とUDP長socket.inet_aton("192.0.2.10")+socket.inet_aton("198.51.100.20")+struct.pack("!BBH",0,17,len(udp)); value=検査値を計算checksum(pseudo+udp) or 65535; udp=checksum欄へ格納udp[:6]+struct.pack("!H",value)+udp[8:]; (udp.hex全体での検証(" "),checksum(pseudo+udp))検査値を含めて再計算すると0になります。算出したchecksumが0の場合は線上で0xffffを使い、IPv4 UDPの「checksumなし」を表す0と分けます。IPv6 UDPでは原則としてchecksumが必要です。
41ビット壊れたデータを検出する
データ先頭のPの最下位ビットを反転し、Qへ変えます。
damaged=試料を複製bytearray(udp); damaged[8]^=1データの1ビットを変更; (bytes変更内容と検証値(damaged[8:]),checksum(pseudo+bytes(damaged)))内容はQINGへ変わり、検証値は0ではなくなります。checksumはこうした破損検出に使いますが、暗号的な認証ではありません。また、検出して破棄してもUDPだけでは元のデータを再送しません。
5破損と欠落を別々に扱う
上位のメッセージ番号を1・2・3とし、2が届かなかった例です。この番号はUDPヘッダーではなくアプリケーションが付けたものです。
sent=上位の送信メッセージ{1:b"A",2:b"B",3:b"C"}; received=確認できたメッセージ{1:b"A",3:b"C"}; missing=未確認の番号sorted(sent.keys()-received.keys()); missing再送候補欠落した番号2が分かります。これは送った番号の集合と、受け取ったという上位の確認情報を使うモデルです。UDPのchecksumは到着したデータを検査するもので、丸ごと届かなかったデータグラムを自分で発見する仕組みではありません。
6同じ番号が二度届いた場合
番号2を再送した後、同じ番号がもう一度届く条件を作ります。
arrivals=再送で重複する到着列[1,3,2,2]; naive=到着ごとの処理[sent[i] for i in arrivals]; unique=番号で重複を除くlist(dict.fromkeys(arrivals)); (naive,unique)二方式の結果単純に毎回処理するとBが二回になります。番号で重複を除くと処理対象は1・3・2の三件ですが、順番はまだ1・2・3ではありません。重複排除と順序の復元は別の規則です。
7順序を必要とする処理へ渡す
三件ともそろったことを確認できた条件で、番号順に並べて連結します。
ordered=確認済み番号を順番に並べるb"".join(sent[i] for i in sorted(unique)); ordered復元したデータABCへ復元できます。この小さな例を実用化するには、確認応答、再送期限、番号の再利用、保存上限、輻輳制御なども設計が必要です。音声のように遅れたデータが不要な用途では、すべてそろうまで待つ方針が適するとは限りません。
まとめ
UDPのchecksumは破損を検出しますが、配送・再送・順序・重複排除を自動で保証しません。
上位の番号、確認、期限の規則を用途に合わせて設計します。
操作履歴と解説を振り返れます。
Python 3
import struct,socket; payload=b"PING"; udp=struct.pack("!HHHH",50000,9000,8+len(payload),0)+payload; udp.hex(" ")'c3 50 23 28 00 0c 00 00 50 49 4e 47'
def checksum(data):
data+=bytes(len(data)%2)
total=sum(struct.unpack("!"+str(len(data)//2)+"H",data))
while total>>16:
total=(total&65535)+(total>>16)
return total^65535pseudo=socket.inet_aton("192.0.2.10")+socket.inet_aton("198.51.100.20")+struct.pack("!BBH",0,17,len(udp)); value=checksum(pseudo+udp) or 65535; udp=udp[:6]+struct.pack("!H",value)+udp[8:]; (udp.hex(" "),checksum(pseudo+udp))('c3 50 23 28 00 0c 8e 7a 50 49 4e 47', 0)damaged=bytearray(udp); damaged[8]^=1; (bytes(damaged[8:]),checksum(pseudo+bytes(damaged)))(b'QING', 65279)
sent={1:b"A",2:b"B",3:b"C"}; received={1:b"A",3:b"C"}; missing=sorted(sent.keys()-received.keys()); missing[2]
arrivals=[1,3,2,2]; naive=[sent[i] for i in arrivals]; unique=list(dict.fromkeys(arrivals)); (naive,unique)([b'A', b'C', b'B', b'B'], [1, 3, 2])
ordered=b"".join(sent[i] for i in sorted(unique)); orderedb'ABC'
演習完了