상세 컨텐츠

본문 제목

홀 펀칭(Hole Punching) 을 이용한 Private IP 간 통신 - C# 관련

프로그래밍 관련/네트워크, 통신

by AlrepondTech 2020. 9. 17. 06:25

본문

반응형

 

 

 

 

 

 

=======================

=======================

=======================

 

 

 

 

 

출처: http://www.sysnet.pe.kr/2/0/1226

 

 

오... 재미있는 사실을 하나 알았습니다. ^^

실전에서 알아보는 홀펀칭 방법.
; http://www.gamedevforever.com/47

 

간단한 예를 들어서, 가정에서 공유기를 이용하여 인터넷에 접속한 A, B 사용자가 있다고 가정할 때 대부분 공유기에 공용 IP 가 할당되기 때문에 서로 간에 통신이 되지 않습니다. 그럴 때 홀 펀칭을 이용해주면 A, B 모두 Private IP 를 사용하고 있는 데도 불구하고 서로 간에 메시지를 보낼 수 있습니다.

테스트를 하기 위해 서버 측 역할을 하는 컴퓨터 한 대와, 각각의 영역에서 Private IP를 가지고 있는 PC 2대가 필요합니다. 실제로 어떻게 패킷이 이동하는지 따라가 볼까요? ^^

우선, 클라이언트가 서버 측에 UDP 메시지를 전송할 것입니다. 이 과정에서 다음과 같은 연결 통로가 구성됩니다.



실제로 위와 같은 그림의 통신이 발생하도록 C# 코드로 구성해볼까요? ^^

일단 서버의 UDP 소켓은 12000 번 포트로 입력을 대기하는 것으로 시작해야 합니다.

===== 서버 측 소스 코드 =====

UdpClient _server;
_server = new UdpClient(12000);
_server.BeginReceive(udpReceiveCallback, _server); // 비동기 데이터 수신

void udpReceiveCallback(IAsyncResult ar) {
    try {
        UdpClient udpServer = ar.AsyncState as UdpClient;
        IPEndPoint remoteEndPoint = null;

        byte[] receiveBytes = udpServer.EndReceive(ar, ref remoteEndPoint);

        // 접속된 클라이언트의 IP 주소와 포트 출력
        Console.WriteLine("Receive from " + remoteEndPoint.Address.ToString() + ":" + remoteEndPoint.Port);
        udpServer.BeginReceive(udpReceiveCallback, udpServer);
    } catch {}
}


보시는 것처럼, 서버는 현재 단순하게 접속된 클라이언트의 IP 주소와 포트를 출력하는 기능만 있습니다.

이어서 클라이언트 측을 구현하면 다음과 같습니다.

===== 클라이언트 측 소스 코드 =====

UdpClient _udpClient = new UdpClient();

private void Form1_Load(object sender, EventArgs e) {
    IPAddress ipAddress = IPAddress.Parse("124.137.26.136");
    IPEndPoint holePunchServer = new IPEndPoint(ipAddress, 12000);

    string uid = Environment.MachineName;
    byte[] uidBytes = Encoding.UTF8.GetBytes(uid);

    _udpClient.Send(uidBytes, uidBytes.Length, holePunchServer);
}

 


위와 같이 클라이언트를 실행하고 서버로 데이터가 전송되면 서버 측에는 어떤 메시지가 출력될까요? 당연히 "Receive from 175.194.21.149:60010" 이 됩니다. 통신 패킷만으로 보면 서버 측에서는 절대 클라이언트의 Private IP와 포트를 알 수 없습니다.

이 때문에, 서버에서 클라이언트로 데이터를 보내야 할 일이 생기면 공유기에 열린 포트를 대상으로 데이터를 전송하게 됩니다. 아래의 그림에서 보는 것처럼, 다시 그 과정이 역으로 진행되는 것입니다.



이 때 공유기는 60010 포트로 들어온 데이터가 수신되어야 할 내부 IP 의 컴퓨터에 대한 정보 (192.168.50.10, 50010포트)를 가지고 있으므로 정상적으로 해당 컴퓨터로 UDP 패킷을 전송할 수 있게 되는 것입니다.




