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

(시리즈 글이 4개 있습니다.)
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




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)
13335정성태4/30/20234710.NET Framework: 2110. C# - FFmpeg.AutoGen 라이브러리를 이용한 기본 프로젝트 구성 - Windows Forms파일 다운로드1
13334정성태4/29/20234384Windows: 250. Win32 C/C++ - Modal 메시지 루프 내에서 SetWindowsHookEx를 이용한 Thread 메시지 처리 방법
13333정성태4/28/20233795Windows: 249. Win32 C/C++ - 대화창 템플릿을 런타임에 코딩해서 사용파일 다운로드1
13332정성태4/27/20233884Windows: 248. Win32 C/C++ - 대화창을 위한 메시지 루프 사용자 정의파일 다운로드1
13331정성태4/27/20233923오류 유형: 856. dockerfile - 구 버전의 .NET Core 이미지 사용 시 apt update 오류
13330정성태4/26/20233577Windows: 247. Win32 C/C++ - CS_GLOBALCLASS 설명
13329정성태4/24/20233769Windows: 246. Win32 C/C++ - 직접 띄운 대화창 템플릿을 위한 Modal 메시지 루프 생성파일 다운로드1
13328정성태4/19/20233447VS.NET IDE: 184. Visual Studio - Fine Code Coverage에서 동작하지 않는 Fake/Shim 테스트
13327정성태4/19/20233855VS.NET IDE: 183. C# - .NET Core/5+ 환경에서 Fakes를 이용한 단위 테스트 방법
13326정성태4/18/20235300.NET Framework: 2109. C# - 닷넷 응용 프로그램에서 SQLite 사용 (System.Data.SQLite) [1]파일 다운로드1
13325정성태4/18/20234597스크립트: 48. 파이썬 - PostgreSQL의 with 문을 사용한 경우 연결 개체 누수
13324정성태4/17/20234430.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234342개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20235149VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20233967개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20233912개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234379개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234233개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234292오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233591오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20233811Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20233951개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20234012개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20234034Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234530C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20234148C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...