IPC: ソケット

クライアント/サーバタイプのネットワークプログラミング。

SYNOPSIS

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>
 
int socket(int domain, int type, int protocol);
int bind(int s, const struct sockaddr *name, size_t namelen);
int listen(int s, int backlog);
int accept(int s, struct sockaddr *addr, size_t *addrlen);
int connect(int s, const struct sockaddr *name, size_t namelen);

DESCRIPTION

クライアント/サーバタイプのネットワークプログラミングを行うには、ソケットを利用します。ソケットプログラミングをする場合、「複数のホストで通信を行う/単一ホスト内で通信を行う」「どのタイプのプロトコルを使用」などのどの手段を利用するか選択します。

● プログラミング手順(サーバ編)

1. ソケットを作る

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

ソケットを作るには、socket()関数を使用します。domain パラメーターは通信を行なうドメインを指定します。これはどのプロトコル・ファミリ(protocol family)を通信に使用するかを指定します。以下では、最も利用される一部のものをリストしています。すべてのファミリを知りたい場合、/usr/include/sys/socket.h を参照してください。プロトコルファミリは、type で指定される型(通信方式)を持っています。protocol はソケットによって使用される固有のプロトコルを指定します。通常それぞれのソケットは、与えられたプロトコル・ファミリの種類ごとに一つのプロトコルのみをサポートするので 0 を指定します。しかし、多くのプロトコルがを存在する場合 /etc/protocols に記述されている固有のプロトコルを指定します。

domain PF_UNIX (AF_UNIX) ローカル通信
  PF_INET (AF_INET) IPv4 インターネット・プロトコル
  PF_INET6 (AF_INET6) IPv6 インターネット・プロトコル
  PF_APPLETALK (AF_APPLETALK) Apple Talk プロトコル
  PF_IPX (AF_IPX) IPX - Novell プロトコル
  PF_NS (AF_NS) XEROX NS プロトコル
  PF_NETLINK (AF_NETLINK) カーネル・ユーザ・デバイス
type SOCK_STREAM 順序性と信頼性があり、双方向のバイト・ストリームを提供します。コネクション型。
  SOCK_DGRAM データグラム(信頼性無し、固定最大長メッセージ) を提供します。コネクションレス型。
  SOCK_ROW 生(直接IPを用いた通信)のネットワーク・プロトコルへのアクセスを提供
protocol IPPROTO_IP 0
  IPPROTO_ICMP control message protocol
  IPPROTO_TCP TCP
  IPPROTO_UDP UDP
  IPPROTO_IPV6 IPv6

ソケットが正常に作られると、ソケット記述子を返し、失敗すると -1 を返します。

次に、ソケットのアドレスの情報を構造体に格納します。その構造体は、ドメイン(プロトコルファミリ)によって異なります。

ローカル通信である PF_UNIX を利用する場合、sockaddr_un 構造体を利用します。

    #include <sys/un.h>
    struct sockaddr_un{
      sa_family_t sun_family;      /* PF_UNIX */
      char        sun_path[108];   /* パスネーム */
    };

インターネット通信である PF_INET を利用する場合、sockaddr_in 構造体を利用します。

    #include <netinet/in.h>
    struct sockaddr_un{
      short       sin_family;      /* ドメイン(PF_INET) */
      u_short     sin_port;        /* ポート番号 */
      struct in_addr  sin_addr;    /* IPアドレス */
      char        sin_zero[8];     /* パディング */
    };

また、IPv6 を利用する場合、sockaddr_in6 構造体を利用します。

2. ソケットに名前をつける(以降、よく使われる PF_INET を利用します)

    int bind(int s, const struct sockaddr *name, int namelen);

ソケット記述子 s とソケットアドレス情報 name を格納する構造体、その構造体のサイズ namelen を指定してソケットに名前をつけます。失敗すると -1 を返します。

    struct sockaddr_in  local;
    memset((char *)&local, 0, sizeof(local));
    local.sin_family = PF_INET;
    local.sin_addr.s_addr  = htonl(INADDR_ANY);
    local.sin_port = htons(9999);
    bind(s, (struct sockaddr *) &local, sizeof(local));

