Linuxネットワークプログラミング(初級編 その2)
目次
サーバ環境
| 製品名 | OpenBlockS 600/R |
| CPU | 600MHz(AMCC PowerPC 405EX) |
| メモリ | 1GB(DDR2 SDRAM) |
| ストレージ | 8GB(Compact Flash) |
はじめに
Linuxネットワークプログラミング(初級編 その1)で説明した通り、サーバを作成する場合、bind時に待ち受けるポート番号が数値指定された場合とサービス指定された場合を考慮する必要があります。
クライアントを作成する場合は、connect時に接続先ポート(サービス)に加え、接続先がホスト名で指定された場合とIPアドレスで指定された場合についても考慮する必要があります。
getaddrinfoを使用すると、上記考慮が不要になり、コードがシンプルになります。
addrinfo型構造体、sockaddr_in型構造体について
getaddrinfoは、アドレス情報を決定するためのヒントを与えることでsockaddr型構造体を得ることができる関数で、ヒントとなる情報を格納した不完全なaddrinfo型構造体を与えると、必要なメンバが全てそろったaddrinfo型構造体を返します。
getaddrinfoについて説明する前に、addrinfoとsockaddr_inを並べて比較してみます(実際のコードはシステムにより異なります)。
addrinfo
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. */
};sockaddr_in
struct sockaddr_in
{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};上記の通り、addrinfo型構造体の中にはsockaddr型構造体が含まれているので、getaddrinfoに最低限の情報を含むaddrinfoを与えることで得られたaddrinfo型構造体のメンバであるsockaddr_in型構造体を、bindのようなソケットを扱う関数に与えることでアドレス情報を指定する仕組みになっています。
getaddrinfoについて
詳細については以下で説明していきますが、getaddrinfoの第三引数としてconst struct addrinfo *hintsを与えると、第四引数で指定したstruct addrinfo **resとして返される仕組みになっています。
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int errcode);引数nodeにはホスト名またはIPアドレスを指定します。NULLと指定し、hints.ai_flags=AI_PASSIVEを指定していた場合、ワイルドカードアドレスとなり、どのインタフェースからの接続でも受け付けるようになります。
引数serviceにはポート番号またはサービス名を指定します。NULLと指定すると、bindを呼び出した時点で短命ポートが自動で割り当てられるため、クライアントプログラムでは基本的にNULLを渡します。
引数hintsにはヒントとなる情報が格納されたaddrinfo型構造体へのポインタを渡します。
引数resには決定されたアドレス情報を格納したaddrinfo型構造体を参照するためのポインタを格納するための変数のポインタを渡します。
resはgetaddrinfoを読んだ時点で割り当てられるため、利用後はfreeaddrinfo()で開放する必要があります。
エラーはgai_strerror()で人が理解できる形式に変換されるので、以下のようにしておくと切り分けに便利です。
if ((errcode = getaddrinfo(・・・)) != 0) {
(void) fprintf(stderr, "getaddrinfo():%s\n", gai_strerror(errcode));
return (-1);
}getaddrinfoの使用例
getaddrinfoを利用しない場合、Linuxネットワークプログラミング(初級編 その1)のbindの項で説明した通り、指定されたポート番号が数値で指定されている場合とサービス名で指定されている場合で処理を分岐する必要があります。また、sockaddr型構造体ではsin_addrやsin_portに直接数値を代入するのではなく、htonsやhtonlでネットワークバイトオーダーに変換する手間も必要となります。
ポート番号ではなくサービス名で指定された場合、getservbyname(引数で指定したサービス名とプロトコルにマッチするエントリをデータベースから探し、そのエントリを収めた servent 構造体を返す関数)を使用してポート番号を取得する必要があります。
また、connectの時はサービス名に加え、IPアドレスについても考慮が必要になります。例えば、ホスト指定が数値であった場合はinet_ptonを使用し、IPアドレスとして解釈できない場合にはgethostbyname2を使用するという複雑な処理が必要となります。
getaddrinfoを利用すれば、サービス名かポート番号か、IPアドレスかホスト名か、といったことを悩まずに、以下のようにシンプルにプログラミングできるようになります。
サーバの場合
const char *hostnm = argv[1];
const char *portnm = argv[2];
struct addrinfo hints, *res0;
int sd;
(void) memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if ((errcode = getaddrinfo(NULL, portnm, &hints, &res0)) != 0) {
(void) fprintf(stderr, "getaddrinfo():%s\n", gai_strerror(errcode));
return (-1);
}
if ((sd = socket(res0->ai_family, res0->ai_socktype, res0->ai_protocol)) == -1) {
perror("socket");
freeaddrinfo(res0);
return (-1);
}
if (bind(sd, res0->ai_addr, res0->ai_addrlen) == -1) {
perror("bind");
(void) close(sd);
freeaddrinfo(res0);
return (-1);
}
if (listen(sd, SOMAXCONN) == -1) {
perror("listen");
(void) close(sd);
freeaddrinfo(res0);
return (-1);
}
freeaddrinfo(res0);クライアントの場合
struct addrinfo hints, *res0;
int sd;
const char *portnm = argv[1];
(void) memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if ((errcode = getaddrinfo(hostnm, portnm, &hints, &res0)) != 0) {
(void) fprintf(stderr, "getaddrinfo():%s\n", gai_strerror(errcode));
return (-1);
}
if ((sd = socket(res0->ai_family, res0->ai_socktype, res0->ai_protocol)) == -1) {
perror("socket");
freeaddrinfo(res0);
return (-1);
}
if (connect(sd, res0->ai_addr, res0->ai_addrlen) == -1) {
perror("connect");
(void) close(sd);
freeaddrinfo(res0);
return (-1);
}
freeaddrinfo(res0);getnameinfoについて
上記のコードには載せませんでしたが、getaddrinfoにより返却されたres0のメンバをgetnameinfoに渡すと、IPアドレス及びポート番号を文字列として取得できます。画面に接続先(元)IPアドレス(ポート番号)を表示させたい場合などに便利です。getnameinfo
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);getnameinfo() 関数は、プロトコルに依存しないかたちでソケットアドレスから対応するホスト名とサービスへの変換を行います。
引数saは、IP アドレスとポート番号の情報を保持している汎用的なソケットアドレス構造体 (sockaddr_in 型または sockaddr_in6 型) へのポインタです。
引数salenはsaのサイズです。
引数hostとservは、(それぞれサイズが hostlen と servlen)呼び出し側で確保された バッファへのポインタで、ホスト名とサービス名を含む NULL 終端された文字列がそれぞれのバッファに格納されます(つまりこのhostとservが得たい情報です)。
ホスト名が不要であることをこの関数に伝えるには、 host に NULL を指定するか、 hostlen に 0 を指定します。同様に、サービス名が不要な場合は、 serv に NULL を指定するか、 servlen に 0 を指定します。
ホスト名とサービス名の両方を不要だと指定することはできません。
引数flagsで getnameinfo() の動作を変えることができます。指定できる値は以下の通り:
| NI_NAMEREQD | 指定すると、ホスト名が決定できなかった場合にエラーを返す。 |
| NI_DGRAM | 指定すると、ストリームベース (TCP) でなくデータグラムベース (UDP) のサービスを対象にする。UDPとTCPで違うサービスを提供しているポート (512-514) に対して必要となる。 |
| NI_NOFQDN | 指定すると、ローカルなホストには fully qualified domain name (FQDN) のホスト名の部分のみを返す。 |
| NI_NUMERICHOST | 指定すると、数値形式のホスト名が返される。 (指定しなくてもノードの名前が決定できない場合は数値形式が返ることがある)。 |
| NI_NUMERICSERV | 指定すると、数値形式のサービス名 (例えばポート番号) が返される (指定しなくてもサービス名が決定できない場合は数値形式が返ることがある)。 |
getnameinfoの使用例
if ((errcode = getnameinfo(res0->ai_addr, res0->ai_addrlen, nbuf, sizeof(nbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV)) != 0) {
(void) fprintf(stderr, "getnameinfo():%s\n", gai_strerror(errcode));
freeaddrinfo(res0);
return (-1);
}
(void) fprintf(stderr, "port=%s\n", sbuf);