6

以下は、問題を示すC#プログラムです。

サーバーはソケットのリッスンを開始します。クライアントはサーバーに接続し、メッセージを送信し、Shutdown(SocketShutdown.Send)を使用して接続の送信半分を閉じ、サーバーにメッセージの終わりがどこにあるかを知らせ、サーバーからの応答を待ちます。サーバーはメッセージを読み取り、長い計算(ここではスリープ呼び出しでシミュレート)を実行し、クライアントにメッセージを送信して、接続を閉じます。

Windowsでは、タイムアウトが発生しても、クライアントの受信コールは常に正確に2分後に失敗し、「接続されたパーティが一定時間後に適切に応答しなかったために接続が失敗したか、接続されたホストが応答しなかったために接続が確立されませんでした」と表示されます。無限に設定します。

LinuxでMonoを使用してプログラムを実行すると、「長時間の操作」を10分に設定してもタイムアウトは発生しませんが、Monoまたは.NETのどちらで実行した場合でもWindowsで発生します。タイムアウトを1秒に設定すると、1秒後にタイムアウトになります。つまり、設定したタイムアウトまたは2分のいずれか短い方でタイムアウトになります。

サーバーがクライアントにメッセージを送信し、クライアントからサーバーへのメッセージがなく、ハーフクローズがない同様のサンプルプログラムは、タイムアウトなしで期待どおりに機能します。

これを回避するには、プロトコルを変更して、メッセージが完了したことをサーバーに示す他の方法を使用します(おそらく、メッセージの前にメッセージの長さを付けます)。しかし、私はここで何が起こっているのか知りたいです。タイムアウトが無限に設定されているときに、Socket.Receiveがハーフクローズ接続でタイムアウトするのはなぜですか?

私が理解していることから、送信の半分だけが閉じられた接続は、データを無期限に受信し続けることができるはずです。Windowsのこのような基本的な部分にバグがある可能性は低いようです。私は何か間違ったことをしていますか?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // Start server thread
            Thread serverThread = new Thread(ServerStart);
            serverThread.IsBackground = true;
            serverThread.Start();

            // Give the server some time to start listening
            Thread.Sleep(2000);

            ClientStart();
        }

        static int PortNumber = 8181;

        static void ServerStart()
        {
            TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, PortNumber));
            listener.Start();
            while (true)
            {
                TcpClient client = listener.AcceptTcpClient();
                Task connectionHandlerTask = new Task(ConnectionEntryPoint, client);
                connectionHandlerTask.Start();
            }
            listener.Stop();
        }

        static void ConnectionEntryPoint(object clientObj)
        {
            using (TcpClient client = (TcpClient)clientObj)
            using (NetworkStream stream = client.GetStream())
            {
                // Read from client until client closes its send half.
                byte[] requestBytes = new byte[65536];
                int bufferPos = 0;
                int lastReadSize = -1;
                while (lastReadSize != 0)
                {
                    lastReadSize = stream.Read(requestBytes, bufferPos, 65536 - bufferPos);
                    bufferPos += lastReadSize; 
                }
                client.Client.Shutdown(SocketShutdown.Receive);
                string message = Encoding.UTF8.GetString(requestBytes, 0, bufferPos);

                // Sleep for 2 minutes, 30 seconds to simulate a long-running calculation, then echo the client's message back
                byte[] responseBytes = Encoding.UTF8.GetBytes(message);
                Console.WriteLine("Waiting 2 minutes 30 seconds.");
                Thread.Sleep(150000);

                try
                {
                    stream.Write(responseBytes, 0, responseBytes.Length);
                }
                catch (SocketException ex)
                {
                    Console.WriteLine("Socket exception in server: {0}", ex.Message);
                }
            }
        }

        static void ClientStart()
        {
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // Set receive timeout to infinite.
                socket.ReceiveTimeout = -1;

                // Connect to server
                socket.Connect(IPAddress.Loopback, PortNumber);

                // Send a message to the server, then close the send half of the client's connection
                // to let the server know it has the entire message.
                string requestMessage = "Hello";
                byte[] requestBytes = Encoding.UTF8.GetBytes(requestMessage);
                socket.Send(requestBytes);
                socket.Shutdown(SocketShutdown.Send);

                // Read the server's response. The response is done when the server closes the connection.
                byte[] responseBytes = new byte[65536];
                int bufferPos = 0;
                int lastReadSize = -1;

                Stopwatch timer = Stopwatch.StartNew();
                try
                {
                    while (lastReadSize != 0)
                    {
                        lastReadSize = socket.Receive(responseBytes, bufferPos, 65536 - bufferPos, SocketFlags.None);
                        bufferPos += lastReadSize;
                    }

                    string responseMessage = Encoding.UTF8.GetString(responseBytes, 0, bufferPos);
                    Console.WriteLine(responseMessage);
                }
                catch (SocketException ex)
                {
                    // Timeout always occurs after 2 minutes. Why?
                    timer.Stop();
                    Console.WriteLine("Socket exception in client after {0}: {1}", timer.Elapsed, ex.Message);
                }
            }
        }
    }
}

