Edited at

C言語で学ぶモダンなソケットAPI入門

More than 1 year has passed since last update.

C言語で学ぶソケットAPI入門 第1回 サーバ編から始まった時は、とりあえずIPv4限定のレガシーなAPIからはじめて、徐々に発展させていこうという目論見でしたが、今後は何をするにしてもモダンなAPIを使ってソケットプログラミングの話を進めたいところです。

そこで今回はクライアントから受け取った文字列を返すエコーサーバプログラムを、モダンなAPIを使って書き直すことを通じてその詳細に迫ります。ソケットプログラミングにおいてモダンなAPIがレガシーな例と比べて優れているのは、処理速度的な点ではなく、現在も未来も含めた様々なケースに柔軟に対応できるという点です。

レガシーな例ではIPv4しか対応できないソケットを作成することしかできませんでしたが、モダンなAPIを使った例では、IPv4とIPv6両方のソケットを用意して届いたデータパケットによって処理を分岐するという、柔軟性のある機能を実装することができます。

更に言うと、IPv4やIPv6という二択限定ではなく、特定のアドレスファミリに依存しないプログラムを作成することができます。モダンなAPIはそのことを意識した実装になっています。

とりわけこれからiOSエンジニアを目指す方におかれましては、既にAppleがIPv4に依存するコードを禁止しているという実情もあるので、IPv4が今も普通に普及している世の中といえども習得している必要がある考え方やスキルです。

と、大口を叩いてみたものの説明が煩雑になるので、今回はIPv4限定にしてしまいますが、IPv6のパケットに対応したプログラムもちょっとした変更で可能です。それは後日、その処理にフォーカスしたものをちゃんと書きます。

#include <sys/socket.h> //socket(), bind(), accept(), listen()

#include <stdlib.h> // exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <netdb.h> // getaddrinfo, getnameinfo, gai_strerror, NI_MAXHOST NI_MAXSERV
#include <string.h> //memset()
#include <unistd.h> //close()
#include <stdio.h> // IO Library

#define MAX_BUF_SIZE 1024

int get_socket(const char*);
void do_service(int);
static inline void do_concrete_service(int);
void echo_back(int);
static inline void usage(int);

int main (int argc, char *argv[]) {

int sock;

if (argc != 2) {
usage(EXIT_FAILURE);
}

if ((sock = get_socket(argv[1])) == -1){
fprintf(stderr, "get_socket() failure.\n");
exit(EXIT_FAILURE);
}

do_service(sock);

close(sock);

return EXIT_SUCCESS;
}

main関数は上記のようになってます。

引数としてポート番号かサービス名を指定するような仕様にしていますが、それが指定されていない場合や余計なものを引数としている場合は、下記のusageという関数を実行して終了させてしまいます。

static inline void usage (int status){

fputs ("\
argument count mismatch error.
\n\
please input a service name or port number.
\n\
"
, stderr);
exit (status);
}

引数が正当だと認められたら、get_socketという今回定義した関数でソケットを作り、制御情報を参照する為のソケットディスクリプタを取得します。

ソケットディスクリプタはファイルディスクリプタとまったく同一のものです。それについてはLinuxのファイルディスクリプタをハックするにて詳細に書きましたので、興味のある方はそちらをご参照下さい。

ソケットディスクリプタを取得した後、サーバとして実際にどういったサービスを提供するかは、今回定義したdo_service関数内にまとめてあります。

最後にソケットをcloseしていますが、実はdo_serviceは無限ループになっていますので、今回のプログラムは、ここに到達しません。

こういったプログラムをデーモン化する場合は、終了時に特定のシグナル受信時の動作を調整するなどして、適切なタイミングでcloseを呼び出せるよう実装します。デーモンプロセスの実装方法についても、別途とり上げます。

一連の流れがわかったところで、get_socket関数の処理をみていきます。この関数では、ソケットを作成してソケットディスクリプタを返すところまでを担っています。

