echoサーバーを書いてみたときのメモ その3 マルチプロセス、ノンブロッキングI/O、I/O多重で複数クライアントを捌く

前回、前々回の続き。

echoサーバーを書いてみたときのメモ その1 ソケットAPIとTCP

echoサーバーを書いてみたときのメモ その2 なぜ複数クライアントを捌けないのか

複数クライアントを同時に捌くために以下の方法で対応してみる。

  • ブロッキングI/Oのまま。forkを使って複数プロセスを立ち上げ、1プロセス1クライアントで対応する
  • ブロッキングI/Oを使って対応する。1プロセスで複数クライアントに対応する
  • I/O多重を使って対応する。1プロセスで複数クライアントに対応する

今回もUNIXネットワークプログラミングにお世話になります。

forkを使ったechoサーバー

前々回作ったechoサーバーをforkを使って、1クライアント毎に1プロセスを割り当てる様にする。 accept 後に fork して、親プロセスではクライアントとの接続用ソケットである connect_dclose 、子プロセスではListen用ソケットである listener_dclose する。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>

void error(char *msg)
{
  fprintf(stderr, "%s:%s\n", msg, strerror(errno));
  exit(1);
}

int read_line(int socket, char *buf, int len)
{
  char *s = buf;
  int slen = len;
  int c = read(socket, s, slen);
  while ((c > 0) && (s[c - 1] != '\n')) {
    s += c;
    slen = -c;
    c = read(socket, s, slen);
  }
  if (c < 0) {
    return c;
  }
  return len - slen;
}

int main(int argc, char *argv[])
{
  int listener_d = socket(PF_INET, SOCK_STREAM, 0);
  if (listener_d == -1) {
    error("socket err");
  }

  struct sockaddr_in name;
  name.sin_family = AF_INET;
  name.sin_port = (in_port_t)htons(30000);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(listener_d, (struct sockaddr *) &name, sizeof(name)) == -1) {
    error("bind err");
  }

  if (listen(listener_d, 1) == -1) {
    error("listen err");
  }

  puts("wait...");

  struct sockaddr_storage client_addr;
  unsigned int address_size = sizeof(client_addr);

  char buf[255];
  while(1) {
    int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
    if (connect_d == -1) {
      error("accept err");
    }

    if (fork() == 0) {
      char *msg = "Hello World!\r\n";
      write(connect_d, msg, strlen(msg));

      read_line(connect_d, buf, sizeof(buf));
      write(connect_d, buf, strlen(buf));
      close(connect_d);
      exit(0);
    }
    close(connect_d);
  }
  return 0;
}

forkを使っていないechoサーバーの場合、 connect_dclose したらFINが送信されクライアントとの接続が切れた。 fork を使ったechoサーバーでは親プロセスが connect_dclose するわけだが大丈夫なのだろうか。UNIXネットワークプログラミングにはこう書いてある。

すべてのファイルやソケットが参照カウンタを持っていることを理解することが必要である

UNIXネットワークプログラミング P103

close すると参照カウンタが1つ減るが、これが0にならないとFINは送信されない。親プロセスで close しても参照カウンタが2から1に減るだけなので、FINは送信されずクライアントとの接続は切れない。

動かしてみる

echoサーバーを起動して確認。

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
fork_echo 11474 ubuntu    3u  IPv4 246242056      0t0  TCP *:30000 (LISTEN)

$ ll /proc/11474/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:09 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:09 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 3 -> socket:[246242056]

クライアントから2つ接続してみる。

クライアント1

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

クライアント2

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

fork を使っていないechoサーバーの場合と異なり、両方に Hello World! とサーバーから返事が来ていることが分かる。

サーバーで確認。

$ ps -xf 
  PID TTY      STAT   TIME COMMAND
 2767 ?        S      0:00 sshd: ubuntu@pts/1
 2768 pts/1    Ss     0:00  \_ -bash
11474 pts/1    S+     0:00      \_ ./fork_echo_server
11479 pts/1    S+     0:00          \_ ./fork_echo_server
11480 pts/1    S+     0:00          \_ ./fork_echo_serve

親プロセスが1つ、子プロセスが2つできてる。さらに確認。

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
fork_echo 11474 ubuntu    3u  IPv4 246242056      0t0  TCP *:30000 (LISTEN)
fork_echo 11479 ubuntu    4u  IPv4 246242057      0t0  TCP 192.168.33.10:30000->192.168.33.1:59013 (ESTABLISHED)
fork_echo 11480 ubuntu    4u  IPv4 246242079      0t0  TCP 192.168.33.10:30000->192.168.33.1:59014 (ESTABLISHED)

$ ll /proc/11474/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:09 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:09 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:09 3 -> socket:[246242056]

$ ll /proc/11479/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:10 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:10 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 4 -> socket:[246242057]

$ ll /proc/11480/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:10 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:10 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:10 4 -> socket:[246242079]

3つのプロセスがそれぞれ1つずつソケットを使ってる。親プロセスはLISTENしてるソケット。子プロセスはクライアントと接続してるソケット。

クライアント2の方で適当な文字列を入力すると、文字列が返ってきてサーバーから切断される。

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!
hoge
hoge
Connection closed by foreign host.

サーバーの状態を確認。接続済みソケットが1つになってることがわかる。子プロセスの close が実行された時にソケットの参照カウンタが0になるため、サーバーはFINを送信して、最終的に接続が切れる。

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
fork_echo 11474 ubuntu    3u  IPv4 246242056      0t0  TCP *:30000 (LISTEN)
fork_echo 11479 ubuntu    4u  IPv4 246242057      0t0  TCP 192.168.33.10:30000->192.168.33.1:59013 (ESTABLISHED)

forkを使って、複数クライアントを同時に取り扱うことができた。

psでサーバー側のプロセスを確認してみる。

$ ps -xf 
  PID TTY      STAT   TIME COMMAND
 2767 ?        S      0:00 sshd: ubuntu@pts/1
 2768 pts/1    Ss     0:00  \_ -bash
11474 pts/1    S+     0:00      \_ ./fork_echo_server
11479 pts/1    S+     0:00          \_ ./fork_echo_server
11480 pts/1    Z+     0:00          \_ [fork_echo_serve] <defunct>

接続が切れた子プロせすがゾンビ状態で残ってしまっている。この問題に対応するためにSIGCHLDシグナルを処理しましょう、といったこともUNIXネットワークプログラミング(5.8 Posixのシグナル処理、5.9 SIGCHLDシグナルの処理)に書いてある。

ノンブロッキングI/Oを使ったechoサーバー

ioctl を使って listener_d (Listen用ソケット)をノンブロッキングにする。ノンブロッキングなソケットに対して accept すると、クライアントからの接続が来てない場合、ブロックせずにすぐに EWOULDBLOCK を返す。

クライアントからの接続が来てる場合は accept でこれまで通り接続済みソケットを返す。この接続済みソケットも ioctl を使ってノンブロッキングにする。ノンブロッキングなソケットに対して read すると、データが到達していない場合、ブロックせずにすぐに EAGAIN を返す。

コードはこんな感じになった。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/ioctl.h>

void error(char *msg)
{
  fprintf(stderr, "%s:%s\n", msg, strerror(errno));
  exit(1);
}