次のプログラムは、socket.Shutdown(SocketShutdown.Send)を使用してメッセージの終了を通知するのではなく、メッセージの前に4バイトのメッセージ長を付けます。このプログラムではタイムアウトは発生しません。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Threading;

namespace WithoutShutdown
{
    class Program
    {
        static void Main(string[] args)
        {
            // Start server thread
            Thread serverThread = new Thread(ServerStart);
            serverThread.IsBackground = true;
            serverThread.Start();

            // Give the server some time to start listening
            Thread.Sleep(2000);

            ClientStart();
        }

        static int PortNumber = 8181;

        static void ServerStart()
        {
            TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any, PortNumber));
            listener.Start();
            while (true)
            {
                TcpClient client = listener.AcceptTcpClient();
                Task connectionHandlerTask = new Task(ConnectionEntryPoint, client);
                connectionHandlerTask.Start();
            }
            listener.Stop();
        }

        static void SendMessage(Socket socket, byte[] message)
        {
            // Send a 4-byte message length followed by the message itself
            int messageLength = message.Length;
            byte[] messageLengthBytes = BitConverter.GetBytes(messageLength);
            socket.Send(messageLengthBytes);
            socket.Send(message);
        }

        static byte[] ReceiveMessage(Socket socket)
        {
            // Read 4-byte message length from the client
            byte[] messageLengthBytes = new byte[4];
            int bufferPos = 0;
            int lastReadSize = -1;
            while (bufferPos < 4)
            {
                lastReadSize = socket.Receive(messageLengthBytes, bufferPos, 4 - bufferPos, SocketFlags.None);
                bufferPos += lastReadSize;
            }
            int messageLength = BitConverter.ToInt32(messageLengthBytes, 0);

            // Read the message
            byte[] messageBytes = new byte[messageLength];
            bufferPos = 0;
            lastReadSize = -1;
            while (bufferPos < messageLength)
            {
                lastReadSize = socket.Receive(messageBytes, bufferPos, messageLength - bufferPos, SocketFlags.None);
                bufferPos += lastReadSize;
            }

            return messageBytes;
        }

        static void ConnectionEntryPoint(object clientObj)
        {
            using (TcpClient client = (TcpClient)clientObj)
            {
                byte[] requestBytes = ReceiveMessage(client.Client);
                string message = Encoding.UTF8.GetString(requestBytes);

                // Sleep for 2 minutes, 30 seconds to simulate a long-running calculation, then echo the client's message back
                byte[] responseBytes = Encoding.UTF8.GetBytes(message);
                Console.WriteLine("Waiting 2 minutes 30 seconds.");
                Thread.Sleep(150000);

                try
                {
                    SendMessage(client.Client, responseBytes);
                }
                catch (SocketException ex)
                {
                    Console.WriteLine("Socket exception in server: {0}", ex.Message);
                }
            }
        }

        static void ClientStart()
        {
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
            {
                // Set receive timeout to infinite.
                socket.ReceiveTimeout = -1;

                // Connect to server
                socket.Connect(IPAddress.Loopback, PortNumber);

                // Send a message to the server
                string requestMessage = "Hello";
                byte[] requestBytes = Encoding.UTF8.GetBytes(requestMessage);
                SendMessage(socket, requestBytes);

                // Read the server's response.
                Stopwatch timer = Stopwatch.StartNew();
                try
                {
                    byte[] responseBytes = ReceiveMessage(socket);
                    string responseMessage = Encoding.UTF8.GetString(responseBytes);
                    Console.WriteLine(responseMessage);
                }
                catch (SocketException ex)
                {
                    // Timeout does not occur in this program because it does not call socket.Shutdown(SocketShutdown.Send)
                    timer.Stop();
                    Console.WriteLine("Socket exception in client after {0}: {1}", timer.Elapsed, ex.Message);
                }
            }
        }
    }
}
4

2 に答える 2

5

この動作は仕様によるものです。クライアントが接続の半分を閉じ、サーバーが閉じを確認すると、クライアントはFIN_WAIT_2状態になり、サーバーが接続を閉じるのを待ちます。http://support.microsoft.com/kb/923200には、FIN_WAIT_2のタイムアウトが2分であると記載されています。接続がFIN_WAIT_2状態のときに、2分間にデータが受信されない場合、クライアントは接続を強制的に閉じます(RSTを使用)。