이론적으로는 저렇게 패킷이 오고가는 것은 알고 있었지만... "Hole Punching" 까지는 생각할 수 없었습니다. 역시 ... 이런 맛에 공부해나가는 즐거움이 있겠지요. ^^

"Hole Punching"에서는 공유기의 NAT 에서 유지되는 매핑 테이블의 도움을 받아 이뤄집니다. 아이디어는 사실 매우 간단합니다. 즉, UDP 서버가 아니라 다른 컴퓨터에서 해당 공유기 IP 의 60010 포트로 데이터를 전송하면 어떨까 하는 것입니다.



물론, 저 상황에서는 데이터를 보낸 측이 "124.137.26.36:12000" 주소가 아니기 때문에 공유기 측에서 버려집니다. 하지만, 저렇게 데이터를 보내는 와중에 클라이언트 측이 "100.100.100.100:15000" 주소로 데이터를 한번 전송해 주면 어떻게 될까요?



공유기 매핑 테이블에는 175.194.21.14:60010 포트에 대해 2가지 원격 주소지가 포함되어 데이터를 받을 수 있게 됩니다. 결과적으로, Private IP 를 가지고 있는 PC 임에도 불구하고 마치 공용 IP 에 연결할 수 있는 것처럼 데이터를 전달받을 수 있게 된 것입니다.

어떠세요? 처음 이 글을 봤을 때 '아~~~' 하는 감탄사가 나오더군요. ^^




일단, 원리는 그렇다 치고 정말 되는지 한번 확인을 해봐야 되지 않을까요? ^^

그래서, 테스트 환경을 준비해 봤습니다. 우선, 제가 테스트 해 볼 수 있는 network 가 '회사 컴퓨터' 와 '집' 입니다. 물론, 집에 있는 컴퓨터는 Access Point 를 통해서 연결하고 있기 때문에 Private IP 입니다. 그런데... 다른 하나의 클라이언트를 대신해 줄 네트워크가 마땅치 않군요. 하지만, 마침 이전에 ^^ 만들어 두었던 아마존 EC2 무료 Windows 서버 Virtual Machine 이 생각났습니다.

 

아마존 EC2 에 새로 추가된 "1년 무료 Windows 서버 인스턴스"가 있다는데, 직접 만들어 볼까요? ^^
; http://www.sysnet.pe.kr/2/0/1224


이렇게 해서 3군데의 네트워크를 확보하고, 서버/클라이언트는 다음과 같이 정했습니다.

회사 컴퓨터: UDP 서버
집 컴퓨터: Hole Punching 을 하게 되는 클라이언트 A
아마존 가상 컴퓨터: Hole Punching 을 하게 되는 클라이언트 B


아마존 가상 컴퓨터 역시 Private IP를 가지고 있습니다. 과연, 아마존 가상 컴퓨터와 집에 있는 컴퓨터 간에 UDP 통신이 가능할까요? 와~~~ 저도 궁금해집니다. ^^

우선, 첨부 파일을 내려 받아서 빌드하면 다음의 2가지 프로젝트가 나옵니다.

 

HolePunchServer: UDP 서버
HolePunchClient: Hole Punching 을 이용해 서로 통신할 UDP 클라이언트


빌드한 후, HolePunchServer 를 회사 컴퓨터에서 실행시킵니다. 기본값이 12800 포트로 대기하고 있기 때문에 만약 여러분들의 환경에 맞지 않다면 변경해 주시면 됩니다.



그 다음, 각각 아마존과 집 컴퓨터에서 HolePunchClient 를 실행시킵니다. (빌드하기 전에, app.config 안의 SERVER_HOST, SERVER_PORT 값을 자신이 테스트 하는 UDP 서버의 값으로 바꿔주어야 합니다.)



이제 양쪽에서 Connect 버튼을 누르면 회사의 UDP 서버로 데이터를 전송합니다. 이 과정에서 UDP 서버는 양쪽 컴퓨터의 Public IP 와 포트를 취합하게 되고, 서로 통신을 할 양쪽의 IP/Port 정보를 클라이언트 측에 내려줍니다. 

 

 

 

반응형

 

