以下は、問題を示す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);
}
}
}
}
}