EXAM06: TCPチャットサーバーmini_serv縮小版
全体の流れ
このコードは、複数クライアント対応の簡易チャットサーバー(TCP)です。
主な流れと意図は以下の通りです。
-
初期化
- コマンドライン引数でポート番号を受け取る(引数が足りなければエラー終了)。
- ソケットを作成し、
127.0.0.1(ローカルホスト)+指定ポートでバインド。 -
listenで接続待ち状態にする。 - クライアント管理配列
clientsやFD集合masterを初期化。
-
イベントループ(for(;;))
-
selectを使い、すべてのクライアントFDとリッスンFDを同時監視。 - 新規接続受付用FD(
lfd)は書き込み監視から除外。
-
-
新規接続処理
-
if (FD_ISSET(lfd, &rset))で新しい接続要求を検知。 -
acceptで新しいクライアントFDを取得。 - FD集合に追加し、クライアントIDを割り当てる。
- 他のクライアントに「新しいクライアントが来た」旨を通知。
-
-
クライアントごとの受信処理
- 各クライアントFDについて、データ受信可能か判定。
-
recvでデータ受信。切断やエラーなら後始末&通知。 - 受信データをクライアントごとのバッファに追記。
- 改行が来たら、その行を「client <id>: <message>」形式で全員に送信。
- バッファサイズを超えないように安全に管理。
意図
-
複数クライアントの同時管理
selectとFD集合を使い、1スレッドで複数クライアントを効率よく扱う。 -
チャット機能の実現
クライアントが送った1行ごとに、他の全クライアントへメッセージを転送。 -
安全なバッファ管理
バッファサイズを超えないように%.*sやroom変数で制御。 -
接続・切断通知
クライアントの接続・切断時に、他のクライアントへ通知メッセージを送る。
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();
-
bindでlfdを上のアドレス構造体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関数を用いる.
まとめ
- 引数チェック: ポート番号必須
- socket(): TCPソケット作成
-
sockaddr_in: 127.0.0.1:ポートを設定 (
htonl,htons) - bind() + listen(): ソケットをアドレスに結びつけ、待ち受け開始
-
FD_ZERO + FD_SET:
select用監視集合にlfdを登録 - 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++;
-
sをmasterに追加 → 次回から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';でバッファをクリア。
まとめ
-
rset = wset = master→ 今回の select 用に集合を準備 -
select実行 → どのFDが準備できたか確認 -
lfdが立っていたら新規接続 →accept - エラーやFD数上限をチェック
- 新しいクライアントを
masterに登録しIDを割り当てる - 「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で安全に本文をコピーし、末尾に改行を付ける。
👉 ここでの room と room2 の違い:
-
room… 受信側の空き容量(クライアントの入力バッファmsgに、あと何文字入れられるか)。 -
room2… 送信側の空き容量(全員に配るバッファsbufの本文部分に、あと何文字入れられるか)。
7. ブロードキャストとリセット
send_all(fd);
sbuf[0] = '\0';
clients[fd].msg[0] = '\0';
j = -1;
-
send_all(fd)でこの行を全員に送信(本人除外)。 - 送信後は
sbufとmsgを空にして、次の行に備える。 -
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-
rbufはrecv一時受け -
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_CLR→close
n > 0→ 文字をclients[fd].msgに詰め、\nが来たら1行メッセージとしてsbufに整形 →send_all(fd)→ バッファクリア
この課題の意図(出題者が見たいポイント)
-
ソケットの基本:
socket/bind/listen/acceptの正しい使い方 -
多重入出力:
selectとfd_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