728x90

 



여기까지야 뭐... 일반적인 Server / Client 통신일 뿐이니 신기할 것이 없습니다. 이제, 분배받은 상대방 NAT 장비에 지정된 포트로 데이터를 전송하기 위해 "Send" 버튼을 누릅니다.



오~~~ 정말, 서로의 NAT 장비에 뚫어놓았던 UDP 서버에 연결된 포트 번호로 데이터를 전송하니 양측에서 데이터를 송/수신하고 있습니다. 게다가 서버 측에서 계속 내려주는 LIST 정보까지 받는 걸로 봐서 공유기가 다수의 컴퓨터에서 오는 데이터를 정상적으로 UDP 클라이언트에 전달해 주는 것을 알 수 있습니다.

직접 해보니.. 정말 신기하군요. ^^




아쉬운 점이라면, 이러한 Hole Punching 이 100% 되는 것은 아닙니다. 원문에도 씌여져 있지만 해외 서비스 통계상으로 80% 이상의 성공율을 보이고 있다고 합니다. 또한 공유기의 특성에 따라 매핑된 IP:포트 테이블을 3초만에 지우는 경우도 있다고 하고 지원되는 NAT 의 다양한 방식으로 인해 (Full Cone, Restricted Cone, Port Restricted Cone, Symmetric) 대응도 다를 수 있다고 합니다.

물론, 안 되는 경우를 위해서 UDP 서버 측에서 직접 메시지 전달을 대행해 주는 코드를 만들어야 겠지만... 그래도 80% 정도의 클라이언트에 대한 네트워크 통신 부하를 서버로부터 없앨 수 있다는 것은 대단한 장점입니다.

좀더 기술적인 부분에 대해서는 다음의 글을 참고하십시오.

 

Peer-to-Peer Communication Across Network Address Translators 
; https://docs.google.com/View?id=dc65vhw7_118g5dp5xf3

Hole Punching
; http://sweeper.egloos.com/2431396

 


참고로, 이 글에서는 UDP 만 설명했지만 TCP 도 가능합니다. 게다가 이 글에 첨부한 프로젝트는 간단하게 테스트를 위해 만들어본 코드라서 여러분들의 환경에 맞게 변경해야 하지만, 다음의 공개 C# 소스 코드를 이용하시면 그런 부담을 덜을 수 있습니다.

Introduction to SharpSTUNT
; http://sharpstunt.codeplex.com/


이렇게 재미있는 기법을 게임 프로그래머들은 이미 다 알고 있었군요. ^^

 

 

--------------------------------------------------------------------------------------------------------------------------------------------------

 

[윤형선] 안녕하세요. 저는 홀펀칭을 공부중인 학생입니다.
홀펀칭이라는 기술을 공부하다 보니 의문점이 있어서 질문하나 드릴게요~^^;

NAT A에 두개의 클라이언트가 물려 있다고 가정하면 어떠한 클라이언트에게 
어떠한 data(infomation)를 보고 어떻게 해당 클라이언트를 찾아 들어가는지 궁금합니다.

자세히 다시 설명드리면
지금까지 제가 공부한 바로는..
1. Client(A)는 서버에게 P2P 통신을 하려는 Client (B)의 Public Ip, Port / Private Ip, Port를 받는다.
2. 서버로 부터 받아온 Public/Private 주소에 UDP 패킷을 보내고 
   if Private 주소에서 받으면 direct로 통신
   if Public 주소에서 받으면, 받은 NAT(B)에서는 받은 패킷의 정보가 없기 때문에 버려진다.
3. Client (B)에서도 Client(A)로 UDP 패킷을 보낸다.
4. NAT(A)에는 이미 NAT(B)로 패킷을 보냈던 정보가 저장이 되어 있기 때문에 Client (A)에게 데이터가 전달된다.

라고 이해를 하고 있습니다. 하지만 이 상황에서 위에 말씀드렸다 싶이 NAT(A)에 두개 이상의 Client가 있다면,
이 Client들 중에 패킷을 보냇던 Client를 어떻게 구별을 하는지 궁금합니다.
혹시 시간이 되신다면 답변해주시면 감사하겠습니다.
그럼 수고하세요..

 

