ネットワークプログラミングの超基礎

簡単なサーバープログラム

では、次に簡単なサーバープログラムを書いてみましょう。
再び、処理の流れの骨格をはっきりさせるために、一切のエラー処理を削除しています。 本当はこんなプログラムは書いてはいけませんが、基本を理解しないまま書くよりはずっとマシなので、ひとまず目をつぶることにします。

このプログラムでやることは、

  1. 接続を待つ
  2. 誰か (クライアント) が接続してきたら、接続
  3. 相手からのメッセージを受信
  4. 相手へ返答を送信
  5. 終了
だいたいこんな感じ。

ここで注意して欲しいことは、サーバーの場合、 次の二種類のソケット、

が使われているということです。
特に、次のページで説明するforkと関係するので、 このことはしっかり頭に入れておいて下さい。


/*
 * server_test.c
 *
 *   コンパイル: cc server_test.c -lsocket -lnsl	(Solaris の場合)
 *   実行:       ./a.out IPアドレス
 */

#include <stdio.h>
#include <string.h>

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


#define SERV_PORT 10001
#define BUFSIZE 1024

int
main(int argc, char *argv[]) {
  struct sockaddr_in serv_addr, client_addr;
  int dataSock, waitSock;
  int addrlen;

  int i, len;
  char buf[BUFSIZE];

  /* 準備手順1: 下準備 (受信ホスト (= 自分) の指定) */
  serv_addr.sin_family       = AF_INET;
  serv_addr.sin_port         = htons(SERV_PORT);
  serv_addr.sin_addr.s_addr  = INADDR_ANY;

  /* 準備手順2: 接続待ち用ソケットを作成する */
  waitSock = socket(PF_INET, SOCK_STREAM, 0);

  /* 準備手順3: 待受用アドレスに bind する */
  bind(waitSock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); 

  /* 準備手順4: クライアントからの接続待ち受け可能状態にする */
  listen(waitSock, 1);


  /* 接続受付手順: クライアントからのコネクション確立要求を受け入れる */
  addrlen = sizeof(client_addr);
  bzero(&client_addr, addrlen);
  dataSock = accept(waitSock, (struct sockaddr*)&client_addr, &addrlen);


  /* ------------------------------------------------------------
     ここまでが、送受信を開始するまでの準備段階
     これから後が本番
     といっても、client の場合とほとんど同じ
     ------------------------------------------------------------ */


  /* 送受信手順: データの送受信 */
  /* まずは送ってみる */
#define MSG "Hello, World.\n"
  send(dataSock, MSG, strlen(MSG) + 1, 0);

  /* 次は受けとってみる */
  /* ここでは (なんとなく) 60 バイトずつ受けとっている
     この数字 (60) にはあまり意味がない */
  while ((len = recv(dataSock, buf, 60, 0)) > 0) {
    /* 受けとったものが \0 で終わる文字列とは限らないので、
       わざわざ for で一文字ずつ表示している */
    for (i = 0; i < len; i++) {
      putc(buf[i], stdout);
    }
  }

  /* 終了手順: 後始末 */
  close(waitSock);
  close(dataSock);

  return 0;
}

サーバーの場合は、相手の出方を見る必要があるので、 ほんのちょっと準備が面倒になっていますが、基本はクライアントと変わりません。

まずは、自分の準備

とりあえず、どのポートで待つのか等の、自分の情報を設定する必要があります。 そのための手順としては、
  1. sockaddr_in 構造体の変数 (ここでは serv_addr) に、自分の情報 (使う IP アドレス、ポート) をツッこむ
  2. socket() でソケットを作る
  3. bind() でソケットにアドレスを割り当てる
となります。 手順1 と 手順2 は逆でも OK。
分かりにくいのは bind() ですね。 ちょっと説明しにくいですが、sockaddr_in 構造体の変数で指定したアドレス/ポートと実際のソケットを対応付ける感じです。

で、相手からの接続を待つための準備

サーバーの場合は、相手 (クライアント) からの接続を待たなければなりません。
が、↑の準備では、ソケットを作って、それとアドレスを対応付けただけで、 そのソケットを使って何をするかはまだ指示していません。
そこで、listen() を使って、待ち受けることができる状態にします (ここで一つめの socket が使われます)。

相手からの接続待ち

関数名からは分かりにくいですが、実際に接続を待つのは accept() になります。
accept()は、クライアントからの接続を待ち、 接続されたらそれを受け入れ、実際の通信用のソケット (二つめのソケット) を生成します。 以降は、このソケットを使って通信することになります。

ここで bzero() という見慣れないのが出てきていますが、これは client_addr を初期化しているだけです。 あまり気にしないで、そのまま書いておきましょう。

で、実際にデータを入れたり出したり

ちゃんと準備できたら、あとは実際にデータを送ったり受けとったりするだけです。 ここはクライアントの場合と全く同じです。 上にも書いたように、実際の通信用のソケットは accept() によって生成されます。
普通のサーバーは、クライアントから受信して、クライアントへ送信、 とすることが多いですが、ここでは、 クライアントの場合と書き方は変わらないということを強調するために、 あえて逆にしています (client_test.c と比較してみて下さい。 MSG 以外は全く一緒になっています)。

実行してみる

↑のプログラムをコンパイルして、実行させてみて下さい。
クライアントはどうするの? と思うかもしれませんが、大丈夫。
別の端末 (とか、別の端末エミュレーターとか) から、 telnet [サーバーが動いてるホスト] [待受ポート] としてみましょう (↑のプログラムをそのままコンパイルしていて、同じホストで telnet するのであれば、 telnet localhost 10001 とします)。

終了するには、Ctrl-] とすると、

telnet> 
というプロンプトが出るので、そこで q [RET] とします。

ということで、サーバーとクライアントの両方のプログラムを自分で書く場合は、 サーバーを先に書いておくと、telnet で動作の確認ができてデバッグが楽です (stream の場合は)。


注意

重要な点なので、これは繰り返して書いておきます。
最初にも書いたとおり、このプログラムでは、エラー処理を全くやってません。 こんなプログラムを書いたら 0 点です。
socket(), bind(), listen(), accept(), send(), recv(), のそれぞれでエラー処理が必要です。 man ペイジでそれぞれの関数の返り値を調べて、適切な処理を付加しましょう。


補足

↑のプログラムで、client_addr がほとんど使われていないことに気付いたでしょうか。
実はこのプログラムの場合は、 「接続受付手順」の三行を、
dataSock = accept(waitSock, NULL, NULL);
と一行で書くことができます。
んじゃ、なんでわざわざ client_addr なんて使ってるの、ってことですが、これを指定しておけば、 接続元 (クライアント) の情報を取得することができるのです。 例えば、元のプログラムの accept() の次の行あたりに、
printf("Connected from %s .\n", inet_ntoa(*(struct in_addr *)&client_addr.sin_addr.s_addr));
とか入れてみましょう。
まえのぺーじにもどる / つぎのぺーじにいく
さいしょにもどる


Last modified: Mon May 21 10:44:18 JST 2007