エンジニア

C/C++でネットワークプログラミングをやってみよう(2)

投稿日:2017年8月23日 更新日:

2話 ソケットプログラミングを実際にやってみよう
概要

今回は実際にソケットのプログラミングをしてみたいと思っています。
目標ですが、TCPプロトコルで簡単なクライアントとサーバを作成し、ソケットのシステムコールの使い方を覚えたいと思います。
UDPに関しては、「6話 NAT超えをしてみよう(P2P通信)」にて説明する予定です。

今回の流れとして、一旦はシステムコールの呼び出しを一連の流れで実装します。
それから、もう少し細かい設定をしていき、最終的にはライブラリ化を行いたいと思います。
そして、このライブラリを使い、次回は簡単なチャットのクライアント・サーバを作成する予定です。

今回作成するのは、クライアントからサーバへ文字列を送れば、サーバからは少し加工した文字列を返してくれる簡単なものとします。
今回はシステムコールを覚えることに目的があるためです。

クライアント ver1

順番的に

  1. TCP/IP用のディスクリプター生成

  2. サーバへ接続

  3. サーバへデータ送信

  4. サーバからデータ受信

  5. ディスクリプターの解放

TCP/IP用のディスクリプター生成
    int sock=socket(PF_INET,SOCK_STREAM,0);
    if (sock<0)
    {
        /* TODO ソケットが生成されない場合のエラー処理 */
        printf("socket failure!!! (%d)", errno);
        exit(0);
    }   

socketは、システムコールです。
第一引数PF_INETはipv4のIPプロトコルです。
第二引数はSOCK_STREAMの場合はTCPになります。PF_INETとSOCK_STREAMを組み合わせることで、このディスクリプターはTCP/IPプロトコル上で動作することになります。

サーバへ接続
    const int port=10101;
    const char* ip_address="127.0.0.1";
    struct sockaddr_in addr={0,};

    /* 接続のために構造体に値を入れる */
    addr.sin_family = AF_INET;
    /* 接続先のポート番号。ビッグエンディアンの32ビットに変換*/
    addr.sin_port = htons(port);
    /* inet_addrはxxx.xxx.xxx.xxxの文字列をビッグエンディアン(ネットワークバイトオーダー)の32ビットに変換する。*/
    addr.sin_addr.s_addr = inet_addr(ip_address);

    /* サーバにコネクトを行う。この関数はブロック型である。
      コネクトのタイムアウトはデバイスドライバーの実装によって違う。
       このタイムアウト値の設定もくせものでOSによっては設定ができない。
       なので、タイムアウトをさせる場合は、別途実装する必要があるが、色々と説明が長くなるので
       ライブラリにまとめる際のコードを参考にして欲しい。

       第二引数のstruct sockaddrのキャストだが、sockの第一引数に何を指定しているかで指定される構造体が違うためにキャストすることになる。
       現代のソフトウェア工学的には気持ち悪い実装かも知れないが、connectがシステムコールである故にやむを得ないところがある。
    */
    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr))!=0)
    {
        /* 失敗*/
        printf("socket connect error!!! (%d)", errno);
        exit(0);
    }
サーバへデータ送信

実際にはsendで転送するのがオススメだが、ファイルの扱いと似ているので、理解のためにwriteで転送している。
実際にはネットワークデバイスではread/writeのインタフェースを持っている。
第二引数はデータのポインターで、第3の引数はデータのサイズである。
今回は文字列を送るので、文字列のサイズ+Zero Terminateの1byteである。

    int write_bytes=write(sock, message, message_size+1);
    if (write_bytes!=message_size+1)
    {
         /* 失敗*/
        printf("write error!!! (%d)", errno);
         exit(0);
    }
サーバからデータの受信

