第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プロトコルのパースを学びます。