Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 5개 있습니다.)
Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
; https://www.sysnet.pe.kr/2/0/13540

개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
; https://www.sysnet.pe.kr/2/0/13548

개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
; https://www.sysnet.pe.kr/2/0/13549

닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
; https://www.sysnet.pe.kr/2/0/13574

닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법
; https://www.sysnet.pe.kr/2/0/13616




C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성

예전 글에서,

.NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12197

비주얼 스튜디오의 경우, 콘솔 프로젝트에도 "Add" / "Docker Support..." 메뉴를 이용하면 쉽게 Docker 컨테이너를 위한 개발 환경을 누릴 수 있다고 했습니다.

그렇다면 IPv4 소켓을 사용하면 어떨까요? ^^

using System.Net;
using System.Net.Sockets;

namespace ConsoleApp2;

internal class Program
{
    static void Main(string[] args)
    {
        Socket socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);

        IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 16000);
        Console.WriteLine(ep);

        socket.Bind(ep);
        socket.Listen(10);
        new Thread(() =>
        {
            byte[] buffer = new byte[10];
            while (true)
            {
                Socket client = socket.Accept();
                Console.WriteLine("Client connected");

                int recvBytes = client.Receive(buffer);
                client.Send(new byte[] { 1, 2, 3, 4 });
                client.Close();
            }
        }).Start();

        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();

        socket.Close();
    }
}

위의 경우, "Add" / "Docker Support..."로 Docker 지원을 추가 후 F5 키를 눌러 실행하면 단순히 컨테이너 내에서 응용 프로그램을 실행만 시킬 뿐, 가장 중요한 16000번 포트를 노출시켜주지 않아 외부에서 테스트하기가 불편합니다.

왜냐하면 비주얼 스튜디오는 docker run을 다음과 같이 실행하기 때문입니다.

docker run -dt ...[생략: 각종 볼륨 매핑 및 환경 변수 설정]... --name ConsoleApp1 --entrypoint tail consoleapp1:dev -f /dev/null 

즉, "-p 16000:16000"과 같은 설정이 있어야 하는 건데요, ASP.NET Core 웹 프로젝트와는 달리 Console App의 경우에는 저것을 자동으로 해주지 않습니다. 따라서 개발자가 직접 설정해야 하는데요, 이에 대해서는 이미 지난 글에서 2가지 방법을 설명했습니다.

  1. 랜덤 포트 매핑 - dockerfile에 EXPOSE + launchSettings.json에 publishAllPorts
  2. 고정 포트 매핑 - csproj에 DockerfileRunArguments를 이용한 포트 명시

하지만, 1번 방법은 C# 프로젝트가 "Microsoft.NET.Sdk.Web"인 경우의 빌드 템플릿에서만 제공하는 기능이므로 콘솔 프로젝트인 "Microsoft.NET.Sdk"라면 DockerfileRunArguments 옵션에 "-P" 옵션을 명시하는 식으로 우회해야 합니다.

결국, 2번 방법만 가능한데요, 여기서는 테스트를 편하게 하도록 고정 포트를 할당하는 것으로 가정하겠습니다.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>

        <!-- Container Tools build properties -->
        <DockerfileRunArguments>-p 16000:16000</DockerfileRunArguments>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
    </ItemGroup>

</Project>

이후 실행하면, 비주얼 스튜디오는 이렇게 -p 옵션을 추가해 docker run을 해줍니다. ^^

docker run -dt ...[생략: 각종 볼륨 매핑 및 환경 변수 설정]... --name ConsoleApp1 -p 16000:16000 --entrypoint tail consoleapp1:dev -f /dev/null 




위의 예제 코드는 IPAddress.Loopback 주소로 바인딩하고 있는데요, 이렇게 되면 컨테이너 외부에서는 접근할 수 없다고 이전 글에서도 언급했습니다. 그러면서 아래와 같은 내용도 언급했는데요,

그나저나, 위의 출력에서 "Container 외부에서 실행"하는 경우 curl의 출력이 "Empty reply from server"인 것이 좀 이상하지 않나요? ^^ 원래는 "Couldn't connect to server"라고 나와야 하는데요, 분량상 이에 대해서는 다른 글에서 자세하게 더 다뤄보겠습니다.


이제 저 현상을 자세하게 살펴보겠습니다. 실습을 위해 서버 측 바인딩을 IPAddress.Any로 바꾸고,

IPEndPoint ep = new IPEndPoint(IPAddress.Any, 16000);

이에 대응하는 소켓 클라이언트를 다음과 같은 코드로 함께 만들어 두겠습니다.

using System.Net;
using System.Net.Sockets;

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        Socket client = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);

        IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 16000);
        Console.WriteLine(ep);

        client.Connect(ep);
        Console.WriteLine("Connected");

        byte[] buffer = new byte[1024];

        client.Send(new byte[] { 1, 2, 3, 4 });
        int recvBytes = client.Receive(buffer);

        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();

        client.Close();
    }
}

자, 이제 서버와 클라이언트 프로그램의 실행을 한 번 해보면, 서버 측 컨테이너 내부의 네트워크 연결이 이렇게 나옵니다.

$ netstat -an | grep 16000
tcp        0      0 0.0.0.0:16000           0.0.0.0:*               LISTEN
tcp        0      0 172.17.0.2:16000        172.17.0.1:40454        FIN_WAIT2