sockに対して何もしなければ、read/recv関数は受信終了までブロックする。
ただし、例えば、サーバからクライアントに1MByteのデータを送信した際に1MByteを全て受信するまでブロックするわけではない。
read/recvするタイミングで受信した分までかサイズに指定している分ーーサンプルでは1024ーーまで読み込むことになる。
実際に全て受信しきれてない場合は、ループで回りながら受信することになるが、この事に関しては次回もう少し詳しく説明を行う。
今のところでは、以下のような形でデータを受信するという感じをつかめて欲しい。

    char buffer[1024];
    int n=read(sock, buffer, 1024);
    if (n<0)
    {
         /* エラー */
        printf("read error!!! (%d)", errno);
         exit(0);
    }
    if (n==0)
    {
         /* 接続が切れた! */
        printf("connect closed!!! (%d)", errno);
         exit(0);
    }
    printf("%s\n", buffer);
ディスクリプターの解放
    close(sock);

これらの実装はgitのレポジトリーにまとめていますので、参考にしてください。
git@github.com:dongho-yoo/network_programming.git/chapter-2/sample1

サーバ ver1

今度はサーバのサンプルです。
流れは、

  1. TCP/IP用のディスクリプター生成します。

  2. bind ポート番号とディスクリプターを結びつけます。

  3. listenディスクリプターをTCPハンドジェーク用に待ち状態にします。

  4. acceptクライアントと確立した接続からクライアントへのディスクリプターを生成します。

  5. データのやり取り

TCP/IP用のディスクリプターの生成に関してはクライアントと同じです。

bind

基本的にディスクリプターとポート番号を結びつけます。クライアントも本当はbindすべきですが、クライアントが特定のポートで待ち受けることをしないので、connect関数の中で空いているデフォルトのポート番号を割り当てています。

 36     int port=atoi(argv[1]);
 37     /* ここからがバインド処理 */
 38     struct sockaddr_in addr={0,};
 39     addr.sin_family = AF_INET;
 40     /* 待ち受けるポート番号。ビッグエンディアンの32ビットに変換*/
 41     addr.sin_port = htons(port);
 42     /* アドレスが複数ある場合は指定するが、そうでもない場合は以下のように適当にアドレスを指定する */
 43     addr.sin_addr.s_addr = htonl(INADDR_ANY);
 44     const size_t addr_size = sizeof(addr);
 45
 46     /* 引数はクライアントのconnectに似ている */
 47     if (bind(sock, (struct sockaddr*)&addr, addr_size)==-1)
 48     {
 49         /* ポートが解放されず、バインドされないケースもある。テスト時はプロセスが落ちたりするので、ポートが解放されない場合がある。
 50            そういう場合は、sockoptでSO_REUSEADDRを使うべき */
 51         printf("bind error!!!(%d)\n",errno);
 52         close(sock);
 53         exit(0);
 54     }
listen

TCPの接続というのは、TCP three-way handshakingを意味します。
以下を参照。
3ウェイ・ハンドシェイク - Wikipedia
実際にはTCPのヘッダーのみのやり取りを3回繰り返す事になります。この threee-way handshakingはデバイスの中で、connectするクライアント側とlisten状態のサーバで行う事になります。
クライアントの説明で、connectのタイムアウト処理を行うのはやや複雑になると話したのはそういう意味です。

 55     /* クライアントの待ち受け
 56      listenの第二引数はbacklogである。筆者がソケットプログラミングを始めた頃では接続がまだ確立されていない状態のクライアントの接続のキューの数だったが、最近は接続確立済みのキューの数になっているようだ。MAN LISTEN(2)を参照
 57      なので、以下の10では、クライアントのコネクション待ち状態は最大10コネクションになり、11番目のコネクションからはコネクトエラーになる。*/
 58     if (listen(sock, 10)==-1)
 59     {
 60         /* エラー */
 61         printf("listen error!!!(%d)\n",errno);
 62         close(sock);
 63         exit(0);
 64     }
 65     printf("listen success!!!\n");
accept

確立したコネクションからディスクリプターを生成します。
この生成されたディスクリプターへ書き込むと実際にクライアント側にデータが送信されます。
ディスクリプターに対して何もしなければ、acceptはブロック型関数になります。

 66 ACCEPT:
 67     /* ディスクリプターsockは、クライアントの待ち受け状態になっている。listenがブロックして待っているわけではない。
 68        acceptでブロックして待つことになる。
 69        connectとbindと引数の指定の仕方が少し違うのに注目。
 70        acceptが成功した場合、accept_addrにはクライアントのipアドレスやポート番号などの値が入ってくる。
 71        */
 72     struct sockaddr_in accept_addr={0,};
 73     socklen_t accpet_addr_size=sizeof(accept_addr);
 74     int sock_client=accept(sock,(struct sockaddr*)&accept_addr, &accpet_addr_size);
 75
 76     if (sock_client==0||sock_client<0)
 77     {
 78         /* エラー */
 79         exit(0);
 80     }