ここで、ポート番号に 9999 を指定していますが、ポート番号の指定にはルールがあります。ポート番号を選択する場合、 特権ユーザー(スーパーユーザーまたはルート)のために 0から 1,023のポート番号が予約されています。 これらのポート番号は、電子メール、FTPおよびHTTPのような標準サービスのためのものです。 もし、自分で作成するサーバーのためにポート番号を選ぶなら、1,024以降を選択してください。また、/etc/services ファイルにシステムに予約されているポートが記述されているので確認しておく必要があります。IPアドレスやポートを指定する際に、htonl(), htons()関数を使っていますが、これはネットワーク形式のバイトオーダーにすることでバイトオーダーが異なるシステム同士でも安心して利用できるようにしています。INADDR_ANY を指定した場合、自分の割り当てられたすべての IP アドレスに対しての要求を受け付けます。

もし、/etc/services ファイルに登録されているサービス利用する場合、getservbyname()関数を利用して/etc/services ファイルからポート番号を得ることが出来ます。

3. クライアントからの接続要求を待つ

    int listen(int s, int backlog);

ソケット記述子 s とクライアントをいくつまで backlog キューに持つか指定(標準的なプログラムは 5 ぐらい)します。

4. ソケットの接続を受ける

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

ソケット記述子 s と接続を許可するクライアントのソケットアドレス情報 name を格納する構造体、その構造体のサイズ namelen を指定します。接続が許可されると、クライアントと通信を行うためのソケット記述子を返し、失敗すると -1 を返します。

    struct sockaddr_in  client;
    int                 s2;
    int                 len;
    memset((char *)&client, 0, sizeof(client));
    len = sizeof(client);
    s2 = sccept(s, (struct sockaddr *) &client, &len);

5. データの送受信

サーバとクライアントの接続が確立したら、read(), write()システムコールや send(), recv()関数を利用してファイルに読み書きするように accept()関数によって作られたソケットに対してデータの送受信を行います。

6. 切断

確立されたコネクションを切断するには、close() システムコールを使用します。

● プログラミング手順(クライアント編)

1. ソケットを作る

サーバでソケットを作ったように、クライアントでも socket()関数を使用してソケットを作ります。

2. サーバへ接続

    int connect(int s, const struct sockaddr *name, int namelen);

ソケット記述子 s と接続したいサーバのソケットアドレス情報 name を格納する構造体、その構造体のサイズ namelen を指定します。失敗すると -1 を返します。

    struct sockaddr_in  server;
    memset((char *)&server, 0, sizeof(server));
    server.sin_family = PF_INET;
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    server.sin_port = htons(9999);
    connect(s, (struct sockaddr *) &server, sizeof(server));

サーバのアドレスと通信するポートを指定します。ここでは、サーバとクライアントが同じであるとして 127.0.0.1 を指定しています。サーバの IP アドレスや名前からホスト情報をきちんと得るには

    struct hostent     *srvhost;
    struct sockaddr_in  server;
    u_long              srvaddr;
 
    srvaddr = inet_addr(hostname);
    srvhost = gethostbyaddr((char *)&srvaddr, sizeof(srvaddr), PF_INET);
    if (srvhost == NULL) {
        srvhost = gethostbyname(hostname);
        if (srvhost == NULL){
            fprintf(stderr,"Could not get IP Address\n");
            exit(1);
        }
    }
    srvaddr = *((unsigned long *)((srvhost->h_addr_list)[0]));
    memset((char *)&server, 0, sizeof(server));
    server.sin_family = PF_INET;
    server.sin_addr.s_addr = srvaddr;
    server.sin_port = htons(9999);
    connect(s, (struct sockaddr *) &server, sizeof(server));

