accept() は、クライアントからの新しい接続を受け入れるために常に別のファイル記述子を作成するように定義されていますが、1 つのクライアントと 1 つの接続のみを受け入れることが事前にわかっている場合、わざわざ新しいファイル記述子を作成する必要はありません。なぜこれが定義された標準に当てはまるのかについての説明はありますか?
6 に答える
API を設計するとき、一般的であることには価値があると思います。2 つの API があるのはなぜですか? 1 つは潜在的に複数の接続を受け入れるためのもので、もう 1 つはより少ないファイル記述子を使用するためのものです。後者のケースは、完全に新しいシステムコールを正当化するほど優先度が高くないように思われます。現在の API で十分であり、それを使用して必要な動作をうまく実装できる場合です。
一方、Windows にはAcceptEx
、以前は関連のない、以前に接続されたソケットを表していた以前のソケット ハンドルを再利用できる機能があります。これは、ソケットが切断された後にソケットを閉じるためにカーネルに再度入ることによるパフォーマンスの低下を避けるためだと思います。あなたが説明しているものとは正確には異なりますが、漠然と似ています。(ただし、縮小ではなく拡大することを意図しています。)
更新: 1 か月後、これに報奨金を設定したのは少し奇妙だと思います。答えは明らかだと思います - 現在のインターフェースはあなたが求めることをうまくやってのけることができます。現在のインターフェイスを使用すると、成功したclose
後に元のソケットを使用でき、accept
誰にも害はありません。
RFC 793で説明されている TCP プロトコルでは、ソケットと接続という用語について説明しています。ソケットは、IP アドレスとポート番号のペアです。接続はソケットのペアです。この意味で、同じソケットを複数の接続に使用できます。この意味でsocket
渡されたものaccept()
は使われている。ソケットは複数の接続に使用でき、渡さsocket
れたaccept()
はそのソケットを表すため、API は接続socket
を表す新しい を作成します。
作成するソケットが呼び出しに使用したソケットと同じでsocket
あることを確認する簡単な方法が必要な場合は、ラッパー FTW を使用します。accept()
accept()
int accept_one (int accept_sock, struct sockaddr *addr, socklen_t *addrlen) {
int sock = accept(accept_sock, addr, addrlen);
if (sock >= 0) {
dup2(sock, accept_sock);
close(sock);
sock = accept_sock;
}
return sock;
}
socket
クライアントとサーバーが相互に接続する方法が必要な場合は、各側に1 つしか作成せずに、そのような API が存在します。API はで、同時オープンconnect()
を達成すると成功します。
static struct sockaddr_in server_addr;
static struct sockaddr_in client_addr;
void init_addr (struct sockaddr_in *addr, short port) {
struct sockaddr_in tmp = {
.sin_family = AF_INET, .sin_port = htons(port),
.sin_addr = { htonl(INADDR_LOOPBACK) } };
*addr = tmp;
}
void connect_accept (int sock,
struct sockaddr_in *from, struct sockaddr_in *to) {
const int one = 1;
int r;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
bind(sock, (struct sockaddr *)from, sizeof(*from));
do r = connect(sock, (struct sockaddr *)to, sizeof(*to)); while (r != 0);
}
void do_peer (char *who, const char *msg, size_t len,
struct sockaddr_in *from, struct sockaddr_in *to) {
int sock = socket(PF_INET, SOCK_STREAM, 0);
connect_accept(sock, from, to);
write(sock, msg, len-1);
shutdown(sock, SHUT_WR);
char buf[256];
int r = read(sock, buf, sizeof(buf));
close(sock);
if (r > 0) printf("%s received: %.*s%s", who, r, buf,
buf[r-1] == '\n' ? "" : "...\n");
else if (r < 0) perror("read");
}
void do_client () {
const char msg[] = "client says hi\n";
do_peer("client", msg, sizeof(msg), &client_addr, &server_addr);
}
void do_server () {
const char msg[] = "server says hi\n";
do_peer("server", msg, sizeof(msg), &server_addr, &client_addr);
}
int main () {
init_addr(&server_addr, 4321);
init_addr(&client_addr, 4322);
pid_t p = fork();
switch (p) {
case 0: do_client(); break;
case -1: perror("fork"); exit(EXIT_FAILURE);
default: do_server(); waitpid(p, 0, 0);
}
return 0;
}
代わりに、パフォーマンスの問題を心配している場合、その心配は見当違いだと思います。TCP プロトコルを使用すると、クライアントとサーバー間のネットワーク上で少なくとも 1 回の完全な往復を待機する必要があるため、別のソケットを処理するための余分なオーバーヘッドは無視できます。そのオーバーヘッドを気にする可能性があるのは、クライアントとサーバーが同じマシン上にある場合ですが、その場合でも、接続が非常に短時間しか存続しない場合にのみ問題になります。接続の寿命が非常に短い場合は、ソリューションを再設計して、安価な通信媒体 (共有メモリなど) を使用するか、データにフレーミングを適用して永続的な接続を使用することをお勧めします。
必須ではないからです。クライアントが1つしかない場合は、操作を1回だけ実行します。余裕のあるファイル記述子がたくさんあります。また、ネットワークオーバーヘッドと比較すると、「オーバーヘッド」はほとんどありません。APIデザイナーとして「最適化」したいのは、何千ものクライアントがある場合です。
listen によって返されたソケットと accept によって返されたソケット記述子の間で唯一異なる点は、新しいソケットが LISTEN 状態ではなく ESTABILISHED 状態にあることです。したがって、listen 関数を呼び出した後に作成されたソケットを再利用できます。他の接続を受け入れます。
答えは、正確に 1 つの接続の特定の例が現在の APIで処理され、最初から API のユース ケースに合わせて設計されているということです。単一ソケットのケースがどのように処理されるかについての説明は、BSD ソケット インターフェイスが最初に発明されたときにソケット プログラムが動作するように設計された方法にあります。
ソケット API は、常に接続を受け入れることができるように設計されています。基本的な原則は、接続が到着したときに、接続を受け入れるかどうかをプログラムが最終決定する必要があるということです。ただし、アプリケーションは、この決定を行う際に接続を見逃すことがあってはなりません。したがって、API は並列処理のみを目的として設計されており、accept()
から別のソケットを返すように指定されていたため、受信したばかりの接続要求についてアプリケーションが決定を下している間、さらに接続要求をリッスンし続けることができましたlisten()
。listen()
これは基本的な設計上の決定であり、どこにも文書化されていません。ソケットプログラムが有用であるためには、そのように動作する必要があると想定されていました。
スレッドが発明される前の昔、Unix ライクなシステムでソケット サーバーを実装するために必要な並列処理は、fork()
. 新しい接続が受け入れられると、プログラムは を使用して 2 つの同一のコピーに分割されfork()
、1 つのコピーが新しい接続を処理し、元のコピーは着信接続の試行をリッスンし続けます。このfork()
モデルでaccept()
は、新しいファイル ハンドルを返しますが、厳密に 1 つの接続を処理するユース ケースがサポートされており、2 番目の「受け入れ」コピーが単一の接続を処理している間に、プログラムの「リッスン」コピーを終了させるだけで実現されました。
次の擬似コードはこれを示しています。
fd = socket();
listen(fd, 1); /* allow 1 unanswered connection in the backlog */
switch (fork())
{
case 0: break; /* child process; handle connection */
case -1: exit (1); /* error. exit anyway. */
default: exit (0); /* parent process; exit as only one connection needed */
}
/* if we get here our single connection can be accepted and handled.
*/
accept_fd = accept(fd);
このプログラミング パラダイムは、サーバーが 1 つの接続を受け入れる場合でも、複数の接続を処理するループにとどまる場合でも、どちらの場合もコードが実質的に同一であることを意味していました。最近では、代わりにスレッドがありfork()
ます。ただし、パラダイムは今日でもこれにとどまっているため、ソケット API を変更またはアップグレードする必要はありません。
accept() は、新しい client を受け入れるように設計されているためです。
そのポート番号でサービスを提供するために特定のポート番号にバインドする必要がある一般的なソケット記述子と、クライアント情報を格納するための構造体と、 client のサイズを格納するための別の int 値の 3 つが必要でした。
サーバーによって受け入れられた特定のクライアントにサービスを提供するための new_socket_descriptor を返します。
最初のパラメーターは、クライアントを受け入れるために使用されるソケット記述子です。同時実行サーバーの場合、常にクライアント接続を受け入れるために使用されます。そのため、accept() 呼び出しによって変更しないでください。
そのため、新しい接続されたクライアントにサービスを提供するために、accept() によって返された新しいソケット記述子。
サーバー ソケット記述子 (第 1 パラメーター) は、サーバー プロパティにバインドします。サーバー プロパティは常に、ポート番号、接続の種類、プロトコル ファミリのすべてが固定されている固定型に設計されています。したがって、同じファイル記述子が何度も使用されます。
もう 1 つのポイントは、これらのプロパティを使用して、特定のサーバー用に作成されたクライアント接続をフィルタリングすることです。
クライアントの場合、クライアントごとに一意のクライアントごとに異なる最小 IP アドレスに関する情報が使用され、これらのプロパティは新しいファイル記述子にバインドされるため、accept() 関数によって常に新しいファイル記述子が返されます。
ノート:-
つまり、クライアントが受け入れるには1つのファイル記述子が必要であり、受け入れる/提供するクライアントの最大数に応じて、クライアントにサービスを提供するためにその量のファイル記述子を使用します。