在對我的具有大量連接的 TCP 服務器進行壓力測驗時,我發現連接請求會在一段時間后拋出 SocketException。例外隨機顯示
每個套接字地址(協議/網路地址/埠)通常只允許使用一次。
要么
由于目標機器主動拒絕,無法建立連接。
作為它的資訊。
這通常會在幾秒鐘后隨機發生,并且會在數萬次連接和斷開連接后隨機發生。為了連接,我使用本地端點IPEndPoint clientEndPoint = new(IPAddress.Any, 0);,我相信它會給我下一個免費的臨時埠。
為了隔離這個問題,我撰寫了這個簡單的程式,它同時運行一個 TCP 服務器和許多并行客戶端,用于一個簡單的計數器:
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
CancellationTokenSource cancellationTokenSource = new();
CancellationToken cancellationToken = cancellationTokenSource.Token;
const int serverPort = 65000;
const int counterRequestMessage = -1;
const int randomCounterResponseMinDelay = 10; //ms
const int randomCounterResponseMaxDelay = 1000; //ms
const int maxParallelCounterRequests = 10000;
#region server
int counterValue = 0;
async void RunCounterServer()
{
TcpListener listener = new(IPAddress.Any, serverPort);
listener.Start(maxParallelCounterRequests);
while (!cancellationToken.IsCancellationRequested)
{
HandleCounterRequester(await listener.AcceptTcpClientAsync(cancellationToken));
}
listener.Stop();
}
async void HandleCounterRequester(TcpClient client)
{
await using NetworkStream stream = client.GetStream();
Memory<byte> memory = new byte[sizeof(int)];
//read requestMessage
await stream.ReadAsync(memory, cancellationToken);
int requestMessage = BitConverter.ToInt32(memory.Span);
Debug.Assert(requestMessage == counterRequestMessage);
//increment counter
int updatedCounterValue = Interlocked.Add(ref counterValue, 1);
Debug.Assert(BitConverter.TryWriteBytes(memory.Span, updatedCounterValue));
//wait random timeout
await Task.Delay(GetRandomCounterResponseDelay());
//write back response
await stream.WriteAsync(memory, cancellationToken);
client.Close();
client.Dispose();
}
int GetRandomCounterResponseDelay()
{
return Random.Shared.Next(randomCounterResponseMinDelay, randomCounterResponseMaxDelay);
}
RunCounterServer();
#endregion
IPEndPoint clientEndPoint = new(IPAddress.Any, 0);
IPEndPoint serverEndPoint = new(IPAddress.Parse("127.0.0.1"), serverPort);
ReaderWriterLockSlim isExceptionEncounteredLock = new(LockRecursionPolicy.NoRecursion);
bool isExceptionEncountered = false;
async Task RunCounterClient()
{
try
{
int counterResponse;
using (TcpClient client = new(clientEndPoint))
{
await client.ConnectAsync(serverEndPoint, cancellationToken);
await using (NetworkStream stream = client.GetStream())
{
Memory<byte> memory = new byte[sizeof(int)];
//send counter request
Debug.Assert(BitConverter.TryWriteBytes(memory.Span, counterRequestMessage));
await stream.WriteAsync(memory, cancellationToken);
//read counter response
await stream.ReadAsync(memory, cancellationToken);
counterResponse = BitConverter.ToInt32(memory.Span);
}
client.Close();
}
isExceptionEncounteredLock.EnterReadLock();
//log response if there was no exception encountered so far
if (!isExceptionEncountered)
{
Console.WriteLine(counterResponse);
}
isExceptionEncounteredLock.ExitReadLock();
}
catch (SocketException exception)
{
bool isFirstEncounteredException = false;
isExceptionEncounteredLock.EnterWriteLock();
//log exception and note that one was encountered if it is the first one
if (!isExceptionEncountered)
{
Console.WriteLine(exception.Message);
isExceptionEncountered = true;
isFirstEncounteredException = true;
}
isExceptionEncounteredLock.ExitWriteLock();
//if this is the first exception encountered, rethrow it
if (isFirstEncounteredException)
{
throw;
}
}
}
async void RunParallelCounterClients()
{
SemaphoreSlim clientSlotCount = new(maxParallelCounterRequests, maxParallelCounterRequests);
async void RunCounterClientAndReleaseSlot()
{
await RunCounterClient();
clientSlotCount.Release();
}
while (!cancellationToken.IsCancellationRequested)
{
await clientSlotCount.WaitAsync(cancellationToken);
RunCounterClientAndReleaseSlot();
}
}
RunParallelCounterClients();
while (true)
{
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
if (keyInfo.Key == ConsoleKey.Escape)
{
cancellationTokenSource.Cancel();
break;
}
}
My initial guess is, that I run out of ephemeral ports because I somehow do not free them correctly. I just Close() and Dispose() my TcpClients in both my client and server code when a request is finished. I thought that would automatically release the port, but when I use netstat -ab in a console, it gives me countless entries like this, even after closing the application:
TCP 127.0.0.1:65000 kubernetes:59996 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:59997 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:59998 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:59999 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60000 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60001 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60002 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60003 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60004 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60005 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60006 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60007 TIME_WAIT
TCP 127.0.0.1:65000 kubernetes:60009 TIME_WAIT
Also, my PC sometimes gets a lot of stutter some time after I exit the application. I assume this is due to Windows cleaning up my leaked port usage?
So I wonder, what am I doing wrong here?
uj5u.com熱心網友回復:
每個套接字地址(協議/網路地址/埠)通常只允許使用一次。...我最初的猜測是,我用完了臨時埠,因為我以某種方式沒有正確釋放它們。
TIME_WAIT 是每個 TCP 連接在主動關閉連接時將進入的完全正常狀態,即在發送資料后顯式呼叫 close 或在退出應用程式時隱式關閉。請參閱此圖(來源
離開 TIME_WAIT 狀態并進入 CLOSED 需要一些超時。只要連接在 TIME_OUT 中,源 ip、埠和目標 ip、埠的特定組合就不能用于新連接。這有效地限制了在一段時間內從一個特定 IP 地址到另一個特定 IP 的可能連接數。請注意,典型的服務器不會遇到這樣的限制,因為它們從不同的系統獲得許多連接,而每個源 IP 只有少數連接。
除了不主動關閉連接外,對此無能為力。如果另一方首先觸發連接(發送 FIN),然后繼續關閉(確認 FIN 并發送自己的 FIN),則不會發生 TIME_WAIT。當然,在您的單個客戶端和單個服務器的特定場景中,這只會將問題轉移到服務器上。
由于目標機器主動拒絕,無法建立連接。
This has another reason. The server does a listen on the socket and gives the intended size of the backlog (the OS might will probably not use exactly this value). This backlog is used to accept new TCP connections in the OS kernel and the server will the call accept to get these accepted TCP connections. If the server calls accept less often than new connections get established the backlog will fill up. And once the backlog is full the server will refuse new connection, resulting in the error you see. In other words: this happens if the server is not able to keep up with the client.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/427541.html
上一篇:recv()不等待資料
