LinuxにおけるSocketプログラミングの核心原理と実装

ネットワーク上のプロセス間通信は、単一マシン内でのIPC(Inter-Process Communication)とは本質的に異なる課題を孕んでいます。ローカル環境では、パイプや共有メモリ、セマフォといった機構がプロセス識別と同期を支えますが、これらはホストを跨いだ通信には適用できません。分散環境では、プロセスを一意に特定するための「アドレス空間」が必要であり、それがTCP/IPスタックによって提供されます。

ネットワーク通信の識別子:三要素モデル

インターネット上で任意のエンドポイントを特定するには、IPアドレス+トランスポートプロトコル+ポート番号という3つの情報が不可欠です。この組み合わせは「ネットワークソケットアドレス」と呼ばれ、通信の起点と終点を明確に定義します。たとえば、192.168.1.10:443/TCP は、IPv4アドレスを持つサーバーのHTTPSサービスを指し、他のプロセスと混同されることはありません。

Socket:抽象化された通信インターフェース

Socketは、Unix系OSにおける「すべてはファイル」という哲学に基づくI/O抽象層です。実際にはカーネルが管理するデータ構造への参照であり、整数型のファイル記述子(file descriptor)としてアプリケーションに公開されます。この設計により、read()write()close()といった標準I/O関数をそのままネットワーク通信に流用できます。

重要なのは、Socketが「レイヤー」ではなくファサードパターンの実装である点です。下位のTCP/IPプロトコルスタックの複雑な状態遷移やパケット構築を隠蔽し、開発者はシンプルな関数呼び出しのみで双方向通信を実現できます。

主要APIの役割と使い方

1. ソケット作成:socket()

int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  • AF_INET: IPv4アドレス族を使用
  • SOCK_STREAM: 接続指向・信頼性のあるバイトストリーム(TCP)
  • IPPROTO_TCP: 明示的にTCPプロトコルを選択

2. アドレス束縛:bind()

struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

サーバー側では、待ち受けポートを明示的に指定する必要があります。ここで注意すべきはバイトオーダーの変換htons()(host-to-network short)でホストバイトオーダーをネットワークバイトオーダー(ビッグエンディアン)に変換しなければ、通信は失敗します。

3. 接続待ち:listen()accept()

listen(sockfd, SOMAXCONN); // キュー長を最大値に設定

int client_fd;
struct sockaddr_storage client_addr;
socklen_t addr_len = sizeof(client_addr);
client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);

listen()はソケットを受動モードに切り替え、accept()は接続要求を待機して、確立した接続ごとに新しいファイル記述子を返します。元のsockfdは引き続き新たなクライアントを受け付けます。

4. データ送受信

接続確立後は、以下のようなI/O関数で通信を行います:

  • send(client_fd, buffer, len, 0) — メッセージ送信
  • recv(client_fd, buffer, sizeof(buffer), 0) — メッセージ受信
  • shutdown(client_fd, SHUT_RDWR) — 双方向の通信終了通知

TCP接続のライフサイクル

三次ハンドシェイク(接続確立)

  1. クライアント → サーバー: SYNパケット(シーケンス番号 J)
  2. サーバー → クライアント: SYN-ACKパケット(ACK=J+1, SYN=K)
  3. クライアント → サーバー: ACKパケット(ACK=K+1)

この時点で両端はESTABLISHED状態となり、データ転送が可能になります。カーネルはこの手順を完全に隠蔽し、アプリケーションはconnect()accept()の戻り値のみを確認すれば十分です。

四次ハンドシェイク(接続終了)

TCPの全二重通信特性ゆえ、各方向の終了を個別に通知する必要があります:

  1. 一方がFINを送信(送信停止の意思表示)
  2. 他方がACKで応答(受信確認)
  3. その後、他方もFINを送信(自身の送信停止)
  4. 最初の送信者がACKで最終確認

この過程で生じるTIME_WAIT状態は、最後のACKが確実に届くよう、2倍のMSL(Maximum Segment Lifetime)時間を待機するための安全機構です。

実践コード:非ブロッキングEchoサーバー

以下の例では、forkによる並列処理を避け、単一プロセスで複数クライアントを扱う簡易実装を示します。

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define PORT 9001
#define BACKLOG 5
#define BUF_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUF_SIZE];

    // ソケット作成
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket creation failed");
        return 1;
    }

    // アドレス再利用設定(テスト時便利)
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // サーバーアドレス構築
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // バインドとリスン
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1 ||
        listen(server_fd, BACKLOG) == -1) {
        perror("binding or listening failed");
        close(server_fd);
        return 1;
    }

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        if ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) == -1) {
            perror("accept failed");
            continue;
        }

        ssize_t bytes_read;
        while ((bytes_read = recv(client_fd, buffer, BUF_SIZE - 1, 0)) > 0) {
            buffer[bytes_read] = '\0';
            printf("Received: %s", buffer);

            // エコーバック
            send(client_fd, buffer, bytes_read, 0);
            
            // クライアントが閉じた場合
            if (bytes_read == 0) break;
        }
        close(client_fd);
    }

    close(server_fd);
    return 0;
}

この実装は、基本的なSocket APIの流れ——socket()bind()listen()accept()recv()/send()close()——を忠実に反映しています。実際の商用システムでは、epollや非同期I/O、スレッドプールなどの高度な技術が追加されますが、その基盤となる概念はここに集約されています。

タグ: linux socket TCP system-programming Network-Programming

8月10日 08:38 投稿