データグラムソケットとは?
データグラムソケット => Datagram Socket ストリームソケット => Stream Socket
データグラムソケットの作成はストリームソケットと似ていますが、主な違いはデータグラムソケットがTCPではなくUDPプロトコルを使用することです。つまり、クライアントとサーバーはデータを送信する前に接続を確立する必要がありません。
これは、接続を維持する必要がないアプリケーションに便利です。クライアントとサーバーは、最初に接続を確立しなくてもデータを送受信できるからです。 例えば、DNSサーバーはUDPプロトコルを使用します。接続を維持する必要がなく、データを送信して応答を受信するだけで済むからです。
動作の流れ

image
画像出典: UDP Server-Client implementation in C
サーバーの動作
Listenerは、ソケットを作成し、ポートにバインドし、着信メッセージを待機することで動作します。
まず、listenerはhints構造体を作成します。この構造体には、ソケット作成に必要な情報(プロトコルファミリ、ソケットタイプ、ポート番号など)が含まれます。次に、
getaddrinfo()関数を呼び出してアドレス情報を取得し、servinfoリンクリストに保存します。次に、listenerは
servinfoリンクリストを走査し、使用可能なアドレス情報を見つけます。そして、socket()関数でソケットを作成します。成功した場合はソケット記述子を返し、失敗した場合は-1を返します。次に、listenerは
bind()関数を呼び出して、ソケットを指定されたポートにバインドします。これにより、listenerはそのポートに送信されたメッセージを受信できるようになります。ソケットのバインドに成功すると、プログラムは
recvfrom()関数からの着信メッセージを待機します。recvfrom()関数はメッセージを受信するまでプログラムをブロックし、メッセージをbufバッファに格納し、送信元のアドレス情報をtheir_addrに格納します。最後に、受信したメッセージと送信元のIPアドレスを画面に表示します。最後に、listenerはソケットを閉じて終了します。
クライアントの動作
Talkerは、ソケットをセットアップし、listenerにメッセージを送信し、ソケットを閉じることで動作します。
まず、talkerはhints構造体を作成します。これは、プロトコルファミリ、ソケットタイプ、ポート番号などのアドレス情報を保持するために使用されます。次に、
getaddrinfo()関数を呼び出してアドレス情報を取得し、servinfoリンクリストに保存します。次に、talkerは
servinfoリンクリストを走査し、使用可能なアドレス情報を見つけます。そして、socket()関数でソケットを作成します。成功した場合はソケット記述子を返し、失敗した場合は-1を返します。ソケットの作成に成功すると、talkerは
sendto()関数を呼び出してソケット経由でメッセージを送信します。成功した場合は送信したバイト数を返し、失敗した場合は-1を返します。最後に、talkerはソケットを閉じます。
サーバー側のデモコード
/*
** listener.c: a datagram sockets "server" demo - データグラムソケットのリスナー
** from Beej's Guide to Network Programming Using Internet Sockets
** https://beej.us/guide/bgnet/html/#datagram
*/
#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 <arpa/inet.h>
#include <netdb.h>
#define MYPORT "4950" /* リッスンポート番号、つまりサーバーのポート番号。クライアントが接続するポート */
#define MAXBUFLEN 100 /* 最大パケットサイズ */
/* プロトコルファミリに応じて、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(void)
{
int sockfd; // リスニングソケット
struct addrinfo hints; // hintsはgetaddrinfo()関数用 - アドレス情報を指定するため
struct addrinfo *servinfo; // servinfoはgetaddrinfo()関数が返すリンクリストを格納 - アドレス情報を保存
struct addrinfo *p; // servinfoリンクリストを走査するため
int rv; // getaddrinfo()関数の戻り値を格納 - エラーコード
int numbytes; // recvfrom()関数の戻り値を格納 - 受信したバイト数
struct sockaddr_storage their_addr; // クライアントのアドレス情報を格納 - sockaddr_storageはIPv4またはIPv6の任意のアドレスを保持可能
char buf[MAXBUFLEN]; // 受信データを保持 - データパケット
socklen_t addr_len; // クライアントアドレス情報の長さを保持 - sockaddr_storage構造体のサイズ
char s[INET6_ADDRSTRLEN]; // クライアントアドレス情報を保持 - アドレス文字列
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET6; // IPv6を使用
hints.ai_socktype = SOCK_DGRAM; // UDPプロトコルを使用
hints.ai_flags = AI_PASSIVE; // ローカルIPアドレスを使用
// アドレス情報servinfoを構築
if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0)
{
// gai_strerrorはエラーコードをエラーメッセージ文字列に変換
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("listener: socket");
continue;
}
// ソケットをバインド
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("listener: bind");
continue;
}
break;
}
// servinfoリンクリストを全て走査しても使用可能なアドレス情報が見つからなかった場合、エラーを出力して終了
if (p == NULL)
{
fprintf(stderr, "listener: failed to bind socket\n");
return 2;
}
// servinfoリンクリストを解放
freeaddrinfo(servinfo);
printf("listener: waiting to recvfrom...\n");
// addr_lenはクライアントアドレス情報の長さを保持
addr_len = sizeof their_addr;
// データパケットを受信しbufに保存、受信したパケットの長さをnumbytesに保存
if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN - 1, 0,
(struct sockaddr *)&their_addr, &addr_len)) == -1)
{
perror("recvfrom");
exit(1);
}
// inet_ntopはクライアントアドレス情報を文字列に変換
printf("listener: got packet from %s\n",
inet_ntop(their_addr.ss_family,
get_in_addr((struct sockaddr *)&their_addr),
s, sizeof s));
printf("listener: packet is %d bytes long\n", numbytes);
// データパケットの末尾を'\0'に設定して、表示できるようにする
buf[numbytes] = '\0';
// データパケットを表示
printf("listener: packet contains \"%s\"\n", buf);
// ソケットを閉じる
close(sockfd);
return 0;
}
クライアント側のデモコード
/*
** talker.c -- a datagram "client" demo - データグラムソケットのトーカー
** from Beej's Guide to Network Programming Using Internet Sockets
** https://beej.us/guide/bgnet/html/#datagram
*/
#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 <arpa/inet.h>
#include <netdb.h>
#define SERVERPORT "4950" // サーバーポート
int main(int argc, char *argv[])
{
int sockfd; // ソケット記述子
struct addrinfo hints; // hintsは必要なアドレスタイプを指定するために使用
struct addrinfo *servinfo; // servinfoは返されたアドレスリンクリストを指す
struct addrinfo *p; // pはservinfoの各アドレスを指す
int rv; // rvはgetaddrinfoの戻り値を格納し、エラー判定に使用
int numbytes; // numbytesはsendtoの戻り値を格納し、送信したバイト数を記録
// 引数の数をチェック
if (argc != 3)
{
fprintf(stderr, "usage: talker hostname message\n");
exit(1);
}
memset(&hints, 0, sizeof hints); // hints構造体を初期化
hints.ai_family = AF_INET6; // IPv6プロトコルを使用
hints.ai_socktype = SOCK_DGRAM; // UDPプロトコルを使用
// getaddrinfo()はアドレス情報を取得し、結果をservinfoに格納
if ((rv = getaddrinfo(argv[1], SERVERPORT, &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("talker: socket");
continue;
}
break;
}
// pがNULLの場合、使用可能なアドレスが見つからなかったことを意味する
if (p == NULL)
{
fprintf(stderr, "talker: failed to create socket\n");
return 2;
}
// データを送信し、送信したバイト数を記録
if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0,
p->ai_addr, p->ai_addrlen)) == -1)
{
perror("talker: sendto");
exit(1);
}
// servinfoを解放
freeaddrinfo(servinfo);
// 送信したバイト数を表示
printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);
// ソケットを閉じる
close(sockfd);
return 0;
}
プログラムのコンパイル
gcc -o talker talker.c
gcc -o listener listener.c
実行と出力
./listener
# talkerを実行する前は、listenerは待機し続ける
listener: waiting to recvfrom...
# 別のターミナルでtalkerプログラムを実行すると、listenerは以下のように出力する
listener: got packet from ::1
listener: packet is 5 bytes long
listener: packet contains "hello"
# 別のターミナルでtalkerプログラムを実行
./talker localhost hello
talker: sent 5 bytes to localhost
潜在的な問題点
エラーハンドリング: 両方のプログラムに堅牢なエラーハンドリングがありません。例えば、クライアントプログラムでは、
sendtoがエラーを返した場合、単にエラーメッセージを表示して終了するだけで、エラーからの回復を試みません。タイムアウト: サーバーが実行されていない場合や、メッセージが転送中に失われた場合、クライアントプログラムは応答を永遠に待ち続け、ハングアップします。これを回避するには、
sendtoおよびrecvfrom呼び出しにタイムアウトを設定することを検討すべきです。メッセージサイズの制限: プログラムは最大100バイトのメッセージしか許可しません。より大きなメッセージを送信する必要がある場合は、それらを小さなチャンクに分割して個別に送信する必要があります。
セキュリティの欠如: プログラムは暗号化や認証を一切使用していないため、ネットワーク上の誰でもメッセージを傍受したり偽造したりする可能性があります。通信を保護する必要がある場合は、TLSやSSHを使用してメッセージを暗号化することを検討すべきです。
この記事のまとめ
この記事のコードは、シンプルなUDPクライアントとサーバープログラムを実装しています。クライアントプログラムはtalker、サーバープログラムはlistenerです。
listenerプログラムを実行し、次にtalkerプログラムを実行します。listenerプログラムはtalkerプログラムからのメッセージを待機します。talkerプログラムが実行されると、listenerプログラムにメッセージを送信し、listenerプログラムはそのメッセージを表示します。
listenerプログラムが実行されていない場合、talkerプログラムはメッセージを送信して終了します。これらのメッセージは、受信するプログラムがないため失われます。
UDPプロトコルはシンプルで軽量なプロトコルであり、非常に使いやすいです。しかし、エラーチェックや回復機能は一切提供せず、メッセージが宛先に確実に届くことも保証しません。そのため、信頼性の高い通信が必要なアプリケーションには適していません。