実行してみよう

一旦、これでクライアントとサーバができました。
以下のレポジトリーをcloneします。
git@github.com:dongho-yoo/network_programming.git/chapter-2/samle1

sample1のディレクトリで、makeコマンドを実行します。

 $ make

するとclientとserverの実行ファイルが生成されます。
サーバを実行します。(ポート番号は12345)

 $ ./server 12345 &

クライアントを実行します。

 $ ./client 127.0.0.1 12345 -m hellow

サーバを終了させる場合は、

 $ ./client 127.0.0.1 12345 -m quit

別のマシーンで、サーバを立ち上げてテストしてもOKです。
ただ、サーバ側のファイアウォール設定でサーバに指定したポートが開いていないといけません。

ここまでで、流れは単純な通信を行う流れです。
ただし、少し機能が少ないです。

まず、read/recv時のタイムアウト処理が入っていません。

このような場合、よく使われるのはシステムコールのselectです。

select

システムコールのプロトタイプは以下のとおりです。

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);

第一引数:設定しているディスクリプターの最大値+1が入ります。
第二引数:ディスクリプターが読み込む準備ができたかどうかをチェックするディスクリプターのリストーーディスクリプターは全てソケットである必要はなく、selectに対応したデバイスならばOKーー
第三引数:ディスクリプターが書き込み準備ができているかどうかをチェックするディスクリプターのリストーーconnectのタイムアウト時にはこちらを使いますーー
第四引数:ディスクリプターがエラーになっているかどうかのチェックをするディスクリプターのリストだが、実際に使うシチュエーションがあまりない。
第五引数:タイムアウト値の設定。

110     /* ディスクリプターを管理する構造体 */
111     fd_set fdz;
112     struct timeval tv = {3,0}; /* タイムアウト3秒 */
113
114     /* このFD_で始まるマクロに注目。
115        実は、OSによってselectの実装が異なるために、fd_setという構造体の型もバラバラである。
116        そのためにFD_で始まるマクロを利用し、使い方を統一している。
117        以下のマクロはfdzにディスクリプターを設定する。
118        実際には複数のディスクリプターを設定できて、どのディスクリプターからの受信したのかが分かるようになる。
119        この際に設定できるディスクリプターのタイプだが、ソケット以外にもselect対応のデバイスは全て一緒に設定できる。
120        その使い方は次回のネットワークフレームワーク作る際に紹介する予定*/
121     FD_ZERO(&fdz); /* fdzの初期化 */
122     FD_SET(sock,&fdz); /* fdzにディスクリプターを設定します。複数ある場合は複数指定できます。 */
123
133     /* この場合は、ディスクリプターの受信待ちなので、第二引数にのみfdzを設定する */
134     int n=select(sock+1, &fdz, 0, 0, &tv);
135     /* 戻り値は、受信データのあるディスクリプターの数なので、戻り値がゼロならばタイムアウトということになる */
136     if (n==0)
137     {
138         /* タイムアウト */
139         fprintf(stderr,"Timout!!!!\n");
140         close(sock);
141         exit(0);
142     }
143     if (n<0)
144     {
145         /* エラー の場合は、ディスクリプターの不正とみなして良い。*/
146         close(sock);
147         exit(0);
148     }
           /* fdzにsockしか設定していないので、受信データのあるディスクリプターは1個しかない */
149     if (n==1)
150     {
151         /* こんな感じで、どのディスクリプターに受信されたかをチェックできる。
152            このプログラムでは、sockしかないので無意味ではあるが。。。 */
153         if (FD_ISSET(sock,&fdz))
154         {
155             char buffer[1024];
156             int n=recv(sock, buffer, 1024,0); /* データ受信 */
157             if (n<0)
158             {
159                  /* エラー */
160                 printf("read error!!! (%d)", errno);
161                  exit(0);
162             }
163             if (n==0)
164             {
165                  /* 接続が切れた! */
166                 printf("connect closed!!! (%d)", errno);
167                  exit(0);
168             }
169             printf("%s\n", buffer);
170          }
171     }

