個人的にシリーズ化させて頂いているC言語で学ぶソケットAPI入門の、第4回目です。
今回はUDPについてのデータ送受信の仕組みとともに、TCPとの違いについて確認していきます。
UDPについては下記のような動きもあり、改めて注目されている通信プロトコルと言えます。
GoogleのQUICプロトコル:TCPからUDPへWebを移行する
##UDPの特徴
UDPはホストツーホストのプロトコルであるIPをアプリケーションレベルに拡張したもので、UDPの場合はアドレッシングにポート番号を使用し、TCPと違い1つ1つのメッセージの境界を保持します。
また、UDPはデータが破損した場合、そのままデータグラムを破棄してしまいますが、TCPとは違い、ベストエフォート型のデータグラムサービスを提供するためのプロトコルであり、データの再送処理など信頼性に関わる処理をしません。
と並べてみると一見TCPとくらべて見劣りしそうなUDPですが、動作が速いことやTCPよりも手軽にメッセージを受信することができるなど良い所も沢山あります。
上記の記事もUDPの特性と良さについて触れておりますので、読まれてみると良いでしょう。
そして、UDPはTCPとは違い、コネクションの接続を確立せずに使用することができるコネクションレスのプロトコルです。このことは下記のソースコードにも現れています。
###実行環境
クライアントソフト Mac OS X 10.10.5 gcc
サーバソフト x86_64のCentOs6.8 gcc
サーバソフトと、クライアントソフト両方紹介しますが、サーバソフトの方が単純ですので先に紹介します。
##サーバソフトのソースコード
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <sys/socket.h> //socket(), bind(), sendto(), recvfrom()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset(), strcmp()
#include <unistd.h> //close()
#define MSG_FAILURE -1
#define MAX_MSGSIZE 1024
#define MAX_BUFSIZE (MAX_MSGSIZE + 1)
int get_socket(const char *);
void sockaddr_init (const char *, unsigned short, struct sockaddr *);
int udp_send(int, const char *, int, struct sockaddr *);
int udp_receive(int, char *, int, struct sockaddr *);
void socket_close(int);
int main(int argc, char* argv[]) {
const char *address = "";
unsigned short port = (unsigned short)atoi(argv[1]);
struct sockaddr servSockAddr, clitSockAddr;
char recvBuffer[MAX_BUFSIZE];
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
if (bind(server_sock, &servSockAddr, sizeof(servSockAddr)) < 0) {
perror("bind() failed.");
exit(EXIT_FAILURE);
}
while(1) {
int recvMsgSize = udp_receive(server_sock, recvBuffer, MAX_BUFSIZE, &clitSockAddr);
if (recvMsgSize == MSG_FAILURE) continue;
printf("message received from %s.\n", inet_ntoa(((struct sockaddr_in *)&clitSockAddr)->sin_addr));
int sendMsgSize = udp_send(server_sock, recvBuffer, recvMsgSize, &clitSockAddr);
if (sendMsgSize == MSG_FAILURE) continue;
}
}
int get_socket(const char *type) {
int sock;
if (strcmp(type, "udp") == 0) {
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
} else if(strcmp(type, "tcp") == 0) {
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
}
if (sock < 0){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
return sock;
}
void sockaddr_init (const char *address, unsigned short port, struct sockaddr *sockaddr) {
struct sockaddr_in sockaddr_in;
sockaddr_in.sin_family = AF_INET;
if (inet_aton(address, &sockaddr_in.sin_addr) == 0) {
if (strcmp(address, "") == 0 ) {
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
}
if (port == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
sockaddr_in.sin_port = htons(port);
*sockaddr = *((struct sockaddr *)&sockaddr_in);
}
int udp_send(int sock, const char *data, int size, struct sockaddr *sockaddr) {
int sendSize;
sendSize = sendto(sock, data, size, 0, sockaddr, sizeof(*sockaddr));
if (sendSize != size) {
perror("sendto() failed.");
return MSG_FAILURE;
}
return sendSize;
}
int udp_receive(int sock, char *buffer, int size, struct sockaddr *sockaddr) {
unsigned int sockaddrLen = sizeof(*sockaddr);
int receivedSize = recvfrom(sock, buffer, MAX_BUFSIZE, 0, sockaddr, &sockaddrLen);
if (receivedSize < 0) {
perror("recvfrom() failed.");
return MSG_FAILURE;
}
return receivedSize;
}
void socket_close(int server) {
if (close(server) < 0) {
perror("close() failed.");
exit(EXIT_FAILURE);
}
}
###26行目から27行目
ソケット作成の処理と、アドレス構造体の初期化処理をそれぞれget_socket()とsockaddr_init()という関数に分離しています。
sockaddr_init()に関してはこれまでのアドレス設定の処理をメインのルーチンから分離しただけです。
get_socket()内ではTCPの時と同じようにsocket()システムコールを使用しています。
TCPとの違いとしては、第2引数にデータグラム型のソケットを作成するためのSOCK_DGRAM、そして第3引数はデータグラム型のエンドツーエンドプロトコルであるIPPROTO_UDPを指定します。ここでも0を指定せずに明示しておきます。
###29行目から32行目
TCPの時と同じようにbind()システムコールを用いて、アドレスをソケットにひも付けますがTCPの時と違い、listen()やaccept()を呼び出さずに、このままメッセージの送受信を行います。
この状態で例のごとく、netstat
で確認してみるとudp 0 0 0.0.0.0:8080 0.0.0.0:*
と表示されているのがわかります。
TCPではクライアントごとに接続を確立する為、accept()の処理によって新たにソケットディスクリプタを取得して、メッセージの送受信を行いますが、UDPの場合は接続を確立しないため、bind()を行ったソケットそのものを使ってメッセージの送受信を行います。
また、クライアントの話になりますが、connect()システムコールも通常使用しません。通常使用しないというのは、使用することもできるからです。
connect()という名前からコネクションを確立するための処理を行うTCP専用のシステムコールかと思いそうになりますが、connect()はメッセージの送受信において、接続先のアドレスを決定するという役割を担うので、UDPの場合も使えます。
もちろん、TCPの場合は実際にSYN接続要求のパケットを相手のサーバに送るので、接続先のアドレスを決定する処理とともにコネクションを確立するための処理を行うことにはなりますが、UDPの場合はそういったコネクション接続のためのパケットは送られず、あくまでそのソケットを利用した通信が、特定のIPアドレスのホストの特定のポート番号に紐付けられたソケットとの通信に限定されるようになります。
###35行目〜36行目
udp_receive()という関数に分離してありますが、recvfrom()システムコールでクライアントからのメッセージを受信している処理です。
今回の場合はrecv()と同じようにデータグラムが到着するまでプログラムの実行をブロックします。デフォルトの挙動はrecv()システムコールの時に説明した通りです。
引数はrecv()システムコールとほとんど同じですが、データグラムを受信した時にはじめて送信元であるクライアントの情報を得るので、第5引数に送信元の情報を格納するsockaddr構造体へのポインタを引数に渡します。
そして、第6引数にその長さを指定するポインタを渡すのですが、渡す際はsockaddr構造体のサイズが格納されていて、recvfrom()の制御から返った時は、実際に格納されたサイズが格納されていることに注意して下さい。
ユーザープロセスへの受信処理としては、TCPと同じようにソケットモジュールの受信キューからデータを転送することになりますが、メッセージは境界が保持されて1つ1つ固まりになっているので、誤って複数のメッセージを受信するということはありません。
また、recvfrom()では指定した受信バッファのサイズよりも大きなペイロードが送られてきた場合は、指定されたサイズ以降のバイト列のデータは破棄されてしまいます。
そのためUDPではアプリケーション要件にあわせて、あらかじめrecevfrom()で確保する受信バッファのサイズを十分大きくしておく必要があります。少なくともsendto()で送られたサイズより大きなサイズが必要です。
ちなみに、UDPデータグラムのペイロードの最大サイズは下位プロトコルであるIPの制約を受けて、65507 (65535(IPパケット長) - 20(IPヘッダ長) - 8(UDPヘッダ長))バイトとなります。
ただし、ネットワーク中に送出される時は更にデータリンクの送信可能ペイロードサイズ(MTU※Ethernetの場合は1500バイト)の制約を受けて、IPデータグラムは分割されてネットワーク中を流れることにはなります。
メッセージの受信バッファへのコピー時、エラーが起きてなければそのまま逐次処理を続けます。
#38行目
clitSockAddrに格納された送信元情報から送信元のIPアドレスを表示します。
#40行目から41行目
udp_send()という関数に分離していますが、sendto()システムコールでクライアントからのメッセージを送信しています。
sendto()はrecvfrom()より、類似のsend()システムコールに仕様が似ており、追加で送信先のホストのアドレス情報を一緒に渡すことになっています。送信先のホストのアドレス情報はrecvfrom()によって取得した構造体を利用します。
そして、TCPと違いエラーによる再送処理を行わないので、送信するデータをバッファに保持しておく必要がありません。その為、sendto()から制御が戻った際は既に下位モジュールにデータが渡されている状態となります。
また、UDPの送信バッファの最大値はシステムによって最大値を決めている場合が多いので、大きなバッファを送信する場合は別途、ソケットの設定を変更するシステムコールを使用する必要があります。このシステムコールについても日を改めて紹介します。
TCPの時のように、接続が閉じられるまで繰り返しデータを送受信する必要はなく、データグラムのメッセージは境界が保持されているので、破棄されていなければsendto()もrecvfrom()もそれぞれ1度ずつ実行すれば対応するメッセージを取得することが可能です。ただし、その順番は保証されません。
ちなみにconnect()を使用した場合、TCPの時と同じようにsend()やrecv()システムコールを利用できることになります。もちろんその送受信原理はTCPとは違いますが。
そして、UDPはベストエフォート側のサービスなので必ずメッセージを受信できるとは限りません。その為、下記のクライアントのコードではアプリケーションレベルで簡易的な再送処理を追加しています。
##クライアントソフトのソースコード
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <sys/socket.h> //socket(), sendto(), recvfrom()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset(), strcmp()
#include <unistd.h> //close()
#include <signal.h> //sigatcion()
#include <errno.h> //erron, EINTR
#define MSG_FAILURE -1
#define MAX_MSGSIZE 1024
#define MAX_BUFSIZE (MAX_MSGSIZE + 1)
#define MAX_TRIES 5
#define TIMEOUT_SECOND 2
int get_socket(const char *);
void sockaddr_init (const char *, unsigned short, struct sockaddr *);
int udp_send(int, const char *, int, struct sockaddr *);
int udp_receive(int, char *, int, struct sockaddr *);
void socket_close(int);
int input(char *, int);
void remove_lf(char *, int);
void sigaction_init(struct sigaction *, void (*)(int));
void catchAlarm(int);
int udp_try_receive (int, struct sockaddr *, struct sockaddr *, char *, int, char *);
int check_correct_server (struct sockaddr *, struct sockaddr *);
int intTries = 0;
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "argument count mismatch error.\n");
exit(EXIT_FAILURE);
}
const char *address = argv[1];
unsigned short port = (unsigned short)atoi(argv[2]);
struct sockaddr servSockAddr, clitSockAddr;
struct sigaction action;
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
sigaction_init(&action, catchAlarm);
if (sigaction(SIGALRM, &action, NULL) < 0) {
perror("sigaction() failure");
exit(EXIT_FAILURE);
}
while(1){
char sendBuffer[MAX_BUFSIZE];
char receiveBuffer[MAX_BUFSIZE];
int inputSize = input(sendBuffer, MAX_BUFSIZE);
if (strcmp(sendBuffer, "quit\n") == 0) {
socket_close(server_sock);
break;
}
int receivedSize = udp_try_receive(server_sock, &servSockAddr, &clitSockAddr, sendBuffer, inputSize, receiveBuffer);
if (check_correct_server(&servSockAddr, &clitSockAddr) == -1) {
continue;
}
remove_lf(receiveBuffer, receivedSize);
printf("server return: %s\n", receiveBuffer);
}
return EXIT_SUCCESS;
}
int get_socket(const char *type) {
int sock;
if (strcmp(type, "udp") == 0) {
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
} else if(strcmp(type, "tcp") == 0) {
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
}
if (sock < 0){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
return sock;
}
void sockaddr_init (const char *address, unsigned short port, struct sockaddr *sockaddr) {
struct sockaddr_in sockaddr_in;
sockaddr_in.sin_family = AF_INET;
if (inet_aton(address, &sockaddr_in.sin_addr) == 0) {
if (strcmp(address, "") == 0 ) {
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
}
if (port == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
sockaddr_in.sin_port = htons(port);
*sockaddr = *((struct sockaddr *)&sockaddr_in);
}
int udp_send(int sock, const char *data, int size, struct sockaddr *sockaddr) {
int sendSize;
sendSize = sendto(sock, data, size, 0, sockaddr, sizeof(*sockaddr));
if (sendSize != size) {
perror("sendto() failed.");
return MSG_FAILURE;
}
return sendSize;
}
int udp_receive(int sock, char *buffer, int size, struct sockaddr *sockaddr) {
unsigned int sockaddrLen = sizeof(*sockaddr);
int receivedSize = recvfrom(sock, buffer, MAX_BUFSIZE, 0, sockaddr, &sockaddrLen);
if (receivedSize < 0) {
perror("recvfrom() failed.");
return MSG_FAILURE;
}
return receivedSize;
}
void socket_close(int server) {
if (close(server) < 0) {
perror("close() failed.");
exit(EXIT_FAILURE);
}
}
int input(char *buffer, int size) {
printf("please enter the characters:");
if (fgets(buffer, size, stdin) == NULL){
fprintf(stderr, "invalid input string.\n");
exit(EXIT_FAILURE);
}
//flush the stdin buffer
if (buffer[strlen(buffer)-1] != '\n') {
int c;
while((c = getc(stdin) != '\n') && (c != EOF)){}
}
return strlen(buffer);
}
void remove_lf(char *buffer, int bufferSize) {
buffer[bufferSize-1] = '\0';
}
void catchAlarm (int ignored) {
intTries += 1;
}
void sigaction_init(struct sigaction *action, void (*handler)(int) ) {
action->sa_handler = handler;
if (sigfillset(&(action->sa_mask)) < 0) {
perror("sigfillset() failure");
exit(EXIT_FAILURE);
}
action->sa_flags = 0;
}
int udp_try_receive (int sock, struct sockaddr *servSockAddr, struct sockaddr *clitSockAddr, char *sendBuffer, int sendSize, char *receiveBuffer) {
int sendedSize = udp_send(sock, sendBuffer, sendSize, servSockAddr);
int receivedSize;
while (1) {
alarm(TIMEOUT_SECOND);
receivedSize = udp_receive(sock, receiveBuffer, MAX_BUFSIZE, clitSockAddr);
if (receivedSize == MSG_FAILURE) {
if (errno == EINTR) {
if (intTries <= MAX_TRIES) {
printf("timed out %d.\n", intTries);
sendedSize = udp_send(sock, sendBuffer, sendSize, servSockAddr);
if (sendedSize == MSG_FAILURE) break;
alarm(TIMEOUT_SECOND);
continue;
} else {
printf("total timed out %d.\n", MAX_TRIES);
exit(EXIT_FAILURE);
}
} else {
exit(EXIT_FAILURE);
}
}
break;
}
alarm(0);
return receivedSize;
}
int check_correct_server (struct sockaddr *sockaddr_1, struct sockaddr *sockaddr_2) {
if( ((struct sockaddr_in *)sockaddr_1)->sin_addr.s_addr != ((struct sockaddr_in *)sockaddr_2)->sin_addr.s_addr ) {
fprintf(stderr, "reveiceid from unknown server.\n");
} else if (ntohs(((struct sockaddr_in *)sockaddr_1)->sin_port) != ntohs(((struct sockaddr_in *)sockaddr_2)->sin_port)) {
fprintf(stderr, "reveiceid from unknown port.\n");
} else {
return EXIT_SUCCESS;
}
return MSG_FAILURE;
}
###41行目から42行目
サーバの時と同じく、ソケットを作成し、アドレス構造体の初期化処理を行います。TCPのクライアントの時と同様の理由で、通常bind()を呼び出す必要はありません。
###44行目から47行目
ここが今回のUDPのプログラムのポイントの1つなのですが、シグナルによってUDPの送信のタイムアウトを検出するための準備をしています。
sigaction_init()という関数に処理を分離していますが、その処理内容はsigaction構造体を作成することです。
この構造体のポインタをsigaction()システムコールに渡して、SIGALRMがプロセスに通知された時のデフォルトの処理を変更します。もしも、この変更処理を省いてデフォルトの処理が実行されてしまうと、今回のプログラムの場合はSIGALRMがプロセスに通知された瞬間にプログラムが終了してしまいます。
SIGALRMがOSからプロセスに通知された時の処理を簡単に説明すると、catchAlarm()関数が実行され、その最中、他のシグナルをブロックするようにしています。実際catchAlarm()関数が実行中の間はたとえばCtrl + C
(SIGINT)を押してもプロセスが終了することはありません。
シグナルに関してはネットワークプログラミングと密接な関係はあるものの、また別途シグナルそのものについての記事を投稿したいと思います。
# 50行目から70行目
sendto()とrecvfrom()については先ほどクライアントの場合に書きましたが、60行目のudp_try_receive()という関数がタイムアウト処理を検出する送受信処理の一連のサブルーチンとなっております。
まず、alarm()システムコールを用いて2秒後にOSからSIGALRMを送ってもらうようにします。recvfrom()はメッセージを受信できないと、デフォルトではプログラムの処理をずっと待たせますが、今回は約2秒後にSIGALRMがプロセスに送られ、エラーを意味する-1を返します。
シグナルを組みわせてソケットAPIを利用する場合は、一連の処理についての理解を深めておく必要がありそうです。
そしてエラーコードを意味するerrnoとしてはEINTRが設定されているので、それを条件に処理を通し、MAX_TRIESに達するまで再送処理を行います。信頼性を保証しているわけではありませんが、アプリケーション側でTCPの再送処理のようなものを行っている状態です。
それでもダメならば何らかの問題が発生しているとして、一度プログラムを終了させてしまいます。
これによってプログラムが延々とブロックされてしまうことを防げます。
もし、特に待たされることなくメッセージを受信できた場合は、alarm()の引数に0を指定して、アラームをオフにします。
###62行目
check_correct_server()関数というメッセージの送信元が正しいかどうかをチェックするルーチンを追加しています。
これによって、クライアントがパケットを送信したホストとアプリケーションからの応答かどうかを確認しておきます。他のホストからパケットが送られてくる可能性はほとんど考えられませんが、現実的に可能ではある以上、入れておくべきでしょう。
###67行目
メッセージの最後は改行になっているはずなので、改行をヌル文字に変換する処理を入れています。
次回は再びTCPソケットに加えて、マルチプロセス処理を組み合わせた手法について見ていこうと思いましたが、どちらかというとソケットプログラミングというより、マルチプロセスの概念が重要な部分になってきますので、マルチプロセスというものについて見ていこうと思います。
###参考書籍