TCPなら1度接続が確立すれば、別の通信ポートを確保するなどをしない限りは、送信元を意識しなくてもいいが、
UDPでは1つのソケットでやり取りしなければならないので、1ソケットにつき1つの送信元情報しか持つことは出来ない。
なのでrecvとsendだけなら、決まった相手と決まったポートへの静的な応答しかできず、
不特定多数が利用するとなった場合の動的な応答に対応することが出来ない。
UDPプロトコルの規格で送信元IPとポートは含まれているので、取得できるはずなのだが、
調べ方が下手なのか、UDP通信時の送信元IPとポートを取得する手段についての、日本語での解説が非常に少ない。
おかげでGoogleで数時間もさまようことにry
調べてみたらrecvfromを使えばいいだけだったっていう。
winsock2を使ったwindows向けの実装。
client.c
#include <stdio.h> #include <winsock2.h> #define SERVICE_PORT 50001 typedef unsigned char BYTE; int main(int argc, char **argv){ WSADATA wsaData; SOCKET sock = 0; struct sockaddr_in addr; BYTE send_buff[]="hello world!!"; char *send_addr = argc >= 2 ? argv[1] : "127.0.0.1"; if(WSAStartup(MAKEWORD(2,0), &wsaData) != 0){ return 0; } sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock == INVALID_SOCKET){ WSACleanup(); return 0; } addr.sin_family = AF_INET; addr.sin_port = htons(SERVICE_PORT); addr.sin_addr.S_un.S_addr = inet_addr(send_addr); if(sendto(sock, send_buff, sizeof(send_buff), 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) == SOCKET_ERROR){ fprintf(stderr, "sendto Error\n"); shutdown(sock, SD_BOTH); closesocket(sock); WSACleanup(); return 0; } printf("send %s(%d) to %s:%d\n", send_buff,sizeof(send_buff), inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); printf("end \n"); return 0; }
指定したサーバ:ポートへ"hello world!!"を送りつけるだけのプログラム。
第1引数にIPアドレスを指定すると、そのIPを送信先に使う。それに対するエラーチェックはない。
コンパイル時にはwinsock2ライブラリの-lw32_2をつける。
server.c
#include <stdio.h> #include <winsock2.h> #define SERVICE_PORT 50001 typedef unsigned char BYTE; int main(int argc, char **argv){ WSADATA wsaData; SOCKET sock; struct sockaddr_in addr; struct sockaddr_in from; BYTE recv_buff[512]; int recv_len = 0; int sockaddr_in_size = sizeof(struct sockaddr_in); if(WSAStartup(MAKEWORD(2,0), &wsaData) != 0){ return 0; } sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock == INVALID_SOCKET){ fprintf(stderr, "socker Error\n"); WSACleanup(); return 0; } addr.sin_family = AF_INET; addr.sin_port = htons(SERVICE_PORT); addr.sin_addr.S_un.S_addr = INADDR_ANY; if(bind(sock, (struct sockaddr *)&addr, sizeof(addr)) == SOCKET_ERROR){ fprintf(stderr, "bind Error\n"); shutdown(sock , SD_BOTH); closesocket(sock); WSACleanup(); return 0; } printf("risten port: %d ...\n", ntohs(addr.sin_port)); // ずっと待つ while(1){ recv_len = recvfrom(sock, recv_buff, 512-1, 0, (struct sockaddr *)&from, &sockaddr_in_size); recv_buff[recv_len-1] = 0; printf("recv: '%s'(%d) from %s:%d\n", recv_buff, recv_len, inet_ntoa(from.sin_addr), ntohs(from.sin_port)); } return 0; }
今回のメイン。
受信にrecvではなくrecvfromを使って構造体sockaddr_inのアドレスを渡して取得する。
コンパイル時にはwinsock2ライブラリの-lw32_2をつける。
サーバ側の結果
risten port: 50001 ...
recv: 'hello world!!'(14) from 127.0.0.1:3378
recv: 'hello world!!'(14) from 192.168.1.2:3399
recv: 'hello world!!'(14) from 192.168.1.5:1055
192.168.1.2にserverがおいてあり、
server上でclientを実行、
メインPC(192.168.1.2)から"client 192.168.1.2"を実行、
ネットブック(192.168.1.5)から"client 192.168.1.2"を実行、
の順番で実行した時の結果。
一つのsocketから複数のIPとポート番号がでたらめに取得できている。
memo
不特定多数との双方向通信をサポートするなら、recvfromを使い、送信元の情報を得る。
TCPと違い切断検知の仕組みがないので、自分で実装する必要がある。
例えば生存パケットを送りつけて送信先からそれに対するリアクションがなかったら、通信できない相手として除外するなどの判定をする。
recvfrom関数の第4引数、第5引数はNULLにすることでrecvと同様に扱える。
recvfrom関数内はデータが受け取れるまで待つので、このままでは同期通信になってしまう。
非同期通信にしたい場合はマルチスレッドにするとか、FD_SETとかFD_ISSETとかselectを使って、ソケットにデータが届いているか確認してからrecvfromを呼び出す等して対処できる。
初期化などする必要がある部分があるかもしれない。
inet_ntoa()関数の返り値はinet_ntoaで定義された静的なバッファのアドレスなので、
複数回呼び出すと前に持っていた情報は消えてしまう。
なので記憶する場合はmemcpy等で記憶するか、構造体in_addrでsin_addrのコピーを取る。
ショートパケットについても調べないとなぁ。
英語ができない男の人って・・・
サーバ側で待ちうけポートを示すメッセージを出力するとき、
"Listen"と書くところを"Risten"と書いてた。しにたい。