CodeCrafters は HTTP サーバーやシェルなどを車輪の再発明するウェブサイトです。C言語で Redis を実装した際に調べた関数や構造体を備忘録としてまとめました。

ソケット通信の全体の流れ

サーバー側のソケット通信は以下の順で関数を呼び出します。

flowchart TD
    A["socket()\nソケットを作成してファイルディスクリプタを取得"]
    B["setsockopt()\nソケットのオプションを設定(SO_REUSEADDR など)"]
    C["bind()\nソケットに IP アドレスとポート番号を紐づける"]
    D["listen()\n接続待ち状態にする"]
    E["accept()\nクライアントからの接続を受け付ける(ブロッキング)"]
    F["read / write\nクライアントとデータをやりとりする"]

    A --> B --> C --> D --> E --> F

各関数のメモ

void setbuf(FILE *stream, char *buf)

ストリーム(FILE *)に対するバッファリング動作を制御する関数です。例えば stdout ストリームに対して設定すると、そこへ書き込む printf などが影響を受けます。

ストリームの代表的なものは以下の3つで、プログラム起動時に自動で用意されます。

ストリーム説明
stdout標準出力。printf はここに書き込む
stderr標準エラー出力。エラーメッセージの出力に使う
stdin標準入力。scanffgets はここから読み込む
引数説明
stream対象のストリーム(stdoutstderr など)
buf使用するバッファへのポインタ。NULL を渡すとバッファリングが無効になる

NULL を渡すと書き込みが即座に反映されます。デバッグ時や、ログをリアルタイムで確認したい場合に使います。

setbuf(stdout, NULL);   // バッファなし(即時出力)
setbuf(stdout, buf);    // buf に指定したバッファを使う

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

ソケットを作成してファイルディスクリプタを返します。失敗時は -1 を返します。

ファイルディスクリプタとは、OS がファイルやソケットなどのリソースを識別するための整数値です。以降の bind()accept() などにこの整数値を渡すことで、同じソケットを操作できます。

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
    printf("Socket creation failed: %s...\n", strerror(errno));
    return 1;
}
// AF_INET   : IPv4
// SOCK_STREAM: TCP(ストリーム型)
// 0         : プロトコルは自動選択
引数代表的な値意味
domainAF_INETIPv4
typeSOCK_STREAMTCP
protocol0自動

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

ソケットのオプションを設定します。Redis 実装でよく使うのは SO_REUSEADDR で、プロセス再起動直後でも同じポートにバインドできるようにします。

引数の意味は以下のとおりです。

引数説明
sockfd対象のソケットのファイルディスクリプタ
levelオプションが属するプロトコル層。ソケット全般のオプションは SOL_SOCKET、TCP 固有のオプションは IPPROTO_TCP を指定する
optname設定するオプション名。level に応じた定数を指定する
optvalオプションの値へのポインタ。有効/無効の切り替えは int 型の 1/0 を使うことが多い
optlenoptval のサイズ(バイト数)
int reuse = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
    printf("SO_REUSEADDR failed: %s\n", strerror(errno));
    return 1;
}
// SOL_SOCKET  : ソケット全般のオプション
// SO_REUSEADDR: TIME_WAIT 状態のポートを再利用できるようにする
// &reuse      : 有効にする(1)
// sizeof(reuse): optval のサイズ

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

ソケットに IP アドレスとポート番号を紐づけます。

引数説明
sockfd対象のソケットのファイルディスクリプタ
addrバインドするアドレス情報(sockaddr_insockaddr * にキャストして渡す)
addrlenaddr のサイズ(バイト数)

sockaddr_in は IPv4 のアドレス情報を保持する構造体です。各フィールドの意味は以下のとおりです。

フィールド説明
sin_familyアドレスファミリ。IPv4 なら AF_INET
sin_portポート番号。htons() でネットワークバイトオーダーに変換する必要がある
sin_addrIP アドレス。INADDR_ANY はすべてのネットワークインターフェースで受け付ける
struct sockaddr_in serv_addr = {
    .sin_family = AF_INET,
    .sin_port   = htons(6379),          // ポート番号(ネットワークバイトオーダーに変換)
    .sin_addr   = { htonl(INADDR_ANY) }, // すべてのインターフェースで受け付ける
};

if (bind(server_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) {
    printf("Bind failed: %s\n", strerror(errno));
    return 1;
}

int listen(int sockfd, int backlog)

ソケットを接続待ち状態にします。

引数説明
sockfd対象のソケットのファイルディスクリプタ
backlog接続待ちキューの最大数。接続が集中した際にあふれた分は拒否される
if (listen(server_fd, 5) != 0) {
    printf("Listen failed: %s\n", strerror(errno));
    return 1;
}

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

クライアントからの接続を受け付け、新しいファイルディスクリプタを返します。接続がくるまでブロックします。

引数説明
sockfdlisten() 済みのソケットのファイルディスクリプタ
addr接続してきたクライアントのアドレス情報が格納される
addrlenaddr のサイズ。呼び出し前にサイズをセットしておく必要がある
struct sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);

int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd == -1) {
    printf("Accept failed: %s\n", strerror(errno));
    close(server_fd);
    return 1;
}

ssize_t read(int fd, void *buf, size_t count)

ファイルディスクリプタからデータを読み込みます。戻り値は実際に読み込んだバイト数で、接続が閉じられた場合は 0、失敗時は -1 を返します。

引数説明
fd読み込み元のファイルディスクリプタ
buf読み込んだデータを格納するバッファへのポインタ
count読み込む最大バイト数
char buf[1024];
ssize_t bytes_read = read(client_fd, buf, sizeof(buf));
if (bytes_read == -1) {
    printf("Read failed: %s\n", strerror(errno));
} else if (bytes_read == 0) {
    printf("Client disconnected\n"); // 接続が閉じられた
}

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

接続中のソケットにデータを送信します。戻り値は実際に送信できたバイト数で、失敗時は -1 を返します。

引数説明
sockfdaccept() で取得したクライアントのファイルディスクリプタ
buf送信するデータへのポインタ
len送信するデータのバイト数
flags送信オプション。通常は 0 を指定する
const char *response = "+PONG\r\n"; // Redis プロトコル(RESP)における単純文字列レスポンス
if (send(client_fd, response, strlen(response), 0) == -1) {
    printf("Send failed: %s\n", strerror(errno));
}

close(client_fd);
close(server_fd);

まとめ

CodeCrafters だと、教科書で読むだけでは掴みにくいソケットの使い方が実際に手を動かして理解できるのでおすすめです。