int get_socket (const char *port) {

struct addrinfo hints, *res;
int ecode, sock;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

memset(&hints, 0, sizeof(hints));

hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;

if ((ecode = getaddrinfo(NULL, port, &hints, &res) != 0)) {
fprintf(stderr, "failed getaddrinfo() %s\n", gai_strerror(ecode));
goto failure_1;
}

if ((ecode = getnameinfo(res->ai_addr, res->ai_addrlen, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV)) != 0){
fprintf(stderr, "failed getnameinfo() %s\n", gai_strerror(ecode));
goto failure_2;

}

fprintf(stdout, "port is %s\n", sbuf);
fprintf(stdout, "host is %s\n", hbuf);

if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
perror("socket() failed.");
goto failure_2;
}

if (bind(sock, res->ai_addr, res->ai_addrlen) < 0) {
perror("bind() failed.");
goto failure_3;
}

if (listen(sock, SOMAXCONN) < 0) {
perror("listen() failed.");
goto failure_3;
}

return sock;

failure_3:
close(sock);
failure_2:
freeaddrinfo(res);
failure_1:
return -1;
}

IPv4限定のレガシーな例とは少し違っています。レガシーな例では、sockaddr_inアドレス構造体を定義して、適切な値を指定してあげる必要がありました。

モダンな方法では、匕ントとなる情報だけを格納したaddrinfo構造体をgetaddrinfo関数に渡し、ヒントをもとに不足している情報を全て格納してもらうという手法をとります。

レガシーな例からなんとなくその雰囲気を感じられたと思いますが、ソケットAPIで指定する各種データは関連性があり、あるデータが指定されると他に必要なデータが自動的に決まってくるという構造になっているので、その特徴を活かしたアルゴリズムになっています。

addrinfo構造体は、ソケットAPIがアドレス情報を参照する際に必要な情報が格納された構造体で、レガシーな例では直接利用したsockaddr構造体も含まれています。

/* Structure to contain information about address of a service provider.  */

struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
socklen_t ai_addrlen; /* Length of socket address. */
struct sockaddr *ai_addr; /* Socket address for socket. */
char *ai_canonname; /* Canonical name for service location. */
struct addrinfo *ai_next; /* Pointer to next in list. */
};

ai_nextというaddrinfoへのポインタのメンバが、今後大事な役目を果たします。構造体のメンバー内に同構造体へのポインタが含まれている場合は、大抵連結リストのデータ構造になっています。

このケースもまさにそうなのですが、勘の良い人はこの事実がアドレスファミリに依存しないプログラミングに関係することにピンと来ているかもしれません。

さて、このaddrinfo構造体のメンバの情報を、bindやsocketといったソケットAPIに引数として渡せば良いので、シンプルかつ柔軟な実装が可能になります。

getaddrinfo関数では第1引数に、ホスト名かIPアドレスの文字列へのポインタを指定します。今回はNULLを指定しますが、その理由は後述します。クライアントプログラムを作成する場合は、"tajima-taso.jp"のようなドメイン名を参照する文字列オブジェクトへのポインタを指定しても良いです。

第2引数にはポート番号かサービス名の文字列へのポインタを指定します。サービス名はLinuxの場合、NISサーバを経由するかローカルホストの/etc/servicesファイルから参照されます。

私の環境では8080番のポート番号が開いているので、./a.out 8080と指定してもいいのですが、/etc/servicesを確認すると8080はwebcacheがサービス名としてマッピングされているので、./a.out webcacheと実行しても同様の指定になります。

第3引数にAI_PASSIVEが指定してある場合、第1引数のNULL指定は、レガシーな例と同じで全てのNICからの接続を受けられるINADDR_ANY(IPv6の場合はIN6ADDR_ANY_INIT)に相当します。AI_PASSIVEについては後述します。

また、glibc限定かもしれませんが、文字列"*"を渡してもNULLと判断するような実装がされているので、"*"と指定しても同様に動作します。ソースコード上はそう実装されているもののドキュメントレベルでは見つけられなかったので、汎用性はないかもしれません。

AF_INETやSOCK_STREAMという指定はレガシーな例と同じです。それぞれIPv4のアドレス体系、ストリーム型のプロトコル = TCPを意味します。