int read_line(int socket, char *buf, int len)
{
  char *s = buf;
  int slen = len;
  int c = read(socket, s, slen);
  while ((c > 0) && (s[c - 1] != '\n')) {
    s += c;
    slen = -c;
    c = read(socket, s, slen);
  }
  if (c < 0) {
    return c;
  }
  return len - slen;
}

int main(int argc, char *argv[])
{
  int listener_d = socket(PF_INET, SOCK_STREAM, 0);
  if (listener_d == -1) {
    error("socket err");
  }

  struct sockaddr_in name;
  name.sin_family = AF_INET;
  name.sin_port = (in_port_t)htons(30000);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(listener_d, (struct sockaddr *) &name, sizeof(name)) == -1) {
    error("bind err");
  }

  if (listen(listener_d, 1) == -1) {
    error("listen err");
  }

  puts("wait...");

  struct sockaddr_storage client_addr;
  unsigned int address_size = sizeof(client_addr);

  // Listenソケットをノンブロッキングにする
  int val = 1;
  if (ioctl(listener_d, FIONBIO, &val) == -1) {
    error("ioctl err");
  }

  // 接続済みソケットを管理するための配列
  int fds[255];
  // 接続済みソケットの数
  int n = 0;

  char buf[255];
  while(1) {
    int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
    if (connect_d < 0) {
      // クライアントの接続がない場合は EWOULDBLOCK
      if (errno != EWOULDBLOCK) {
        error("accept err");
      }
    } else {
      // 接続済みソケットをノンブロッキングにする
      if (ioctl(connect_d, FIONBIO, &val) == -1) {
        error("ioctl err");
      }
      fds[n] = connect_d;
      n++;

      char *msg = "Hello World!\r\n";
      write(connect_d, msg, strlen(msg));
    }

    int i = 0;
    while (i < n) {
      // 接続済みソケットを順番に処理
      int conn = fds[i];
      if (read_line(conn, buf, sizeof(buf)) < 0) {
        // データが届いてない場合は EAGAIN
        if (errno != EAGAIN) {
          error("read err");
        }
        i++;
      } else {
        // データが届いてる場合はechoしてclose
        write(conn, buf, strlen(buf));
        close(conn);
        n--;
      }
    }
  }
  return 0;
}

ここで、UNIXネットワークプログラミングに書いてあった2つのステップを思い出す。

入力操作は次の2段階で構成されている。 - データの用意ができるまで待ち、 - そのデータをカーネルからプロセスにコピーする。

ソケットに関する入力では、最初のステップでは普通のネットワークからのデータの到着を待つ。パケットが到着すると、カーネル内のバッファにコピーされる。2つ目のステップでは、このデータをカーネルのバッファからアプリケーションのバッファにコピーすることになる。

Unixネットワーキングプログラミング P.140

acceptread がステップ1でブロックしてしまうのが原因で、ブロッキングI/Oを使ったechoサーバーでは複数クライアントを捌けなかった。ノンブロッキングI/Oではステップ1でデータが用意されていない場合、ブロックせずに何らかの返り値をすぐに返す。そのため、ブロックせずに複数クライアントを同時に捌くことができる。

動かしてみる

ノンブロッキングI/Oを使ったechoサーバーを起動して、確認してみる。

$ ps -xf
  PID TTY      STAT   TIME COMMAND
 2767 ?        S      0:00 sshd: ubuntu@pts/1
 2768 pts/1    Ss     0:00  \_ -bash
11508 pts/1    R+     0:02      \_ ./non_blocking_echo_server

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
non_block 11508 ubuntu    3u  IPv4 246242707      0t0  TCP *:30000 (LISTEN)

$ ll /proc/11508/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:49 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:49 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:49 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 3 -> socket:[246242707]

クライアントから2つ接続してみる。

クライアント1

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

クライアント2

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

forkを使ったechoサーバーと同様に、両方とも Hello World! とサーバーから返事が来てる。

サーバー側を確認。

$ ps -xf 
  PID TTY      STAT   TIME COMMAND
 2767 ?        S      0:00 sshd: ubuntu@pts/1
 2768 pts/1    Ss     0:00  \_ -bash
11508 pts/1    R+     3:14      \_ ./non_blocking_echo_server

forkを使ったechoサーバーと異なり、echoサーバーのプロセスは1つしかない。さらに確認してみる。

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
non_block 11508 ubuntu    3u  IPv4 246242707      0t0  TCP *:30000 (LISTEN)
non_block 11508 ubuntu    4u  IPv4 303901660      0t0  TCP 192.168.33.10:30000->192.168.33.1:59337 (ESTABLISHED)
non_block 11508 ubuntu    5u  IPv4 305166582      0t0  TCP 192.168.33.10:30000->192.168.33.1:59338 (ESTABLISHED)

$ ll /proc/11508/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:49 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:49 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:49 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 3 -> socket:[246242707]
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:53 4 -> socket:[303901660]
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:53 5 -> socket:[305166582]

echoサーバーのプロセスが3つのソケット(Listen用1つ、接続済み2つ)を使ってる。

クライアント2の方で適当な文字列を入力すると、文字列が返ってきてサーバーから切断される。

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!
hoge
hoge
Connection closed by foreign host.

サーバーの状態を確認。接続済みソケットが1つになってることが分かる。

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
non_block 11508 ubuntu    3u  IPv4 246242707      0t0  TCP *:30000 (LISTEN)
non_block 11508 ubuntu    4u  IPv4 303901660      0t0  TCP 192.168.33.10:30000->192.168.33.1:59337 (ESTABLISHED)

$ ll /proc/11508/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 12 07:49 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 12 07:49 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:49 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:50 3 -> socket:[246242707]
lrwx------ 1 ubuntu ubuntu 64 Mar 12 07:53 4 -> socket:[303901660]

複数クライアントを同時に捌くことができた。だけど、複数のソケットを管理するコードが微妙だし、 strace すると分かるがクライアントからの接続がないとずっとループ処理が動いている。これは無駄。次のI/O多重を使うともっと良くなる。

I/Oの多重化を使ったechoサーバー

epoll を使ってI/Oの多重化を使ったechoサーバーを書く。

readは、2つのステップを行うが、I/Oの多重化では2つのステップのうちの1つ目、データの用意ができるまで待つ、という部分だけを切り出して行う。データが用意できるまでブロックし、データが用意できたらカーネルからプロセスに戻ってくる。この時、1つのディスクリプタだけでなく複数のディスクリプタに対してデータの用意を待つことができる。

I/Oの多重化のためのシステムコールはいくつかあるが、 epoll を使う。 epoll では、 epoll_create epoll_ctl epoll_wait という3つのシステムコールを組み合わせる。

epoll_create

epoll_create の定義。manを読むと size の値は正でないといけないがなんでもいいらしい、現在は使われていない。

#include <sys/epoll.h>

int epoll_create(int size);

以下のような感じでepollファイルディスクリプタをオープンする。

// epollファイルディスクリプタをオープン
int epfd;
if ((epfd = epoll_create(100)) < 0) {
  error("epoll_create err");
}

epoll_ctl

epoll_ctl の定義。epollファイルディスクリプタと監視対象のディスクリプタとの関連を操作する。 op の値として EPOLL_CTL_ADD を指定すると監視対象として追加できる。

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctlを使って、echoサーバーの listener_d をepollの監視対象にする。

