1

サーバー(192.168.1.5:3001)は、Linux 3.2を実行しており、一度に1つの接続のみを受け入れるように設計されています。クライアント(192.168.1.18)は、Windows7を実行しています。接続はワイヤレス接続です。どちらのプログラムもC++で記述されています。

これは、10回の接続/切断サイクルで9回うまく機能します。10番目の(ランダムに発生する)接続では、サーバーが接続を受け入れ、後で実際に書き込みを行うと(通常、30秒以上後)、Wireshark(スクリーンショットを参照)によると、古い古い接続に書き込みを行っているように見えます。クライアントが(しばらく前に)FINしたポート番号を使用しますが、サーバーはまだFINしていません。そのため、クライアントとサーバーの接続が同期していないように見えます。クライアントは新しい接続を確立し、サーバーは前の接続への書き込みを試みます。この壊れた状態になると、後続のすべての接続試行は失敗します。壊れた状態は、最大ワイヤレス範囲を30分超えることで開始できます(10の場合の9の前のように、これは機能しますが、壊れた状態が発生する場合があります)。

リンクの背後にあるWiresharkのスクリーンショット

スクリーンショットの赤い矢印は、サーバーがデータの送信を開始したとき(Len!= 0)を示しています。これは、クライアントがデータを拒否し、サーバーにRSTを送信した時点です。右端にある色付きの点は、使用されているクライアントポート番号ごとに1つの色を示しています。その色の残りのドットがあった後、1つまたは2つのドットがどのように表示されるかに注意してください(そして時間列に注意してください)。

サーバープロセスを強制終了して再起動すると、(次に発生するまで)自動的に解決されるため、問題はサーバー側にあるように見えます。

うまくいけば、コードはあまりにも異常ではありません。listen()のキューサイズパラメータを0に設定しました。これは、現在の接続を1つだけ許可し、保留中の接続を許可しないことを意味すると思います(代わりに、1を試しましたが、問題はまだありました)。コード内で「//error」が表示されているトレースプリントとして表示されるエラーはありません。

// Server code

mySocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (mySocket == -1)
{
  // error
}

// Set non-blocking
const int saveFlags = ::fcntl(mySocket, F_GETFL, 0);
::fcntl(mySocket, F_SETFL, saveFlags | O_NONBLOCK);

// Bind to port

// Union to work around pointer aliasing issues.
union SocketAddress
{
  sockaddr myBase;
  sockaddr_in myIn4;
};

SocketAddress address;
::memset(reinterpret_cast<Tbyte*>(&address), 0, sizeof(address));
address.myIn4.sin_family = AF_INET;
address.myIn4.sin_port = htons(Port);
address.myIn4.sin_addr.s_addr = INADDR_ANY;
if (::bind(mySocket, &address.myBase, sizeof(address)) != 0)
{
  // error
}
if (::listen(mySocket, 0) != 0)
{
  // error
}


// main loop
{
  ...
  // Wait for a connection.
  fd_set readSet;
  FD_ZERO(&readSet);
  FD_SET(mySocket, &readSet);
  const int aResult = ::select(getdtablesize(), &readSet, NULL, NULL, NULL);
  if (aResult != 1)
  {
    continue;
  }
  // A connection is definitely waiting.
  const int fileDescriptor = ::accept(mySocket, NULL, NULL);
  if (fileDescriptor == -1)
  {
    // error
  }

  // Set non-blocking
  const int saveFlags = ::fcntl(fileDescriptor, F_GETFL, 0);
  ::fcntl(fileDescriptor, F_SETFL, saveFlags | O_NONBLOCK);

  ...
  // Do other things for 30+ seconds.
  ...
  const int bytesWritten = ::write(fileDescriptor, buffer, bufferSize);
  if (bytesWritten < 0)
  {
    // THIS FAILS!! (but succeeds the first ~9 times)
  }

  // Finished with the connection.
  ::shutdown(fileDescriptor, SHUT_RDWR);
  while (::close(fileDescriptor) == -1)
  {
    switch(errno)
    {
    case EINTR:
      // Break from the switch statement. Continue in the loop.
      break;
    case EIO:
    case EBADF:
    default:
      // error
      return;
    }
  }
}

したがって、accept()呼び出し(SYNパケットが送信される時点であると想定)とwrite()呼び出しの間のどこかで、クライアントのポートが以前に使用されたクライアントポートに変更されます。

したがって、問題は、サーバーが接続を受け入れて(したがって、ファイル記述子を開き)、以前の(現在は古くて死んでいる)接続/ファイル記述子を介してデータを送信するのはどうしてですか?欠落しているシステムコールに何らかのオプションが必要ですか?

4

2 に答える 2

1

まだ完成していませんが、コメントでわかったことをまとめた回答を提出しています。重要なポイントはカバーしていると思います。

クライアントを一度に1つずつ処理するサーバーがあります。接続を受け入れ、クライアント用にデータを準備し、データを書き込み、接続を閉じます。問題は、データの準備手順が、クライアントが待機するよりも長くかかる場合があることです。サーバーがデータの準備でビジー状態になっている間、クライアントはあきらめます。

クライアント側では、ソケットが閉じられると、FINが送信され、クライアントに送信するデータがなくなったことをサーバーに通知します。これで、クライアントのソケットはFIN_WAIT1状態になります。

サーバーはFINを受信し、ACKで応答します。(ACKは、ユーザースペースプロセスの助けを借りずにカーネルによって実行されます。)サーバーソケットはCLOSE_WAIT状態になります。ソケットは読み取り可能になりましたが、サーバープロセスはデータ準備フェーズでビジーであるため、気づきません。