AI_PASSIVEという指定は第1引数のNULLと組み合わせて使うケースが多いと思います。これは何かというと、完成させてもらうaddrinfo構造体をbindで利用する場合に指定します。

bindはソケットを明示的なアドレス情報で紐付ける時に利用するシステムコールですので、AI_PASSIVEはgetaddrinfo関数に対して、サーバ用のaddrinfo構造体を完成させてほしいという依頼をするためのフラグとして作用します。こういった接続を待ち受けるソケットのことをpassive socketと言います。

第4引数にはaddrinfo構造体のポインタへのポインタを渡します。ここから参照外しをして完成されたaddrinfo構造体へのポインタを参照することができます。

なお、addrinfo構造体のメモリ領域の確保は内部で動的メモリ確保のライブラリを利用しているため、不必要となった時は必ずfreeaddrinfo関数によってその領域を解放してやらないとメモリリークが発生してしまいます。

エラーがあった場合は、その返り値となる整数をgai_strerror関数に渡せば、対応するエラー内容を格納した文字列へのポインタを返すので、その内容を標準エラー出力へ出力するようにしています。

続いてgetnameinfo関数を呼び出していますが、これはソケット通信の処理自体には不要です。

アドレス構造体からホスト名(IPアドレス)と、サービス名(ポート番号)を参照する場合に利用する関数です。

第1引数にソケットアドレス構造体へのポインタ、第2引数にそのオブジェクトのサイズ、第3引数に、ホスト名が格納されるべきバッファへのポインタ、第4引数にそのサイズ、第5引数にサービス名が格納されるバッファへのポインタ、第6引数にそのサイズ、第7引数に各種フラグを指定します。

hbufのサイズはNI_MAXHOST、sbufのサイズはNI_MAXSERVとして領域を確保していますが、それぞれgetnameinfo関数で取得するホスト名文字列の最大長、getnameinfoで用いるサービス名文字列の最大長を想定して、netdb.hにて定義されています。

私の環境では


netdb.h

#  define NI_MAXHOST      1025

# define NI_MAXSERV 32

となっていました。

第7引数で、NI_NUMERICHOSTとNI_NUMERICSERVのフラグを立てていますが、それぞれ数値形式のホスト名を、数値形式のサービス名(ポート番号)をバッファに格納するフラグになります。

./a.out 8080 として実行すれば、

port is 8080

host is 0.0.0.0

のように表示されます。

続いて、socketシステムコールを実行してソケットを作成します。引数として渡している3つの値はレガシーな例では、定数リテラルを指定していましたが、モダンな例では取得したaddrinfo構造体のメンバに必要な値が格納されているので、それらを利用して作成します。これによってアドレスファミリに依存しないプログラムが作成できるようになります。

ちなみにsocketの処理としては内部的にプロトコル情報を指定したsocket構造体を作成し、fileオブジェクトへのポインタを格納するメンバに作成したfileオブジェクトへのポインタを格納してから、最後にfileオブジェクトをfd_installしているという流れになります。

レガシーな例でも説明しましたが、bindはソケットとアドレス情報を紐付ける為のシステムコールです。ソケットはプロトコルの他にアドレス情報が紐付いていなければ、リモートホストからのメッセージを受け付けることができませんので、その為に必要な処理です。

IPアドレスは問題ないでしょうが、ポート番号は既に使われているものがあるとソケットを紐付けることができませんので、空いているポート番号やサービス名を指定するようにして下さい。

listenを呼び出す例は、アドレス構造体を利用している以外はレガシーの例と同じです。

復習しておくと、TCPのソケットプログラムは、コネクションを確立した通信を実現する為、クライアントの接続ごとにソケットを作成する仕様になっています。

今回はbits/socket.hに定義されているSOMAXCONNというマクロ定数を用いて、最大のキューのサイズを指定しています。

SOMAXCONNの意味するところは、カーネルがデフォルト値として定義しているlistenで受け付けることができるキューの数です。私の環境では128です。

インフラエンジニアの方は、この値をみてピンと来る方もいるのではないでしょうか?

接続要求の多いデータベースのようなアプリケーションが稼働しているサーバの場合、この数ではキューが溢れ、接続を取りこぼすなどの事態が考えられるので、動的にこの値を引き上げるケースが多いと思います。

