最初に、動機を説明するための少しの背景:私は非常に単純なselect()ベースのTCP "ミラープロキシ"に取り組んでいます。これにより、ファイアウォールで接続された2つのクライアントが互いに間接的に通信できます。両方のクライアントがこのサーバーに接続し、両方のクライアントが接続されるとすぐに、クライアントAがサーバーに送信したすべてのTCPバイトは、クライアントBに転送され、逆も同様です。
これは多少なりとも動作しますが、クライアントAがサーバーに接続し、クライアントBが接続する前にデータの送信を開始した場合、サーバーにはデータを置く場所がありません。大量のRAMを使用してしまう可能性があるため、RAMにバッファリングしたくありません。また、クライアントBが必要とする可能性があるため、単にデータをドロップしたくありません。そこで、クライアントBが接続するまで、クライアントAのソケットでselect()-for-read-readyを行わない3番目のオプションに進みます。そうすれば、クライアントAはすべての準備が整うまでブロックします。
多かれ少なかれ機能しますが、クライアントAのソケットでread-for-read-readyを選択しないことの副作用は、クライアントAがサーバーへのTCP接続を閉じることを決定した場合、サーバー少なくとも、クライアントBが来て、サーバーが最終的にクライアントAのソケットで読み取り準備を選択し、保留中のデータを読み取り、その後、ソケットがクローズされた通知を取得するまで、その事実について通知されません。 recv()は0を返します)。
クライアントAがTCP接続を閉じたときにサーバーが何らかの方法で(タイムリーに)知る方法があれば、それを好むでしょう。これを知る方法はありますか?その場合(たとえば、select()を1分に1回起動して、すべてのソケットでIsSocketStillConnected(sock)を呼び出すことができます(そのような関数が存在する場合))。
私の質問への答えは「いいえ、あなたがTCPスタックを必要なプライベートソケット状態情報にアクセスするために喜んで変更できない限りではない」です」と思われます。
それができないので、私の解決策は、すべてのクライアントからデータを常に読み取るようにプロキシサーバーを再設計し、パートナーがまだ接続していないクライアントから到着したデータを破棄することでした。これは最適ではありません。これは、プロキシを通過するTCPストリームが、TCPを使用するプログラムが期待する信頼性の高い順序配信のストリームのようなプロパティを持たないことを意味しますが、私の目的で十分です。
ソケットがデータではなく実際に閉じられているかどうかを確認したい場合は、recv()
にMSG_PEEK
フラグを追加して、データが到着したか、0
またはエラーが発生するかを確認できます。
/* handle readable on A */
if (B_is_not_connected) {
char c;
ssize_t x = recv(A_sock, &c, 1, MSG_PEEK);
if (x > 0) {
/* ...have data, leave it in socket buffer until B connects */
} else if (x == 0) {
/* ...handle FIN from A */
} else {
/* ...handle errors */
}
}
何らかのデータを送信した後にAが閉じたとしても、プロキシはおそらくFINをBに転送する前にまずそのデータをBに転送したいので、すべてのデータを読み取った後よりも早くAが接続でFINを送信したことを知る意味はありません送信しました。
A TCP接続は、両側がFINを送信するまで閉じられたとは見なされません。ただし、Aがそのエンドポイントを強制的にシャットダウンした場合、その上でデータを送信しようとした後、 EPIPE
を受け取ります(SIGPIPE
を抑制したと仮定します)。
ミラープロキシアプリケーションをもう少し読んだ後、これはファイアウォールトラバーサルアプリケーションであるため、これらのピアが実際に相互に通信できることを確認できる小さな制御プロトコルが実際に必要なようです。制御プロトコルがある場合、利用可能な多くのソリューションがありますが、私が提唱するのは、接続の1つがそれ自体をサーバーとして記述し、他の接続がそれ自体をクライアントとして記述することです。その後、接続を確立するサーバーが存在しない場合、クライアントの接続をリセットできます。サーバーはクライアント接続をタイムアウトまで待つことができます。サーバーはデータを開始しないでください。クライアントが接続されていない場合は、サーバー接続をリセットできます。これにより、切断された接続のデータをバッファリングする問題が解消されます。
あなたが見ているように、私は問題を見ません。 Aがサーバーに接続してデータを送信して閉じると仮定します。メッセージを戻す必要はありません。サーバーは、サーバーAがソケットAを読み取り、データをBに送信すると、Bが接続するまでデータを読み取りません。最初の読み取りは、Aが送信したデータを返し、2番目はソケットがいずれの場合も0または-1を返しますclosed、server closeB。Aが大量のデータを送信すると仮定します。Aのsend()メソッドは、サーバーが読み取りを開始してバッファを消費するまでブロックします。
0、1、2、11、22または-1を返すselectを持つ関数を使用します。
-1:1つ/両方のソケットが無効です
int WhichSocket(int sd1, int sd2, int seconds, int microsecs) {
fd_set sfds, efds;
struct timeval timeout={0, 0};
int bigger;
int ret;
FD_ZERO(&sfds);
FD_SET(sd1, &sfds);
FD_SET(sd2, &sfds);
FD_SET(sd1, &efds);
FD_SET(sd2, &efds);
timeout.tv_sec=seconds;
timeout.tv_usec=microsecs;
if (sd1 > sd2) bigger=sd1;
else bigger=sd2;
// bigger is necessary to be Berkeley compatible, Microsoft ignore this param.
ret = select(bigger+1, &sfds, NULL, &efds, &timeout);
if (ret > 0) {
if (FD_ISSET(sd1, &sfds)) return(1); // sd1 has data
if (FD_ISSET(sd2, &sfds)) return(2); // sd2 has data
if (FD_ISSET(sd1, &efds)) return(11); // sd1 has an error
if (FD_ISSET(sd2, &efds)) return(22); // sd2 has an error
}
else if (ret < 0) return -1; // one of the socket is not valid
return(0); // timeout
}
私にとっての解決策は、ソケットのステータスをポーリングすることでした。
Windows 10では、次のコードが機能しているように見えました(ただし、他のシステムにも同等の実装が存在するようです)。
WSAPOLLFD polledSocket;
polledSocket.fd = socketItf;
polledSocket.events = POLLRDNORM | POLLWRNORM;
if (WSAPoll(&polledSocket, 1, 0) > 0)
{
if (polledSocket.revents &= (POLLERR | POLLHUP))
{
// socket closed
return FALSE;
}
}
プロキシをプロトコルの汎用プロキシにする必要がある場合は、データを送信し、送信後すぐにclose
を呼び出すクライアントも処理する必要があります(一方向のデータ転送のみ)。
したがって、クライアントAがデータを送信し、Bへの接続が開かれる前に接続を閉じる場合、心配しないで、通常Bにデータを転送するだけです(Bへの接続が開かれるとき)。
このシナリオでは、特別な処理を実装する必要はありません。
プロキシは、次の場合に閉じられた接続を検出します。
read
は、Bへの接続が開かれ、Aからの保留中のデータがすべて読み取られた後にゼロを返します。または