// listener_dソケットをepollの監視対象とする
struct epoll_event ev;
memset(&ev, 0, sizeof ev);
ev.events = EPOLLIN;
ev.data.fd = listener_d;
if ((epoll_ctl(epfd, EPOLL_CTL_ADD, listener_d, &ev)) < 0) {
  error("epoll_ctl error");
}

epoll_wait

epoll ファイルディスクリプタの I/O イベントを待つ。 timeout に-1を指定すると準備ができたファイルディスクリプタができるまで待ち続ける。返り値は準備ができているファイルディスクリプタの数。

第2引数の events には呼び出し可能なイベントが格納される。

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

listener_d を監視対象にしてから、 epoll_wait を呼ぶ。 timeout-1 を指定しているので、 listener_d のクライアントからの接続が来るまでブロックする。

  struct epoll_event events[MAX_EVENTS];
  while(1) {
    int fd_count = epoll_wait(epfd, events, MAX_EVENTS, -1);

    // 準備ができたディスクリプタを順番に処理
    int i;
    for (i = 0; i < fd_count; i++) {
      if (events[i].data.fd == listener_d ){
        // クライアントが接続してきた時の処理
      } else {
        // 準備ができたディスクリプタがlisterner_dではない場合の処理
      }
    }
  }
}

クライアントが接続してきた時の処理は、単一クライアントにしか対応できないechoサーバーの時と同様に accept を呼び出す。 epoll_wait でソケットの準備ができるまで待ったので、この accept ではブロックしない。この accept で取得できる接続済みソケットのファイルディスクリプタをepollの監視対象とする。

  struct epoll_event events[MAX_EVENTS];
  while(1) {
    int fd_count = epoll_wait(epfd, events, MAX_EVENTS, -1);

    // 準備ができたディスクリプタを順番に処理
    int i;
    for (i = 0; i < fd_count; i++) {
      if (events[i].data.fd == listener_d ){
        // クライアントが接続してきた時の処理
        int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
        if (connect_d == -1) {
          error("accept err");
        }

        char *msg = "Hello World!\r\n";
        write(connect_d, msg, strlen(msg));

        // connect_dソケットを監視対象とする
        memset(&ev, 0, sizeof ev);
        ev.events = EPOLLIN;
        ev.data.fd = connect_d;
        if ((epoll_ctl(epfd, EPOLL_CTL_ADD, connect_d, &ev)) < 0) {
          error("epoll_ctl error");
        }
      } else {
        // 準備ができたディスクリプタがlisterner_dではない場合の処理
      }
    }
  }
}

準備ができたディスクリプタlisterner_d ではない場合というのは、つまり準備ができたディスクリプタがクライアントとの接続用ソケットのディスクリプタである場合である。なので、クライアントから文字列が届きソケットから取得する準備ができたということである。この状態で read を呼び出してもブロックされる時間は、step2のデータをカーネルからプロセスにコピーする部分だけなので、ほとんどない。

  struct epoll_event events[MAX_EVENTS];
  while(1) {
    int fd_count = epoll_wait(epfd, events, MAX_EVENTS, -1);

    // 準備ができたディスクリプタを順番に処理
    int i;
    for (i = 0; i < fd_count; i++) {
      if (events[i].data.fd == listener_d ){
        // クライアントが接続してきた時の処理
        int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
        if (connect_d == -1) {
          error("accept err");
        }

        char *msg = "Hello World!\r\n";
        write(connect_d, msg, strlen(msg));

        // connect_dソケットを監視対象とする
        memset(&ev, 0, sizeof ev);
        ev.events = EPOLLIN;
        ev.data.fd = connect_d;
        if ((epoll_ctl(epfd, EPOLL_CTL_ADD, connect_d, &ev)) < 0) {
          error("epoll_ctl error");
        }
      } else {
        // 準備ができたディスクリプタがlisterner_dではない場合の処理
        int connect_d = events[i].data.fd;

        read_line(connect_d, buf, sizeof(buf));

        write(connect_d, buf, strlen(buf));
        close(connect_d);

        // closeしたソケットを監視対象から削除
        epoll_ctl(epfd, EPOLL_CTL_DEL, connect_d, &ev);
      }
    }
  }
}

epollを使ったechoサーバー

コードはこんな感じになった。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>

const int MAX_EVENTS = 10;

void error(char *msg)
{
  fprintf(stderr, "%s:%s\n", msg, strerror(errno));
  exit(1);
}

int read_line(int socket, char *buf, int len)
{
  char *s = buf;
  int slen = len;
  int c = read(socket, s, slen);
  while ((c > 0) && (s[c - 1] != '\n')) {
    s += c;
    slen = -c;
    c = read(socket, s, slen);
  }
  if (c < 0) {
    return c;
  }
  return len - slen;
}

int main(int argc, char *argv[])
{
  int listener_d = socket(PF_INET, SOCK_STREAM, 0);
  if (listener_d == -1) {
    error("socket err");
  }

  struct sockaddr_in name;
  name.sin_family = AF_INET;
  name.sin_port = (in_port_t)htons(30000);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(listener_d, (struct sockaddr *) &name, sizeof(name)) == -1) {
    error("bind err");
  }

  if (listen(listener_d, 1) == -1) {
    error("listen err");
  }

  puts("wait...");

  struct sockaddr_storage client_addr;
  unsigned int address_size = sizeof(client_addr);

  char buf[255];

  // epollファイルディスクリプタをオープン
  int epfd;
  if ((epfd = epoll_create(100)) < 0) {
    error("epoll_create err");
  }

  // listener_dソケットをepollの監視対象とする
  struct epoll_event ev;
  memset(&ev, 0, sizeof ev);
  ev.events = EPOLLIN;
  ev.data.fd = listener_d;
  if ((epoll_ctl(epfd, EPOLL_CTL_ADD, listener_d, &ev)) < 0) {
    error("epoll_ctl error");
  }

  struct epoll_event events[MAX_EVENTS];
  while(1) {
    int fd_count = epoll_wait(epfd, events, MAX_EVENTS, -1);

    // 準備ができたディスクリプタを順番に処理
    int i;
    for (i = 0; i < fd_count; i++) {
      if (events[i].data.fd == listener_d ){
        // 準備ができたディスクリプタがlistener_dということは
        // 新しいクライアントが接続してきたということ
        int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
        if (connect_d == -1) {
          error("accept err");
        }

        char *msg = "Hello World!\r\n";
        write(connect_d, msg, strlen(msg));

        // connect_dソケットを監視対象とする
        memset(&ev, 0, sizeof ev);
        ev.events = EPOLLIN;
        ev.data.fd = connect_d;
        if ((epoll_ctl(epfd, EPOLL_CTL_ADD, connect_d, &ev)) < 0) {
          error("epoll_ctl error");
        }

      } else {
        // connect_dの準備ができたということは
        // クライアントからのデータが届いたということ
        int connect_d = events[i].data.fd;

        read_line(connect_d, buf, sizeof(buf));

        write(connect_d, buf, strlen(buf));
        close(connect_d);

        // closeしたソケットを監視対象から削除
        epoll_ctl(epfd, EPOLL_CTL_DEL, connect_d, &ev);
      }
    }
  }
  return 0;
}

動かしてみる

I/Oの多重化を使ったechoサーバーを起動して、確認してみる。

