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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  131  132  [133]  134  135  ...
NoWriterDateCnt.TitleFile(s)
1730정성태8/11/201422077개발 환경 구성: 234. Royal TS의 터미널(Terminal) 연결에서 한글이 깨지는 현상 해결 방법
1729정성태8/11/201418139오류 유형: 236. SqlConnection - The requested Performance Counter is not a custom counter, it has to be initialized as ReadOnly.
1728정성태8/8/201430167.NET Framework: 453. C# - 오피스 파워포인트(Powerpoint) 파일을 WinForm에서 보는 방법파일 다운로드1
1727정성태8/6/201420415오류 유형: 235. SignalR 오류 메시지 - Counter 'Messages Bus Messages Published Total' does not exist in the specified Category. [2]
1726정성태8/6/201419349오류 유형: 234. IIS Express에서 COM+ 사용 시 SecurityException - "Requested registry access is not allowed" 발생
1725정성태8/6/201421309오류 유형: 233. Visual Studio 2013 Update3 적용 후 Microsoft.VisualStudio.Web.PageInspector.Runtime 모듈에 대한 FileNotFoundException 예외 발생
1724정성태8/5/201426067.NET Framework: 452. .NET System.Threading.Thread 개체에서 Native Thread Id를 구하는 방법 - 두 번째 이야기 [1]파일 다운로드1
1723정성태7/29/201458270개발 환경 구성: 233. DirectX 9 예제 프로젝트 빌드하는 방법 [3]파일 다운로드1
1722정성태7/25/201420992오류 유형: 232. IIS 500 Internal Server Error - NTFS 암호화된 폴더에 웹 애플리케이션이 위치한 경우
1721정성태7/24/201424021.NET Framework: 451. 함수형 프로그래밍 개념 - 리스트 해석(List Comprehension)과 순수 함수 [2]
1720정성태7/23/201422023개발 환경 구성: 232. C:\WINDOWS\system32\LogFiles\HTTPERR 폴더에 로그 파일을 남기지 않는 설정
1719정성태7/22/201425963Math: 13. 동전을 여러 더미로 나누는 경우의 수 세기(Partition Number) - 두 번째 이야기파일 다운로드1
1718정성태7/19/201435220Math: 12. HTML에서 수학 관련 기호/수식을 표현하기 위한 방법 - MathJax.js [4]
1716정성태7/17/201434890개발 환경 구성: 231. PC 용 무료 안드로이드 에뮬레이터 - genymotion
1715정성태7/13/201430526기타: 47. 운영체제 종료 후에도 USB 외장 하드의 전원이 꺼지지 않는 경우 [3]
1714정성태7/11/201420852VS.NET IDE: 92. Visual Studio 2013을 지원하는 IL Support 확장 도구
1713정성태7/11/201444567Windows: 98. 윈도우 시스템 디스크 용량 확보를 위한 "Package Cache" 폴더 이동 [1]
1712정성태7/10/201432848.NET Framework: 450. 영문 윈도우에서 C# 콘솔 프로그램의 유니코드 출력 방법 [3]
1711정성태7/10/201438029Windows: 97. cmd.exe 창에서 사용할 폰트를 추가하는 방법 [1]
1710정성태7/8/201430535개발 환경 구성: 230. 유니코드의 Surrogate Pair, Supplementary Characters가 뭘까요?파일 다운로드2
1709정성태7/8/201427323VS.NET IDE: 91. Visual Studio에서 32/64비트 IIS Express 실행하는 방법
1708정성태7/7/201424688VS.NET IDE: 90. Visual Studio - 사용자 정의 정적 분석 규칙 만드는 방법 [3]파일 다운로드1
1707정성태7/4/201422967.NET Framework: 449. C#에서 C++로 VARIANT 넘겨주는 방법파일 다운로드1
1706정성태7/3/201421370.NET Framework: 448. .NET SmartClient 컨트롤을 윈도우 8/2012에서 활성화하는 방법파일 다운로드1
1705정성태7/2/201435034VC++: 78. 보이어-무어(Boyer-Moore) 알고리즘이 정말 빠를까? [6]파일 다운로드1
1704정성태7/2/201421628.NET Framework: 447. w3wp.exe AppPool 재생(recycle)하는 방법 정리
... 121  122  123  124  125  126  127  128  129  130  131  132  [133]  134  135  ...