問題
TCPを用いたサーバを作る場合、listen()するソケットとクライアントと送受信を行うソケットは別に出来る。
その為、epollやlibev, libuvなどを用いれば、クライアントの情報をまとめた構造体のリストなどを作る必要がなく、イベントが発生したクライアントの情報に直接アクセスが可能だ。
しかしUDPのサーバを作る場合、ネット上のサンプルを見るとlisten()というか待ち受けるソケットとクライアントと送受信を行うソケットが同じものしか出てこない。
この場合、クライアントの情報をまとめた構造体のリストを用意し、何か受信するたびにどのクライアントから受信したのかをリストから探さなければならない。
リストを全部探して見つからない場合にだけ、新規の通信だと認識出来る。
効率が悪いことこの上ない。
解決方法
connect() system callは、実はUDPにも使える。
良くあるのは、クライアント側の例だ。
UDPだけれども一か所しか通信しない場合、connect()を使うことで通信先を束縛し、その後の通信ではsend()やrecv()などの宛先を指定しないタイプのsystem callを使うことが出来る。
そしてこのconnect()はさらにサーバ側で使うと、TCPのサーバのように振る舞うことが可能なのだ。
- TCPのlisten()に相当するソケットでbind()してrecvfrom()をして待つ。
- recvfrom()したら、新たにソケットをsocket()で生成する。
- 新たなソケットに、listen()しているソケットと同じアドレスをbind()する。
- さらにrecvfrom()して取得した相手のアドレスに対し、connect()する。
- あとはTCPと同じように、新たなソケットに対しsend()/recv()などで送受信を行う。
新しいソケットを生成してbind()し、connect()するだけでTCPのサーバと同じように振る舞うのだ。
ただし、すべてのソケットで
const int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&on, (socklen_t)sizeof(on));
を行う必要がある。
適用例
UDPサーバの良くある例の一つであるechoサーバのように1リクエスト1レスポンスで処理が終了してしまうものにはこの実装はあまり必要がない。
クライアントと何回か送受信を行い、また同時に複数の接続先と通信が発生するような状況で威力を発揮する。
OpenVPNのようなVPNサーバに使ったり、MQTTのサーバに使ったり。
サンプル
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>
int accept_handle(int cfd, struct sockaddr_in *local)
{
struct sockaddr_in remote;
socklen_t remote_len;
char buff[128];
int fd;
int err;
const int on = 1, off = 0;
remote_len = sizeof(remote);
err = recvfrom(cfd, buff, sizeof(buff), 0, (struct sockaddr *)&remote, &remote_len);
if (err < 0) {
goto out;
}
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
goto out;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&on, (socklen_t)sizeof(on));
err = bind(fd, (struct sockaddr *)local, sizeof(*local));
if (err != 0) {
goto out;
}
err = connect(fd, (struct sockaddr *)&remote, remote_len);
if (err != 0) {
goto out;
}
printf("accepted connection from fd=%d %s:%d\n", fd, inet_ntop(AF_INET, &remote.sin_addr, buff, INET6_ADDRSTRLEN), ntohs(remote.sin_port));
send(fd, buff, sizeof(buff), 0);
return fd;
out:
printf("%s(): error occurred errno=%d\n", __func__, errno);
return -1;
}
int main(void)
{
int err;
struct sockaddr_in local;
struct sockaddr_in remote;
socklen_t remote_len;
int fd;
fd_set rfds;
int fds[10] = { -1 };
size_t i = 0;
const int on = 1, off = 0;
size_t loop_max = sizeof(fds) / sizeof(fds[0]);
int maxfd = -1;
char buff[128];
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
goto out;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&on, (socklen_t)sizeof(on));
inet_pton(AF_INET, "0.0.0.0", &local.sin_addr);
local.sin_port = htons(28385);
err = bind(fd, (struct sockaddr *)&local, sizeof(local));
if (err != 0) {
goto out;
}
for (i = 0; i < loop_max; ++i) {
fds[i] = -1;
}
while (1) {
maxfd = fd;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
for (i = 0; i < loop_max; ++i) {
if (fds[i] != -1) {
FD_SET(fds[i], &rfds);
if (maxfd < fds[i]) {
maxfd = fds[i];
}
}
}
err = select(maxfd + 1, &rfds, NULL, NULL, NULL);
if (err < 0) {
goto out;
}
for (i = 0; i < loop_max; ++i) {
if (fds[i] != -1 && FD_ISSET(fds[i], &rfds)) {
remote_len = sizeof(remote);
err = recv(fds[i], buff, sizeof(buff), 0);
if (err < 0) {
printf("close from %d\n", fds[i]);
fds[i] = -1;
}
else {
printf("recvfrom %d\n", fds[i]);
send(fds[i], buff, sizeof(buff), 0);
}
}
}
if (FD_ISSET(fd, &rfds)) {
for (i = 0; i < loop_max; ++i) {
if (fds[i] == -1) {
fds[i] = accept_handle(fd, &local);
break;
}
}
}
}
return 0;
out:
return -1;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
int main(void)
{
int err;
struct sockaddr_in remote;
socklen_t remote_len;
int fd;
fd_set rfds;
int fds[10] = { -1 };
size_t i = 0;
const int on = 1, off = 0;
size_t loop_max = sizeof(fds) / sizeof(fds[0]);
int maxfd = -1;
char buff[128];
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
goto out;
}
inet_pton(AF_INET, "127.0.0.1", &remote.sin_addr);
remote.sin_port = htons(28385);
err = connect(fd, (struct sockaddr *)&remote, sizeof(remote));
if (err != 0) {
goto out;
}
printf("connected\n");
sendto(fd, buff, sizeof(buff), 0, (struct sockaddr *)&remote, sizeof(remote));
recv(fd, buff, sizeof(buff), 0);
sendto(fd, buff, sizeof(buff), 0, (struct sockaddr *)&remote, sizeof(remote));
recv(fd, buff, sizeof(buff), 0);
return 0;
out:
printf("error occurred\n");
return -1;
}