🌟

EXAM06: TCPチャットサーバーmini_serv縮小版

に公開

全体の流れ

このコードは、複数クライアント対応の簡易チャットサーバー(TCP)です。
主な流れと意図は以下の通りです。


  1. 初期化

    • コマンドライン引数でポート番号を受け取る(引数が足りなければエラー終了)。
    • ソケットを作成し、127.0.0.1(ローカルホスト)+指定ポートでバインド。
    • listen で接続待ち状態にする。
    • クライアント管理配列 clients やFD集合 master を初期化。
  2. イベントループ(for(;;))

    • select を使い、すべてのクライアントFDとリッスンFDを同時監視。
    • 新規接続受付用FD(lfd)は書き込み監視から除外。
  3. 新規接続処理

    • if (FD_ISSET(lfd, &rset)) で新しい接続要求を検知。
    • accept で新しいクライアントFDを取得。
    • FD集合に追加し、クライアントIDを割り当てる。
    • 他のクライアントに「新しいクライアントが来た」旨を通知。
  4. クライアントごとの受信処理

    • 各クライアントFDについて、データ受信可能か判定。
    • recv でデータ受信。切断やエラーなら後始末&通知。
    • 受信データをクライアントごとのバッファに追記。
    • 改行が来たら、その行を「client <id>: <message>」形式で全員に送信。
    • バッファサイズを超えないように安全に管理。

意図

  • 複数クライアントの同時管理
    select とFD集合を使い、1スレッドで複数クライアントを効率よく扱う。

  • チャット機能の実現
    クライアントが送った1行ごとに、他の全クライアントへメッセージを転送。

  • 安全なバッファ管理
    バッファサイズを超えないように %.*sroom 変数で制御。

  • 接続・切断通知
    クライアントの接続・切断時に、他のクライアントへ通知メッセージを送る。


1. 初期化

👉 「ローカルホストの指定ポートで接続待ちするサーバの土台を作って、select で監視する準備を完了する」


if (ac != 2) die_arg();
  • 引数が1つ(=プログラム名を除いて「ポート番号」だけ)でなければエラー終了。
  • die_arg()"Wrong number of arguments\n" を出して exit(1)

int lfd = socket(AF_INET, SOCK_STREAM, 0);
  • サーバのリスニングソケットを作成。
  • AF_INET = IPv4, SOCK_STREAM = TCP, 0 = デフォルトプロトコル(TCPが選ばれる)。
  • 失敗したら < 0 になる。

struct sockaddr_in a; memset(&a, 0, sizeof(a));
a.sin_family        = AF_INET;
a.sin_addr.s_addr   = htonl(2130706433);
a.sin_port          = htons((unsigned short)atoi(av[1]));
  • ソケットのアドレス構造体 sockaddr_in をゼロクリアして初期化。

  • sin_family = AF_INET → IPv4。

  • a.sin_addr.s_addr = htonl(2130706433)

    • 2130706433 は 127.0.0.1 を10進数にした値。
    • htonl でネットワークバイトオーダに変換。つまり「ローカルホスト専用サーバ」になる。
  • a.sin_port = htons((unsigned short)atoi(av[1]))

    • 引数のポート番号文字列を整数化し、ネットワークバイト順に直して設定。
    • htons は host-to-network short。

if(bind(lfd, (struct sockaddr*)&a, sizeof(a)) < 0) die();
if(listen(lfd, 128) < 0) die();
  • bindlfd を上のアドレス構造体 a に結びつける。
    → 「127.0.0.1:指定ポート」で待ち受けますよ、の意味。

  • listen で接続待ち状態にする。

    • 第二引数 128 は接続待ちキューの最大長。

FD_ZERO(&master);
FD_SET(lfd, &master);
maxfd = lfd;
memset(clients, 0, sizeof(clients));
  • master という fd_set を空集合に初期化。

  • FD_SET(lfd, &master);リスニングソケットを監視対象に追加。
    → 今後 select で「新規接続あり」を検出できる。

  • maxfd = lfd;

    • select に渡す引数用。現在はリスニングソケットが最大FD。
  • clients 配列をゼロ初期化。

    • 各クライアントの情報(id や msg バッファ)を空にする。

sockaddr_in構造体

接続先のIPアドレスやポート番号の情報を保持するために,sockaddr_in構造体が 用意されており,各ソケットは,bindシステムコールによって sockaddr_in構造体のデータと関連づけられる.sockaddr_in構造体は次のように 定義されている.

/usr/include/netinet/in.h:
   struct in_addr {
      u_int32_t s_addr;
   };

   struct sockaddr_in {
      u_char  sin_len;    (古いOSでは存在しない)
      u_char  sin_family; (アドレスファミリ.今回はAF_INETで固定)
      u_short sin_port;   (ポート番号)
      struct  in_addr sin_addr;    (IPアドレス)
   };