あとは、サーバの処理が多い場合、処理中の別の接続を受け付けることが出来ないので、別のスレッドで分岐するようにします。

サーバの分岐

acceptしたあと、サンプル1のようにクライアントの受信待ちになると次のacceptができない。selectを使って、listen用のディスクリプターとクライアント用のディスクリプターを両方設定すれば、接続の対応とクライアントの処理を両方行うことはできるが、クライアントの数が多かったり、処理そのものが多かったりすると接続処理に回るまで時間がかかる。なので、accept後はスレッドか子プロセスに分岐するのが一般的である。

カーネルからするとスレッドとプロセスはジョブとしては大した差はない。ただ、スレッドとプロセスの大きな違いはメモリ空間を共有するかであり、スレッドはメモリ空間を共有しているために、メモリ関係のエラーになった場合プロセスごと強制終了ーーカーネルからプロセスを削除ーーされる。ただ、子プロセスは親プロセスを複製しているので、子プロセスがメモリを壊しても親プロセスとは関係ないために子プロセスだけジョブから削除される。

故にサーバプログラミングは子プロセスを使うとサーバがダウンすることはかなり少なくなる。*1

ただし、forkは親プロセスのメモリ空間を複製するために生成にオーバーヘッドがかかるのとメモリの使用率が非常に高くなるので、サーバの目的によって、どのように分岐させるかを工夫すべきである。
サンプルでは、スレッドで分岐している。

 16 static void* peer_connected(void* param)
 17 {
 18     int sockclient=(int)(intptr_t)param;
 19
 20     char buffer[1024];
 21     if (recv(sockclient,buffer,sizeof(buffer),0)>0)
 22     {
 23         char send_buffer[64];
 24         sprintf(send_buffer, "received %s", buffer);
 25         send(sockclient, send_buffer, strlen(send_buffer)+1, 0);
 26         close(sockclient);
 27
 28         printf("SERVER: receved %s\n", buffer);
 29
 30         /* クライアントからのメッセージが"quit"ならば、終了 */
 31         if (strcmp(buffer, "quit")==0)
 32         {
 33             g_quit=1;
 34             break;
 35         }
 36     }
 37     return 0;
 38 }
                                                      :
                                                      :
117         int sock_client=accept(sock,(struct sockaddr*)&accept_addr, &accpet_addr_size);
118
119         if (sock_client==0||sock_client<0)
120         {
121             /* エラー */
122             exit(0);
123         }
               /* スレッド生成 */
124         pthread_t th;
134         if (pthread_create(&th,0,peer_connected,(void*)sock_client)!=0)
135         {
136             /* 各peerに関して、スレッド終了待ちをする必要がないので */
137             pthread_detach(th);
138         }

ここまでのサンプルは、以下のレポジトリーにあります。実行方法はサンプル1と同じです。
git@github.com:dongho-yoo/network_programming.git/chapter-2/samle2

説明したものをまとめてライブラリ化します。TCP/IPに特化したライブラリにします。
こうすることでネットワークプログラミングがしやすくなります。
ライブラリにまとめる際に少し細かい設定などを行なっていますので、ソースを参考にしてください。
サンプル2との違いは、connectの非同期処理でタイムアウトができるようにしたり、send時のSIGPIPE対応などが入っています。
実はソケットプログラミングはこのような細かな処理を行うのが大変かもしれませんが、ライブラリとしてまとめておけば、そこからは気にすることはありません。

wp_tcp_scok.h
/**                                                                                                                                                                                                                                      
* Copyright (c) 2017 WonderPlanet Inc. All Rights Reserved.
* Authorized by dongho.yoo.
*/

#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif

/** @brief ソケットのディスクリプターの型を定義 */
typedef int sock_t;
typedef int addr_ipv4_t;