$ lsof -i:30000
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
epoll_ech 2836 ubuntu    3u  IPv4  57737      0t0  TCP *:30000 (LISTEN)

$ ll /proc/2836/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar  5 12:50 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar  5 12:50 ../
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 3 -> socket:[57737]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 4 -> anon_inode:[eventpoll]

ソケットだけではなく、epoll用のディスクリプタを使っていることが分かる。

クライアントから2つ接続してみる。

クライアント1

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

クライアント2

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

forkを使ったechoサーバーと同様に、両方とも Hello World! とサーバーから返事が来てる。

サーバー側を確認。

$ lsof -i:30000
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
epoll_ech 2836 ubuntu    3u  IPv4  57737      0t0  TCP *:30000 (LISTEN)
epoll_ech 2836 ubuntu    5u  IPv4  60491      0t0  TCP 192.168.33.10:30000->192.168.33.1:49526 (ESTABLISHED)
epoll_ech 2836 ubuntu    6u  IPv4  60493      0t0  TCP 192.168.33.10:30000->192.168.33.1:49527 (ESTABLISHED)

$ ll /proc/2836/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar  5 12:50 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar  5 12:50 ../
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 0 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 1 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 2 -> /dev/pts/1
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 3 -> socket:[57737]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:50 4 -> anon_inode:[eventpoll]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:53 5 -> socket:[60491]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 12:53 6 -> socket:[60493]

$ sudo strace -s 1024 -p 2836
strace: Process 2836 attached
epoll_wait(4,

$ ps x | grep epoll_echo_server
 2836 pts/1    S+     0:00 ./epoll_echo_server

2つの接続済みソケットが作成されている。また、 strace の結果から read ではなく epoll_waitブロッキングしていることが分かる。 ps の結果からecho_serverのプロセスは1つだけであることも分かる。

epollを使ってI/O多重を行うことで、1つのプロセスでも複数クライアントを捌くことができた。

ブロックしてはいけない

I/O多重によってソケットのブロックをなくし、1プロセスで複数クライアントを捌けるようになったが、ブロックしてしまう可能性はまだ残っている。これは問題で、1プロセスで動いているのでブロックが発生してしまうとechoサーバー全体の処理が止まってしまう。

まず、epollでディスクリプタの準備ができたソケットから読み込むようにしたが、準備ができた かもしれない だけであって実施に読み込むとブロッキングしてしまう場合があるらしい。そのため、たとえepollを使ったとしてもソケットをノンブロッキングI/Oにしておくなどの対応が必要である。

他のI/Oでもブロックしてはいけない。例えば、ファイル読み込みや他のサーバーとの通信など。これらのI/Oでも、ノンブロッキングI/Oや非同期I/O(POSIX AIO インターフェース)、またはスレッドを使いI/O処理を別スレッドに任せるような非同期処理を行う必要がある。

また、ソケットからの読み込みのブロックはなくすことができたが、書き込みでブロックしてしまう場合がある。アプリケーションからカーネル内のバッファに書き込むが、この時バッファが一杯だとブロックしてしまう。ここでもやはりブロックしないようにノンブロッキングI/OやI/O多重、非同期I/O、スレッドなどで工夫する必要がある。

レベルトリガーとエッジトリガー

epollの通知方法としてレベルトリガーとエッジトリガーとがある。デフォルトはレベルトリガー。エッジトリガーの方が良い場面が良く分からなかったけど、どうやら書き込みのときに便利っぽい。

2008-07-07

epoll, エッジトリガー, EPOLLRDHUP - 誰かの役に立てばいいブログ

libev

epollはLinuxで使えるが、BSDでは使えないらしい。代わりにkqueueというシステムコールがある。このようなプラットフォーム依存を隠蔽化したライブラリとしてlibevがある。libevを使ったechoサーバーも書いてみた。libevではepollの場合と異なり、ループ処理を自分で書く必要がなく、コールバックを登録するコードになる。

libevをインストール。

$ sudo apt-get install libev-dev

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <ev.h>

void error(char *msg)
{
  fprintf(stderr, "%s:%s\n", msg, strerror(errno));
  exit(1);
}

int read_line(int socket, char *buf, int len)
{
  char *s = buf;
  int slen = len;
  int c = read(socket, s, slen);
  while ((c > 0) && (s[c - 1] != '\n')) {
    s += c;
    slen = -c;
    c = read(socket, s, slen);
  }
  if (c < 0) {
    return c;
  }
  return len - slen;
}

void connect_callback(EV_P_ ev_io *watcher, int revents)
{
  char buf[255];
  read_line(watcher->fd, buf, sizeof(buf));
  write(watcher->fd, buf, strlen(buf));
  ev_io_stop(EV_A_ watcher);
  close(watcher->fd);
  free(watcher);
}

void listener_callback(EV_P_ ev_io *watcher, int revents)
{
  struct sockaddr_storage client_addr;
  unsigned int address_size = sizeof(client_addr);

  int connect_d = accept(watcher->fd, (struct sockaddr *)&client_addr, &address_size);
  if (connect_d == -1) {
    error("accept err");
  }
  char *msg = "Hello World!\r\n";
  write(connect_d, msg, strlen(msg));

  struct ev_loop *l;
  ev_io *connect_watcher;
  connect_watcher = malloc(sizeof(client_addr));
  l = watcher->data;

  // connect_dを監視
  ev_io_init(connect_watcher, connect_callback, connect_d, EV_READ);
  ev_io_start(l, connect_watcher);
}

int main(int argc, char *argv[])
{
  int listener_d = socket(PF_INET, SOCK_STREAM, 0);
  if (listener_d == -1) {
    error("socket err");
  }

  struct sockaddr_in name;
  name.sin_family = AF_INET;
  name.sin_port = (in_port_t)htons(30000);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(listener_d, (struct sockaddr *) &name, sizeof(name)) == -1) {
    error("bind err");
  }

  if (listen(listener_d, 1) == -1) {
    error("listen err");
  }

  puts("wait...");

  // イベントループの初期化
  struct ev_loop *loop;
  ev_io watcher;

  loop = ev_default_loop(0);
  watcher.data = loop;

  // listener_dを監視
  ev_io_init(&watcher, listener_callback, listener_d, EV_READ);
  ev_io_start(loop, &watcher);

  // イベントループ開始
  ev_loop(loop, 0);

  close(listener_d);
  return 0;
}

コンパイル

$  gcc libev_echo_server.c -l ev -o libev_echo_server

動かして

$ ./libev_echo_server

確認。 epoll_wait でブロックしてることがわかる。

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
libev_ech 13301 ubuntu    3u  IPv4 566626035      0t0  TCP *:30000 (LISTEN)

$ ll /proc/13301/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar 18 13:03 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar 18 13:03 ../
lrwx------ 1 ubuntu ubuntu 64 Mar 18 13:03 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar 18 13:03 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar 18 13:03 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar 18 13:03 3 -> socket:[566626035]
lrwx------ 1 ubuntu ubuntu 64 Mar 18 13:03 4 -> anon_inode:[eventpoll]
lrwx------ 1 ubuntu ubuntu 64 Mar 18 13:03 5 -> anon_inode:[eventfd]

