TCPのストリームとは?データ転送の特徴も!(バイトストリーム:連続データ:セグメント化:順序保証:UDPとの違いなど)
ネットワーク通信を学ぶうえで、TCPのストリームという概念は避けて通れない重要なテーマです。Webブラウジングやファイル転送、メール送受信など、私たちが日常的に使うインターネットサービスの多くはTCPを基盤としています。しかし、「ストリームとは何か」「データはどのように流れているのか」という点を深く理解している方は意外と少ないかもしれません。
この記事では、TCPのバイトストリームの仕組みから、セグメント化・順序保証・UDPとの違いまで、幅広く解説していきます。ネットワーク初心者の方でも理解できるよう、具体的なサンプルコードも交えながら丁寧に説明していきますので、ぜひ最後までご覧ください。
TCPのストリームとは「切れ目のない連続データの流れ」である
それではまず、TCPのストリームの本質的な意味について解説していきます。
TCPにおける「ストリーム」とは、バイト列が途切れることなく連続して流れる通信方式のことを指します。川の流れのように、送信側が送ったデータが順番通りに受信側へ届くイメージです。これを「バイトストリーム」と呼ぶことも多く、TCP通信の根幹をなす概念と言えるでしょう。

バイトストリームの基本概念
バイトストリームとは、データを「1バイトずつの連続した列」として扱う考え方です。送信側がどのようなサイズでデータを書き込んでも、受信側には途切れのない一本のデータ列として届きます。
たとえば、送信側が「100バイト」「200バイト」「50バイト」と3回に分けてデータを送ったとしても、受信側から見れば「350バイトの連続したデータ」として認識されます。このようなメッセージ境界が存在しない点が、バイトストリームの大きな特徴です。
以下のPythonコードで、TCPサーバとクライアントの基本的なストリーム通信を確認してみましょう。
# TCPサーバー側のサンプルコード
import socket
サーバーソケットの作成
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 9000))
server_socket.listen(1)
print("サーバー待機中...")
conn, addr = server_socket.accept()
print(f"接続元: {addr}")
データを受信(バイトストリームとして受け取る)
data = conn.recv(1024)
print(f"受信データ: {data.decode('utf-8')}")
conn.close()
server_socket.close()
出力結果:
サーバー待機中...
接続元: ('127.0.0.1', 54321)
受信データ: こんにちは、TCPストリームの世界へ!
# TCPクライアント側のサンプルコード
import socket
クライアントソケットの作成
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('localhost', 9000))
データをストリームとして送信
message = "こんにちは、TCPストリームの世界へ!"
client_socket.sendall(message.encode('utf-8'))
client_socket.close()
出力結果:
(送信完了、エラーなし)
このように、TCPではSOCK_STREAMというソケットタイプを使用することで、ストリーム通信を実現しています。
連続データとしての扱われ方
TCPストリームでは、アプリケーションが送信するデータは「連続したバイト列」として扱われます。これは、受信側のアプリケーションが任意のタイミングで任意のサイズだけデータを読み取れることを意味するでしょう。
実際の通信では、OSのTCPバッファにデータが溜まっていき、アプリケーションがそこから必要な分だけ読み取ります。送信側が一度に送ったデータが、受信側では複数回に分けて読み取られることも珍しくありません。
| 送信側の動作 | 送信バイト数 | 受信側の動作 | 受信バイト数 |
|---|---|---|---|
| 1回目の送信 | 100バイト | 1回目の受信 | 150バイト(まとめて) |
| 2回目の送信 | 200バイト | 2回目の受信 | 200バイト |
| 3回目の送信 | 50バイト | 3回目の受信 | なし(バッファ内) |
このように、送受信の回数やサイズは必ずしも一致しません。TCPストリームを扱うアプリケーションは、この特性を前提とした設計が必要です。
ストリームとメッセージ指向の違い
TCPのストリームに対して、「メッセージ指向」という考え方があります。メッセージ指向では、1回の送信が1つのまとまったメッセージとして届くことが保証されます。
TCPはストリーム指向であるため、メッセージの境界が自動的には保存されません。アプリケーション側で独自のプロトコルを設けて、メッセージの区切りを管理する必要があります。HTTPやFTPなどのプロトコルが独自のヘッダや区切り文字を持つのは、まさにこのためです。
TCPのセグメント化とデータ転送の仕組み
続いては、TCPのセグメント化とデータ転送の詳細な仕組みを確認していきます。
アプリケーションが送信したバイトストリームは、ネットワーク上を流れる際に「セグメント」と呼ばれる単位に分割されます。このセグメント化の仕組みを理解することで、TCPがなぜ信頼性の高い通信を実現できるのかが見えてきます。
セグメントとMSS(最大セグメントサイズ)
TCPがデータを分割する際の最大サイズを、MSS(Maximum Segment Size)と呼びます。MSSはTCPハンドシェイク時に双方が交渉して決定するもので、一般的なイーサネット環境では1460バイト前後が使われることが多いです。
たとえば、10,000バイトのデータを送信する場合、TCPは自動的に複数のセグメントに分割して送信します。受信側はこれらのセグメントを受け取り、元のバイトストリームに再組み立てします。
# セグメント化を意識したデータ送信のサンプル
import socket
def send_large_data(host, port, data):
# 大きなデータをTCPで送信する
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
# sendallはデータが完全に送信されるまで繰り返し送信する
# TCPが内部でセグメント化を行う
client_socket.sendall(data.encode('utf-8'))
print(f"送信完了: {len(data)} バイト")
client_socket.close()
ゴリラとドラゴンフルーツに関する大量データを送信
large_message = "ゴリラ情報:" + "A" * 5000 + "ドラゴンフルーツ情報:" + "B" * 5000
send_large_data('localhost', 9001, large_message)
出力結果:
送信完了: 10032 バイト
このコードではsendallメソッドを使用していますが、内部ではTCPがMSSに基づいてデータを複数のセグメントに分けて送信しています。
シーケンス番号による順序管理
TCPセグメントには「シーケンス番号」が付与されます。これは、バイトストリームの中でそのセグメントが何バイト目から始まるかを示す番号です。
受信側はシーケンス番号を見ることで、受け取ったセグメントを正しい順序に並べ直せます。ネットワークの都合でセグメントが到着順序が入れ替わっても、シーケンス番号によって正しい順序を復元できるのです。
| セグメント番号 | シーケンス番号 | データサイズ | 内容(例) |
|---|---|---|---|
| 1番目 | 1000 | 1460バイト | ドラゴンフルーツデータ前半 |
| 2番目 | 2460 | 1460バイト | ドラゴンフルーツデータ後半 |
| 3番目 | 3920 | 800バイト | アボカドデータ |
ACKと再送制御の役割
TCPでは、受信側がセグメントを受け取ると「ACK(確認応答)」を送信側に返します。送信側はACKを受け取ることで、データが正しく届いたことを確認します。
一定時間内にACKが返ってこない場合、TCPは自動的にセグメントを再送します。この仕組みにより、パケットロスが発生しても通信の信頼性が保たれるのです。再送制御はTCPの重要な機能のひとつであり、アプリケーション側が意識する必要はありません。
TCPの順序保証と信頼性を支える機能
続いては、TCPが持つ順序保証と信頼性を支えるさまざまな機能を確認していきます。
TCPがUDPなどと比べて「信頼性が高い」と言われる理由は、単純なデータ転送にとどまらない多くの仕組みを持っているからです。ここでは特に重要な機能を詳しく見ていきましょう。
フロー制御(ウィンドウサイズ)
TCPには「フロー制御」という仕組みがあります。受信側のバッファが溢れないよう、送信側が送るデータ量を調整する機能です。受信側は「ウィンドウサイズ」という値を送信側に伝えることで、今どれだけのデータを受け入れられるかを通知します。
送信側はウィンドウサイズの範囲内でのみデータを送信できます。受信側のバッファが空いてきたらウィンドウサイズを増やし、逆に混雑していれば小さくします。これにより、受信側の処理能力を超えてデータが流れ込むことを防げるのです。
# ウィンドウサイズ(受信バッファ)を確認するサンプル
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 9002))
server_socket.listen(1)
受信バッファサイズを設定(フロー制御のウィンドウサイズに影響)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
現在の受信バッファサイズを取得
rcvbuf = server_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
print(f"受信バッファサイズ: {rcvbuf} バイト")
server_socket.close()
出力結果:
受信バッファサイズ: 131072 バイト(OSが倍にする場合がある)
輻輳制御(ネットワークの混雑管理)
フロー制御が「受信側のバッファ」を守る仕組みであるのに対し、輻輳制御は「ネットワーク全体の混雑」を管理する仕組みです。ルータやスイッチなどのネットワーク機器が処理しきれなくなると、パケットが廃棄されます。
TCPはACKが届かない状況を検知することでネットワークの混雑を推測し、送信量を自動的に絞ります。代表的なアルゴリズムとして「スロースタート」「輻輳回避」「高速再転送」などがあります。これらはOSのTCPスタックが自動で処理するため、アプリケーション開発者が直接制御する必要はありません。
3ウェイハンドシェイクによる接続確立
TCPの通信は、データ送信の前に「3ウェイハンドシェイク」と呼ばれる手順でコネクションを確立します。SYN・SYN-ACK・ACKという3つのパケットを交わすことで、双方が通信の準備ができていることを確認します。
この接続確立の仕組みが、TCPのコネクション型通信の特徴そのものです。確立されたコネクションの上でバイトストリームが流れるため、順序保証や再送制御が正しく機能するわけです。
| ステップ | 送信元 | 送信先 | パケット種別 | 意味 |
|---|---|---|---|---|
| 1 | クライアント | サーバー | SYN | 接続要求 |
| 2 | サーバー | クライアント | SYN-ACK | 接続受け入れ+確認 |
| 3 | クライアント | サーバー | ACK | 確認応答 |
TCPとUDPの違いを比較して理解する
続いては、よく比較されるTCPとUDPの違いを詳しく確認していきます。
ネットワークの勉強をしていると、必ずTCPとUDPの比較が出てきます。どちらもインターネットの通信で広く使われているプロトコルですが、その特性は大きく異なります。
UDPの特徴とストリームとの対比
UDPは「User Datagram Protocol」の略で、コネクションレス型のプロトコルです。TCPのような3ウェイハンドシェイクは行わず、データを「データグラム」という単位でそのまま送り出します。
UDPにはバイトストリームという概念がなく、1回の送信が1つの独立したデータグラムとして扱われます。順序保証も再送制御もありませんが、その分オーバーヘッドが少なく高速という特性があります。
# UDP通信のサンプル(データグラム方式)
import socket
UDPソケットはSOCK_DGRAMを使用
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
コネクション確立なしでそのまま送信
message = "ボルト・ナット在庫情報"
udp_socket.sendto(message.encode('utf-8'), ('localhost', 9003))
print("UDPで送信完了(到達保証なし)")
udp_socket.close()
出力結果:
UDPで送信完了(到達保証なし)
TCPがSOCK_STREAMであるのに対し、UDPはSOCK_DGRAMを使用します。この違いがそれぞれのプロトコルの本質的な差を表しています。
TCPとUDPの使い分け
TCPとUDPはそれぞれ得意な用途が異なります。どちらが「優れている」ということはなく、用途に応じた使い分けが重要です。
| 比較項目 | TCP | UDP |
|---|---|---|
| 通信方式 | コネクション型(ストリーム) | コネクションレス型(データグラム) |
| 順序保証 | あり | なし |
| 再送制御 | あり | なし |
| 速度 | 比較的遅い | 高速 |
| 主な用途 | HTTP、FTP、メール | 動画配信、DNS、VoIP |
| ヘッダサイズ | 20〜60バイト | 8バイト |
ファイル転送やWebブラウジングのようにデータの完全性が最優先される場合はTCPが適しています。一方、リアルタイム動画通話やゲームのように多少のデータ欠損よりもレイテンシの低さが重要な場合はUDPが選ばれます。
QUIC:新しいプロトコルへの進化
近年注目されているのが「QUIC」というプロトコルです。QUICはUDPの上に独自のストリーム機能と信頼性制御を実装したもので、TCPの信頼性とUDPの速度を両立しようとする試みです。
HTTP/3はQUICを採用しており、従来のHTTP/2(TCP)と比べてコネクション確立の遅延削減や、複数ストリームの独立した管理が可能になっています。TCPとUDPの二択だけでなく、こうした新しいプロトコルの動向も追っておくと、ネットワークの理解がより深まるでしょう。
まとめ
この記事では、「TCPのストリームとは?データ転送の特徴も!」というテーマで、バイトストリームの概念からセグメント化・順序保証・UDPとの違いまで幅広く解説してきました。
TCPのストリームとは、送受信双方の間で確立されたコネクション上を流れる「途切れのないバイト列」のことです。データはセグメント単位に分割されてネットワークを流れ、シーケンス番号・ACK・再送制御によって確実に相手へ届けられます。フロー制御と輻輳制御がネットワーク全体の安定も守っており、これらすべてがTCPの信頼性を支えています。
UDPとの違いを理解することで、プロトコルの選択眼も養えるでしょう。「信頼性が必要な場面ではTCP、速度優先の場面ではUDP」という基本的な判断軸を持ちながら、QUICのような新技術にも目を向けていくと、ネットワーク全体の理解がさらに深まるはずです。ぜひ今回の内容を参考に、実際のコードを動かしながらTCPストリームの動作を体感してみてください。