성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법</h1> <p> excludedportrange로 인해,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > "Administered port exclusions" 설명 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12293'>https://www.sysnet.pe.kr/2/0/12293</a> </pre> <br /> 간혹 비주얼 스튜디오에서 생성한 웹 애플리케이션을 실행하는 경우에서조차도,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Visual Studio - 웹 애플리케이션 실행 시 "Unable to connect to web server 'IIS Express'." 오류 발생 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12265'>https://www.sysnet.pe.kr/2/0/12265</a> </pre> <br /> 포트 충돌이 발생하곤 합니다. 이런 문제를 해결하려면, 우리도 사용해야 할 포트를 excludedportrange로 등록한 후 Socket.IOControl로 연결하면 된다고 설명했는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13439'>https://www.sysnet.pe.kr/2/0/13439</a> </pre> <br /> 혹시, 그럼 저걸 이용해서 ASP.NET Core 웹 프로젝트를 실행할 때의 포트 충돌 문제를 해결할 수 있을까요? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그러려면 가장 중요한 것이, ASP.NET Core 프레임워크에서 Socket을 사용자 정의할 수 있어야 합니다. 다행히, 이 기능이 .NET 6+부터 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.transport.sockets.sockettransportoptions'>SocketTransportOptions</a>을 통해 지원하기 시작했는데요, 코드는 간단하게 다음과 같이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System.Net.Sockets; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; namespace WebApplication1 { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); <span style='color: blue; font-weight: bold'>builder.Services.Configure<SocketTransportOptions>(options => { options.CreateBoundListenSocket = endpoint => { Socket socket = SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); return socket; }; });</span> // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); } } } </pre> <br /> SocketTransportOptions가 구현한 CreateDefaultBoundListenSocket의 소스 코드는 이런 식인데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public static Socket CreateDefaultBoundListenSocket(EndPoint endpoint) { Socket listenSocket; switch (endpoint) { case FileHandleEndPoint fileHandle: listenSocket = new Socket( new SafeSocketHandle((IntPtr)fileHandle.FileHandle, ownsHandle: true) ); break; case UnixDomainSocketEndPoint unix: listenSocket = new Socket(unix.AddressFamily, SocketType.Stream, ProtocolType.Unspecified); break; case IPEndPoint ip: listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); if (ip.Address.Equals(IPAddress.IPv6Any)) { listenSocket.DualMode = true; } break; default: listenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); break; } if (!(endpoint is FileHandleEndPoint)) { listenSocket.Bind(endpoint); } return listenSocket; } </pre> <br /> 따라서 우리는 저 코드를 이용해 다음과 같은 식으로 만들면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > builder.Services.Configure<SocketTransportOptions>(options => { options.CreateBoundListenSocket = endpoint => { return CreateSocketFromExcludedPortRange(endpoint); }; }); private static Socket CreateSocketFromExcludedPortRange(EndPoint endpoint) { switch (endpoint) { case FileHandleEndPoint: case UnixDomainSocketEndPoint: return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); } Socket socket = // ...[생략: <a target='tab' href='https://www.sysnet.pe.kr/2/0/13439'>Port Exclusion Ranges</a>를 고려한 소켓 생성]... return socket; } </pre> <br /> 간단하죠? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 정작 구현하다 보면 이게 그렇게 간단하게 끝나지 않습니다. 왜냐하면 Port Exclusion을 이용한 IOControl 코드는 관리자 권한에서만 동작하기 때문에, 반드시 비주얼 스튜디오 프로세스부터 관리자 권한으로 띄워 두어야 합니다.<br /> <br /> 그리고 이러한 동작은 대개의 경우 개발자 머신에서만 유효할 것입니다. 실제 현업에 배포할 때는 대개의 경우 80/443 포트를 점유하거나, Reverse Proxy나 Container 환경에서 미리 정의한 바로 그 포트로만 서비스를 할 것이기 때문에 딱히 Port Exclusion을 사용할 필요가 없습니다.<br /> <br /> 따라서 이런 것을 고려했을 때, 대충 다음과 같은 식으로 코드를 작성해 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Principal; namespace WebApplication1 { public class Program { #if DEBUG [DllImport("Iphlpapi.dll")] internal static extern uint CreatePersistentTcpPortReservation(ushort startPort, ushort numberOfPorts, out long token); [DllImport("Iphlpapi.dll")] internal static extern uint LookupPersistentTcpPortReservation(ushort startPort, ushort numberOfPorts, out long token); const int ERROR_NOT_FOUND = 1168; const int ERROR_INVALID_PARAMETER = 87; const int SIO_ASSOCIATE_PORT_RESERVATION = -1744830362; const int WSAEACCES = 10013; #endif public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); #if DEBUG builder.Services.Configure<SocketTransportOptions>(options => { options.CreateBoundListenSocket = endpoint => { return CreateSocketFromExcludedPortRange(endpoint); }; }); #endif // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); } #if DEBUG // ClickOnce - 관리자 권한 상승하는 방법 // <a target='tab' href='https://www.sysnet.pe.kr/2/0/950'>https://www.sysnet.pe.kr/2/0/950</a> public static bool IsAdministrator() { WindowsIdentity identity = WindowsIdentity.GetCurrent(); if (null != identity) { WindowsPrincipal principal = new WindowsPrincipal(identity); return principal.IsInRole(WindowsBuiltInRole.Administrator); } return false; } private static Socket CreateSocketFromExcludedPortRange(EndPoint endpoint) { if (IsAdministrator() == false) { return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); } switch (endpoint) { case FileHandleEndPoint: case UnixDomainSocketEndPoint: return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); } Socket listenSocket; switch (endpoint) { case IPEndPoint ip: listenSocket = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); if (ip.Address.Equals(IPAddress.IPv6Any)) { listenSocket.DualMode = true; } break; default: listenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); break; } endpoint = DetermineEndpoint(listenSocket, endpoint); listenSocket.Bind(endpoint); return listenSocket; } private static EndPoint DetermineEndpoint(Socket socket, EndPoint endpoint) { IPEndPoint? ip = endpoint as IPEndPoint; if (ip == null) { return endpoint; } ushort startPort = (ushort)ip.Port; long token = 0; for (ushort i = startPort; i <= ushort.MaxValue; i++) { ushort port = (ushort)IPAddress.HostToNetworkOrder((short)i); uint status = LookupPersistentTcpPortReservation(port, 1, out token); if (status == ERROR_NOT_FOUND) { status = CreatePersistentTcpPortReservation((ushort)port, 1, out token); } if (status == 0) { byte[] resToken = BitConverter.GetBytes(token); try { int result = socket.IOControl(SIO_ASSOCIATE_PORT_RESERVATION, resToken, null); if (result == 0) { return new IPEndPoint(ip.Address, i); } } catch (SocketException) { } } } return endpoint; } #endif } } </pre> <br /> 그런데, 여러분들이 해결해야 할 문제가 정말 저 코드를 사용해가면서까지 필요한 것인가? 라는 질문을 해야 합니다. 그러니까, 단순히 로컬에서 디버깅 실행 중 포트 충돌이 나는 귀찮음을 피하기 위해 저런 더 귀찮은 코드를 넣어야 하는 것이 바람직하냐는 것입니다.<br /> <br /> 사실 저 정도의 처리는 Port Exclusion 없이도 그냥 Bind를 무작정 시켜 예외가 나는 것을 catch하는 식으로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > builder.Services.Configure<SocketTransportOptions>(options => { options.CreateBoundListenSocket = endpoint => { return CreateServerSocket(endpoint); }; }); private static Socket CreateServerSocket(EndPoint endpoint) { switch (endpoint) { case FileHandleEndPoint: case UnixDomainSocketEndPoint: return SocketTransportOptions.CreateDefaultBoundListenSocket(endpoint); } Socket listenSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); IPEndPoint? ip = endpoint as IPEndPoint; if (ip == null) { listenSocket.Bind(endpoint); return listenSocket; } if (ip.Address.Equals(IPAddress.IPv6Any)) { listenSocket.DualMode = true; } Exception? lastException = null; int portNumber = ip.Port; for (int i = portNumber; i < ushort.MaxValue; i++) { IPEndPoint ep = new IPEndPoint(ip.Address, i); try { listenSocket.Bind(ep); Console.WriteLine($"Binding: {ep}"); return listenSocket; } catch (SocketException ex) { lastException = ex; } } throw lastException; } </pre> <br /> 처리하는 것이 더 단순해서 좋습니다. ^^ 하지만, 여전히 의문이군요. 1) 비주얼 스튜디오를 관리자 권한으로 실행시키고 2) 저 코드를 넣어서 처리하느니, 저라면 그냥 설정에서 포트 번호를 다른 것으로 바꾸고 말겠습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로, .NET 6+부터 저렇게 서버 소켓을 사용자 정의할 수 있게 되었는데요, 마찬가지로 웹 서버에 접속한 클라이언트와의 통신을 담당하는 소켓도 <a target='tab' href='https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.connections.features.iconnectionsocketfeature'>IConnectionSocketFeature</a>로 구하는 것이 가능해졌습니다.<br /> <br /> 따라서, 요청 중에 다음과 같이 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Net.Sockets; namespace WebApplication1.Pages { public class IndexModel : PageModel { private readonly ILogger<IndexModel> _logger; public IndexModel(ILogger<IndexModel> logger) { _logger = logger; } public void OnGet() { <span style='color: blue; font-weight: bold'>var socketFeature = this.HttpContext.Features.Get<IConnectionSocketFeature>(); Socket clntSock = socketFeature.Socket;</span> Console.WriteLine($"{clntSock.RemoteEndPoint} - {clntSock.LocalEndPoint}"); } } } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); var app = builder.Build(); app.Use(next => { return async context => { <span style='color: blue; font-weight: bold'>var socketFeature = context.Features.Get<IConnectionSocketFeature>(); Socket clntSock = socketFeature.Socket;</span> Console.WriteLine($"{clntSock.RemoteEndPoint} - {clntSock.LocalEndPoint}"); await next(context); }; }); </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2101&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2049
(왼쪽의 숫자를 입력해야 합니다.)