$ sudo strace -s 1024 -p 13301
strace: Process 13301 attached
epoll_wait(4,

おしまい

echoサーバーを書きながら、いろいろなI/Oモデルを試してみた。Node.jsは元々libevとlibeio(スレッドを使った非同期処理でI/Oを行うためのライブラリ)を使っていたが、現在は両方ともlibuvが使われているらしい。

UNIXネットワークプログラミングを読みながらコードを書くと、ネットワークの話とコードがつながっていく感じがして楽しかった。

コードはこちら。

github.com

echoサーバーを書いてみたときのメモ その2 なぜ複数クライアントを捌けないのか

前回、echoサーバーを書いてみた。その続き。

echoサーバーを書いてみたときのメモ その1 ソケットAPIとTCP

このechoサーバーだと同時に複数のクライアントを捌けない。どうしてか?実際に試して見る。クライアントから2回telnetでつないでみる。

クライアント1

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

クライアント2

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.

2つ目の接続では、 Hello World! が表示されていない。

サーバー側で接続を確認してみる。

netstatでみると複数の接続ができてる。

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN
tcp        0      0 192.168.33.10:30000     192.168.33.1:64085      ESTABLISHED
tcp        0      0 192.168.33.10:30000     192.168.33.1:64084      ESTABLISHED

しかし、ソケットに対するディスクリプタを確認すると、1つしかない(64084ポートを使ってる方)。

$ lsof -i:30000
COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 2316 ubuntu    3u  IPv4  52901      0t0  TCP *:30000 (LISTEN)
echo_serv 2316 ubuntu    4u  IPv4  52902      0t0  TCP 192.168.33.10:30000->192.168.33.1:64084 (ESTABLISHED)

$ ll /proc/2316/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar  5 03:12 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar  5 03:12 ../
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:12 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 3 -> socket:[52901]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 4 -> socket:[52902]
$ ps x | grep echo_server
 2316 pts/0    S+     0:00 ./echo_server

psのSTATが S+ なので割り込み可能なスリープ状態でフォアグラウンドのプロセスグループに入っているという状態。

straceで見てみると、readでスリープしている。

$ sudo strace -p 2316
strace: Process 2316 attached
read(4,

クライアント1で文字列を入力

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!
hoge
hoge
Connection closed by foreign host.

接続がサーバー側から切られて、クライアント2のtelnetHello World! が表示される。

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

サーバー側で確認してみると新しい接続に変わってることがわかる。

$ lsof -i:30000
COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 2316 ubuntu    3u  IPv4  52901      0t0  TCP *:30000 (LISTEN)
echo_serv 2316 ubuntu    4u  IPv4  56445      0t0  TCP 192.168.33.10:30000->192.168.33.1:64085 (ESTABLISHED)

 ll /proc/2316/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar  5 03:12 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar  5 03:12 ../
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:12 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 3 -> socket:[52901]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 4 -> socket:[56445]

まとめると、 read から処理が進まず、クライアント2の接続に対するacceptが呼ばれない。そのためサーバーはクライアントを1つずつしか処理できない。

I/Oモデル

前回に引き続きUNIXネットワークプログラミングを参考にする。

UNIXネットワークプログラミングを読むと5種類のI/Oがあるって書いてある。

Unixネットワーキングプログラミング P.140

さらに、入力操作は2つのステップから成ると書いてある。

入力操作は次の2段階で構成されている。

  1. データの用意ができるまで待ち、
  2. そのデータをカーネルからプロセスにコピーする。

ソケットに関する入力では、最初のステップでは普通のネットワークからのデータの到着を待つ。パケットが到着すると、カーネル内のバッファにコピーされる。2つ目のステップでは、このデータをカーネルのバッファからアプリケーションのバッファにコピーすることになる。

Unixネットワーキングプログラミング P.140

ブロッキングI/O、ノンブロッキングI/O、I/Oの多重化、シグナル駆動I/Oの4つはステップ1をどう扱うかが異なる。ステップ2については同じ。非同期I/Oだけが他と異なる方法でステップ1,2を扱う。

非同期I/Oは、Linuxだとaioとして提供されているが、ググって色々読んでみた感じだとあまり使われてないっぽい?

同期I/O操作と非同期I/O操作

I/Oモデルとは別の観点で同期I/O操作と非同期I/O操作というのがある。

Posix.1 ではこれら2つの語を以下の様に定義している。

  • 同期I/O操作では、これを要求したプロセスは要求したI/O操作が完了するまでブロックする
  • 非同期I/O操作では、これを要求したプロセスはブロックしない

この定義によれば、最初の4つのI/Oモデル、すなわちブロッキング、非ブロッキング、多重化I/OおよびシグナルI/O操作のすべてにおいてプロセスはブロックする。非同期I/Oモデルのみが非同期I/Oの定義に一致している。

UNIXネットワーキングプログラミング P.144

書いてみたechoサーバーのI/Oモデル

書いてみたechoサーバーで使ってる readブロッキングI/Oである。ブロッキングI/Oではステップ1と2が終わるまで元のプロセスに処理が戻らない。 read で処理が止まっていたのはステップ1で、クライアントからのデータの到達を待っているからである。クライアントからのデータが届くまでechoサーバーのプロセスはブロックされる。

複数クライアントを同時に捌くために、echoサーバーを以下の方法で書き換えてみようと思う。

  • ブロッキングI/Oのまま。forkを使って複数プロセスを立ち上げ、1プロセス1クライアントで対応する
  • ブロッキングI/Oを使って対応する。1プロセスで複数クライアントに対応する
  • I/O多重を使って対応する。1プロセスで複数クライアントに対応する

echoサーバーを書いてみたときのメモ その1 ソケットAPIとTCP

echoサーバーを書いた日の日記。

Head First C の11章を参考に、クライアントから文字列を受け取り、受け取った文字列をそのままクライアントへ返すechoサーバーを作る。echo サーバーは30000ポートを使用することにする。途中疑問に思ったところは主にUNIXネットワークプログラミングで調べた。

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI

  • 作者: W.リチャードスティーヴンス,W.Richard Stevens,篠田陽一
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 1999/07
  • メディア: 単行本
  • 購入: 8人 クリック: 151回
  • この商品を含むブログ (35件) を見る

Head First C ―頭とからだで覚えるCの基本

Head First C ―頭とからだで覚えるCの基本

環境。VagrantとUbuntuで。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.1 LTS"
$ uname -a
Linux ubuntu-xenial 4.4.0-38-generic #57-Ubuntu SMP Tue Sep 6 15:42:33 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.2' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.2)

以下のコードから始める。 while の無限ループ内でクライアントからデータを受け取ってレスポンスを返すような処理を書いていく。

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

void error(char *msg)
{
  fprintf(stderr, "%s:%s\n", msg, strerror(errno));
  exit(1);
}

int main(int argc, char *argv[])
{
  puts("wait...");
  while(1) {
  }
  return 0;
}
$ gcc echo_server.c -o echo_server
$ ./echo_server

30000ポートでは何も動いていないことを確認。

$ lsof -i:30000

echo_serverが開いているファイルディスクリプタを見ておく。

$ ps -x | grep echo_server
 9932 pts/0    R+     0:46 ./echo_server
 9942 pts/1    S+     0:00 grep --color=auto echo_server
$ ll /proc/9932/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Oct  9 07:56 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Oct  9 07:56 ../
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:56 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:56 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:56 2 -> /dev/pts/0

全体の流れ

  1. ソケットを作成する
  2. ソケットをバインドする
  3. Listen状態になる
  4. コネクションを取り出す
  5. クライアントから文字列を受け取る
  6. クライアントへ文字列を返す

ソケットを作成する

まず、ソケットを作成する処理を追加。

@@ -2,6 +2,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
+#include <sys/socket.h>
 
 void error(char *msg)
 {
@@ -11,6 +12,10 @@ void error(char *msg)
 
 int main(int argc, char *argv[])
 {
+  int listener_d = socket(PF_INET, SOCK_STREAM, 0);
+  if (listener_d == -1) {
+    error("socket err");
+  }
   puts("wait...");
   while(1) {
   }

ソケットとは、UNIXネットワークプログラミングに以下のように書いてある。

TCPコネクションのソケットペア(socket pair)は、コネクションの両方のエンドポイントを定義する、ローカルIPアドレス、ローカルTCPポート、リモートIPアドレス、およびリモートTCPポートの4つ組である。あるソケットペアは、インターネット中の特定のコネクションを一意に識別する。

 各エンドポイントを識別する2つの値、すなわちIPアドレスとポート番号は、多くの場合ソケット(socket)と呼ばれる。

UNIXネットワークプログラミング 第2版 Vol.1

socket関数

socket関数の定義。カーネル内にソケットを作成し、ソケットに対応したディスクリプタを返す。

#include <sys/socket.h>

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

PF_INET とは? man socket を見ると AF_INET になってる。

/usr/include/x86_64-linux-gnu/sys/socket.h を確認してみると以下の記述が。

/* This operating system-specific header file defines the SOCK_*, PF_*,
   AF_*, MSG_*, SOL_*, and SO_* constants, and the `struct sockaddr',
   `struct msghdr', and `struct linger' types.  */
#include <bits/socket.h>

/usr/include/x86_64-linux-gnu/bits/socket.h を確認すると。

/* Protocol families.  */
#define PF_UNSPEC       0       /* Unspecified.  */
#define PF_LOCAL        1       /* Local to host (pipes and file-domain).  */
#define PF_UNIX         PF_LOCAL /* POSIX name for PF_LOCAL.  */
#define PF_FILE         PF_LOCAL /* Another non-standard name for PF_LOCAL.  */
#define PF_INET         2       /* IP protocol family.  */
・
・
・
/* Address families.  */
#define AF_UNSPEC       PF_UNSPEC
#define AF_LOCAL        PF_LOCAL
#define AF_UNIX         PF_UNIX
#define AF_FILE         PF_FILE
#define AF_INET         PF_INET
・
・
・

AF_INET でも PF_INET でも同じ 2 という値で、IP protocol family と書いてある。

UNIXネットワークプログラミングを読むと以下のように書いてあった。

AFプリフィックスは"アドレスファミリ"の意であり、PFプリフィックスは"プロトコルファミリ"の意である。もともとは単一のプロトコルファイミリが複数のアドレスファミリをサポートすることが意図されており、ソケットの作成にはPF値を、ソケットアドレス構造体にはAF値が用いられてきた。実際には複数のアドレスファミリをサポートするプロトコルファミリがサポートされたことはなく、<sys/socket.h>ヘッダでは、あるプロトコルのPF値はそのプロトコルのAF値と同じ値を持つように定義されている。

UNIXネットワークプログラミング 第2版 Vol.1

だそうです。

AF_INETの場合、以下の3種類のソケットを作成できる。

  • tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
  • udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
  • raw_socket = socket(AF_INET, SOCK_RAW, protocol);

この記述からも、PF_* という定数からも、 socket 関数からいろいろなプロトコルに対応したソケットが作れる雰囲気が出てる。

ソケットのディスクリプタ

int listener_d = socket(AF_INET, SOCK_STREAM, 0) により作成されたTCPソケットのソケットのディスクリプタを取得できる。ファイルディスクリプタがファイル入出力のストリームを識別するように、ソケットのディスクリプタはソケットを識別できる数値である。

このディスクリプタ( listener_d )に対応したTCPソケットがカーネルで管理されているということか。

動かしてみる

この段階でも30000ポートは使われてない。

$ lsof -i:30000

しかし、ファイルディスクリプタを確認してみると、ソケットが作成されていることが分かる。

$ ll /proc/9950/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Oct  9 07:58 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Oct  9 07:58 ../
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:58 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:58 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:58 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 07:58 3 -> socket:[29062]

ポートをバインドする

ポートをバインドする処理を追加。

@@ -3,6 +3,7 @@
 #include <string.h>
 #include <errno.h>
 #include <sys/socket.h>
+#include <arpa/inet.h>
 
 void error(char *msg)
 {
@@ -16,6 +17,13 @@ int main(int argc, char *argv[])
   if (listener_d == -1) {
     error("socket err");
   }
+
+  struct sockaddr_in name;
+  name.sin_family = AF_INET;
+  name.sin_port = (in_port_t)htons(30000);
+  name.sin_addr.s_addr = htonl(INADDR_ANY);
+  bind(listener_d, (struct sockaddr *) &name, sizeof(name));
+
   puts("wait...");
   while(1) {
   }

bind関数

bind関数は、ソケットにプロトコル特有のアドレスを割り当てる。

#include <sys/socket.h>

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

ソケットアドレス構造体

bind関数の第2引数にプロトコル特有のアドレスのポインタを指定する。プロトコル特有のアドレスを表す構造体をソケットアドレス構造体と呼ぶ。

ソケットAPIにおいて、ソケットアドレス構造体は、プロセスからカーネルへ、カーネルからプロセスへの両方向で受け渡しされる。ソケットアドレス構造体の名前は sockaddr_ で始まり、 IPv4のソケットアドレス構造体は sockaddr_in

TCPソケットのアドレスは、IPアドレスとポート番号の組である。

htons 関数はshort型の値をネットワークバイトオーダー(ビッグエンディアン)のバイト列にする。 htonl 関数はlong型の値をネットワークバイトオーダーのバイト列にする。 (参考: http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/CIPINTRO/NETWORK/endian.html

これらの関数を使って、ネットワークバイトオーダーで、ポート番号とIPアドレスを指定する。INADDR_ANYは0を差しワイルドカードであることを示す。

bindのようなソケットAPIは、いろいろなプロトコルのソケットに対応する必要があるので、IPv4用の sockaddr_in を引数に取るわけにはいかない。そこで、 sockaddr という構造体に一旦キャストしてから引数として渡す。

動かしてみる

特に変化なし。

$ lsof -i:30000
$ ll /proc/9971/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Oct  9 08:10 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Oct  9 08:10 ../
lrwx------ 1 ubuntu ubuntu 64 Oct  9 08:10 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 08:10 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 08:10 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 08:10 3 -> socket:[29203]

LISTEN状態になる

@@ -26,6 +26,10 @@ int main(int argc, char *argv[])
     error("bind err");
   }
 
+  if (listen(listener_d, 1) == -1) {
+    error("listen err");
+  }
+
   puts("wait...");
   while(1) {
   }

listen関数

listen関数により、TCPの状態が初期状態であるCLOSEからLISTENになる。

#include <sys/socket.h>

int listen(int sockfd, int backlog);

コネクションキュー

第2引数は、第1引数で指定したソケットに対するコネクションキューのサイズ。このコネクションキューは、ESTABLISH状態のコネクションが格納される。

このキューの大きさは、カーネルパラメータであるnet.core.somaxconnより大きな値が指定した場合はnet.core.somaxconnの大きさになる。

SYN_RCVD状態のコネクションが格納されるキューも別にある。こちらはカーネルパラメータnet.ipv4.tcp_max_syn_backlog 最大値が決められる。

http://wiki.bit-hive.com/linuxkernelmemo/pg/listen%20backlog%20%A1%DA3.6%A1%DB

キューに入れなかったリクエストはどうなるか。UNIXネットワークプログラミングを読むと

クライアントからのSYNが到着した際にキューが満杯だった場合、TCPはRSTを送信せず、単に到達したSYNを無視する。これは、この状態が過度的な状態であると考えられるためで、クライアントはSYNを再送することにより、運がよければキューの空きを見つけることが可能である。

UNIXネットワークプログラミング 第2版 Vol.1

キューの大きさを1にして、同時接続を行うと段々と処理が行われることが分かる。

動かしてみる

listen 関数によって30000ポートがTCPのLISTEN状態になった。

$ lsof -i:30000
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 9992 ubuntu    3u  IPv4  29544      0t0  TCP *:30000 (LISTEN)

netstat コマンドでも確認できる。

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN

では、 telnet でつないでみる。Vagrantで動かしてるUbuntu上動いているecho サーバーに、ホストOSであるMacからつないでみる。

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.

サーバー上で確認してみると、ESTABLISHED状態の接続ができていることがわかる。

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN
tcp        0      0 192.168.33.10:30000     192.168.33.1:64304      ESTABLISHED

TCPの状態遷移図を見ながらだとイメージしやすい。

http://image.itmedia.co.jp/ait/articles/0402/13/wi-fig06.gif

引用元:第16回 信頼性のある通信を実現するTCPプロトコル(3) (3/4):基礎から学ぶWindowsネットワーク - @IT

ESTABLISHEDになってるということは3ウェイハンドシェークが完了したってこと。tcpdumpでパケットを覗いてみる。tcpdumpを実行してから、telnetで再度つなぐ。

$ tcpdump -i vboxnet0

telnetでつなぐと以下のように出力された。

13:14:16.795321 IP 192.168.33.1.65088 > 192.168.33.10.30000: Flags [S], seq 2760695165, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 654960331 ecr 0,sackOK,eol], length 0
13:14:16.795364 IP 192.168.33.10.30000 > 192.168.33.1.65088: Flags [S.], seq 3180502780, ack 2760695166, win 28960, options [mss 1460,sackOK,TS val 1405176 ecr 654960331,nop,wscale 7], length 0
13:14:16.796340 IP 192.168.33.1.65088 > 192.168.33.10.30000: Flags [.], ack 1, win 4117, options [nop,nop,TS val 654960334 ecr 1405176], length 0

3ウェイハンドシェークされてる。

tcpdumpを実行したまま、telnetを切断すると、クライアントからサーバーへのFIN,ACKが送られるが、サーバーからのACKとFINが送られないことが分かる。クライアントは何度かFIN,ACKを送るが最終的には、RST,ACKを送り強制的に接続を切った。

キューからESTABLISH状態のコネクションを取り出す

コネクションキューの先頭からESTABLISH状態のコネクションを1つ取り出す。キューが空の場合、プロセスはブロックされスリープ状態となる。

@@ -32,6 +32,12 @@ int main(int argc, char *argv[])
 
   puts("wait...");
+ 
+  struct sockaddr_storage client_addr;
+  unsigned int address_size = sizeof(client_addr);
   while(1) {
+    int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
+    if (connect_d == -1) {
+      error("accept err");
+    }
   }
   return 0;
 }

accept関数

#include <sys/socket.h>

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

accept関数は、クライアントとのTCPコネクションを参照しているソケットディスクリプタを返す。socket関数で取得したソケットディスクリプタとは別のものを差している。

第2引数で指定したソケットアドレス構造体にクライアントのアドレスが格納される。

動かしてみる

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN     
tcp        0      0 192.168.33.10:30000     192.168.33.1:49697      ESTABLISHED
$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 10264 ubuntu    3u  IPv4  31554      0t0  TCP *:30000 (LISTEN)
echo_serv 10264 ubuntu    4u  IPv4  31555      0t0  TCP 192.168.33.10:30000->192.168.33.1:49697 (ESTABLISHED)
$ ll /proc/10264/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Oct  9 13:44 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Oct  9 13:44 ../
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 3 -> socket:[31554]
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 4 -> socket:[31555]

telnetで接続を切ってみる。今度は、クライアントからのFIN,ACKに対し、サーバーからACKが返ってきた。

サーバー側のソケットは、CLOSE_WAIT状態のまま残っている。

$ ll /proc/10264/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Oct  9 13:44 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Oct  9 13:44 ../
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 3 -> socket:[31554]
lrwx------ 1 ubuntu ubuntu 64 Oct  9 13:44 4 -> socket:[31555]

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN     
tcp        1      0 192.168.33.10:30000     192.168.33.1:49697      CLOSE_WAIT 

$ lsof -i:30000
COMMAND     PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 10264 ubuntu    3u  IPv4  31554      0t0  TCP *:30000 (LISTEN)
echo_serv 10264 ubuntu    4u  IPv4  31555      0t0  TCP 192.168.33.10:30000->192.168.33.1:49697 (CLOSE_WAIT)

CLOSE_WAIT状態は、FINに対してACKを送って、アプリケーションからのクローズ待ち。クローズ処理漏れがあるとこの状態のTCPソケットが残ったままになってしまう。

ソケットを閉じる

@@ -4,6 +4,7 @@
 #include <errno.h>
 #include <sys/socket.h>
 #include <arpa/inet.h>
+#include <unistd.h>
 
 void error(char *msg)
 {
@@ -38,6 +39,8 @@ int main(int argc, char *argv[])
     if (connect_d == -1) {
       error("accept err");
     }
+
+    close(connect_d);
   }
   return 0;
 }

close関数

ファイルでも利用するclose関数で、ソケットをクローズする。

#include <unistd.h>

int close(int sockfd);

ソケットに対してclose関数を実行すると、ソケットディスクリプタが使用できなくなる。カーネルでは、TCPソケットの送信バッファ内のデータを通信先へ送ってから、TCPコネクションの終了シーケンス(FINとACKを相互に送り合う)が実行される。

動かしてみる

telnetでつなぐとすぐにサーバー側から切断される。今度はサーバー側からclose関数で接続を切っている。接続が切られたのでTIME_WAIT状態になっている。

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN
tcp        0      0 192.168.33.10:30000     192.168.33.1:50509      TIME_WAIT
$ ll /proc/10314/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Oct 10 02:18 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Oct 10 02:18 ../
lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Oct 10 02:18 3 -> socket:[31911]

tcpdumpを見ると、3ウェイハンドシェークの後、FINとACKを相互に送りあって接続を閉じている。

02:17:52.031983 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [S], seq 537180097, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 658332935 ecr 0,sackOK,eol], length 0
02:17:52.032002 IP 192.168.33.10.30000 > 192.168.33.1.50509: Flags [S.], seq 169597332, ack 537180098, win 28960, options [mss 1460,sackOK,TS val 2223648 ecr 658332935,nop,wscale 7], length 0
02:17:52.032175 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [.], ack 1, win 4117, options [nop,nop,TS val 658332935 ecr 2223648], length 0
02:17:52.032372 IP 192.168.33.10.30000 > 192.168.33.1.50509: Flags [F.], seq 1, ack 1, win 227, options [nop,nop,TS val 2223648 ecr 658332935], length 0
02:17:52.032627 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [.], ack 2, win 4117, options [nop,nop,TS val 658332935 ecr 2223648], length 0
02:17:52.032643 IP 192.168.33.1.50509 > 192.168.33.10.30000: Flags [F.], seq 1, ack 2, win 4117, options [nop,nop,TS val 658332935 ecr 2223648], length 0
02:17:52.032654 IP 192.168.33.10.30000 > 192.168.33.1.50509: Flags [.], ack 2, win 227, options [nop,nop,TS val 2223649 ecr 658332935], length 0

ソケットに書き込む

クライアントから文字列を読み込む前に、 Hello World! を返すようにしてみる。

@@ -40,6 +40,9 @@ int main(int argc, char *argv[])
       error("accept err");
     }
 