という具合にコーディングすると良い。ちょっとやりすぎかな...

3. データの送受信

サーバとクライアントの接続が確立したら、read(), write()システムコールや send(), recv()関数を利用してファイルに読み書きするようにソケットに対してデータの送受信を行います。

4. 切断

確立されたコネクションを切断するには、close() システムコールを使用します。

SAMPLE

サーバプログラム(echo_s.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
int main()
{
    int s, s2, len;
    struct sockaddr_in local, client;
    char str[BUFSIZ];
 
    if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        exit(1);
    }
 
    memset((char *)&local, 0, sizeof(local));
    local.sin_family = PF_INET;
    local.sin_addr.s_addr  = htonl(INADDR_ANY);
    local.sin_port = htons(9999);
    if (bind(s, (struct sockaddr *)&local, sizeof(local)) < 0) {
        perror("bind");
        exit(1);
    }
 
    if (listen(s, 5) < 0) {
        perror("listen");
        exit(1);
    }
 
    for(;;) {
        printf("Waiting for a connection...\n");
        memset((char *)&client, 0, sizeof(client));
        len = sizeof(client);
        if ((s2 = accept(s, (struct sockaddr *)&client, &len)) < 0) {
            perror("accept");
            exit(1);
        }
 
        printf("Connected.\n");
 
        do {
            int n;
            n = recv(s2, str, BUFSIZ, 0);
            if (n <= 0) {
                if (n < 0) {
                    perror("recv");
                }
                break;
            }
 
            if (send(s2, str, n, 0) < 0) {
                perror("send");
                break;
            }
        } while (1);
 
        close(s2);
    }
 
    return 0;
}

クライアントプログラム(echo_c.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
int main()
{
    int s, t;
    struct sockaddr_in server;
    char str[BUFSIZ];
 
    if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        exit(1);
    }
 
    memset((char *)&server, 0, sizeof(server));
    server.sin_family = PF_INET;
    server.sin_addr.s_addr  = inet_addr("127.0.0.1");
    server.sin_port = htons(9999);
    if (connect(s, (struct sockaddr *)&server, sizeof(server)) < 0) {
        perror("connect");
        exit(1);
    }
 
    printf("Connected.\n");
 
    while(printf("> "), fgets(str, BUFSIZ, stdin), !feof(stdin)) {
        if (send(s, str, strlen(str), 0) < 0) {
            perror("send");
            exit(1);
        }
 
        if ((t=recv(s, str, BUFSIZ, 0)) > 0) {
            str[t] = '\0';
            printf("echo> %s", str);
        } else {
            if (t < 0) {
                perror("recv");
            } else {
                printf("Server connection closed\n");
            }
            exit(1);
        }
    }
 
    close(s);
 
    return 0;
}

これをコンパイルし実行すると以下のようになります。

  << SHELL-1 >>
    # gcc echo_s.c -o echo_s -lsocket -lnsl
    # gcc echo_c.c -o echo_c -lsocket -lnsl
    # ./echo_s
    Waiting for a connection...

SHELL-1 でサーバが起動されたら。別のシェルでクライアントプログラムを起動してみます。このサンプルでは、サーバの IP アドレスを 127.0.0.1 と指定しているのでサーバとクライアントプログラムを同一ホストで実行しなくてはなりません。">"プロンプトが表示されたら、文字列を入力して改行するとサーバーから入力した内容が返ってきたことを "echo>"の後に表示します。

  << SHELL-2 >>
    # ./echo_c
    Trying to connect...
Connected.
> hello, world. echo> hello, world. >

クライアントがサーバに接続すると以下のようにサーバーに Connected. とメッセージが表示されます。

  << SHELL-1 >>
    # ./echo_s
    Waiting for a connection...
    Connected.

SEE ALSO

socket(3), bind(3), listen(3), accept(3), connect(3), send(3), recv(3), read(2), write(2)

 

戻る