UDP送受信方法

一応、Windows 環境での場合については途中注釈を入れていますが、基本的には Linux などで使うためのメモです。

  1. ヘッダとライブラリ
  2. ソケットの初期化
  3. ソケットをバインド
  4. 送信
  5. 受信
  6. ソケットの破棄
  7. WinSock の場合
  8. サンプルコード
  9. ネタもと
  10. 履歴

ヘッダとライブラリ

socket を利用するには以下のヘッダとライブラリが必要です。

# Windows環境(WinSock2)では sys/socket.h などは存在しませんのでその辺りは適当に。

ヘッダ
sys/types.h
sys/socket.h
unistd.h
netdb.h
resolv.h(solaris?)
netinet/in.h
arpa/inet.h
winsock2.h(windows)
ws2_tcpip.h(windows)
ライブラリ
libsocket(solaris?)
libnsl(solaris?)
ws2_32.lib(windows)

ソケットの初期化

UDP/IP(だけとは限らないけど)での通信を行う際にはソケットディスクリプタを準備します。

 

たとえばファイルの入出力で、ファイルポインタを使った標準入出力(fopenなど)がある程度一般的ですが…

UNIXの基本としては int 型の識別ID (これをディスクリプタと呼ぶ) で管理された情報を取り扱います。これをソケットでも同様に扱う為のIDがソケットディスクリプタです。 ファイル入出力と同様の低レベルAPI(fread/fwriteなどではなくてread/writeなどのファイルディスクリプタを使うシステムコール)をソケットディスクリプタに対して使用することも出来たりしますw

WinSDK に慣れてる人は、通信用のハンドルだと思えば話は早いかと思います。

# 実際、socket関数の戻り値はソケットハンドル ‘SOCKET’ だし

int socket ( int domain, int type, int protocol );

返値がソケットディスクリプタで、エラー時には -1 が返ります。

WinSockの場合は、先にも書いた様に SOCKET ハンドルが返ってきて、その場合のエラー判定は INVALID_SOCKET かどうかで判定します。

 

domain にはプロトコルファミリを指定します。

頻繁に使われそうな物を以下列挙(詳しくは man socket とか)

PF_UNIX,PF_LOCAL
ローカル通信に使用
PF_INET
IPv4 でのインターネットプロトコル
PF_INET6
IPv6 でのインターネットプロトコル

PF_XXX じゃなくて、AF_XXX では?という方もいるかと思いますが・・・。