또한, 클라이언트를 실행한 (컨테이너 외부에 해당하는) 윈도우 호스트 측의 연결 상황을 보면,

C:\temp> netstat -ano | findstr 16000
  TCP    0.0.0.0:16000          0.0.0.0:0              LISTENING       9324
  TCP    127.0.0.1:16000        127.0.0.1:32826        FIN_WAIT_2      9324
  TCP    127.0.0.1:32826        127.0.0.1:16000        CLOSE_WAIT      40240
  TCP    [::]:16000             [::]:0                 LISTENING       9324
  TCP    [::1]:16000            [::]:0                 LISTENING       26120

위의 출력에서 9324는 com.docker.backend.exe 프로세스입니다. 즉, docker run 시 "-p 16000:16000" 옵션을 준 경우, Docker Desktop for Windows는 해당 포트를 com.docker.backend.exe에서 미리 열어두고 있는 것입니다.

따라서 위의 연결을 정리해 보면 대략 이런 네트워크 연결 구조가 나옵니다.

클라이언트 <---> 16000 포트 com.docker.backend <---> docker 가상 네트워크 <---> 16000 포트 socket server in docker container

그렇다면 만약, 서버 소켓 프로그램을 종료해 두고 컨테이너만 실행해 둔 채로 클라이언트를 실행해 보면 어떨까요? 서버는 없어졌지만, 여전히 com.docker.backend가 살아 있기 때문에 16000 포트로의 연결은 정상적으로 이뤄지지만 곧바로 소켓이 닫혀 버리는 현상이 나옵니다.

바로 이것이 "localhost"로 바인딩하고 있는 ASP.NET Core로 웹 브라우저 접속을 했을 때 "Empty reply from server" 응답이 나오는 이유입니다. 즉, 웹 브라우저는 com.docker.backend가 열어 놓고 있는 포트로 TCP 연결은 되었지만, com.docker.backend 측은 컨테이너 내부에 연결의 실질적인 대상이 되는 포트가 열려 있지 않아 곧바로 소켓 연결을 끊어버리기 때문에 나타나는 현상입니다.

그리고, 왜? "localost/127.0.0.1"로 바인딩한 ASP.NET Core를 컨테이너 외부에서 접속하지 못하는 이유도 위의 컨테이너 측 연결에서 찾을 수 있습니다.

tcp        0      0 172.17.0.2:16000        172.17.0.1:40454        FIN_WAIT2

보는 바와 같이, 외부의 연결을 컨테이너 내부의 소켓 연결을 중재할 때 "172.17.0.2"로 시도를 하기 때문에 "localhost/127.0.0.1"로 바인딩하고 있는 서버 소켓에 대해서는 동작하지 않았던 것입니다.




중요하지 않은 이야기 하나 덧붙이자면.

Docker Desktop for Windows의 경우, 포트 처리에서 다소 이상한 점이 하나 있습니다. 아래는 위에서 출력한 netstat를 클라이언트 연결이 없을 때를 보여줍니다.

C:\temp> netstat -ano | findstr 16000
  TCP    0.0.0.0:16000          0.0.0.0:0              LISTENING       9324
  TCP    [::]:16000             [::]:0                 LISTENING       9324
  TCP    [::1]:16000            [::]:0                 LISTENING       26120

윈도우 호스트 측의 연결을 컨테이너 내부로 연결하기 위해 "0.0.0.0", "[::]" 주소에 대해 com.docker.backend 프로세스가 대기하는 것은 알겠는데요, 특이하게도 26120 pid를 가진 프로세스는 유독 IPv6인 [::1] Loopback 주소에 대해 연결을 지정하고 있습니다. 이 프로세스의 이름은 wslrelay.exe인데,

wslrelay.exe --mode 2 --vm-id {541d40ee-8df6-4f85-b1ca-8088aa0e2e41} --handle 2156

어떤 의도로 이렇게 작성된 것인지는 모르겠습니다. ^^; 어쨌든, 127.0.0.1의 연결 시도는 com.docker.backend 프로세스가, [::1]의 연결 시도는 wslrelay가 중계합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 2/3/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13583정성태3/25/20241707Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20242053Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20242148개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241635닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20242048오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20242265닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20242544닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20242005닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20242191닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20242076닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/20241987닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/20242238닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/20242089닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/20242011닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/20241938닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/20242039닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/20241997오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/20242057오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20241928닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
13564정성태2/22/20242159Windows: 259. Hyper-V Generation 1 유형의 VM을 Generation 2 유형으로 바꾸는 방법
13563정성태2/21/20242142디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
13562정성태2/21/20242129오류 유형: 896. ASP.NET - .NET Framework 기본 예제에서 System.Web에 대한 System.IO.FileNotFoundException 예외 발생
13561정성태2/20/20242247닷넷: 2218. C# - (예를 들어, Socket) 비동기 I/O에 대한 await 호출 시 CancellationToken을 이용한 취소파일 다운로드1
13560정성태2/19/20242349디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
13559정성태2/19/20243205오류 유형: 895. ASP.NET - System.Security.SecurityException: 'Requested registry access is not allowed.'
13558정성태2/18/20242491닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...