/* 一度に読み込むバッファーのサイズ。システムによって最適な値がある。これは実証値を使うべきだが、一旦4KBytesで。 */
#define WRITE_ONCE_BYTES (4096)

/* 一度に読み込むバッファーのサイズ */
#define READ_ONCE_BYTES (4096)

/**
* @brief サーバへ接続します。
* @param ipaddress 文字列のIPアドレス
* @param port ポート番号
* @param timeout タイムアウト(ミリ秒)、timeoutがゼロの場合はブロック
* @return ソケットのディスクリプター
*/
extern sock_t wps_connect_s(const char* ipaddress, int port, int timeout);
/**
* @brief サーバへ接続します。
* @param ipaddress ビッグエンディアンの32ビットのipv4のipアドレス
* @param port ポート番号
* @param timeout タイムアウト(ミリ秒)、timeoutがゼロの場合はブロック
* @return ソケットのディスクリプター
* @retval -1 ソケット生成失敗
* @retval -2 タイムアウト
*/
extern sock_t wps_connect(addr_ipv4_t ipaddress, int port, int timeout);
/**
* @brief データを送信します。
* @param data 送信するデータ
* @param data_size 送信するデータサイズ
* @return 送信したデータのバイト数
*/
extern int wps_send(sock_t sock, const void* data, const size_t data_size);
/**
* @brief データを受信します。
* @param data 受信するバッファー
* @param data_size 受信するバッファーサイズ
* @param timeout ミリ秒、ゼロの場合はブロック
*/
extern int wps_recv(sock_t sock, void* buffer, const size_t buffer_size, int timeout);
/**
* @brief リッスン状態のソケットを生成します。
* @param ipaddress IPアドレス、NULLの場合はデフォルトのIPアドレスで取得。
* @param port 待ち受けるポート番号
* @param backlog TCPハンドジェーク終了時のクライアントをacceptまで貯められる数
* @param force 1の場合は、ポートがすでに使われていたり、解放されてない場合にも強制的にバインドする
* @return ソケットディスクリプター
*/
extern sock_t wps_listen(const char* ipaddress, int port, int backlog, int force);
/**
* @brief 接続を確立させて、クライアントとのディスクリプターを生成する
* @param sock_for_listen リッスン状態のディスクリプター
* @param ip_address 受け取るクライアントのipアドレス(NULLの場合は無視される)
* @param port 受け取るクライアントのport番号(NULLの場合は無視される)
*/
extern sock_t wps_accept(sock_t sock_for_listen, addr_ipv4_t* ip_address, int* port);
/**
* @brief ディスクリプターを削除します。
* @param sock ディスクリプター
*/
extern void wps_close(sock_t sock);
/**
* @brief ソケットの受信を待ちます。
* @param sock ディスクリプター
* @param timeout タイムアウト
* @return 0ならタイムアウト
*/
extern int wps_wait(sock_t sock,int timeout);
#ifdef __cplusplus
}
#endif
wp_tcp_scok.c
/**                                                                                                                                                                                                                                      
* Copyright (c) 2017 WonderPlanet Inc. All Rights Reserved.
* Authorized by dongho.yoo.
*/
#include "wp_sock.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <errno.h>

#define PRINT_ERROR(...) fprintf(stdout,__VA_ARGS__)