ポート番号やIPアドレスはネットワークバイトオーダー (big endian) になって いないといけない.このため,整数をネットワークバイトオーダーに変換する htons関数を用いる.


まとめ

  1. 引数チェック: ポート番号必須
  2. socket(): TCPソケット作成
  3. sockaddr_in: 127.0.0.1:ポートを設定 (htonl, htons)
  4. bind() + listen(): ソケットをアドレスに結びつけ、待ち受け開始
  5. FD_ZERO + FD_SET: select 用監視集合に lfd を登録
  6. maxfd と clients[] 初期化

2. メインループ初期化

👉 「接続待ちソケットを監視し、実際に新しいクライアントを登録して全員に知らせる」


1. select の準備

rset = wset = master;
FD_CLR(lfd, &wset);
  • master(監視対象のFD集合)をコピーして、今回の select 用の rset(読み込み監視用)と wset(書き込み監視用)にする。
  • FD_CLR(lfd, &wset);
    → リスニングソケット lfd は「書き込み可能かどうか」は関係ないので wset から外している。
    (接続待ちソケットに対して send することはないため)

2. select の呼び出し

if (select(maxfd + 1, &rset, &wset, NULL, NULL) <= 0) continue;
  • select を実行。

    • maxfd+1 → 0から maxfd までのFDを調べる。
    • rset に「読み込み可能」なFDが、wset に「書き込み可能」なFDがマークされる。
  • 戻り値が 0 以下なら(エラーやイベントなし)→ continue で次ループへ。


3. 新規接続があるかチェック