+    char *msg = "Hello World!\r\n";
+    write(connect_d, msg, strlen(msg));
+
     close(connect_d);
   }
   return 0;

write関数

ファイルに書き込む時にも使うwrite関数でソケットにデータを書き込む。

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

writeによりデータが、カーネル内のソケットの送信バッファにコピーされる。ソケットバッファに空きがない場合、ブロックされプロセスはスリープ状態になる。writeの成功はバッファへのコピーが完了しただけであり、通信相手にデータが届いたわけではない。

ソケットの送信バッファ内のデータは、カーネルによってTCPのMSSやIPのMTUのサイズに分割されて、通信先へ送信される(調べた感じだと通常はIPのMTUのサイズの方が大きいみたい)。

動かしてみる

3ウェイハンドシェークの内容から、クライアントがサーバーへ伝えているmssが1460バイトであることが分かる。

12:10:03.903979 IP 192.168.33.1.52745 > 192.168.33.10.30000: Flags [S], seq 732193526, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 661451291 ecr 0,sackOK,eol], length 0
12:10:03.904111 IP 192.168.33.10.30000 > 192.168.33.1.52745: Flags [S.], seq 389803173, ack 732193527, win 28960, options [mss 1460,sackOK,TS val 3007057 ecr 661451291,nop,wscale 7], length 0
12:10:03.904157 IP 192.168.33.1.52745 > 192.168.33.10.30000: Flags [.], ack 1, win 4117, options [nop,nop,TS val 661451291 ecr 3007057], length 0

