홀 펀칭(Hole Punching)을 이용한 Private IP 간 통신 - C#
오... 재미있는 사실을 하나 알았습니다. ^^
실전에서 알아보는 홀펀칭 방법.
; http://www.gamedevforever.com/47
간단한 예를 들어서, 가정에서 공유기를 이용하여 인터넷에 접속한 A, B 사용자가 있다고 가정할 때 대부분 공유기에 공용 IP가 할당되기 때문에 서로 간에 통신이 되지 않습니다. 그럴 때 홀 펀칭을 이용해주면 A, B 모두 Private IP를 사용하고 있는데도 불구하고 서로 간에 메시지를 보낼 수 있습니다. (사설 네트워크는 A, B, C 클래스 별로 각각 10.0.0.0, 172.16.0.0, 192.168.0.0을 예약)
테스트를 하기 위해 서버 측 역할을 하는 컴퓨터 한 대와, 각각의 영역에서 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 서버 인스턴스"가 있다는데, 직접 만들어 볼까요? ^^
; https://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 정보를 클라이언트 측에 내려줍니다.
여기까지야 뭐... 일반적인 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/
이렇게 재미있는 기법을 게임 프로그래머들은 이미 다 알고 있었군요. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]