クライアントはFINのACKを受信し、FIN_WAIT2状態になります。クライアントコードを表示していないので、クライアントのユーザースペースで何が起こっているのかわかりませんが、それは重要ではないと思います。

サーバープロセスは、ハングアップしたクライアントのデータをまだ準備しています。それは他のすべてに気づいていません。その間に、別のクライアントが接続します。カーネルはハンドシェイクを完了します。この新しいクライアントは、しばらくの間サーバープロセスから注意を引くことはありませんが、カーネルレベルでは、2番目の接続が両端で確立されます。

最終的に、サーバーのデータ準備(最初のクライアント用)が完了します。write()を試みます。サーバーのカーネルは、TCPがその情報を通信しないため、最初のクライアントがデータを受信する意思がなくなったことを認識していません。したがって、書き込みは成功し、データが送信されます(wiresharkリストのパケット10711)。

クライアントはこのパケットを受け取り、サーバーが知らなかったことを知っているため、カーネルはRSTで応答します。クライアントソケットは読み取りと書き込みの両方ですでにシャットダウンされており、おそらく閉じられており、おそらくすでに忘れられています。

wiresharkトレースでは、サーバーは15バイトのデータのみをクライアントに送信したいと考えていたため、write()は正常に完了した可能性があります。しかし、サーバーがFINを送信するshutdown()とclose()を実行する機会を得る前に、RSTはすぐに到着しました。RSTが受信されると、サーバーはそのソケットでそれ以上パケットを送信しなくなります。これでshutdown()とclose()が実行されますが、オンザワイヤー効果はありません。

これで、サーバーは最終的に次のクライアントを受け入れる準備ができました()。それは別の遅い準備ステップを開始し、2番目のクライアントがすでにしばらく待っているため、スケジュールよりもさらに遅れています。クライアント接続の速度がサーバーが処理できる速度まで低下するまで、問題は悪化し続けます。

修正は、準備ステップ中にクライアントが電話を切ったときにサーバープロセスに通知し、すぐにソケットを閉じて次のクライアントに移動するようにする必要があります。それをどのように行うかは、データ準備コードが実際にどのように見えるかによって異なります。CPUにバインドされた大きなループの場合は、ソケットの定期的なチェックを挿入する場所を見つける必要があります。または、親プロセスがソケットを監視している間に、データの準備と書き込みを行う子プロセスを作成します。子プロセスが終了する前にクライアントが電話を切った場合は、子プロセスを強制終了します。他の解決策も可能です(F_SETOWNのように、ソケットで何かが発生したときにプロセスにシグナルを送信します)。

于 2012-07-03T02:12:56.527 に答える
0

ああ、成功!サーバーがクライアントのSYNを受信して​​おり、accept()が呼び出される前に、サーバーのカーネルが別のSYNとの接続を自動的に完了していたことがわかりました。したがって、確実にリスニングキューがあり、キューで2つの接続が待機していることが原因の半分でした。

原因の残りの半分は、質問から省略された情報に関係していました(上記の誤った仮定のために、それは無関係だと思いました)。プライマリ接続ポート(Aと呼びます)と、この質問のすべてに関する問題のあるセカンダリ接続ポート(Bと呼びます)がありました。適切な接続順序は、Aが接続を確立する(A1)、次にBが接続を確立しようとする(B1になる)... 200msの時間枠内(私はすでに何年も前に書かれた100msからタイムアウトを2倍にしたので)私は寛大だと思った!)。200ms以内にB接続が得られない場合は、A1をドロップします。したがって、B1はサーバーのカーネルとの接続を確立し、受け入れられるのを待ちます。A2が接続を確立する次の接続サイクルでのみ受け入れられ、クライアントもB2接続を送信します。サーバーはA2接続を受け入れ、次にBキューの最初の接続であるB1を取得します(まだ受け入れられていません。キューはB1、B2のように見えます)。これが、クライアントがB1を切断したときに、サーバーがB1のFINを送信しなかった理由です。したがって、サーバーの2つの接続はA2とB1であり、これらは明らかに同期していません。デッド接続であるB1に書き込もうとするため、A2とB1をドロップします。次に、次のペアはA3とB2で、これらも無効なペアです。サーバープロセスが強制終了され、TCP接続がすべてリセットされるまで、同期がとれていない状態から回復することはありません。したがって、サーバーの2つの接続はA2とB1であり、これらは明らかに同期していません。デッド接続であるB1に書き込もうとするため、A2とB1をドロップします。次に、次のペアはA3とB2で、これらも無効なペアです。サーバープロセスが強制終了され、TCP接続がすべてリセットされるまで、同期がとれていない状態から回復することはありません。したがって、サーバーの2つの接続はA2とB1であり、これらは明らかに同期していません。デッド接続であるB1に書き込もうとするため、A2とB1をドロップします。次に、次のペアはA3とB2で、これらも無効なペアです。サーバープロセスが強制終了され、TCP接続がすべてリセットされるまで、同期がとれていない状態から回復することはありません。

したがって、解決策は、Bソケットで待機するタイムアウトを200ミリ秒から5秒に変更することでした。何日も頭をかいていた(そしてスタックオーバーフローに置いてから24時間以内に修正した)このような単純な修正!また、メインのselect()呼び出しにソケットBを追加し、それをaccept()してすぐにclose()することで、漂遊B接続から回復させました(これは、B接続の確立に5秒以上かかった場合にのみ発生します)。 )。それをselect()に追加し、listen()バックログパラメーターがヒントであるというパズルのピースを追加することを提案してくれた@AlanCurryに感謝します。

于 2012-07-03T07:24:47.850 に答える