if (FD_ISSET(lfd, &rset)) {
  • lfd(リスニングソケット)が読み込み可能になっていたら、新しい接続要求が来ている。

4. accept で接続を受け入れる

struct sockaddr_in c; socklen_t len = sizeof(c);
int s = accept(lfd, (struct sockaddr*)&c, &len);
  • 新しいクライアントソケット s を作成。
  • c には相手のIPアドレスなどが入る(ここでは使っていない)。

5. エラーや上限チェック

if(s < 0) continue;
else if(s >= FD_SETSIZE) {close(s); continue;}
  • accept が失敗したら無視。
  • select が扱えるのは FD_SETSIZE(通常1024)未満。
    もしそれ以上なら close(s) して破棄。

6. 新しいクライアントの登録

FD_SET(s, &master);
if(s > maxfd) maxfd = s;
clients[s].id = next_id++;
  • smaster に追加 → 次回から select で監視対象になる。
  • maxfd を更新。
  • clients[s].id に新しいクライアントIDを割り当てる。

7. 到着メッセージを作成して全員に送る

int k = sprintf(sbuf, "server: client %d just arrived\n", clients[s].id);
(void)k;
send_all(s);
sbuf[0] = '\0';
  • sbuf に「server: client N just arrived\n」を作る。
  • send_all(s) で「新しいクライアントが来た」というメッセージを全員に送信(ただし本人 s には送らない)。
  • sbuf[0] = '\0'; でバッファをクリア。

まとめ

  1. rset = wset = master → 今回の select 用に集合を準備
  2. select 実行 → どのFDが準備できたか確認
  3. lfd が立っていたら新規接続 → accept
  4. エラーやFD数上限をチェック
  5. 新しいクライアントを master に登録しIDを割り当てる
  6. 「client N just arrived」を全員にブロードキャスト

3. メインループ後半


👉 「既存クライアントからの通信を処理し、切断 or メッセージ行単位で全員に配布」**


1. 監視対象を総当たり

for (int fd = 0; fd <= maxfd; fd++) {
    if (fd == lfd) continue;
    if (!(FD_ISSET(fd, &rset) && FD_ISSET(fd, &master))) continue;
  • 0..maxfd のFDを全部走査。
  • lfd(接続待ちソケット)は除外。
  • select で「読み込み可能」かつ、まだ監視対象 (master) に入っているFDだけ処理。

2. 受信処理

int n = recv(fd, rbuf, sizeof(rbuf), 0);
  • クライアントからのデータを受信して rbuf に入れる。
  • n は受信したバイト数。0 なら切断、<0 ならエラー。

3. 切断処理

if(n <= 0) {
    int k = sprintf(sbuf, "server: client %d just left\n", clients[fd].id);
    (void)k;
    send_all(fd);
    sbuf[0] = '\0';
    FD_CLR(fd, &master);
    clients[fd].msg[0] = '\0';
    close(fd);
    continue;
}
  • 切断時は「server: client X just left\n」を sbuf に作って全員に送信(本人除外)。
  • 監視集合から外し、msg を空にし、ソケットを閉じる。

4. バッファへの追記

int j = (int)strlen(clients[fd].msg);
int room = (int)sizeof(clients[fd].msg) - 1 - j;
if (room < 0) room = 0;
if (room < n) n = room;
  • j … 既に溜まっている文字数。
  • room … このクライアントの受信バッファ msg の残り容量(-1\0 終端のため)。
  • 受け取った n バイトが room より多ければ切り詰める(あふれ防止)。

5. 1文字ずつ追加して改行検出

for (int i = 0; i < n; i++, j++){
    clients[fd].msg[j] = rbuf[i];
    if (clients[fd].msg[j] == '\n'){
        clients[fd].msg[j] = '\0';
  • rbuf の内容を msg に追記。
  • \n を見つけたら、その直前までを1行として確定。
  • \n\0 に置き換えて文字列化。

6. 送信用メッセージの組み立て

        int hdr = sprintf(sbuf, "client%d: ", clients[fd].id);
        int room2 = (int)sizeof(sbuf) - hdr - 2;
        if (room2 < 0) room2 = 0;
        sprintf(sbuf + hdr, "%.*s\n", room2, clients[fd].msg);
  • sbuf"clientX: " というヘッダを書き込む。
  • hdr = 書いた文字数。
  • room2 = sbuf の本文部分に残っている容量(\n\0 の2バイトを確保するため -2)。
  • %.*s で安全に本文をコピーし、末尾に改行を付ける。

👉 ここでの roomroom2 の違い

  • room受信側の空き容量(クライアントの入力バッファ msg に、あと何文字入れられるか)。
  • room2送信側の空き容量(全員に配るバッファ sbuf の本文部分に、あと何文字入れられるか)。

7. ブロードキャストとリセット

        send_all(fd);
        sbuf[0] = '\0';
        clients[fd].msg[0] = '\0';
        j = -1;
  • send_all(fd) でこの行を全員に送信(本人除外)。
  • 送信後は sbufmsg を空にして、次の行に備える。
  • j = -1; は for の末尾で j++ されて 0 に戻る → 次の行は msg[0] から書き込める。

8. ループ後の処理

clients[fd].msg[j] = '\0';
  • 改行が来なかった場合でも、途中までの内容をきちんと \0 終端しておく。
  • これで次回受信時に追記できる。

🔑 要約

  • 切断時 → 「just left」を全員に通知し、FDを閉じる

  • データ受信時 → クライアントの行バッファに追記

    • 改行が来たら → "clientX: ..." に整形して全員に配布
    • 来なければそのまま保持(次回以降で続きが来る)

各部の意味(要点だけ丸暗記用)

  • typedef struct s_client { int id; char msg[1000000]; }
    クライアントごとの 受信中1行バッファ\n が来るまで貯める。

  • static fd_set master, rset, wset;

    • master: 監視対象の集合(全FD)
    • rset, wset: select 用の作業コピー
  • sbuf / rbuf

    • rbufrecv 一時受け
    • sbuf今から全員に配る1本の放送メッセージ を作る場所(共有バッファ)
  • send_all(int except_fd)
    直前に sprintf で入れた sbuf を、今回 select書ける と判定されたFDに限り配る。except_fd(本人)には送らない。

  • accept
    新FDを master に入れ、id を発行して「just arrived」を sbuf に作り、send_all(s)

  • recv
    n <= 0 → 切断: 「just left」を全員へ → FD_CLRclose
    n > 0 → 文字を clients[fd].msg に詰め、\n が来たら1行メッセージとして sbuf に整形 → send_all(fd) → バッファクリア


この課題の意図(出題者が見たいポイント)

  • ソケットの基本socket/bind/listen/accept の正しい使い方

  • 多重入出力selectfd_set の正しい運用(master を保持しつつ、毎回コピーした集合で select

  • ノンブロッキング的思考

    • ブロッキングソケットでも select を使えば「詰まない」処理手順が書ける
    • “遅いクライアントを切らない”=送信でブロックしない設計 が必要
  • ライン指向の処理\n 区切りでメッセージを完成させてから配る

  • 安全な文字列処理%.*s で上限をかける等のオーバーフロー対策

  • int lfd = socket(AF_INET, SOCK_STREAM, 0);
  • if(bind(lfd, (struct sockaddr*)&a, sizeof(a)) < 0) die();
  • if(listen(lfd, 128) < 0) die();
  • if(select(maxfd + 1, &rset, &wset, NULL, NULL) <= 0) continue;
  • int s = accept(lfd, (struct sockaddr*)&c, &len);
  • int n = recv(fd, rbuf, sizeof(rbuf), 0);

仕組みの覚え方(スラスラ言えるように)

  • “masterは名簿、rset/wsetは当番表”
    毎ターン、名簿を当番表にコピーして select → 読める/書ける人だけ手を挙げる。
  • “1行バッファ”
    文字は貯める、\n が来たら発言成立→整形→配る。
  • “配るときは無理しない”
    今書ける人にだけ渡す。書けない人にはあとで(=送信キューに積む)。

Discussion