はじめに
前回の記事の続きです。
前回は肝心の通信機能ではなく、その前のXcodeをどう使うかという前段階の前段階のお話で終わってしまいました。
今回はちゃんと通信のお話です。
前回紹介した書籍や調べたwebサイトの情報を元にとりあえず動作確認という感じのコードを書いてみます。
サーバー
サーバー側の手順は大まかにいうと
・ソケットの作成(socket関数)
↓
・ポートの割り当て(bind関数)
↓
・通信の受け入れ開始(listen関数)
↓
・接続の確立(accept関数)
↓
・データの送受信(recv関数、send関数)
↓
・ソケットを閉じる(close関数)
というような流れになります。
実際に書いたコードはこんな感じ
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFSIZE 32768
#define PORT 3456
int main(int argc, const char * argv[]) {
int s0;
int s;
struct sockaddr_in server;
struct sockaddr_in client;
unsigned int len;
long rn;
char recv_buf[BUFSIZE];
//ソケットの作成
if ((s0 = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
//ポートの割り当て
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(PORT);
if (bind(s0, (struct sockaddr *) &server, sizeof server) < 0) {
perror("bind");
exit(EXIT_FAILURE);
}
//接続キューの割り当て。通信の受け入れ開始
if (listen(s0, 5) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
//接続の確立
len = sizeof client;
if ((s = accept(s0, (struct sockaddr *) &client, &len)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
//データの取得
if ((rn = recv(s, recv_buf, BUFSIZE - 1, 0)) >= 0) {
recv_buf[rn] = '\0';
printf("receive from '%s'\n", inet_ntoa(client.sin_addr));
printf("receive data '%s'\n", recv_buf);
}
close(s);
close(s0);
return 0;
}
コマンドラインの解析も面倒なので受け入れポートは固定、一応各関数はif文で覆ってどんなエラーが発生するかを確認できるようにした最低限のコードです。
参考にしたのは、
書籍:基礎からわかるTCP/IPネットワ-ク実験プログラミング: Linux/FreeBSD対応
等の書籍、サイトです。ありがとうございます。
socket() 関数
socketは新規に通信用のソケットを作り、それを識別するID番号(ディスクリプタ)を返す関数です。
どんなソケットを作るかを3つの引数で決定します。
TCP/IPなら、第1引数は AF_INET 、第2引数は SOCK_STREAM 、第3引数は 0 です。
ちなみに第1引数のAF_はアドレス・ファミリーの頭文字でWin32で使われます。同じ定数でもBSD系の場合はプロトコル・ファミリーなので、PF_で始まります。
4Dのプラグイン用ですので、Win用とMac用の2つのプラグインが必要なので、ここはソースの移植性重視でAF_の方を採用します。
作成に失敗した時は-1を返すのでif文でそれをトラップしてエラー処理をします。他の関数も戻り値が-1だとエラーなので同じ構文でエラー処理をしています。
この関数の詳細は以下のサイトで
socket - システムコールの説明
socket 関数 (winsock2.h)
ちょくと:socket
bind() 関数
bind関数はsocket関数で作ったソケットにIPアドレスとポートを結びつける関数です。
第1引数には、先ほど作成したソケットのディスクリプタ
第2引数はIPアドレスとポート情報を設定したsockaddr_in
型の構造体。渡す時はsockaddr
型ポインタにキャストして参照渡しします。
第3引数はsockaddr_in
構造体のサイズです。
この関数の詳細は以下のサイト
bind - システムコールの説明
bind 関数 (winsock.h)
ちょくと:bind
sockaddr_in構造体
通信に使うアドレスやポートを指定するデータを納めた構造体です。プロトコルに応じて中の構造が変化する構造体ですのでCでは共用体とマクロ定義で宣言して使いやすい形になってます。
sockaddr (元の型)
sockaddr_in (IPv4)
sockaddr_in6 (IPv6)
今回はIPv4ですので、sockaddr_in
構造体を使います。
sin_familyフィールド にはselect()
関数のところでも出てきたアドレスファミリーを指定します。TCP/IPなので、 AF_INET を指定します。
sin_addr.s_addrフィールド にはIPアドレス。32bit(4byte)の数値をいれます。ここの入れる数値はネットワークバイトオーダー(ビッグエンディアン)になっている必要があるので、 htonl()関数 でホストバイトオーダーからネットワークバイトオーダーに変換します。この関数を噛ませておけば、ホストコンピュータのバイトオーダーを気にすることがなくなりますので移植性が向上します。
bind()
関数でのこのフィールドで指定するIPアドレスは、通信を受け入れるホストコンピュータのIPアドレスです。複数のNICがある時に特定のNICからの通信だけを受け入れる場合は、このフィールドでIPアドレスを指定します。
受け入れIPアドレスを指定しない場合は、 INADDR_ANY とします。
sin_portフィールド には通信を受け入れるポート番号を指定します。このフィールドもネットワークバイトオーダーですので、 htons()
関数でホストバイトオーダーからネットワークバイトオーダーに変換します。
listen()関数
ディスクリプタで指定したソケットへの接続の受け入れを開始します。
第1引数にはsocket()関数で取得したディスクリプタ
第2引数には接続要求を待機させるためのキューのサイズを渡します。キューを最大にするには SOMAXCONN定数 をいれます。
今回のテストプログラムではキューの挙動の検証のため 5 に設定してます。
listen()
を実行前は、クライアントからの接続要求はconnect()
関数実行時に Connection refused エラーを出して失敗します。
listen()
関数は接続待機のためのキューを設定し、接続を待つことなく即座に戻りますが、ホストに対する接続は受け入れ状態になりクライアントからの接続が可能になります。
キューがいっぱいになるまで、クライアントからは、接続が確立しsend()
関数なども正常にデータを送信したように見えます。
キューがいっぱいになると、クライアントはキューが空くまでconnect()
関数が実行をブロックします。キューが空くとブロックが解除されて処理が再開します。(実際の実行結果では)
キューがいっぱいなのかどうか、キューに待機中の接続要求が存在するのかなどの情報を取得する方法はありません。
listen - システムコールの説明
listen 関数 (winsock2.h)
ちょくと:liten
accept()関数
第1引数のディスクリプタで指定されたソケットの接続待ちキューの中から最初の待機中の接続を取り出して接続を確立し、新しいソケットを作ってそれを返す関数です。
戻り値は、この通信のための新しいソケットのディスクリプタです。socket()
関数で得たディスクリプタとは違う独立したソケットのディスクリプタなので、socket()
関数でのソケットを閉じても、このディスクリプタ(ソケット)は有効で接続を維持します。
新しいソケットのためのポートはbind()
で割当てたポートではなく、空いている別のポートが割り当てられます。そのため、bind()
で割当てたポートはそのまま接続待ち状態を維持します。
第2引数は、接続してきたクライアントの情報(IPアドレス、ポート番号)を戻すためのバッファへのポインタを渡します。バッファの構造はbind()の時に解説したsockaddr
構造体です。
第3引数は、このバッファのサイズなのですが、bind()
とは違い、実際に接続してきたクライアントの状況によって実際のサイズが変わるため、参照渡しで設定します。正常に完了するとここに実際のサイズが戻ってきます。呼び出し時には、用意したバッファのサイズ(sockaddr_in
のサイズ)を入力しておきます。
デフォルトでは、この関数は ブロッキングモード で動作するので、キューの中に待機している接続がない場合、クライアントが接続要求を出すまでプログラムの実行をブロックし続けます。
accept - システムコールの説明
accept 関数 (winsock2.h)
ちょくと:accept
recv()関数
ソケットを通してデータを受信する関数です。
第1引数は受信するソケット(今回ならaccept()
で得たソケット)のディスクリプタ
第2引数は受信するバッファのポインタ
第3引数はそのバッファのサイズ(1回の呼び出しで受信する最大バイト数)
第4引数はフラグでrecv()関数の動作を修飾します。単純に受信するだけなら 0 を設定。
戻り値は実際に受信したデータのバイト数です。
recv()
関数はデフォルトでブロッキングモードで動作します。接続が維持されていてクライアントからデータが届かない限りプログラムの実行をブロックし続けます。
データが届くと戻り、実際にバッファに読み込んだバイト数を戻り値に戻します。
1回の実行で届いたデータをすべて読み込む保証はありません。必要なら数回実行しデータを読み込みます。
クライアントがclose()
等を実行して接続が切れると、戻り値が 0 で即座に戻ります。これを利用してデータの読み込み完了を判断しもよいのですが、通常はHTTPのようにプロトコルで通信の完了等を判断するようにします。
続く処理は、バッファの最後に\0
を入れてC文字列にして、ターミナルに出力します。 ついでに、accept()
関数で戻ってきたsockaddr_in
構造体からリモートホストのIPアドレスを取得して出力してます。
recv - システムコールの説明
recv 関数 (winsock2.h)
ちょくと:recv
close()関数
通信を切断しソケットを閉じ、関連するリソースを解放する関数です。
第1引数は閉じるソケットのディスクリプタです。
socket()
関数で作ったソケットとaccept()
関数で得たソケットは別物ですのでそれぞれcloseが必要です。
Winの場合、closesocket()
関数を使うようですが、詳細はまだ調べてません。移植の際には要確認です。
ちょくと:closesocket
closesocket 関数 (winsock2.h)