目次
- はじめに
- システムコールの使用順序
- ソケットを作る(クライアント/サーバで使用):socket
- ソケットに名前を付ける(クライアント/サーバで使用):bind
- ソケットで接続の受け付けを開始する(サーバで使用):listen
- クライアントからの接続を受け付ける(サーバで使用):accept
- ソケットに接続する(クライアントで使用):connect
- ソケットからメッセージを受信する(クライアント/サーバで使用):recv
- ソケットにメッセージを送信する(クライアント/サーバで使用):send
- ソケットの入出力を停止する(クライアント/サーバで使用):shutdown
サーバ環境
製品名 | OpenBlockS 600/R |
CPU | 600MHz(AMCC PowerPC 405EX) |
メモリ | 1GB(DDR2 SDRAM) |
ストレージ | 8GB(Compact Flash) |
はじめに
C言語でネットワークプログラミングを学ぶことで、サーバの仕組みについての理解を深めたいと思います。
参考書は例解UNIXプログラミング教室とLinuxネットワークプログラミングバイブルです。
システムコールの使用順序
以下のソケットを操作する関数を、「サーバでのシステムコールの使用順序」の順番で呼び出すことでサーバとしての動作を、「クライアントでのシステムコールの使用順序」の順番で呼び出すことでクライアント動作を実現できます。
ソケットを操作する関数
ソケットを作る | socket |
ソケットに名前を付ける | bind |
接続を受け付ける | listen |
接続を受け入れる | accept |
接続を要求する | connect |
読む | recv |
書く | send |
ソケットの入出力を停止する | shutdown |
ソケットを閉じる | close |
サーバでのシステムコールの使用順序
socket→bind→listen→accept→recv/send→shutdown→close
クライアントでのシステムコールの使用順序
socket→connect→recv/send→shutdown→close
ソケットを作る(クライアント/サーバで使用):socket
SYNOPSIS #include <sys/socket.h> int socket(int domain, int type, int protocol);
返り値はソケット記述子で、エラーが起きると-1が返ります。
引数domainには以下の通信領域の中から指定します。
AF_INET | IPv4 |
AF_INET6 | IPv6 |
AF_UNIX | コンピュータ内のプロセス間通信用 |
引数typeには以下のソケットの型からどちらかを指定します。
SOCK_STREAM | TCP |
SOCK_DGRAM | UDP |
引数protocolには通信プロトコルを示すプロトコル番号を指定します。0を指定すればdomainとtypeで指定した通信領域とソケット型から決まる規定のプロトコルが決まるため、通常は0を指定します。
エラー処理も含めたソケットの作成例は以下のようになります。
int sd; if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); return (-1); }
※perrorはstderrに対してエラーメッセージ(str)とerrnoに対応するシステムエラーメッセージを出力します。
ソケットに名前を付ける(クライアント/サーバで使用):bind
サーバで使用する場合、サーバ側のアドレスやポートの設定になります。クライアントで使用する場合、クライアント側のアドレスやポートの設定になります。bindを使用しない場合、IPアドレスは接続に使用できるIPアドレス、ポート番号はテンポラリポートが自動的に割り当てられるため、一般的にクライアントプログラムではbindを使用しません。
SYNOPSIS #include <sys/socket.h> int bind(int sd, const struct sockaddr *name, socklen_t namelen);
成功すると返り値は0、失敗すると返り値は-1になります。
引数sdはソケット記述子、引数nameはsockaddr型の変数へのポインタ、namelenは名前のバイト数を指定します。
引数nameの型はTCPのIPv4を使う時はstruct sockaddr_in型として名前を作ります。IPv6の場合はstruct sockaddr_in6型になります。そして、bindに渡す際に、struct sockaddr *型にキャストします。
sockaddr_in型の定義は以下のようになっています(システムにより異なる)
struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };
上記のうち、struct in_addrは以下のようになっています(システムにより異なる)
struct in_addr { in_addr_t s_addr; }
sin_portは16ビット、s_addrは32ビットで、バイト順序はビッグデンディアンになります。このバイト順序をネットワークバイト順序(ネットワークバイトオーダー)といいます。これに対してコンピュータが使っているバイト順序をホストバイト順序(ホストバイトオーダー)といいます。
ソケットに名前を付けるコード例(サーバ用途を想定)は以下のようになります(エラー処理も含む)。
int sd; /* struct sockaddr_in型のsaを定義 */ struct sockaddr_in sa; /* saの先頭からsizeof(sa)分を0で埋める */ memset (&sa, 0, sizeof(sa)); /* 通常はAF_INETを指定 */ sa.sin_family = AF_INET; /* ホストバイト順序で表現された16ビット整数(80)をネットワークバイト順序に変換 */ sa.sin_port = htons(80); /* ホストバイト順序で表現された32ビット整数をネットワークバイト順序に変換 */ sa.sin_addr.s_addr = htonl(INADDR_ANY); /* sdはソケット記述子。&saをstruct sockaddr_inからsockaddrへのポインタ型に変換 */ if (bind(sd, (struct sockaddr *)&sa, sizeof(sa)) == -1) { perror("bind"); (void) close(sd); return (-1); }
INADDR_ANY(0.0.0.0)は自ホストが複数IPアドレスを持っている場合に、それらのどのアドレス宛でも接続を受け入れる設定です。
ポート番号の指定は、以下のように直接指定しています。
sa.sin_port = htons(80);
ポート番号については、数値で指定されている場合とサービス名で指定されている場合が想定されるため、処理を分岐するコードに修正します。
※IPアドレスについてもホスト名指定される可能性はありますが、通常はIPアドレスで指定すると考えられるため、サーバ用途としては考慮しないこととします(クライアント用途でホスト名指定する場合の例は本ページconnectのところで説明します)。
手順としては、isdigit()により数値かどうかを判断し、数値であればhtonsで処理を行います。数値で無い場合はサービス名と判断し、getservbyname()関数によりポート番号を取得します。
※getservbyname関数について
#include <netdb.h> struct servent *getservbyname(const char *name, const char *proto);
getservbyname()関数はproto(例えばtcp)を使用するサービス名nameにマッチする情報をサービス情報データから検索し、servent構造体を返します。
servent 構造体は
struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number */ char *s_proto; /* protocol to use */ }
s_portはネットワークバイトオーダーになっています。
上記を考慮した修正後のコードは以下のようになります。
/* 引数としてポート番号またはサービス名のどちらかで指定されると仮定 */ const char *portnm = argv[1]; struct sockaddr_in sa; struct in_addr addr; int sd, portno; struct servent *se; /* アドレス情報をゼロクリア */ memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; /* ホストバイト順序で表現された32ビット整数をネットワークバイト順序に変換 */ sa.sin_addr.s_addr = htonl(INADDR_ANY); /* ポート番号が数値で指定された場合 */ /* 指定されたポート番号の先頭が数値(文字列)かを判断 */ if (isdigit(portnm[0])) { /* 数値化すると0以下ならエラー */ if ((portno = atoi(portnm)) <= 0) { (void) fprintf(stderr, "bad port no\n"); return (-1); } /* ホストバイトオーダーをネットワークバイトオーダーに変換してsa.sin_portに代入 */ sa.sin_port = htons(portno); /* ポート番号がサービス名で指定された場合 */ } else { /* 存在しないサービス名 */ if ((se = getservbyname(portnm, "tcp")) == NULL) { (void) fprintf(stderr, "getservbyname():error\n"); return (-1); /* サービス名が存在した場合 */ } else { /* servent構造体のs_portはネットワークバイトオーダーのためそのまま代入可 */ sa.sin_port = se->s_port; } } /* ntohsでネットワークバイトオーダーをホストバイトオーダーに変換してそのままstderrに出力させる */ (void) fprintf(stderr, "port=%d\n", ntohs(sa.sin_port)); if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); return (-1); } /* sdはソケット記述子。&saをstruct sockaddr_inからsockaddrへのポインタ型に変換 */ if (bind(sd, (struct sockaddr *)&sa, sizeof(sa)) == -1) { perror("bind"); (void) close(sd); return (-1); }
ソケットで接続の受け付けを開始する(サーバで使用):listen
listen関数を使用すると、ソケットに接続要求を入れるための待ち行列を付けて、受付を始めます。
SYNOPSIS #include <sys/socket.h> int listen(int sd, int backlog);
引数sdはソケット記述子、backlogは待ち行列の長さです。実際にソケットに付けられる待ち行列の長さはシステムによって異なります。SOMAXCONNを指定すると、システムでの最大値を指定できます(※Linuxでは128に定義されています)
ソケットで接続の受け付けを開始するコード例は以下のようになります(エラー処理も含む)。
int sd; if (listen(sd, SOMAXCONN) == -1) { perror("listen"); (void) close(sd); return (-1); }
クライアントからの接続を受け付ける(サーバで使用):accept
SYNOPSIS #include <sys/socket.h> int accept(int sd, struct sockaddr *addr, socklen_t *addrlen);
引数sdはソケット記述子です。引数addrはクライアントのソケットの名前で、接続してきたクライアントのアドレスやポート番号を含むsockaddr型の変数へのポインタです。接続元がsockaddr_inかsockaddr_in6かによってサイズが異なる(IPv6のsockaddr_in6の方が大きい)ため、acceptで接続元のアドレス情報を取得する場合には、十分サイズの大きいsockaddr_strage型構造体を用意し、その変数をsockaddr型にキャストします。引数addrlenはaddrの長さを返却してもらうための値結果引数です。
accept関数はクライアントからの接続を受け入れると、その通信路に繋がった新しいソケットを一つ作り、返り値としてそのソケットの記述子を返します。エラーが起きた場合には-1を返します。
最初に引数sで指定したソケット記述子を3と仮定します。クライアントからの接続要求が来ると、クライアントとデータのやりとりをするために新たにソケット記述子4をつくり、返却します。元のソケット記述子3はaccept前と同じ状態で、そのソケットを再びacceptすれば新たなクライアントからの接続要求を受け付けられます。上記の例では次に5が返ります。
accept関数は1つも接続待ちが無い場合ブロックします。つまり、処理がacceptまでくるとプログラムが停止し、以降の処理を行いません。
ソケットをノンブロッキングにすると待たないようにもできますが、ループなどを用いているとCPUを消費してしまうため、他の処理と多重化したい場合は後述のselectやpollなどを利用してソケットの準備状態を見るようにします。
コード例を以下に載せます。
int ns, sd; struct sockaddr_strage ca; socklen_t calen; calen = (socklen_t) sizeof(ca); if ((ns = accept(sd, (struct sockaddr *)&ca, &calen)) == -1) { if (errno != EINTR) { perror("accept"); } }
※正常なacceptの呼び出しでエラーとなるケースとして、シグナルの割り込みによるEINTR(Interrupted system call)があり、その対策としてperrorの条件から除外します。
実際は無限ループの中でacceptを呼び出し、接続があったタイミングでrecv,send等、次の処理を行うように記述する必要がありますが、ここでは割愛します。
ソケットに接続する(クライアントで使用):connect
クライアントはソケットを作った後にconnect関数でサーバに接続要求を出すようにします。
SYNOPSIS #include <sys/socket.h> int connect(int sd, const struct sockaddr *name, socklen_t addrlen);
引数sdはsocketで作成したソケット記述子、引数nameは接続先サーバのアドレスやポート番号を含むsockaddr型の構造体へのポインタ、namelenは名前のバイト数です。成功すると0が、失敗すると-1が返ります。
acceptとは異なり、connectの場合には引数sdが示すソケット自体に通信路が繋がり、新しいソケットはできません。
単純なコード例を以下に載せます。
struct sockaddr_in sa; memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_port = htons(80); sa.sin_addr.s_addr = inet_addr("1.1.1.1"); connect(s, (struct sockaddr *)&sa, sizeof(sa));
inet_addr関数は点切り十進数記法で表現されたIPv4アドレスの文字列をネットワークバイト順序のin_addr_t型に変換するライブラリ関数です。上記のコードはIPアドレスとポート番号の指定方法を以下のように改善できます。
まずIPアドレスについて説明します。上記のコードでは以下の部分です。
sa.sin_addr.s_addr = inet_addr("1.1.1.1");
ここで使用しているinet_addr()は、入力が不正な場合、INADDR_NONE (普通は -1) を返します。 -1 は有効なアドレス (255.255.255.255) なので、この関数を使うと問題になるかもしれないため、この関数を使うのは避け、代わりにinet_ptonを使用します。
※inet_ptonについて
#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst);
文字列 src を、アドレスファミリー af(AF_INET か AF_INET6) のネットワークアドレス構造体に変換し、dst にコピーします。成功する (ネットワークアドレスが正常に変換される) と、inet_pton() は 1 を返します。 src が指定されたアドレスファミリーに対する正しいネットワークアドレス表記でない場合には 0 を返します。
afがAF_INETの場合、src はドット区切りの 10 進数形式 “ddd.ddd.ddd.ddd” の IPv4 ネットワークアドレス文字列へのポインタです。このアドレスは struct in_addr に変換されて dst にコピーされます。
ホスト指定がIPアドレスであった場合は上記のinet_ptonを使用すれば良いですが、IPアドレスとして解釈できない場合にはgethostbyname2(以下参照)を使用し、ホスト名からホスト情報をhostent型構造体で取得し、取得したホスト情報からアドレス情報(h_addr_list)を得ます。gethostbyname2は、与えられた名前からDNS問合せ、/etc/hosts、NISのいずれかからIPアドレスを調べます。
※gethostbyname2について
#include <netdb.h> #include <sys/socket.h> struct hostent *gethostbyname2(const char *name, int af);
与えられたホスト名nameに対応するアドレスファミリー af(AF_INET か AF_INET6) の構造体hostentを返します。hostent 構造体は
struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses */ } #define h_addr h_addr_list[0] /* for backward compatibility */
hostent 構造体のメンバは以下の通りです。
h_name | ホストの正式名 (official name)。 |
h_aliases | ホストの別名の配列。配列は NULL ポインタで終端される。 |
h_addrtype | アドレスのタイプ。現在はすべて AF_INET または AF_INET6 である。 |
h_length | バイト単位で表したアドレスの長さ。 |
h_addr_list | ホストのネットワークアドレスへのポインタの配列。配列は NULL ポインタで終端される。ネットワークアドレスはネットワークバイトオーダー形式である。 |
h_addr | h_addr_list の最初のアドレス。過去との互換性を保つためのものである。 |
次にポート番号について見ていきます。先ほどのコード例では以下のように直接指定していました。
sa.sin_port = htons(80);
ポート番号についてもIPアドレス同様、数値で指定されている場合とサービス名で指定されている場合が想定されるため処理を分岐していきます。
手順としては、isdigit()により数値かどうかを判断し、数値であればhtonsで処理を行います。数値で無い場合はサービス名と判断し、getservbyname()関数によりポート番号を取得します。
※getservbyname関数について
#include <netdb.h> struct servent *getservbyname(const char *name, const char *proto);
getservbyname()関数はprotoを使用するサービス名nameにマッチする情報をサービス情報データから検索し、servent構造体を返します。
servent 構造体は
struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number */ char *s_proto; /* protocol to use */ }
s_portはネットワークバイトオーダーになっています。
修正後のコードは以下のようになります。
/* 第一引数としてIPアドレスまたは名称のどちらかで指定されると仮定 */ const char *hostnm = argv[1]; /* 第二引数としてポート番号またはサービス名のどちらかで指定されると仮定 */ const char *portnm = argv[2]; struct sockaddr_in sa; struct in_addr addr; int sd, portno; struct hostent *host; struct servent *se; /* アドレス情報をゼロクリア */ memset(&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; /* ここからIPアドレスの処理 */ /* hostnmがddd.ddd.ddd.ddd表記でなく名称指定の場合 */ if (inet_pton(AF_INET, hostnm, &addr) == 0) { /* ホスト名が名称としてホスト情報取得 */ if ((host = gethostbyname2(hostnm, AF_INET)) == NULL) { (void) fprintf(stderr, "gethostbyname2():error\n"); return (-1); } /* hostent型構造体のhostのメンバーh_addr_listをキャストしてstruct in_addr型のaddrにコピー */ (void) memcpy(&addr, (struct in_addr *) *host->h_addr_list, sizeof(struct in_addr)); } /* struct in_addr型のaddrをドット区切りの 10 進数形式 "ddd.ddd.ddd.ddd" の IPv4 ネットワークアドレス(文字列)に変換。 返り値はbufへのポインタなのでIPアドレスがstderrに出力される */ (void) fprintf(stderr, "addr=%s\n", inet_ntop(AF_INET, &addr, buf, sizeof(buf))); sa.sin_addr = addr; /* ここからポート番号の処理 */ /* ポート番号先頭が数値 */ if (isdigit(portnm[0])) { /* 数値化すると0以下ならエラー */ if ((portno = atoi(portnm)) <= 0) { (void) fprintf(stderr, "bad port no\n"); return (-1); } /* ネットワークバイトオーダーに変換してsa.sin_portに代入 */ sa.sin_port = htons(portno); /* ポート番号がサービス名で指定された */ } else { /* 存在しないサービス名 */ if ((se = getservbyname(portnm, "tcp")) == NULL) { (void) fprintf(stderr, "getservbyname():error\n"); return (-1); /* 存在した場合 */ } else { /* servent構造体のs_portはネットワークバイトオーダーのためそのまま代入可 */ sa.sin_port = se->s_port; } } /* (確認用)ntohsでネットワークバイトオーダーをホストバイトオーダーに変換してそのままstderrに出力させる */ (void) fprintf(stderr, "port=%d\n", ntohs(sa.sin_port)); if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); return (-1); } if ((connect(sd, (struct sockaddr *) &sa, sizeof(sa)) == -1) { perror("connect"); (void) close(sd); return (-1); }
ソケットからメッセージを受信する(クライアント/サーバで使用):recv
SYNOPSIS #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags);
引数sockfdはソケット、bufは受信メッセージを入れる受信バッファへのポインタ、lenはbufのサイズです。
flagsはいくつか種類があり、flagsをMSG_DONTWAITに指定することで、1回のrecv()の単位でだけノンブロッキングモードを選択できます。その他のflagsにはMSG_OOB,MSG_PEEK,MSG_WAITALL等があります(詳細は割愛)。通常0を指定します。
返り値は実際に受信したサイズ(必ずしも1回で全て受信できるわけではなく、何回か呼ぶこともあります)。エラーの場合は-1を返します。
recvはソケットに受け取るメッセージが存在しなかった場合、受信用のコールはメッセージが到着するまで待ちます。そして、1バイト以上受信できた場合に戻ります(デフォルトのブロッキングモードの場合)。
ソケットにメッセージを送信する(クライアント/サーバで使用):send
SYNOPSIS #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);
引数sockfdはソケット、bufは送信メッセージの入ったバッファ、lenはbufのサイズです。
flagsはいくつか種類があり、flagsをMSG_DONTWAITに指定することで、1回のrecv()の単位でだけノンブロッキングモードを選択できます。その他のflagsにはMSG_DONTROUTE,MSG_OOB等があります(詳細は割愛)。通常0を指定します。
send()は指定したサイズを送信し終えてから戻ります(ブロッキングモードの場合)。返り値は実際に送信したサイズです(正確にはOSの送信バッファに書き込んだサイズ)。エラーの場合は-1を返します。
受信したメッセージをクライアントに送信し返すだけのコード例を載せます。
int sd; char buf[512], *ptr; ssize_t len; for (;;) { if ((len = recv(sd, buf, sizeof(buf), 0)) == -1) { perror("recv"); break; } if (len == 0) { (void) fprintf(stderr, "recv:EOF\n"); break; } buf[len] = '\0'; if ((ptr = strpbrk(buf, "\r\n")) != NULL) { *ptr = '\0'; } (void) fprintf(stderr, "%s\n", buf); len = (ssize_t) strlen(buf); if ((len = send(sd, buf, (size_t)len, 0)) == -1) { perror("send"); break; } }
ソケットの入出力を停止する(クライアント/サーバで使用):shutdown
SYNOPSIS #include <sys/socket.h> int shutdown(int sd, int how);
引数sdにソケット記述子を指定します。引数howには以下のいずれかの定数を指定します。
SHUT_RD | 今後はこのソケットでデータを受信しない。 |
SHUT_WR | 今後はこのソケットでデータを送信しない。通信相手にはEOFが送られる。 |
SHUT_RDWR | 今後はこのソケットでデータを送受信しない。 |
成功すると0が、失敗すると-1が返ります。