--------------------------------------------------------------------------------------------------------------------------------------------------

 

윤형선님의 질문을 일단 제가 이해한데로 답변을 해보겠습니다.

쉬운 예로, 채팅을 한다고 가정할 때 NAT(A: 192.168.0.1)에 Alice(192.168.0.2:포트3000), Bob(192.168.0.3:포트6000) 사용자가 묶여 있습니다. 그 두 사용자가 홀펀칭 서버(100.100.100.100)에 접속했다고 가정해 보겠습니다. 그럼, NAT(A)에는 다음과 같은 정보가 들어있습니다.

192.168.0.2:포트3000 <-> 192.168.0.1:포트9000, 192.168.0.1:포트9000 <-> 100.100.100.100:포트8000
192.168.0.3:포트6000 <-> 192.168.0.1:포트9001, 192.168.0.1:포트9001 <-> 100.100.100.100:포트8000

이런 상태에서, Tom 이라는 사용자가 Alice 와 채팅을 하고 싶다면, 홀펀칭 서버로부터 Alice 가 192.168.0.1:포트9000 에 매핑된 것을 확인할 수 있습니다. 마찬가지로 Bob이라면 192.168.0.1:포트9001임을 알 수 있고.

설명이 되었나요? ^^

 

--------------------------------------------------------------------------------------------------------------------------------------------------

 

[알렉스] 안녕하세요, 정성태님. 
좋은 글에 감사를 드립니다. 하지만 소스코드와 작성하신 글의 내용이 이해가 잘 안되는 부분이 있어 문의드립니다. 

글에는 

"물론, 저 상황에서는 데이터를 보낸 측이 "124.137.26.36:12000" 주소가 아니기 때문에 공유기 측에서 버려집니다. 하지만, 저렇게 데이터를 보내는 와중에 클라이언트 측이 "100.100.100.100:15000" 주소로 데이터를 한번 전송해 주면 어떻게 될까요?"

즉 클라이언트가 100.100.100.100:15000으로 데이터를 한번 전송해 주어야 NAT에 매핑 테이블에 기록이 되어 100.100.100.100:15000으로부터 클라이언트에게 데이터를 보낼 수 있다는 것인데, 100.100.100.100:15000이 만일 NAT아래에 있다면 클라이언트가 패킷을 보낼 수도 없을텐데 그러한경우엔 어떻게 하는건지 궁금합니다. 

--------------------------------------------------------------------------------------------------------------------------------------------------

100.100.100.100:15000 이 NAT 아래에 있다고 해도 상관없습니다. 결국 그 NAT 도 192.168.50.10 컴퓨터를 중계하고 있는 NAT 처럼 동작할 것이기 때문에 포트가 열리게 됩니다. 175.194.21.149 NAT 처럼 100.100.100.100 에도 NAT 을 하나 넣어서 IP 매핑 테이블을 직접 그려 보시면 왜 그런지 금방 아시게 될 것입니다.

단지, 문제가 된다면 100.100.100.100 과 192.168.50.10 컴퓨터가 같은 NAT 에 속해 있을 경우입니다. 바로 그 경우가 홀펀칭이 잘 안되는 대표적인 사례입니다. 위에서 제가 참고하라는 글 중에 "Peer-to-Peer Communication Across Network Address Translators" 의 "3.3 Peers Behind a Common NAT" 내용을 보시면 이에 대해 자세하게 설명되어 있는데요. 이런 경우, 해당 NAT 이 'hairpin' 변환을 지원하면 패킷이 전송되는데, 현실적으로 볼 때 대부분의 NAT 에서 hairpin 변환이 지원되지 않는다고 합니다.

따라서 이런 경우를 포함해서 홀펀칭이 안되는 상황을 고려해 별도의 릴레이 역할을 하는 서버를 하나 두어야 합니다. "실전에서 알아보는 홀펀칭 방법." 글에서도 나오지만 홀펀칭이 100% 되지는 않습니다. ^^

--------------------------------------------------------------------------------------------------------------------------------------------------

