第2章:ソケットプログラミング

はじめに

ソケットは、ネットワーク通信の基本的なインターフェースです。本章では、Berkeley Sockets APIを使用したTCPサーバーの実装を学びます。

---

1. ソケットの概念

1.1 ソケットとは

ソケット = ネットワーク通信のエンドポイント

+--------+                        +--------+
|        |  Socket <──> Socket    |        |
| Client |========================| Server |
|        |      TCP/IP Network    |        |
+--------+                        +--------+

アドレス = IPアドレス + ポート番号
例: 192.168.1.1:8080

1.2 ソケットの種類

SOCK_STREAM (TCP):
- 接続型
- 信頼性あり
- 順序保証
- Webサーバーで使用

SOCK_DGRAM (UDP):
- 非接続型
- 信頼性なし
- 順序保証なし
- DNSなどで使用

---

2. TCPサーバーの基本手順

2.1 サーバー側の手順

1. socket()   - ソケット作成
      ↓
2. setsockopt() - オプション設定
      ↓
3. bind()     - アドレス割り当て
      ↓
4. listen()   - 接続待機
      ↓
5. accept()   - 接続受付
      ↓
6. recv/send  - データ通信
      ↓
7. close()    - ソケット終了

2.2 クライアント側の手順

1. socket()   - ソケット作成
      ↓
2. connect()  - サーバーに接続
      ↓
3. send/recv  - データ通信
      ↓
4. close()    - ソケット終了

---

3. 関数の詳細

3.1 socket()

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

// パラメータ
domain:   AF_INET (IPv4) / AF_INET6 (IPv6)
type:     SOCK_STREAM (TCP) / SOCK_DGRAM (UDP)
protocol: 0 (自動選択)

// 戻り値
成功: ファイルディスクリプタ
失敗: -1

// 使用例
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
    perror("socket");
    exit(1);
}

3.2 setsockopt()

#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

// よく使うオプション
// SO_REUSEADDR: サーバー再起動時にアドレスを即座に再利用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

// SO_REUSEPORT: 複数プロセスで同じポートをリッスン(負荷分散)
setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

3.3 bind()

#include <sys/socket.h>
#include <netinet/in.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// アドレス構造体
struct sockaddr_in {
    sa_family_t    sin_family;  // AF_INET
    in_port_t      sin_port;    // ポート番号(ネットワークバイトオーダー)
    struct in_addr sin_addr;    // IPアドレス
};

// 使用例
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;  // すべてのインターフェース
addr.sin_port = htons(8080);        // ポート8080

if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    perror("bind");
    exit(1);
}

3.4 listen()

#include <sys/socket.h>

int listen(int sockfd, int backlog);

// backlog: 保留中の接続のキューサイズ
// 典型的な値: SOMAXCONN(システム最大値)

if (listen(server_fd, SOMAXCONN) < 0) {
    perror("listen");
    exit(1);
}

3.5 accept()

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 戻り値: 新しいソケット(クライアントとの通信用)

struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
    perror("accept");
}

// クライアントのIPアドレスを取得
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
std::cout << "Client connected: " << client_ip << std::endl;

3.6 recv() / send()

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

// フラグ
// 0: デフォルト
// MSG_DONTWAIT: 非ブロッキング
// MSG_PEEK: データを読むがバッファから削除しない

// 受信例
char buffer[4096];
ssize_t bytes_received = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
    buffer[bytes_received] = '\0';
    std::cout << "Received: " << buffer << std::endl;
} else if (bytes_received == 0) {
    std::cout << "Client disconnected" << std::endl;
} else {
    perror("recv");
}

// 送信例
const char* response = "HTTP/1.1 200 OK\r\n\r\nHello!";
ssize_t bytes_sent = send(client_fd, response, strlen(response), 0);

---

4. バイトオーダー

4.1 ネットワークバイトオーダー

#include <arpa/inet.h>

// ホスト → ネットワーク
uint16_t htons(uint16_t hostshort);   // short
uint32_t htonl(uint32_t hostlong);    // long

