C++によるネットワークプログラミングは、低レイヤで高い制御性を持つ一方、誤解や落とし穴が非常に多い分野です。
表面的に動くコードを書けても、「高負荷」「複数接続」「切断」「遅延」「セキュリティ」といった現実的条件に直面すると破綻しやすいのが特徴です。
本記事では、TCP/UDPの本質 → ソケットAPI → I/Oモデル → プロトコル設計 → 実務での注意点という順序で、
「間違いなく理解すべきポイント」に絞って整理します。
ネットワーク通信の基本構造
TCPとUDPの本質的な違い
| 項目 | TCP | UDP |
|---|---|---|
| 信頼性 | あり(再送・順序保証) | なし |
| 順序保証 | あり | なし |
| 境界 | なし(ストリーム) | あり(データグラム) |
| 遅延 | 比較的大きい | 小さい |
| 主な用途 | HTTP, HTTPS, SSH, DB | DNS, VoIP, ゲーム |
重要な理解
- TCPは「バイト列のストリーム」
→ メッセージ境界は保持されない - UDPは「1送信=1データグラム」
→ 境界は保持されるが、欠落・順序逆転はあり得る
この違いを誤解すると、設計レベルで破綻します。
C++におけるソケットプログラミングの位置づけ
C++自体は長らくネットワークAPIを標準で持っていません。
そのため実務では以下のいずれかを使います。
- POSIXソケット(Linux / macOS)
- Winsock(Windows)
- 抽象化ライブラリ(Boost.Asio など)
TCPサーバの基本フロー
socket()bind()listen()accept()send()/recv()close()(Windowsはclosesocket())
ここで重要なのは
accept() は「新しい通信専用ソケット」を返す
という点です。
listen用ソケットと通信ソケットは役割が異なります。
OS差分の正確な理解
Linux / macOS(POSIX)
- クローズ:
close(fd) - エラー取得:
errno - ヘッダ:
sys/socket.h,netinet/in.hなど
Windows(Winsock)
- 初期化必須:
WSAStartup() - 後処理:
WSACleanup() - クローズ:
closesocket() - エラー取得:
WSAGetLastError() - 一般的なヘッダ:
winsock2.h
クロスプラットフォーム対応を自前でやるのはコストが高く、実務では Boost.Asio 等の利用が現実的です。
ブロッキングI/OとノンブロッキングI/O
ブロッキングI/O
recv()はデータが来るまで待つ- 実装は簡単
- 同時接続数が増えるとスケールしにくい
ノンブロッキングI/O
recv()は即時復帰- データがなければ
EAGAIN / EWOULDBLOCK - イベント通知(select / poll / epoll / kqueue)が必要
実務上の注意
ノンブロッキングでは
- send() が部分送信で戻る
- send() 自体が失敗(EAGAIN)する
という2パターンがあり得ます。
そのため「送れた分だけ進める」「送れなければ次の書き込み可能イベントを待つ」という制御が必須です。
TCPで必ず理解すべき3つの落とし穴
TCPはメッセージ境界を保持しない
send("HELLO", 5)
しても、受信側では
- 2バイト + 3バイト
- 5バイト一括
- 他のデータと結合
など、任意の形で届きます。
プロトコルレベルで境界を定義する必要がある
代表的な方法
- 長さプレフィックス方式(例:先頭4バイトにサイズ)
- 区切り文字方式(
\nなど) - 固定長ヘッダ + 可変長ボディ
※ HTTPも「常に長さを前置する」わけではなく、Content-Length / chunked / 接続クローズなど 複数の終端定義方式があります。
send() / recv() は部分的にしか処理しない
- 必要サイズに達するまで 必ずループ処理
- 戻り値を厳密にチェックする
ネットワークバイトオーダー(エンディアン)
- ネットワークバイトオーダー:ビッグエンディアン
- 使用関数:
htons,htonlntohs,ntohl
異なるアーキテクチャ間通信では必須です。
同時接続処理モデル
1接続1スレッド
- 実装が簡単
- スレッド数増加で非効率
スレッドプール
- CPU利用効率が良い
- 実装コスト中程度
イベント駆動
- select / poll:FD数増加で線形コスト
- epoll(Linux)/ kqueue(macOS):大量接続向き
非同期I/O(Boost.Asio)
- OS差分吸収
- 高性能
- 設計思想の理解が必要
UDPプログラミングの現実
UDPは「軽い」=「簡単」ではありません。
UDPを使う場合、必要に応じて以下を自前実装します。
- シーケンス番号
- 再送制御
- 順序整列
- 輻輳制御
多少欠けても良い・低遅延が最優先という要件にのみ適します。
TLS / HTTPSはライブラリ前提で考える
TLSは単なる暗号化ではなく
- 証明書検証
- 鍵交換
- SNI / ALPN
など複雑な要素を含みます。
現実的な選択肢
- OpenSSL(低レイヤ・高難度)
- Boost.Asio + OpenSSL
- libcurl(HTTPクライアント)
- Boost.Beast(HTTPサーバ)
実務で必須の追加知識(見落とされがち)
SIGPIPE対策(Unix系)
切断済みソケットへの send でプロセスが終了する可能性あり。
MSG_NOSIGNAL 等で対策が必要。
TIME_WAIT とポート再利用
サーバ再起動時の bind() 失敗対策としてSO_REUSEADDR を検討。
Nagleアルゴリズム
低遅延が必要な場合は TCP_NODELAY を検討。
まとめ
C++ネットワークプログラミングは
- TCP=ストリーム
- 境界はアプリ層で定義
- 部分送受信は常に起こる
- I/Oモデル選択が設計の要
- 実務ではライブラリ活用が合理的
以上、C++によるネットワークプログラミングについてでした。
最後までお読みいただき、ありがとうございました。