[완료] 각기 다른 사설망(NAT)에 들어있는 클라이언트를 서로 연결해주기 (메신저)
http://kldp.org/node/122704

--------------------------------------------------------------------------------------------------------------------------------------------------

결국 A에서 B로 공인IP 및 포트로 한번 연결 시도하면 상대방은 A로 연결할 때 A가 B로 연결시도 했기 때문에 만들어진 공유기의 홀을 통해 접속이 가능한 거네요. 예전에 네트워크 공부할 때 사설망끼리의 P2P는 어떻게 연결이 가능한지 궁금했었는데 말끔히 해소되었습니다.^^ 좋은 글 감사합니다.

 

 

 

=======================

=======================

=======================

 

 

 

 

출처: http://kldp.org/node/122704

 

각기 다른 사설망(NAT)에 들어있는 클라이언트를 서로 연결해주기 (메신저)

 

 

…는 UDP 홀펀칭 말고 다른 방법이 없을까요?

TCP로는 구현이 불가능한 것인지 여쭤보고 싶습니다. (_ _)


저는 이런 시나리오를 생각했습니다.
1) 먼저 클라이언트 A가 TCP 방식을 통해 socket() bind() connect()로 명시적으로 포트를 선택하여 서버와 접속을 합니다.
2) 이를 통해 서버는 클라이언트 A의 실제 IP와 포트 번호를 알게 됩니다. 이를 클라이언트 B에게 전달합니다.
3) 클라이언트 A는 shutdown()을 통해 서버와의 연결을 우아하게 종료합니다. (하지만, 소켓을 폐기하는것은 아닙니다.)
4) 클라이언트 A는 연결을 종료했으므로 1)에서 만들어둔 소켓을 사용해 listen()을 시작합니다.
5) 클라이언트 B는 서버로 얻은 정보를 통해 클라이언트A에게 connect()합니다.
6) TCP 연결이 완성됩니다.


..가 안 되더군요; 정확히는 4)가 안 됩니다. 3)에서 shutdown()은 성공했다고 나오던데 listen()을 하면
이미 연결이 되어 있다는 wsagetlasterror() 에러 메시지가 뜹니다.

그냥 비연결적인 UDP로 해야 하는 것일까요? 달리 물을 곳이 없어서 이곳에 여쭤봅니다..

‹ 게임 엔진이란건 어떻게 만드는 건가요?바이오스로 키보드 드라이버 어떻게하는지좀 도와주세요... ›

음..

글쓴이: 익명 사용자 (미확인) 작성 일시: 월, 2011/04/18 - 1:23오후

서로 다른 소켓을 열어서 같은 포트에 바인딩하고 하나는 listen하고 하나는 connect를 하게 하면 될듯하군요

REUSE_ADDRESS 였던가.. 하여간 소켓 옵션을 통해서 멀티바인딩은 허용이 될꺼구요.

단, MSDN의 정확한 정보를 확인해보시기 바랍니다.

오래되서 가물가물하네요..

»

감사합니다

글쓴이: templars 작성 일시: 월, 2011/04/18 - 1:30오후

그렇군요 소켓을 두 개 써서 같은 포트에 바인드.. 바로 해보겠습니다ㅎㅎ

»

정말 감사합니다

글쓴이: templars 작성 일시: 월, 2011/04/18 - 9:08오후

일단 localhost상에서는 정상적으로 작동하네요. 소켓 옵션 SO_REUSEADDR을 이용하여 하나의 포트에 두 소켓을 바인드 한 다음, connect()를 shutdown()으로 Graceful하게 종료 후, 다른 소켓을 사용하여 listen() accept()를 수행하는데 성공했습니다. 정말 감사합니다.

»

성공하셨다고 해서 질문 드립니다.

글쓴이: 익명 사용자 작성 일시: 수, 2015/06/17 - 2:06오후

SO_REUSEADDR이 JAVA에서 setReuseAddress 옵션이랑 같은거 아닌가요??
왜 저는 setReuseAddress 옵션을 true로 줘도 "Address already in use: JVM_Bind" 에러 메세지가 나올까요?

»

 

 

 

 

=======================

=======================

=======================

 

 

 

반응형


관련글 더보기

댓글 영역