TCPのストリーム通信の切れ目・区切りについて
TCPの勉強をしていたら、TCPはストリーム型という説明がされていました。
イメージがしやすいUDPから話すと、UDPはデータグラムを処理するプロトコルです。データグラムはその名の通りデータのことで、1つのデータを送るときに1つのUDPパケットが生成され送られます。(MSSに収まる範囲)
引用元:https://www.techscore.com/tech/Java/JavaSE/Network/4/
一方でTCPではスリーウェイハンドシェイク後にコネクションが貼られ、以降コネクションが切断されるまでストリーミング的にデータがやり取りされる挙動を行います。
ちなみにソケット通信において、送信側でsendしたデータを受信側でrecvした場合、sendが複数回であったとしてもrecvではひとまとめにされることがあります。(その逆もあり) 「sendとrecvが1対1になる保証はない」のがストリーム型の性質です。
疑問
このとき疑問に思ったのは「TCPプロトコルにおけるデータの切れ目は何で規定されるのか?」ということです。
例えば「こんにちは」という言葉をTCPを経由して送った場合、受信元では「こんにちは」を受け取ったとしてもそのままではこれ以降に文字が続いているかどうかを判断できません。
ならば、どこかで「こんにちは(ここが最後だよ)」というメッセージを付与しているのではないか? もしくは別の方法にてTCPストリームの区切りを表現しているのでは? というのが調べたかった内容です。
調査結果
TCPでの実装ではなく、その上位のプロトコルやアプリケーションによって実装される。
方法としては以下の手段が考えられます。
- 特定の文字をデータの終わりにつけて判別できるようにする(CRLFなど)
- メッセージを固定長にする(固定長データが届いたらそれで完了という判断)
- パケット内のヘッダ(L7)にてデータのサイズを書き込んでしまう
- 送信側が書き込みをshutdownする→このとき受信側は今来ているものをrecvするだけでよい
- アプリケーションで制御する
socket通信の場合(L4)
以下のようなコードを書き実行してみましょう。
この例ではアプリケーションで制御する方法を書いてみます。
※recvが値を返すけどそのデータサイズがない場合は終了したということ
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.connect(('127.0.0.1', 50001))
data = 'a' * 1024 * 1024 * 5 + 'bbb'
server.send(data)
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 50001))
print('Server listen')
server.listen(1)
(connection, client) = server.accept()
while True:
try:
print('Client connected', client)
data = connection.recv(1024)
if not data:
print "break"
break
print(data)
except socket.error:
connection.close()
break
実行するとserver.pyは以下の出力となります。
('Client connected', ('127.0.0.1', 63949))
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
('Client connected', ('127.0.0.1', 63949))
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
('Client connected', ('127.0.0.1', 63949))
bbb
('Client connected', ('127.0.0.1', 63949))
break
recv時に1024byte単位で分割するように受け取っている(data = connection.recv(1024)
)のでこのような形になっています。
ただしWiresharkでパケットを見てみると、〜〜〜〜aaaaaabbbとなっているようにTCPストリームとしてはくっついて飛んできていますね。うまいことsocketのrecvは指定した1024byteごとに分割してくれているということがわかります。
さてキャプチャをもう少し見てみるとこのような形で50001にlistenしているサーバにTCP通信が発生しています。
Lengthに注目してみると16388が連続した後に7467が発生していますね。これにより、この画面をみるとLengthが縮まった=データは終わりっぽいぞということがわかります。
これをプログラムで表現しているのがここです。
if not data:
print "break"
break
recvをして1024byteづつTCPストリームから取得したデータを分割していった最後は0byteのデータが帰ってくることになりますので、これをもってデータの切れ目と判断することができますね。
注意事項
この方法を用いる場合、例えばデータ通信が途切れてしまったときでもdata = 0が成り立ってしまいその時点のデータのお尻が切れ目ということになってしまいます。
不明点
- HTTPはどのように実装されているのか?軽く調べた限りわからなかった
- telnet/FTPなどはCRLFを区切り文字としてデータの切れ目を判断しているとのこと
参考
- https://docs.python.org/ja/3/library/socket.html#socket.socket.recv
- https://teratail.com/questions/99174
- http://hp.vector.co.jp/authors/VA019876/sokrpg/doc/SockFAQ/sfaq01.html
- http://www.ne.jp/asahi/hishidama/home/tech/socket/index.html#%E9%9B%BB%E6%96%87%E3%81%AE%E7%B5%82%E4%BA%86%E6%A4%9C%E7%9F%A5