// ネットワーク → ホスト
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

// 例
uint16_t port = 8080;
uint16_t net_port = htons(port);  // ネットワークバイトオーダーに変換

4.2 IPアドレスの変換

#include <arpa/inet.h>

// 文字列 → バイナリ
int inet_pton(int af, const char *src, void *dst);

// バイナリ → 文字列
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

// 例
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.1", &addr);

char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, str, INET_ADDRSTRLEN);

---

5. 非ブロッキングソケット

5.1 fcntl()でノンブロッキング

#include <fcntl.h>

// ソケットを非ブロッキングに設定
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

// 非ブロッキングrecv
ssize_t bytes = recv(sockfd, buffer, sizeof(buffer), 0);
if (bytes < 0) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // データがまだ到着していない
        // → 他の処理を行う
    } else {
        // 本当のエラー
        perror("recv");
    }
}

5.2 なぜ非ブロッキングが必要か

ブロッキング:
accept() → 接続が来るまでブロック
recv()   → データが来るまでブロック

問題: 1つのクライアントを待っている間、
     他のクライアントを処理できない

解決策:
1. マルチプロセス/マルチスレッド
2. I/O多重化 + 非ブロッキング ← webservで使用

---

6. 基本的なサーバー実装

6.1 シンプルなHTTPサーバー

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    // 1. ソケット作成
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return 1;
    }

    // 2. オプション設定
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. アドレスにバインド
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        return 1;
    }

    // 4. リッスン開始
    if (listen(server_fd, SOMAXCONN) < 0) {
        perror("listen");
        return 1;
    }

    std::cout << "Server listening on port 8080..." << std::endl;

    // 5. メインループ
    while (true) {
        // 接続を受け付け
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(server_fd,
                               (struct sockaddr*)&client_addr,
                               &client_len);

        if (client_fd < 0) {
            perror("accept");
            continue;
        }

        // リクエストを受信
        char buffer[4096];
        ssize_t bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
        if (bytes > 0) {
            buffer[bytes] = '\0';
            std::cout << "Request:\n" << buffer << std::endl;
        }

        // レスポンスを送信
        const char* response =
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/html\r\n"
            "Content-Length: 13\r\n"
            "\r\n"
            "Hello, World!";

        send(client_fd, response, strlen(response), 0);

        // 接続を閉じる
        close(client_fd);
    }

    close(server_fd);
    return 0;
}

---

7. エラーハンドリング

7.1 よくあるエラー

// bind()のエラー
EADDRINUSE   // アドレスが使用中
             // → SO_REUSEADDRを設定、または別のポートを使用

EACCES       // 特権ポート(1-1023)に非rootでバインド
             // → 1024以上のポートを使用

// accept()のエラー
EAGAIN       // 非ブロッキングで接続待ちがない
EINTR        // シグナルで中断

// recv()のエラー
EAGAIN       // 非ブロッキングでデータがない
ECONNRESET   // 相手が接続をリセット

7.2 エラー処理パターン

int createServerSocket(int port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        throw std::runtime_error("socket() failed: " +
                                 std::string(strerror(errno)));
    }

    int opt = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        close(fd);
        throw std::runtime_error("setsockopt() failed");
    }

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

    if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        close(fd);
        throw std::runtime_error("bind() failed on port " +
                                 std::to_string(port));
    }

    if (listen(fd, SOMAXCONN) < 0) {
        close(fd);
        throw std::runtime_error("listen() failed");
    }

    return fd;
}

---

まとめ

本章で学んだこと:

  • ソケットの概念: エンドポイント、アドレス
  • TCPサーバーの手順: socket, bind, listen, accept
  • 関数の詳細: 各システムコールの使い方
  • バイトオーダー: ネットワークバイトオーダーへの変換
  • 非ブロッキング: fcntlでの設定
  • 基本実装: シンプルなHTTPサーバー

次章では、HTTPプロトコルのパースを学びます。