"Hello World!"という文字列のバイト数は、mssより小さいので、1回だけデータが送られている。

12:10:03.904402 IP 192.168.33.10.30000 > 192.168.33.1.52745: Flags [P.], seq 1:15, ack 1, win 227, options [nop,nop,TS val 3007057 ecr 661451291], length 14
12:10:03.904436 IP 192.168.33.1.52745 > 192.168.33.10.30000: Flags [.], ack 15, win 4117, options [nop,nop,TS val 661451291 ecr 3007057], length 0

mss(1460バイト)より大きなバイト数にしてみる。

char *msg = "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901\r\n";

1つのTCPセグメントだけでは送信仕切れず、2つのTCPセグメントに分割して送っていることがわかる。

12:11:56.140718 IP 192.168.33.10.30000 > 192.168.33.1.52797: Flags [.], seq 1:1449, ack 1, win 227, options [nop,nop,TS val 3035101 ecr 661563206], length 1448
12:11:56.140725 IP 192.168.33.10.30000 > 192.168.33.1.52797: Flags [P.], seq 1449:1464, ack 1, win 227, options [nop,nop,TS val 3035101 ecr 661563206], length 15
12:11:56.140867 IP 192.168.33.1.52797 > 192.168.33.10.30000: Flags [.], ack 1464, win 4072, options [nop,nop,TS val 661563207 ecr 3035101], length 0

