サンプルで覚えるNetwork Programing Echoサーバ(その1)
C/C++でコーディングをすることはあるけど、ネットワークプログラムを書いたことがない人を対象とした備忘録です。 簡単なサンプルを基にできるだけ詳しく説明するようにつとめます。 ただ、コードは言葉で説明するより、ソースコードを実際に読み、コンパイルして実行するのが一番理解が早いと思います。本を読むだけでは、なかなか使えるレベルまで行くのは難しいかもしれません。やはり実際に手を動かすのが一番。 急がば回れ! ここでは、以下の環境での使用を前提にしています。
- CentOS6.8 x86_64
それでは、シンプルなEchoサーバを作ってみます。 サーバプログラムの主な流れは、以下の様になるかと思います。
- ソケットの作成(ソケットオプション設定を含む) socket()を呼び出してソケットディスクリプタを取得、以降ではこのディスクリプタによりソケット経由で通信する。
- バインド ソケットにIPアドレスを紐付ける。
- リッスン クライアントからの接続数を制御するためのキューを指定し、待ち受け状態にする。
- アクセプト リッスンされているソケットから接続の受付を行う。
ソケットの作成から、リッスンするところまでのプログラムは、おおよそ以下server_socket関数内の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
int server_socket( int port_num ) { int soc, opt; socklen_t opt_len; struct sockaddr_in server_addr; /* ソケットの作成 */ if ( ( soc = socket( AF_INET, SOCK_STREAM, 0 ) ) == -1 ) { perror( "socket" ); return -1; } /* ソケットオプション(再利用フラグ)設定 */ opt = 1; opt_len = sizeof( opt ); if ( setsockopt( soc, SOL_SOCKET, SO_REUSEADDR, &opt, opt_len ) == -1 ) { perror( "setsockopt" ); close( soc ); return -1; } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(port_num); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* バインド(ソケットにアドレスを指定) */ if ( bind( soc, (struct sockaddr *)&server_addr, sizeof(server_addr) ) == -1 ) { perror( "bind" ); close( soc ); return -1; } /* 最大アクセスバックログの指定 (128?)*/ if ( listen( soc, SOMAXCONN ) == -1 ) { perror( "listen" ); close( soc ); return -1; } return soc; } |
- socket関数を呼びます。(7行目) 第一引数のAF_INETは、IPv4インターネットのソケットであることを指定している。 それ以外のApple Talkや、IPXを指定することもできますが、通常のプログラムではまずAF_INET以外はないと思います。 第二引数は、通信方式を指定します。TCPプロトコルで通信する場合は、SOCK_STREAM、UDPの場合は、 SOCK_DGRAMです。特別なプログラムをしない限り、この2つを知っていればOKです。 第三引数は、プロトコルを指定しますが、与えられた一つのプロトコルのみの機能で良ければ通常0にします。 するとOSが適切なプロトコルを選択します。
- setsockopt関数を呼んで、ソケットのオプション設定を行います。 setsockoptは、必須では無く必要に応じてオプションの設定を行うことになります。 (15行目) ここでは、ソケットの再利用を SO_REUSEADDR で設定しています。
- struct sockaddr_in 型の変数を初期化し、AF_INET(IPv4インターネットプロトコル)、ポート番号、INADDR_ANY(全てのローカルインターフェイス)を指定します。
- bind関数を呼んでソケットに3で指定したIPアドレス等を紐付けます。
- listenを呼び出して、1で作成したsocketを待ち受け状態にします。 SOMAXCONNは、接続待ちのキューの数で、今時のlinuxですと128になっているようです。 もし何か特別な理由が有り値を変更したい場合は、第二引数に数値を記述します。
リッスンしているソケットから接続を受け付けるのは通常accept関数で行います。acceptを無限ループで処理する関数のプログラムを以下に示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
void accept_loop( int soc ) { char h_buf[NI_MAXHOST]; //IP address char s_buf[NI_MAXSERV]; //Port number struct sockaddr from; int acc; socklen_t len; for ( ;; ) { len = (socklen_t)sizeof( from ); if ( ( acc = accept( soc, &from, &len ) ) == -1 ) { if ( errno != EINTR ) { perror( "accept" ); } } else { getnameinfo( &from, len, h_buf, sizeof( h_buf ), s_buf, sizeof( s_buf ), NI_NUMERICHOST | NI_NUMERICSERV ); fprintf( stdout, "accept client IP:%s Port:%s\n", h_buf, s_buf ); /* 送受信ループ */ send_receive( acc ); close( acc ); } } } |
accept関数は、クライアントからの接続要求があると接続を受け付けます。接続要求が無い場合は、その先の処理は行われず、この状態でブロックされます。 接続に成功するとそのソケットのディスクリプター(非負の整数値)を返します。 また、エラーの場合は-1が返されます。 このサンプルコードでは接続成功後、getnameinfo関数を利用してクライアントのIPアドレスとポート番号を取得し標準出力しています。 受信したなんらかしらのデータは、次に示すsend_receive関数で行っています。 ここでの処理は、並列化を行っていないので1つのクライアントが接続中の場合は、他のクライアントは処理されません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
void send_receive( int acc ) { char buf[512], *ptr; ssize_t len; for ( ;; ) { /* 受信 */ if ( ( len = recv( acc, buf, sizeof( buf ), 0 ) ) == -1 ) { perror( "recv" ); break; } if ( len == 0 ) { /* Receive EOF */ fprintf( stdout, "recveive EOF\n" ); break; } /* 改行してしまうので */ buf[len] = '\0'; if ( ( ptr = strpbrk( buf, "\r\n" ) ) != NULL ) { *ptr = '\0'; } fprintf( stdout, "Received from client: %s\n", buf ); /* クライアントから受信した文字列にOKを追加 */ my_strconcat( buf, ":OK\r\n", sizeof( buf ) ); len = (ssize_t)strlen( buf ); /* 応答 */ if ( ( len = send( acc, buf, (size_t)len, 0 ) ) == -1 ) { perror( "send" ); break; } } } void my_strconcat( char *dst, const char *src, size_t dst_size ) { int j = 0; for ( ; j < dst_size; j++ ) { if ( dst[j] == '\0' ) break; } int i = 0; for ( ; i < (dst_size - j); i++ ) { if ( src[i] == '\0' ) { dst[i + j] = '\0'; return; } dst[i + j] = src[i]; } dst[i + j] = '\0'; } |
ここで作成したsend_receive関数は、accept 関数が返したディスクリプターを引数に取って、recvとsend関数の第一引数に渡します。 この関数内の無限ループ(for)は、サーバもしくはクライアントが接続を切るまで続きます。 recv関数でクライアントからのデータを受信しbufに保存します。 bufに改行があればそれをnull文字で置き換え、標準出力します。 また、my_strconcat関数(文字列を連結する関数)で”:OK\r\n” を連結しsend関数でクライアントに送り返します。
あとは、main関数をちょこっと作成しサーバとして起動するだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int main( int argc, char *argv[] ) { int soc; int port_num; char *endptr; /* 引数にポート番号を指定する */ if ( argc <= 1 ) { fprintf( stdout, "server port is not set.\n" ); return EX_USAGE; } port_num = strtol(argv[1], &endptr, 10); if ( ( soc = server_socket( port_num ) ) == -1 ) { fprintf( stdout, "Invalid server socket (%s)\n", argv[1]); return EX_UNAVAILABLE; } accept_loop(soc); close(soc); return EX_OK; } |
クライアントプログラムについては、また次回サンプルを載せます。 今回のソースファイルは、ここからダウンロードできますので、是非コンパイルして実行してみて下さい。 以下実行してみた例。
サーバ側
accept client IP:192.168.1.2 Port:58202
Received from client: hoge
クライアント側(telnetを利用)
Trying 192.168.1.1…
Connected to 192.168.1.1.
Escape character is ‘^]’.
hoge
hoge:OK
この記事へのコメントはこちら
コメントを投稿するにはログインしてください。