こっちの方が正しいらしいので(ぉ

# 実際 define PF_XXX AF_XXX とかされてるから変わらないw

 

type はソケットの種類を指定します。

各プロトコルファミリではソケットは主によく使われるのは二種類・・・ストリーム型とデータグラム型が存在します。

IP ではストリーム型が TCP, データグラム型が UDP という様な感じですが。

ということで UDP 通信の場合は SOCK_DGRAM を指定します。

SOCK_STREAM
ストリーム型ソケット
SOCK_DGRAM
データグラム型ソケット
SOCK_RAW
生のソケット(直接プロトコルにアクセス)
SOCK_RDM
データグラム型ソケット(順序保証は無いが信頼性は高い)
SOCK_SEQPACKET
データグラム型ソケット (SCTPなど)
SOCK_PACKET
廃止されてる

protocol には使用するプロトコルを指定します。

UDP通信をする場合は、0 or IPPROTO_UDP を指定しますが、歴史的な経緯もあり、ほぼ 0 指定で問題ないかと(通常各プロトコルファミリには一つのプロトコルが対応する)

 

ソケットをバインド

次に作成したソケットディスクリプタと実際のアドレス・ポートをバインド・・・結びつける作業を行います。

ソケットに名前を付けるとも言います。

UDP/IPでの通信を行う場合、基本的にはIPアドレスとポートの組み合わせで管理されているため、自分に複数のIPアドレスが登録されている場合、それぞれ個別のアプリケーションでバインドすることもできます。

# IPアドレスをanyアドレスとして、どんなアドレスでもOKでポートだけ指定する、という様な事もできます(むしろそっちの方が標準的w)

 

名前付けのルールはプロトコルファミリ(アドレスファミリ)毎に違います。

ここではUDP(IP)通信の場合のみの説明を行います・・・調査が面倒なので(ぉ

int bind ( int sockfd, struct sockaddr *my_addr, socklen_t addrlen );

成功時は 0 を、エラー時には -1 を返します。

WinSock では失敗時は SOCKET_ERROR です。

bind しようとしたポートが既に他のアプリケーションなどに使われていたりする場合が有りますが、その場合 bind は失敗します。

対処策は・・・そのアプリケーションを終了するか、ポートを変更するかしか有りません。

# その他、REUSEADDR とかあるけどとりあえず割愛

 

sockfd には先ほど作成したソケットディスクリプタを使用します。

my_addr にはバインドするポート情報を示すアドレス構造体を指定します。

IPv4の場合、このアドレス構造体は sockaddr_in, IPv6の場合、sockaddr_in6 を使用します。

 

addrlen には上記 my_addr のサイズ(sizeof( sockaddr_in ) or sizeof( sockaddr_in6 ))を指定します。

 

アドレス構造体の詳細に関しては 別途参照

 

バインドの必要性に関して

実は、これに関しては、実は単に送信したいだけ、という場合ならば必要なかったりします。

基本的には、OSのルーティング設定に応じて、適切な送信元IPアドレス/ポート番号を勝手に選んでくれるので、むしろ bind しない方が色々な面で楽です。

送信側のbindが必要な場合は結構特殊な状況かもしれませんw

ちなみに、IPアドレスあるいはポート番号だけ指定して、他はOS側に自動的に選択してもらう、という事も可能です。

その場合は my_addr のアドレス情報をそれぞれ 0 設定することで、IP未指定ならルーティング設定に応じたアドレスを、ポート未指定なら30000番台辺りで空いているポートを、それぞれOS側で自動的に割り振りをしてくれます。

 

送信

実際に送信する場合の関数は、まんま sendto ですw

また、ここでは説明しませんが、connect を使用することで send が使えたりします。

int sendto ( int sock, const void *msg, size_t len,
             int flags, const struct sockaddr *to, socklen_t tolen );

返値は送信したデータのバイト数。

エラー時には -1 が返ります(Winsock では、SOCKET_ERROR)。

 

sock にはソケットディスクリプタを指定します。

msg には送信したいデータのポインタを、len にはそのデータのサイズを指定します。

flags にはフラグを指定します。

MSG_OOB
帯域外データを送信する。
下位プロトコルで帯域外データをサポートしている必要がある。
MSG_DONTROUTE
ゲートウェイを使用せず、直接ネットワークに接続されているホストにだけ送る。
MSG_DONTWAIT
送信時にブロッキングを行わない。EAGAINが返った場合にはリトライする必要がある。
MSG_NOSIGNAL
(ストリーム型ソケットで)相手側の接続が切れた場合に SIGPIPE を発行しない。

to には送信先のアドレス構造体を、tolen にはそのサイズを指定します。

これに関しても bindの時 と同じです。

自分の情報は bind で名前付けをして、送信先は直接指定する・・・という感じですかね。

 

受信

送信と同様に、受信の関数は recvfrom と、そのまんまな名前です。

# sendtoの場合と同様に、connect することで recv 等が使えますが・・・その場合、connect された相手のパケットしかとれません。

int recvfrom ( int sock, void *buf, size_t len,
               int flags, struct sockaddr *from, socklen_t *fromlen );

返値は受信したデータのバイト数。

エラー時には -1 が返ります(Winsock では、SOCKET_ERROR)。

もし返値が 0 の場合が有れば、それもエラーの可能性が有ります。

 

sock にはソケットディスクリプタを指定します。

 

buf には受信データを格納するバッファのアドレス、len にはバッファのサイズを指定します。

バッファサイズよりも大きいパケットを受け取った場合にはパケットロスしてしまう可能性が有ります。

通常 UDP では(EthernetのMTUに併せて) 1500byte 程度で充分じゃないかなと(^^;

# 若しくは上位層プロトコルとして決めちゃうとか(Ethernet=下位層プロトコルなので・・・)

 

flags にはフラグを指定します。

MSG_OOB
通常のデータストリームで受信できない帯域外データの受信を行う。
MSG_PEEK
受信キューの最初のデータを返すときに、キューからデータを削除しない。
MSG_WAITALL
要求したバッファが一杯になるまで操作を停止する。
MSG_NOSIGNAL
(ストリーム型ソケットで)相手側の接続が切れた場合に SIGPIPE を発行しない。
MSG_TRUNC
パケットが渡したバッファよりも長かった場合も、その長さを返す。(もしかして、切りつめられる?
packet ソケットのみ有効。
MSG_ERRQUEUE
キューに入れられたエラーがソケットのエラーキューから取り出せるようになる。

from にはアドレス構造体のポインタを、fromlen にはそのサイズを指定する socklen_t のポインタを指定します。

(WinSockでは、socklen_t は無いので int です。)

受信後には fromlen に設定した変数に、実際にコピーされた from のサイズが入ります。

# 詳細には 別途参照 してください。

 

ソケットの破棄

最期に、ソケットが不必要になったら、close を呼ぶことでソケットディスクリプタを破棄します。

(WinSockの場合、closesocket を使用します。CloseHandleじゃないらしい。ハンドルなのに。)

int close ( int fd );

この辺りは普通にファイルと同じですね。

返値は成功時 0、エラー時は -1 です。

 

WinSock の場合

まず、socket 関数を呼ぶ前に、WinSock の初期化を行う必要が有ります。

int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );

これを行わないと、socket 関数なども失敗します。

返値は成功時 0、エラー時はそれ以外です。

 

wVersionRequested ・・・長いな(笑)・・・には、WinSock のバージョンを WORD にして入れます。

例えば、WinSock2.0 を使用するには MAKEWORD(2, 0) などとします。

 

lpWSAData には、WinSock の詳細情報を受け取る、WSADATA 構造体のアドレスを指定します。

typedef struct WSAData {
  WORD              wVersion;
  WORD              wHighVersion;
  char              szDescription[WSADESCRIPTION_LEN+1];
  char              szSystemStatus[WSASYS_STATUS_LEN+1];
  unsigned short    iMaxSockets;
  unsigned short    iMaxUdpDg;
  char FAR         *lpVendorInfo;
} WSADATA, *LPWSADATA;

とりあえず家の環境では(自作機, Win2000 SP2)、以下の様になりました。

wVersion       : '2'
wHighVersion   : '514'
szDescription  : "WinSock 2.0"
szSystemStatus : "Runing"
iMaxSockets    : '0'
iMaxUdpDg      : '0'
lpVendorInfo   : "(null)"

また、終了時にWinSock の破棄を行う必要が有ります。

int WSACleanup ( void );

エラーの取得や、エラーコードの設定を行うことも可能です。 使い方は・・・GetLastError, SetLastError と同じ様な感じです。

int WSAGetLastError ( void );
void WSASetLastError ( int iError );

 

サンプルコード

UDPで送受信を行うコードを埋め込んだサンプルです。

ここではサンプルとして送信・受信をしていますが・・・

自分で出したパケットを自分で受け取れる訳ではないので、対向側に受信→送信を行う物を準備する必要有りかも(^^;

/*
 *  UDP送受信サンプル
 *    Copyright (C) 1999-2005 ksworks
 */
#if !defined _WIN32 // 適当な機種依存回避
typedef int                 SOCKET
#  define INVALID_SOCKET    (-1)
#  define SOCKET_ERROR      (-1)
#  define closesocket(s)    close(s)
#endif // !defined _WIN32

int result;
int mysock;

char *local_port = "10000";                 // ポートは適当
char *peer_port = "10001";                  // こっちも適当
char *peer_node = "localhost";              // 今回はlocalhost内部で帰結させる

struct addrinfo *local_addr = NULL;         // 自ポート情報取得
struct addrinfo *peer_addr = NULL;          // 対応ポート情報取得
struct addrinfo hint = { 0 };               // アドレス取得指定
struct sockaddr_storage rcvaddr = { 0 };    // 受信アドレス情報バッファ
socklen_t addrlen = sizeof( rcvaddr );      // rcvaddr データサイズ

char *snd_dat = "ほげほげほげほげほげほげほげほげ"; // 送信データ
char rcvbuf[1500] = "";                             // 受信バッファ


// 自ポート情報取得
hint.ai_flags    = AI_PASSIVE;
hint.ai_family   = PF_UNSPEC;
hint.ai_socktype = SOCK_DGRAM;  // addrinfo でサービス名も取得する場合必須
hint.ai_protocol = 0;

result = getaddrinfo ( NULL, local_port, &hint, &local_addr );
if ( result != 0 )
{
    return 1;
}

// 対向側アドレス構造体の準備
hint.ai_flags    = 0;   // hint は flags のみ書き換えて再利用する
result = getaddrinfo ( peer_node, peer_port, &hint, &peer_addr );
if ( result != 0 )
{
    return 2;
}

// ソケットの初期化
mysock = socket ( local_addr->ai_family,
                  local_addr->ai_socktype,
                  local_addr->ai_protocol );
if ( mysock == INVALID_SOCKET )
{
    return 3;
}

// アドレス・ポートをバインド
result = bind ( mysock, local_addr->ai_addr, local_addr->ai_addrlen );
if ( result == SOCKET_ERROR )
{
    return 4;
}

// UDP送信
result = sendto ( mysock, snd_dat, strlen( snd_dat ), 0,
                  peer_addr->ai_addr, peer_addr->ai_addrlen );
if ( result <= 0 )
{
    return 5;
}

// UDP受信
result = recvfrom ( mysock, mybuffer, sizeof(mybuffer), 0,
                    (struct sockaddr*)&svraddr, &addrlen );
if ( result <= 0 )
{
    return 6;
}

// 後片付け
freeaddrinfo ( local_addr );
freeaddrinfo ( peer_addr );
closesocket ( mysock );

return result;

ネタ元

** 履歴