はじめに
シンプルなTCPサーバーとクライアントと比較して、本記事のサーバー・クライアントプログラムは以下の点が改良されています。
inet_ntop()関数を使用することで、サーバープログラムがIPv4とIPv6の両方のアドレスを正しく処理できるようになりました。getaddrinfo()関数を使用することで、サーバープログラムがIPv4とIPv6の両方のアドレスを正しく処理できるようになりました。fork()関数を使用することで、サーバープログラムが複数のクライアントからの接続要求を同時に処理できるようになりました。waitpid()関数を使用することで、サーバープログラムが子プロセスの終了を正しく処理できるようになりました。
サーバーコード
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT "43866" /* ポート番号 */
#define BACKLOG 10 /* 最大接続要求数 */
#define MAXDATASIZE 100 /* 最大データ転送量 */
/* シグナルハンドラ関数。子プロセスの終了を処理 */
void sigchld_handler(int s)
{
/* waitpid() が errno を変更する可能性があるため、保存してから復元 */
int saved_errno = errno;
/* 全ての子プロセスの終了を待つ */
while (waitpid(-1, NULL, WNOHANG) > 0)
;
/* errno を復元 */
errno = saved_errno;
}
/* sockaddr 構造体からIPアドレスを取得 */
void *get_in_addr(struct sockaddr *sa)
{
/* アドレスファミリを判定 */
if (sa->sa_family == AF_INET)
{
/* IPv4アドレスを返す */
return &(((struct sockaddr_in *)sa)->sin_addr);
}
/* IPv6アドレスを返す */
return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}
/* sockaddr 構造体からポート番号を取得 */
int get_in_port(struct sockaddr *sa)
{
/* アドレスファミリを判定 */
if (sa->sa_family == AF_INET)
{
/* IPv4ポート番号を返す */
return (((struct sockaddr_in *)sa)->sin_port);
}
/* IPv6ポート番号を返す */
return (((struct sockaddr_in6 *)sa)->sin6_port);
}
/* メイン関数 */
int main(int argc, char *argv[])
{
int sockfd, new_fd; /* sockfd: 待受ソケット, new_fd: データ通信用ソケット */
struct addrinfo hints, *servinfo, *p; /* hints: addrinfo構造体設定用, servinfo: アドレス情報格納, p: servinfo 走査用 addrinfo ポインタ */
struct sockaddr_storage their_addr; /* クライアントのIPアドレスとポート番号を格納 */
socklen_t sin_size; /* accept() 用 */
struct sigaction sa; /* sigaction() 用 */
int yes = 1; /* setsockopt() 関数設定用 */
int rv; /* getaddrinfo() 関数の戻り値格納用 */
char s[INET6_ADDRSTRLEN]; /* inet_ntop() 関数が返すアドレス情報の文字列格納用 */
int numbytes_send; /* send() 関数の戻り値格納用 */
int numbytes_recv; /* recv() 関数の戻り値格納用 */
char buf[MAXDATASIZE]; /* 受信データ格納用 */
/* hints 構造体を初期化 */
memset(&hints, 0, sizeof hints);
/* hints 構造体を設定 */
hints.ai_family = AF_UNSPEC; /* IPv4 または IPv6 を指定しない */
hints.ai_socktype = SOCK_STREAM; /* TCP ストリームソケット */
hints.ai_flags = AI_PASSIVE; /* 自ホストのIPアドレスを使用 */
/* アドレス情報を取得 */
if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0)
{
/* エラーメッセージを表示 */
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
/* servinfo 構造体のリンクリストを走査 */
for (p = servinfo; p != NULL; p = p->ai_next)
{
/* ソケットを作成 */
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
/* エラーメッセージを表示 */
perror("server: socket");
continue;
}
/* ソケットオプションを設定 */
// SO_REUSEADDR オプションはローカルアドレスとポートの再利用を許可するために使用
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
{
/* エラーメッセージを表示 */
perror("setsockopt");
exit(1);
}
/* ソケットをバインド */
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
/* サーバーへの接続失敗 */
close(sockfd);
perror("server: bind");
continue;
}
break;
}
/* 結果のリンクリストを解放 */
freeaddrinfo(servinfo);
/* バインドが成功したか確認 */
if (p == NULL)
{
fprintf(stderr, "server: failed to bind\n");
exit(1);
}
/* ソケットを待受状態にする */
if (listen(sockfd, BACKLOG) == -1)
{
perror("listen");
exit(1);
}
/* シグナルハンドラ関数 */
sa.sa_handler = sigchld_handler; /* 全てのゾンビプロセスを処理 */
sigemptyset(&sa.sa_mask); /* 全てのシグナルをブロック */
sa.sa_flags = SA_RESTART; /* 中断されたシステムコールを再開 */
if (sigaction(SIGCHLD, &sa, NULL) == -1)
{
/* エラーメッセージを表示 */
perror("sigaction");
exit(1);
}
printf("server: waiting for connections...\n");
/* accept() のメインループ */
while (1)
{
sin_size = sizeof their_addr;
/* 接続要求を受け付ける */
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1)
{ /* 接続要求の受け付けに失敗 */
perror("accept");
continue;
}
/* IPアドレスを文字列に変換 */
inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr), s, sizeof s);
printf("server: got connection from %s, port %d, socketfd %d\n", s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
/* 子プロセスを作成して新しい接続要求を処理 */
if (!fork())
{
/* 子プロセスは待受ソケットを必要としない */
close(sockfd);
/* 子プロセスがデータ転送を処理 */
/* 送信メッセージを構築 */
char *msg = "Hello world from server!";
int msg_len = strlen(msg);
/* クライアントにメッセージを送信 */
if ((numbytes_send = send(new_fd, msg, msg_len, 0)) == -1)
perror("send");
/* 送信メッセージを表示 */
printf("server: sent message: %s (%d bytes) to %s, port %d, sockfd %d\n", msg, numbytes_send, s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
/* クライアントからのメッセージを受信 */
if ((numbytes_recv = recv(new_fd, buf, MAXDATASIZE - 1, 0)) == -1)
{
perror("recv");
exit(1);
}
buf[numbytes_recv] = '\0';
printf("server: received message: %s (%d bytes) from %s, port %d, sockfd %d\n", buf, numbytes_recv, s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
/* データ通信用ソケットを閉じる */
close(new_fd);
printf("\n");
/* 子プロセスを終了 */
exit(0);
}
/* 親プロセスはデータ通信用ソケットを必要としない */
close(new_fd);
}
return 0;
}
/* References: Page 31-33, Beej's Guide to Network Programming Using Internet Sockets */
クライアントコード
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT "43866" /* ポート番号 */
#define MAXDATASIZE 100 /* 1回あたりの最大データ転送量 */
/* sockaddr を取得、IPv4 または IPv6: */
void *get_in_addr(struct sockaddr *sa)
{
/* アドレスファミリを判定 */
if (sa->sa_family == AF_INET)
{
/* IPv4アドレスを返す */
return &(((struct sockaddr_in *)sa)->sin_addr);
}
/* IPv6アドレスを返す */
return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}
int main(int argc, char *argv[])
{
int sockfd; /* sockfd: socket() 関数の戻り値格納用 */
char buf[MAXDATASIZE]; /* バッファ。受信データ格納用 */
struct addrinfo hints, *servinfo, *p; /* hints: addrinfo構造体設定用, servinfo: アドレス情報格納, p: servinfo 走査用 addrinfo ポインタ */
int rv; /* getaddrinfo() 関数の戻り値格納用 */
char s[INET6_ADDRSTRLEN]; /* inet_ntop() 関数が返すアドレス情報の文字列格納用 */
int numbytes_recv; /* recv() 関数の戻り値格納用 */
int numbytes_send; /* send() 関数の戻り値格納用 */
/* 引数の数をチェック */
if (argc != 2)
{
fprintf(stderr, "usage: ./client hostname\n");
exit(1);
}
/* hints 構造体を初期化 */
memset(&hints, 0, sizeof hints);
/* hints 構造体を設定 */
hints.ai_family = AF_UNSPEC; /* IPv4 または IPv6 を指定しない */
hints.ai_socktype = SOCK_STREAM; /* TCP ストリームソケット */
/* アドレス情報を取得 */
if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
/* エラーメッセージを表示 */
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
/* servinfo 構造体のリンクリストを走査 */
for (p = servinfo; p != NULL; p = p->ai_next)
{
/* ソケットを作成 */
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
/* エラーメッセージを表示 */
perror("client: socket");
continue;
}
/* サーバーに接続 */
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
/* サーバーへの接続失敗 */
close(sockfd);
perror("client: connect");
continue;
}
break;
}
/* servinfo 構造体を全て走査しても接続できなかった場合 */
if (p == NULL)
{
fprintf(stderr, "client: failed to connect to server\n"); /* サーバーにデータを送信 */
return 2;
}
/* アドレス情報を文字列に変換 */
inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof s);
/* 接続したサーバーのアドレス情報を表示 */
printf("client: connecting to %s port %s\n", s, PORT);
/* servinfo 構造体を解放 */
freeaddrinfo(servinfo);
/* データを受信 */
if ((numbytes_recv = recv(sockfd, buf, MAXDATASIZE - 1, 0)) == -1)
{
/* データ受信失敗 */
perror("recv");
exit(1);
}
/* 文字列終端文字を追加 */
buf[numbytes_recv] = '\0';
/* 受信したデータを表示 */
printf("client: received '%s' from server.\n", buf);
/* 送信するデータを構築 */
char *msg = "Hello world from client!";
int msg_len = strlen(msg);
/* サーバーにデータを送信 */
if ((numbytes_send = send(sockfd, msg, msg_len, 0)) == -1)
{
/* データ送信失敗 */
perror("send");
}
printf("client: sent '%s' (%d bytes) to server.\n", msg, numbytes_send);
/* ソケットを閉じる */
close(sockfd);
printf("client: closed socketfd %d.\n", sockfd);
return 0;
}
/* References: Page 31-33, Beej's Guide to Network Programming Using Internet Sockets */
コンパイル
gcc server.c -o server
gcc client.c -o client
実行と出力
$ ./server
server: waiting for connections...
server: got connection from 127.0.0.1, port 56836, socketfd 4
server: sent message: Hello world from server! (24 bytes) to 127.0.0.1, port 56836, sockfd 4
server: received message: Hello world from client! (24 bytes) from 127.0.0.1, port 56836, sockfd 4
$ ./client localhost
client: connecting to
client: received 'Hello world from server!' from server.
client: sent 'Hello world from client!' (24 bytes) to server.
client: closed socketfd 3
サーバー起動前にクライアントを実行すると、以下のエラーメッセージが表示されます。
$ ./client localhost
client: connect: Connection refused
client: failed to connect to server