TCP(Transmission Control Protocol)は、信頼性の高い接続指向通信を提供するトランスポート層プロトコルです。クライアント・サーバー(C/S)モデルやブラウザ・サーバー(B/S)モデルの基盤として広く利用されています。
TCPヘッダ構造 TCPヘッダには以下のフィールドが含まれます:
- **ソース/宛先ポート:**通信を行うプロセスを識別します。
- **シーケンス番号:**送信側が送信したデータバイトの順序を示します。
- **確認番号:**受信側が正しく受信した直前のバイト数を返します(ACKフラグが立っている場合に有効)。
- 制御フラグ:
URG:緊急データポインタの有効性ACK:確認番号の有効性PSH:受信アプリケーションに即時データ読み取りを要求RST:接続の強制再設定(リセットパケット)SYN:接続確立要求(同期パケット)FIN:接続終了通知(終了パケット)
ウィンドウサイズ:受信バッファの空き容量を示し、フロー制御に使用されます。 チェックサム:データの整合性を検証します。 緊急ポインタ:緊急データの位置を示します。
粘包(Packet Sticky)問題 TCPはストリーム指向のプロトコルであるため、複数のアプリケーションメッセージが単一のTCPセグメントに結合されたり、単一の大きなメッセージが複数のセグメントに分割されたりすることがあります。これにより、受信側でメッセージの境界が不明確になり、データの解析が困難になります。
発生要因
- **送信側の連続送信:**高速で複数のデータを送信すると、TCP/IPスタックがNagleアルゴリズムやセグメント結合により複数のメッセージを1つのパケットにまとめます。
- **受信側の処理遅延:**受信バッファに複数のメッセージが蓄積され、一度に読み取られると境界が失われます。
- **滑動ウィンドウの動的変更:**ネットワーク状況に応じてウィンドウサイズが変化し、パケットの分割・結合が発生します。
解決策
- **固定長メッセージ:**各メッセージを固定サイズで送信。簡易だが無駄なバイトが発生し、柔軟性に欠けます。
- **メッセージヘッダ方式:**各メッセージの先頭に長さ情報(例:4バイトの整数)を追加。受信側はまずヘッダを読み取り、その後指定長分のデータを取得します。
- **区切り文字:**メッセージ間に特殊文字(例:
\n、\0)を挿入。受信側で区切り文字を検出して分割します。 - **心拍パケット:**一定間隔で空のデータを送信し、通信の活性状態を維持。境界の明確化にも寄与します。
- **高度なアプリケーションプロトコル:**HTTP/2やQUICのように、フレーム構造を明確に定義したプロトコルを利用します。
滑動ウィンドウとフロー制御 TCPは滑動ウィンドウ方式でフロー制御を行います。送信側は受信側の受信可能容量(ウィンドウサイズ)に基づいて送信量を制御します。これにより、受信バッファのオーバーフローを防ぎ、ネットワークの過負荷を回避します。
例:チャットアプリで、1メッセージずつ送信して返信を待つ(一発一応)と効率が悪くなります。一方、複数メッセージを一度に送信し、後でACKを受けて処理する方式(滑動ウィンドウ)では、待機時間を有効に活用でき、通信効率が大幅に向上します。
TCP接続の確立と解放 信頼性の高さは、以下のプロセスによって保証されます:
- **三度のハンドシェイク:**接続確立(SYN → SYN-ACK → ACK)
- **シーケンス番号と確認番号:**送信データの順序と欠落の検出
- **四度のハンドシェイク:**接続終了(FIN → ACK → FIN → ACK)
UDPとの比較
| 項目 | UDP | TCP |
|---|---|---|
| 接続性 | 接続なし(無接続) | 接続あり |
| 信頼性 | 非信頼 | 信頼 |
| 転送形式 | データグラム単位 | バイトストリーム |
| オーバーヘッド | 低 | 高 |
| 使用例 | リアルタイム音声、DNS | Web、ファイル転送、メール |
TCPクライアント・サーバー実装
サーバー側(受信) サーバーは以下の手順で通信を開始します:
socket():IPv4/TCP用ソケット作成bind():ローカルIPとポートにバインドlisten():接続要求の待ち受け状態に移行accept():クライアント接続を受付、新しいソケットを返すrecv()/send():データの受信・送信close():ソケットを破棄
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int create_tcp_server(int port) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) return -1;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
close(server_fd);
return -1;
}
if (listen(server_fd, 5) == -1) {
close(server_fd);
return -1;
}
return server_fd;
}
int handle_client(int server_fd) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);
if (client_fd == -1) return -1;
char buffer[1024] = {0};
ssize_t bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (bytes > 0) {
printf("受信: %s\n", buffer);
}
const char* response = "サーバーからの返信";
send(client_fd, response, strlen(response), 0);
close(client_fd);
return 0;
}
クライアント側(送信) クライアントは以下の手順でサーバーに接続します:
socket():ソケット作成connect():サーバーへ接続要求send()/recv():データ送受信close():ソケット解放
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int create_tcp_client(const char* ip, int port) {
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) return -1;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_addr.sin_addr);
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
close(client_fd);
return -1;
}
return client_fd;
}
int send_file_data(int client_fd, const char* filename) {
FILE* fp = fopen(filename, "rb");
if (!fp) return -1;
char buffer[1024];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
if (send(client_fd, buffer, bytes_read, 0) == -1) {
fclose(fp);
return -1;
}
}
fclose(fp);
return 0;
}
ファイル転送の実装 ファイル転送では、ファイル名を最初に送信し、その後バイナリデータを連続して送信します。受信側はファイル名を受信後、同名のファイルを作成し、受信データを書き込みます。
注意点として、recv() の戻り値が 0 の場合、接続が正常に切断されたことを示します。これにより、送信側がファイル転送を完了したことを検出できます。
ファイル転送の例では、送信側が read() でファイルを読み込み、send() で送信。受信側は recv() で受信し、write() でファイルに書き込みます。この際、バッファのクリアやエラーチェックが不可欠です。
エラー対応:Address already in use
サーバー再起動時に「Address already in use」エラーが発生するのは、前回の接続が TIME_WAIT 状態でポートを占有しているためです。対策として、setsockopt() で SOL_SOCKET の SO_REUSEADDR オプションを有効にします。
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
これにより、同じアドレス・ポートを再利用できるようになります。