C# - HttpClient에서의 ephemeral port 재사용
이 글의 테스트는 .NET Framework 4.8 + Windows 10에서 진행했고, 결과는 환경마다 다를 수 있습니다.
DynamicPortRangeStartPort : 1024
DynamicPortRangeNumberOfPorts : 977
AutoReusePortRangeStartPort : 15000
AutoReusePortRangeNumberOfPorts : 1000
지난 글에서,
C# - HttpWebRequest, WebClient와 ephemeral port 재사용
; https://www.sysnet.pe.kr/2/0/12448
다룬 코드를 HttpClient로 바꿔보면 어떻게 될까요? 역시 서버 코드는 그대로 두고 클라이언트만 교체해,
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp2
{
class Program
{
static void Main(string[] args)
{
string ipAddr = args[0];
int port = int.Parse(args[1]);
int numberOf = int.Parse(args[2]);
Console.WriteLine(ThreadPool.SetMaxThreads(1100, 1100));
Console.WriteLine(ThreadPool.SetMinThreads(1000, 1000));
ServicePointManager.ReusePort = true;
ConcurrentQueue<Task> clients1 = new ConcurrentQueue<Task>();
Uri uri = new Uri($"http://{ipAddr}:{port}");
int exceptionCount = 0;
try
{
for (int i = 0; i < numberOf; i++)
{
try
{
HttpClient wc = new HttpClient();
Task task = wc.GetAsync(uri);
clients1.Enqueue(task);
}
catch (Exception e)
{
exceptionCount++;
Console.WriteLine(e.ToString());
}
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
while (true)
{
Console.WriteLine($"{clients1.Count}, {exceptionCount}, Pid == {Process.GetCurrentProcess().Id}, ServicePointManager.ReusePort = {ServicePointManager.ReusePort}");
Thread.Sleep(5000);
}
}
}
}
실행하면 다음과 같은 결과가 나옵니다.
// 서버 측 포트 17000, 17001 Listen
D:\temp> ConsoleApp1
# of 17000: 0, 17001: 0
# of 17000: 0, 17001: 0
# of 17000: 68, 17001: 0
# of 17000: 641, 17001: 242
# of 17000: 641, 17001: 242
# of 17000: 641, 17001: 242
// #1 클라이언트 측 - 17000 포트로 1001개 접속 시도
// ConsoleApp2.exe localhost 17000 1001
C:\WINDOWS\system32> netstat -ano | findstr 18476
TCP 127.0.0.1:1036 127.0.0.1:17000 ESTABLISHED 18476
TCP 127.0.0.1:1037 127.0.0.1:17000 ESTABLISHED 18476
TCP 127.0.0.1:1044 127.0.0.1:17000 ESTABLISHED 18476
...[생략]...
TCP 127.0.0.1:1966 127.0.0.1:17000 ESTABLISHED 18476
TCP 127.0.0.1:1973 127.0.0.1:17000 ESTABLISHED 18476
TCP 127.0.0.1:1999 127.0.0.1:17000 ESTABLISHED 18476
// #2 클라이언트 측 - 17001 포트로 1001개 접속 시도
// ConsoleApp2.exe localhost 17001 1001
C:\WINDOWS\system32> netstat -ano | findstr 2708
TCP 127.0.0.1:1024 127.0.0.1:17001 ESTABLISHED 2708
TCP 127.0.0.1:1025 127.0.0.1:17001 ESTABLISHED 2708
TCP 127.0.0.1:1026 127.0.0.1:17001 ESTABLISHED 2708
...[생략]...
TCP 127.0.0.1:1996 127.0.0.1:17001 ESTABLISHED 2708
TCP 127.0.0.1:1997 127.0.0.1:17001 ESTABLISHED 2708
TCP 127.0.0.1:1998 127.0.0.1:17001 ESTABLISHED 2708
TCP 127.0.0.1:2000 127.0.0.1:17001 ESTABLISHED 2708
사용한 포트가 DynamicPortRangeStartPort/DynamicPortRangeNumberOfPorts 범위이므로, HttpClient는 (ServicePointManager.ReusePort 설정에 상관없이) AutoReuse에 대한 배려가 없습니다. 아마도 소켓 자원에 대한 풀링을 제공하는 탓에,
You're using HttpClient wrong and it is destabilizing your software
; https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
거기까지는 고려하지 않은 듯합니다. 그렇다고는 해도, 어쨌든 만약 Reverse Proxy 등의 응용 프로그램을 만든다면 HttpClient가 좋은 선택은 아닙니다.
그런데 왠지, .NET Core 환경에서는 HttpClient의 저런 동작이 문제가 될 것만 같습니다. 예전에도 언급했지만,
ServicePointManager.DefaultConnectionLimit와 HttpClient의 관계
; https://www.sysnet.pe.kr/2/0/12023
.NET Framework과는 달리 .NET Core에서는 HttpWebRequest, WebClient는 결국 내부적으로 HttpClient를 생성하기 때문인데, 그렇다면 .NET Core에서는 AutoReuse를 위해 Socket 클래스를 직접 다뤄야 하는 걸까요?
이게 좀 재미있는데요, .NET 5 환경에서 HttpWebRequest를 사용해 위의 코드를 다시 테스트해보면,
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp3
{
class Program
{
static void Main(string[] args)
{
string ipAddr = args[0];
int port = int.Parse(args[1]);
int numberOf = int.Parse(args[2]);
ThreadPool.SetMaxThreads(1100, 1100);
ThreadPool.SetMinThreads(1000, 1000);
ConcurrentQueue<HttpWebRequest> clients1 = new ConcurrentQueue<HttpWebRequest>();
Uri uri = new Uri($"http://{ipAddr}:{port}");
int exceptionCount = 0;
for (int i = 0; i < numberOf; i++)
{
Task.Factory.StartNew(() =>
{
var request = (HttpWebRequest)WebRequest.Create(uri);
clients1.Enqueue(request);
try
{
request.GetResponse();
}
catch
{
Interlocked.Increment(ref exceptionCount);
}
});
}
while (true)
{
Console.WriteLine("Pid == " + Process.GetCurrentProcess().Id);
Console.ReadLine();
}
}
}
}
이번엔 .NET Framework의 HttpWebRequest와 동일하게 AutoReuse 영역의 포트를 사용합니다. 아마도 HttpClientHandler가 다른 구현체를 사용하고 있는 것이 아닌가... 예상합니다. (혹시 이에 대해 아시는 분은 덧글 부탁드립니다. ^^)
(
첨부 파일은 이 글의 예제 프로젝트를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]