この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。RFC 9293とMicrosoft LearnのTcpClient公式資料を確認し、localhostでTCPのbyte streamを観察しながら、実プロトコルで必要な「区切り」の設計まで整理しています。検証ステータス:📘 公式仕様確認済み・実行未検証
TCPでは、送信側が Write を2回呼んでも、受信側が2回に分かれて受け取れる保証はありません。TCPが扱うのはメッセージの列ではなく、順序付きのbyte streamだからです。
この違いを知らないと、「たまにJSONが途中で切れる」「2件のデータがくっつく」といった不具合につながります。
まず試す
PowerShellを2つ開きます。
受信側:
$l = [Net.Sockets.TcpListener]::new(
[Net.IPAddress]::Loopback,
50555
)
$l.Start()
$c = $l.AcceptTcpClient()
$s = $c.GetStream()
$b = New-Object byte[] 1024
$n = $s.Read($b, 0, $b.Length)
[Text.Encoding]::UTF8.GetString($b, 0, $n)
$s.Dispose()
$c.Dispose()
$l.Stop()
送信側:
$c = [Net.Sockets.TcpClient]::new('127.0.0.1', 50555)
$s = $c.GetStream()
foreach ($x in 'HELLO', 'WORLD') {
$b = [Text.Encoding]::UTF8.GetBytes($x)
$s.Write($b, 0, $b.Length)
}
$s.Dispose()
$c.Dispose()
受信側で HELLOWORLD と見える場合もあれば、1回のReadでは途中までしか取れない場合もあります。
送信回数と受信回数は対応しない
sequenceDiagram participant C as Client participant T as TCP byte stream participant S as Server C->>T: Write "HELLO" C->>T: Write "WORLD" T-->>S: Read "HELLOWORLD" のこともある T-->>S: Read "HEL" + 次のReadで残り のこともある
TCPが保証するのは「順序」や「信頼性」であって、アプリが行ったWriteの境界ではありません。
bufferを3byteにしてみる
受信bufferを3byteへ縮めると、1回のReadでは全体を取れないことが目で分かります。
$b = New-Object byte[] 3
この実験で、「Readしたら1メッセージ完成」という設計が危険だと分かります。
実プロトコルではどう区切るか
代表的な方法は3つあります。
| 方法 | 例 | 特徴 |
|---|---|---|
| 区切り文字 | 改行まで1件 | テキスト向き |
| 固定長 | 常に128byte | 単純だが柔軟性低め |
| length prefix | 先頭4byteに本文長 | バイナリ/API向き |
たとえば改行区切りなら、Readするたびに文字列化して終わりではなく、受信bufferへためて「改行が来たところまで」を1件として取り出します。
仕事でどう使えるか
TCPソケット通信
独自プロトコル
固定長電文
機器通信
TCP上で流れるJSONやCSV
古い業務システムとの連携
「テスト環境では毎回1回で受信できた」だけでは、正しい実装の根拠になりません。通信量、OS、ネットワーク状況、送信タイミングが変わると分割単位も変わり得ます。
注意点
Read()の戻り値nは実際に受信したbyte数bufferサイズいっぱいまで必ず埋まるわけではない
Read() = 0は接続終了を示す重要な状態UTF-8文字は複数byteなので、文字境界も考慮が必要
本番プロトコルでは最大メッセージ長を決める
まとめ
TCPはbyte streamであり、メッセージ境界を保持しない
Write回数とRead回数は一致しない
アプリ側で区切りルールが必要
改行、固定長、length prefixなどを使う
Readの戻り値と接続終了を正しく扱う