以上の処理を経て接続待ち状態になっているソケットディスクリプタをmain関数に返します。続いて、そのソケットディスクリプタをdo_service関数に渡して、実際のサービスを実行します。

void do_service (int sock) {

char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
struct sockaddr_storage from_sock_addr;
int acc_sock;
socklen_t addr_len;

for (;;) {
addr_len = sizeof(from_sock_addr);
if ((acc_sock = accept(sock, (struct sockaddr *) &from_sock_addr, &addr_len)) == -1) {
perror("accept() failed.");
continue;
} else {
getnameinfo((struct sockaddr *) &from_sock_addr, addr_len, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV);

fprintf(stderr, "port is %s\n", sbuf);
fprintf(stderr, "host is %s\n", hbuf);

do_concrete_service(acc_sock);

close(acc_sock);
}
}
}

do_service関数内では、acceptシステムコールを実行してクライアントとの実際の通信処理を行う新たなソケットを作成し、具体的なサービス処理を担うdo_concrete_service関数へそのソケットディスクリプタを渡しています。

acceptについてもレガシーな例の時説明した通りです。3ウェイハンドシェイクに成功したsocket構造体のfileオブジェクトを格納するメンバにソケットディスクリプタを紐付けて(fd_install関数)そのソケットディスクリプタを返します。

この場合from_sock_addr変数にクライアントのアドレス情報が格納されていますので、それをもとにgetnameinfoを実行して、クライアントのアドレス情報を表示しています。

レガシーな例では、クライアントからの情報を格納するのにsockaddr_in構造体を利用していましたが、この構造体はIPv4のアドレス情報を格納する為だけに利用するものでした。

そこで、どのようなアドレスファミリに対しても対応できるように、アドレス情報を格納するのに十分な格納領域をもつsockaddr_storageを定義して、必要に応じてキャストして利用するという手法に切り替えています。

static inline void do_concrete_service (int sock) {

echo_back(sock);
}

do_concrete_service関数内では、サービス処理にフォーカスした関数を実行しています。

今回の場合はクライアントからのメッセージをそのまま送り返すecho_backという関数を実行します。この処理をわけたのは手軽に内容を変更できるようにする為です。

ちなみに、私の環境のgccでは-O2までの最適化を行うとインライン展開しました。

void echo_back (int sock) {

char buf[MAX_BUF_SIZE];
ssize_t len;

for (;;) {
if ((len = recv(sock, buf, sizeof(buf), 0)) == -1) {
perror("recv() failed.");
break;
} else if (len == 0) {
fprintf(stderr, "connection closed by remote host.\n");
break;
}

if (send(sock, buf, (size_t) len, 0) != len) {
perror("send() failed.");
break;
}
}
}

送受信処理に関しては、もともと抽象度が高い実装になっているので、この段階になってくるとレガシーな例とほとんど同じです。

今回は、実際の処理の内容自体はレガシーの例とほとんど変わらないものになりましたが、プロトコルやアドレスファミリに依存しないプログラミングを行う為の基本形となるものを作ることができました。

以後ソケットAPIの例を提示する時は、こちらのAPIを利用して作成していくことにします。

「getaddrinfo関数の関数の使い方はわかった! あとはIPv4、IPv6どちらをどう使えば良いのかだけ知りたいんだ!」

その方法についてはRFC6555にGoogle Chrome と Mozilla Firefoxが採用している方法が掲載されているので、気になる方は下記を参考にしてみて下さい。

Happy Eyeballs: Success with Dual-Stack Hosts

なおgetaddrinfo関数については少し前にスタックオーバーフローの脆弱性が発見されていました(現在は修正されている)ので、getaddrinfoに限らず使用するglibcなどのライブラリや、ソフトウェアのアップデートには気を遣うことが重要です。


参考にした各ソースコード

Linuxカーネル:2.6.11

glibc:glibc-2.12.1

CPU:x86_64

※バージョンについては特に理由がありませんが、古すぎず新しすぎずみたいなところです。


OS

CentOS release 6.8


コンパイラ

gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-17)