static sock_t tcp_sock()
{
    sock_t sock;
#ifndef MSG_NOSIGNAL
    /* BSD系はこれを指定しないと、write時にサーバ側からデータがくるとシグナルが発生して、
    プロセスが終了する。*/
    signal(SIGPIPE, SIG_IGN);
#endif

    if ((sock= socket(AF_INET, SOCK_STREAM, 0))<0)
    {
        PRINT_ERROR("socket create error !");
        return -1;
    }
    return sock;
}
/** ディスクリプターをノンブロックする関数*/
static void sock_non_block(sock_t sock, int nonblock)
{
    fcntl(sock, F_SETFL, nonblock?O_NONBLOCK:0);
}
static int is_valid_sock(int sock)
{
    int error=0;
    socklen_t len;

    if (sock==0)
    {
        return 0;
    }
    /* ソケットが無効化どうかをチェックする */
    if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len)!=0)
    {
        return errno;
    }

    if (error==0 ||
            error==EISCONN)
    {
        return 1;
    }

    PRINT_ERROR("socket invalid reason:%d", error);
    return 0;
}
static int is_ready_connect(int sock, int timeout)
{
    int res=0;
    fd_set fds;
    struct timeval tv={timeout/1000,(timeout%1000)*1000};

    FD_ZERO(&fds);
    FD_SET(sock, &fds);

    /* selectでディスクリプターが書き込み可能になるまで待つ。
       MAN CONNECT(2)を参照した。
    */
    res = select(sock+1, 0, &fds, 0, &tv);

    if (res<0)
    {
        PRINT_ERROR("select error errno:%d", errno);
        return res;
    }

    if (res==0)
    {
        return 0;
    }
    if (FD_ISSET(sock, &fds)!=0)
    {
        if (is_valid_sock(sock))
        {
            return 1;
        }
    }

    return 0;
}
/** connectのタイムアウト版 
    複雑に見えるが、実際にはTCPのHandshake終了待ちを手動で行っているだけ。
*/
sock_t wps_connect_s(const char* ipaddress, int port, int timeout)
{
    return wps_connect(inet_addr(ipaddress),port,timeout);
}
sock_t wps_connect(addr_ipv4_t ipaddress, int port, int timeout)
{
    int sock;
    struct sockaddr_in addr={0,};
    int val;
    int retryCnt=timeout/1000;

#ifndef MSG_NOSIGNAL
    /* BSD系はこれを指定しないと、write時にサーバ側からデータがくるとシグナルが発生して、
    プロセスが終了する。*/
    signal(SIGPIPE, SIG_IGN);
#endif

    if ((sock=tcp_sock())<0)
    {
        PRINT_ERROR("socket create error !");
        return -1;
    }

    if (timeout!=0)
    {
        val = 1;
        /*  タイムアウトにする場合には、一旦ソケットをノンブロック状態にする。*/
        sock_non_block(sock, 1);
   timeout=timeout/1000; /* 秒数に変える */
    }

    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=ipaddress;

__RETRY:
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr))!=0)
    {
        if (timeout==0)
        {
            PRINT_ERROR("connect error!\n");
            return -1;
        }

        if (errno==EAGAIN)
        {
            /* ノンブロック時に発生するエラーでこれは仕様なのでリトライ*/
            goto __RETRY;
        }
        if (errno==EINPROGRESS)
        {
            /* まだ書き込み可能な状態でない場合のエラー */
            int res;
__WAIT:
            /* この中で書き込み可能になるまで待つ。1秒タイムアウト*/
            res = is_ready_connect(sock, 1000);
            if (res<0)
            {
                PRINT_ERROR("connect error... (%d)", errno);
                return -1;
            }
            if (res==0)
            {
                if (retryCnt--!=0)
                {
                    PRINT_ERROR("timeout so... retry");
                    goto __WAIT;
                }
                // retryCnt is zero.
                PRINT_ERROR("connect timeout (%d)", errno);
                return 0;
            }

            /* ノンブロック解除 */
            sock_non_block(sock, 0);
            return sock;
        } /* if EINPROGRESS */
        return 0;
    } /* connect retry or failure. */

    return sock;
}
static int __send(sock_t sock, const void* data, const size_t data_size)
{
#ifdef MSG_NOSIGNAL
    /* 第4引数のMSG_NOSIGNALを指定しないと、こちらが書き込んでいる際にコネクションが切れたり、peer側で書き込みがあった際にシグナルのSIGPIPEが発生してプロセスが落ちてしまう */
    return send(sock,data, data_size,MSG_NOSIGNAL);
#else
    return send(sock,data, data_size,0);
#endif
}
int wps_send(sock_t sock,const void* data,const size_t data_size)
{
    size_t sum=data_size;
    char* p=(char*)data;
    /* WRITE_ONCE_BYTES ずつ書き込む */
    while(sum!=0)
    {
        const size_t size=(sum<WRITE_ONCE_BYTES)?sum:WRITE_ONCE_BYTES;
        int n =__send(sock, p, size);
        p+=n;
        sum-=n;
    }
    return data_size;
}
int wps_recv(sock_t sock, void* buffer, const size_t buffer_size, int timeout)
{
    int sum=buffer_size;
    char* p=(char*)buffer;
    if (timeout!=0)
    {
        fd_set fdz;
        struct timeval tv={timeout/1000,(timeout%1000)*1000};
        FD_ZERO(&fdz);
        FD_SET(sock,&fdz);
        int res = select(sock+1,&fdz,0,0,&tv);
        if (res<0)
        {
            /* エラー */
            return -2;
        }
        if (res==0)
        {
            /* タイムアウト */
            return -1;
        }
    }
    /* recvに関しては今の状態でREAD_ONCE_BYTESずつ読み込むのは不可能。
    その理由は、相手が送ったデータのサイズがわからないためである。
    なので、recvの方は、プロトコルの実装のところで、READ_ONCE_BYTESずつ読み込む処理を入れる。*/
    return recv(sock,buffer,buffer_size,0);
}
sock_t wps_listen(const char* ipaddress, int port, int backlog, int force)
{
    sock_t sock;
    struct sockaddr_in addr={0,};
    const size_t addr_size = sizeof(addr);

    if ((sock=tcp_sock())<0)
    {
        PRINT_ERROR("socket create error !");
        return -1;
    }

    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    addr.sin_addr.s_addr=ipaddress?inet_addr(ipaddress):htonl(INADDR_ANY);

    if (bind(sock,(struct sockaddr*)&addr, addr_size)==-1)
    {
        if ((errno==EADDRINUSE||errno==EACCES)&&force==1)
        {
            /* ポートが解放されなかったり、すでに使われている場合に、force==1の場合は強制的にバインドする */
            int on=1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
            if (bind(sock, (struct sockaddr*)&addr, addr_size)==-1)
            {
                /* これでもダメならお手上げ!*/
                PRINT_ERROR("bind error!!!(%d)\n",errno);
                return -1;
            }
        }
        else
        {
            PRINT_ERROR("bind error!!!(%d)\n",errno);
            close(sock);
            return -1;
        }
    }
    if (listen(sock, backlog)==-1)
    {
        /* エラー */
        PRINT_ERROR("listen error!!!(%d)\n",errno);
        close(sock);
        return -1;
    }

    return sock;
}
sock_t wps_accept(sock_t sock_for_listen,addr_ipv4_t* ip_address, int* port)
{
    struct sockaddr_in accept_addr={0,};
    socklen_t accpet_addr_size=sizeof(accept_addr);
    int sock_client=accept(sock_for_listen,(struct sockaddr*)&accept_addr, &accpet_addr_size);
    if (ip_address!=0)
    {
        *ip_address=accept_addr.sin_addr.s_addr;
    }
    if (port!=0)
    {
        *port=ntohs(accept_addr.sin_port);;
    }

    return sock_client;
}
void wps_close(sock_t sock)
{
    close(sock);
}
int wps_wait(sock_t sock,int timeout)
{
    int res=0;
    fd_set fdz;
    struct timeval tv={timeout/1000,(timeout%1000)*1000};

    FD_ZERO(&fdz);
    FD_SET(sock, &fdz);

    /* 汎用的に待つ処理をするので、読み込み時と書き込み時、両方待つようにする */
    return select(sock+1, &fdz, &fdz, 0, &tv);
}

上記のまとめたライブラリで、サンプル2も実装しています。ライブラリとサンプル2の実装は以下のレポジトリーから取得します。
git@github.com:dongho-yoo/network_programming.git/chapter-2/samle3

次回予告

次回は、今回まとめたライブラリを利用して、チャットサーバを作成します。
キーになるのは、構造体を送受信するので、プロトコルの設計やプロトコルのパックの仕方などの説明が入るのと複数が同時に接続をするので、複数のディスクリプターの場合の管理の仕方が焦点となります。
クライアント側は、フレームワーク化をする予定です。
機能はシンプルでも構造的には本格的なものを目指しています。

*1:Apacheもデフォルトではforkで子プロセスを生成している。tomcatのようにvm上で動いているものはスレッドでも全体が死ぬことはない。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-, ,

© WonderPlanet Inc.