Windows Server 2003のデフォルトでは、TCP接続状態がFIN_WAIT_2に2分間設定された後、TCP接続を閉じる必要があります。

この古いApacheの記事は、タイムアウトの理由を示唆しています。悪意のあるアプリケーションや誤動作しているアプリケーションは、接続の端を決して閉じず、オペレーティングシステムのリソースを占有することにより、接続のもう一方の端をFIN_WAIT_2に無期限に保持する可能性があります。

Linuxにもタイムアウトがあるようです。値を確認するには

$ cat / proc / sys / net / ipv4 / tcp_fin_timeout

Linuxでタイムアウトが発生しなかった理由がわかりません。おそらくそれはループバック接続であり、したがってDoS攻撃は問題ではないか、ループバック接続がtcp_fin_timeout設定を使用しない別のコードを使用しているためですか?

結論:オペレーティングシステムには、接続がタイムアウトする正当な理由があります。シャットダウンをアプリケーション層のシグナリングメカニズムとして使用することは避け、代わりに実際のアプリケーション層の方法を使用してください。

于 2012-11-29T04:38:59.247 に答える
0

Socket.ReceiveReceiveTimeout2分の上限があるようです。これはレジストリで指定されていると思われるものですが、これの真実または変更するキーのいずれかの具体的な証拠は見つかりませんでした。これは、LinuxとWindowsでの動作の違いを説明している可能性があります。

私はこれについてさまざまな解決策を検討しましたが、最も簡単な(そして機能したのは1つだけ)のは、おそらくサーバー側に数秒ごとにハートビートを送信させることです。基本的に、これにより、2分間のタイムアウトが発生しないようになります。

string boundary = string.Format("--{0}--", Guid.NewGuid());
byte[] boundaryBytes = Encoding.ASCII.GetBytes(boundary);

//Every 15 seconds write a byte to the stream.
for (int i = 0; i < 10; i++)
{
    stream.WriteByte(0);
    Thread.Sleep(15000);
}

//Indicate where the end of the heartbeat bytes is.
stream.Write(boundaryBytes, 0, boundaryBytes.Length);

//Same code as before.
try
{
    stream.Write(responseBytes, 0, responseBytes.Length);
}
catch (SocketException ex)
{
    Console.WriteLine("Socket exception in server: {0}", ex.Message);
}

ここで行ったことは、長時間実行されるタスクをシミュレートすることです(合計で2.5分間スリープします)が、タイムアウトを防ぐために15秒ごとに1バイトをストリームに書き込みます。

これを行う際の問題は、応答の開始時に不要なゴミが大量に発生することです。これがboundaryBytes出番です。これらを使用すると、不要なビットを実際の結果から明確に分離できます。重要なことは、クライアントは境界が何であるかを前もって認識している必要があるということです。

編集:

以下のコメントから、削除するsocket.Shutdown(SocketShutdown.Send)とうまくいくように見えたことがわかります。私はこれについて自分で疑問に思いましたが、実際には調査しませんでした。

私が理解していないのは、このメソッドを呼び出すと効果がある理由です。逆コンパイルを行うと、そのShutdownメソッドは基本的shutdownに、pinvokeを介して基盤となるWinSockライブラリ(ws2_32.dll)のメソッドを呼び出し、エラー処理を行ってから、ソケットを切断するように設定します。他の情報がない場合、これは、そのWinSock呼び出しで2分間の問題が発生したことを示しています。

イベントビューアでWinSockロギングを有効にしてこれを診断しようとしましたが、これが発生した理由を示す明らかなものは何もなかったようです。

WinSockレベルでさらに調査を行うと、次の質問が出てきました。

シャットダウン後にWinsockrecvが機能しない

Winsock2を使用してSend()とRecv()を乗算します

クライアントが接続の送信半分のみを閉じるのに、HTTPサーバーが接続を閉じるのはなぜですか?

.Net Socket.Disconnectに2分かかるのはなぜですか?

socket.Shutdown(SocketShutdown.Send)後で受信にソケットを使用する場合は、あまり良い考えではないというのが一般的なテーマのようです。このメソッドがsocket.Connectedプロパティをfalseに設定するという事実は、非常にわかりやすいかもしれません。

上記のリストの最後のリンクは、2分間の状況でマークにかなり近いように見えましたが、OPはレジストリ設定を参照していますが、それらが何であるかは示していません。

于 2012-11-25T10:09:40.707 に答える