C# - 동일한 IP:Port로 바인딩 가능한 서버 소켓
Python flask app을 실습하면서, 실수로 PyCharm에서도 디버깅을 시작하고 명령행에서도 "flask run"을 했는데요, 동일하게 127.0.0.1:5000으로 바인딩한 응용 프로그램이 아무런 문제 없이 잘 실행이 됩니다.
재미있는 것은, 이후에 실행된 응용 프로그램이 소켓 accept를 할 수 있고, 그 프로그램이 종료하면 다시 예전 프로그램이 아무런 일도 없었다는 듯이 소켓 accept 동작을 이어갑니다.
실제로 netstat로 확인하면 이렇게 서로 다른 프로세스(python.exe)가 동일한 IP:Port 바인딩을 열고 있습니다.
C:\temp> netstat -ano | findstr 5000 | findstr LISTEN
TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 20332
TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 10220
/*
PS> Get-Process -Id (Get-NetTcpConnection -LocalPort 5000).OwningProcess
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
144 9 1568 6292 0.02 18424 1 wslhost
*/
오호... 재미있군요. ^^ 이에 대해 검색해 보니 저같은 사람이 이미 있었습니다.
Flask allows multiple server instances to listen on the same port
; https://stackoverflow.com/questions/47786463/flask-allows-multiple-server-instances-to-listen-on-the-same-port
문서를 보면,
SO_EXCLUSIVEADDRUSE socket option
; https://learn.microsoft.com/en-us/windows/win32/winsock/so-exclusiveaddruse
In the case where the first bind sets no options or SO_REUSEADDR, and the second bind performs a SO_REUSEADDR, the second socket has overtaken the port and behavior regarding which socket will receive packets is undetermined. SO_EXCLUSIVEADDRUSE was introduced to address this situation.
첫 번째 프로그램이 아무런 옵션 없이, 또는 SO_REUSEADDR를 사용해 바인딩한 경우 이후의 프로그램에서 SO_REUSEADDR를 사용해 해당 바인딩을 점유할 수 있다고 합니다.
정말 그런지 테스트를 해볼까요? ^^ 우선, 아무런 옵션 없이 준 경우로,
using System;
using System.Net.Sockets;
using System.Net;
internal class Program
{
static void Main(string[] args)
{
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Loopback, 11000);
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
listener.Bind(localEndPoint);
listener.Listen(10);
while (true)
{
Console.WriteLine("Waiting for a connection...");
listener.Accept();
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
빌드해 2개의 인스턴스를 띄워보면 첫 번째를 제외하고는 화면에 아래와 같은 식의 에러 메시지가 나오는 것을 볼 수 있습니다.
System.Net.Sockets.SocketException (10048): Only one usage of each socket address (protocol/network address/port) is normally permitted.
at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, String callerName)
at System.Net.Sockets.Socket.DoBind(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.Sockets.Socket.Bind(EndPoint localEP)
at Program.Main(String[] args)
자, 그럼 테스트를 쉽게 하기 위해 명령행 인자의 수에 따라 SO_REUSEADDR 옵션을 제어하도록 바꾼 다음,
if (args.Length >= 1)
{
listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
listener.Bind(localEndPoint);
listener.Listen(10);
/* 첫 번째 인스턴스
C:\temp> ConsoleApp1
Waiting for a connection...
*/
/* 두 번째 인스턴스
C:\temp> ConsoleApp1 1
System.Net.Sockets.SocketException [...예외 발생...]
*/
실행해 보면, 보는 바와 같이 처음 실행한 프로그램에서 아무런 옵션을 주지 않으면 두 번째 프로그램에서 SO_REUSEADDR 옵션을 주더라도 이전과 마찬가지로 예외가 발생합니다. 즉, 문서의 내용이 틀린 것인데요, 어쩌면 문서의 내용에서 "Minimum supported client"가 "Windows 2000 Professional"이니만큼 저 당시에는 SO_EXCLUSIVEADDRUSE 옵션이 명시적으로 필요했을지도 모릅니다.
하지만, 2개의 프로그램 모두 SO_REUSEADDR 옵션을 적용해 실행하는 것은 잘 됩니다.
/* 첫 번째 인스턴스
C:\temp> ConsoleApp1 1
Waiting for a connection...
*/
/* 두 번째 인스턴스
C:\temp> ConsoleApp1 1
Waiting for a connection...
*/
그러니까, "flask"는 윈도우 버전인 경우 명시적으로 (굳이?) SO_REUSEADDR 옵션을 적용하고 있었던 것입니다.
저렇게 보면, SO_EXCLUSIVEADDRUSE 옵션이 왜 있는 것인가??? 의문입니다. 혹시 이 옵션에 대한 차이점을 재현할 수 있는 방법을 아시는 분은 덧글 부탁드립니다.
아울러, SO_REUSEADDR 옵션의 클라이언트 측 소켓 사용은 아래의 글에서 예시를 한번 든 적이 있습니다.
윈도우 환경에서 클라이언트 소켓의 최대 접속 수 (2) - SO_REUSEADDR
; https://www.sysnet.pe.kr/2/0/12432
그리고, 윈도우의 경우 HttpListener를 사용하면 동일한 포트에 대해 (점유하는 방식이 아닌) 경로를 달리해 바인딩하는 것을 지원하는 것도 가능하니 참고하시고. ^^
IIS의 80 포트를 공유하는 응용 프로그램 만드는 방법
; https://www.sysnet.pe.kr/2/0/1555
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]