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)
13384정성태6/26/20233707.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제파일 다운로드1
13383정성태6/26/20233481개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/20233532.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20233948오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233381스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233424스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233304오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20237176오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/20233516.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/20233148스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/20233258오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/20234571오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233248개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233309개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/20233476개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/20233273개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/20233427개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/20233547오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
13366정성태6/10/20233318.NET Framework: 2128. C# - 윈도우 시스템에서 지원하는 암호화 목록(Cipher Suites) 나열파일 다운로드1
13365정성태6/8/20233066오류 유형: 863. MODIFY FILE encountered operating system error 112(failed to retrieve text for this error. Reason: 15105)
13364정성태6/8/20233870.NET Framework: 2127. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 [1]
13363정성태6/7/20233460스크립트: 49. 파이썬 - "Transformers (신경망 언어모델 라이브러리) 강좌" - 1장 2절 코드 실행 결과
13362정성태6/1/20233374.NET Framework: 2126. C# - 서버 측의 요청 제어 (Microsoft.AspNetCore.RateLimiting)파일 다운로드1
13361정성태5/31/20233792오류 유형: 862. Facebook - ASP.NET/WebClient 사용 시 graph.facebook.com/me 호출에 대해 403 Forbidden 오류
13360정성태5/31/20233180오류 유형: 861. WSL/docker - failed to start shim: start failed: io.containerd.runc.v2: create new shim socket
13359정성태5/19/20233499오류 유형: 860. Docker Desktop - k8s 초기화 무한 반복한다면?
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...