クライアントからデータを受け取る

read関数で読む。read_line関数というラッパー関数を作り1行ずつ読むようにする。

@@ -12,6 +12,22 @@ void error(char *msg)
   exit(1);
 }
 
+int read_line(int socket, char *buf, int len)
+{
+  char *s = buf;
+  int slen = len;
+  int c = read(socket, s, slen);
+  while ((c > 0) && (s[c - 1] != '\n')) {
+    s += c;
+    slen = -c;
+    c = read(socket, s, slen);
+  }
+  if (c < 0) {
+    return c;
+  }
+  return len - slen;
+}
+
 int main(int argc, char *argv[])
 {
   int listener_d = socket(PF_INET, SOCK_STREAM, 0);
@@ -32,6 +48,8 @@ int main(int argc, char *argv[])
   }
 
   puts("wait...");

   struct sockaddr_storage client_addr;
   unsigned int address_size = sizeof(client_addr);
+
+  char buf[255];
   while(1) {
@@ -40,8 +58,9 @@ int main(int argc, char *argv[])
       error("accept err");
     }
 
+    read_line(connect_d, buf, sizeof(buf));
+
+    write(connect_d, buf, strlen(buf));
 
     close(connect_d);
   }

read関数

ファイルから読む時にも使用するread関数を使う。

#include <unistd.h>

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

ファイルディスクリプタとは異なり、ソケットのディスクリプタに対するreadは、要求したバイト数より少ない入出力を行うことがある。これはカーネル内のソケットのバッファが限界に達するからである。そのため、ループで文字列の終端(ソケットの場合は \0 ではなく \r\n )に達するまで何回もreadを呼ぶ必要がある。

おしまい

コードはこんな感じ。次は複数クライアントの同時接続に対応しようと思う。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

void error(char *msg)
{
  fprintf(stderr, "%s:%s\n", msg, strerror(errno));
  exit(1);
}

int read_line(int socket, char *buf, int len)
{
  char *s = buf;
  int slen = len;
  int c = read(socket, s, slen);
  while ((c > 0) && (s[c - 1] != '\n')) {
    s += c;
    slen = -c;
    c = read(socket, s, slen);
  }
  if (c < 0) {
    return c;
  }
  return len - slen;
}

int main(int argc, char *argv[])
{
  int listener_d = socket(PF_INET, SOCK_STREAM, 0);
  if (listener_d == -1) {
    error("socket err");
  }

  struct sockaddr_in name;
  name.sin_family = AF_INET;
  name.sin_port = (in_port_t)htons(30000);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(listener_d, (struct sockaddr *) &name, sizeof(name)) == -1) {
    error("bind err");
  }

  if (listen(listener_d, 1) == -1) {
    error("listen err");
  }

  puts("wait...");

  struct sockaddr_storage client_addr;
  unsigned int address_size = sizeof(client_addr);

  char buf[255];
  while(1) {
    int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
    if (connect_d == -1) {
      error("accept err");
    }

    char *msg = "Hello World!\r\n";
    write(connect_d, msg, strlen(msg));

    read_line(connect_d, buf, sizeof(buf));

    write(connect_d, buf, strlen(buf));

    close(connect_d);
  }
  return 0;
}