web-dev-qa-db-ja.com

UDPホールパンチングの実装

UDPホールパンチングを実現しようとしています。私の理論は この記事 とこれ WIKIページ に基づいていますが、C#コーディングでいくつかの問題に直面しています。これが私の問題です:

投稿されたコードを使用して ここ リモートマシンに接続し、同じポートで着信接続をリッスンできるようになりました(2つのUDPクライアントを同じポートにバインドします)。

何らかの理由で、同じポートへの2つのバインディングは、互いにデータの受信をブロックします。接続に応答するUDPサーバーがあるので、他のクライアントをポートにバインドする前に最初に接続すると、応答が返されます。

別のクライアントをポートにバインドすると、どちらのクライアントでもデータは受信されません。

以下は私の問題を示す2つのコード部分です。最初にリモートサーバーに接続してNATデバイスでルールを作成し、次にリスナーを別のスレッドで起動して着信パケットをキャプチャします。次に、コードはパケットをローカルIPに送信して、リスナーがパケットを取得できるようにします。 2つ目は、これが機能することを確認するために、ローカルIPにパケットを送信するだけです。 NATデバイスをまったく使用せずにパケットを自分自身に送信しているため、これは実際の穴あけではないことを私は知っています。この時点で問題が発生しています。NATデバイスの外部にあるコンピューターを使用して接続しても、これが変わるとは思いません。

[編集] 2012年2月4日ネットワーク上の別のコンピューターとWireShark(パケットスニファー)を使用してリスナーをテストしてみました。他のコンピューターから着信するパケットが表示されますが、リスナーUDPクライアント(udpServer)または送信者UDPクライアント(クライアント)によって受信されません。

[編集] 2010年2月5日ポートでリッスンする2番目のUDPクライアントのみに存在するパケットの最初の送受信後に最初のUDPクライアントを閉じる関数呼び出しを追加しました。これは機能し、そのポートでネットワーク内からパケットを受信できます。次に、ネットワークの外部からパケットを送受信しようとします。何か見つけたらすぐに投稿します。

このコードを使用して、リスニングクライアントでデータを取得します。

static void Main(string[] args)
{
    IPEndPoint localpt = new IPEndPoint(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);

    ThreadPool.QueueUserWorkItem(delegate
    {
        UdpClient udpServer = new UdpClient();
        udpServer.ExclusiveAddressUse = false;
        udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        udpServer.Client.Bind(localpt);

        IPEndPoint inEndPoint = new IPEndPoint(IPAddress.Any, 0);
        Console.WriteLine("Listening on " + localpt + ".");
        byte[] buffer = udpServer.Receive(ref inEndPoint); //this line will block forever
        Console.WriteLine("Receive from " + inEndPoint + " " + Encoding.ASCII.GetString(buffer) + ".");
    });

    Thread.Sleep(1000);

    UdpClient udpServer2 = new UdpClient(6000);

    // the following lines work and the data is received
    udpServer2.Connect(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);
    udpServer2.Send(new byte[] { 0x41 }, 1);

    Console.Read();
}

次のコードを使用すると、クライアントとサーバー間の接続とデータ転送の後、リスニングUDPクライアントは何も受信しません。

static void Main(string[] args)
{
    IPEndPoint localpt = new IPEndPoint(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);

    //if the following lines up until serverConnect(); are removed all packets are received correctly
    client = new UdpClient();
    client.ExclusiveAddressUse = false;
    client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    client.Client.Bind(localpt);
    remoteServerConnect(); //connection to remote server is done here
                           //response is received correctly and printed to the console

    ThreadPool.QueueUserWorkItem(delegate
    {
        UdpClient udpServer = new UdpClient();
        udpServer.ExclusiveAddressUse = false;
        udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        udpServer.Client.Bind(localpt);

        IPEndPoint inEndPoint = new IPEndPoint(IPAddress.Any, 0);
        Console.WriteLine("Listening on " + localpt + ".");
        byte[] buffer = udpServer.Receive(ref inEndPoint); //this line will block forever
        Console.WriteLine("Receive from " + inEndPoint + " " + Encoding.ASCII.GetString(buffer) + ".");
    });

    Thread.Sleep(1000);

    UdpClient udpServer2 = new UdpClient(6000);

    // I expected the following line to work and to receive this as well
    udpServer2.Connect(Dns.Resolve(Dns.GetHostName()).AddressList[0], 4545);
    udpServer2.Send(new byte[] { 0x41 }, 1);

    Console.Read();
}
23
brooc

私が正しく理解していれば、穴あけ用の仲介サーバーを使用して、それぞれ異なるNATの背後にある2つのクライアント間でピアツーピア通信を試みていますか?

数年前、私はc#でまったく同じことをしました。まだコードを見つけていませんが、必要に応じていくつかのポインターを提供します。

まず、UDPはコネクションレス型プロトコルであるため、udpclientでConnect()関数を使用しません。この関数が実際に行うのは、UDPソケットの機能を非表示にすることだけです。

次の手順を実行する必要があります。

  1. 特定のポートで、ファイアウォールによってブロックされていないポートを持つサーバーでUDPソケットを開きます(例:Bindこのソケットを選択したポート(例:23000)に)
  2. 最初のクライアントでUDPソケットを作成し、23000のサーバーに何かを送信します。このソケットをバインドしないでください。 udpを使用してパケットを送信すると、Windowsは自動的にソケットに空きポートを割り当てます。
  3. 他のクライアントからも同じことをします
  4. サーバーは、2つの異なるポートを持つ2つの異なるアドレスの2つのクライアントから2つのパケットを受信しました。サーバーが同じアドレスとポートでパケットを送り返すことができるかどうかをテストします。 (これが機能しない場合は、何か問題があったか、NATが機能していません。ポートを開かずにゲームをプレイできる場合は、機能していることがわかります:D)
  5. サーバーは、接続されている各クライアントに他のクライアントのアドレスとポートを送信する必要があります。
  6. これで、クライアントは、UDPを使用してサーバーから受信したアドレスにパケットを送信できるようになります。

NATで使用されるポートは、おそらくクライアントPCと同じポートではないことに注意してください。サーバーは、この外部ポートをクライアントに配布する必要があります。 送信するには外部アドレスと外部ポートを使用する必要があります!

また、NATは、この種のポート転送をサポートしていない可能性があることにも注意してください。一部のNATは、割り当てられたポートのすべての着信トラフィックをクライアントに転送します。これは、必要なことです。ただし、一部のNATは、着信パケットは、他のクライアントパケットをブロックする可能性があるため、アドレス指定されます。これは、標準のパーソナルユーザールーターを使用している場合はほとんどありません。

15
MHGameWork

編集:さらに多くのテストを行った後、UPnPを有効にしない限り、これはまったく機能しないようです。したがって、ここで書いたことの多くは役立つかもしれませんが、多くの人はUPnPを有効にしていないため(セキュリティ上のリスクがあるため)、UPnPは機能しません。

PubNubをリレーサーバーとして使用するコードを次に示します:)。このコードは完全ではないため、テストせずに使用することはお勧めしません(安全であるか、正しい方法であるかはわかりませんか?idk私はネットワークの専門家ではありません)が、アイデアが得られるはずです。何をすべきかについて。少なくともこれまでのところ、趣味のプロジェクトではうまくいきました。不足しているものは次のとおりです。

  • クライアントがLAN上にあるかどうかをテストします。私はあなたのLANと別のネットワーク上のデバイスの両方に送信するだけですが、それは非常に非効率的です。
  • たとえば、クライアントがプログラムを閉じた場合に、クライアントがリスニングを停止したときのテスト。これはUDPであるため、ステートレスであるため、メッセージをvoidに送信するかどうかは問題ではありませんが、誰もメッセージを取得していない場合は、おそらくそうすべきではありません。
  • Open.NAT を使用してプログラムでポート転送を行いますが、これは一部のデバイスでは機能しない可能性があります。具体的には、少し安全性が低く、UDPポート1900を手動でポート転送する必要があるUPnPを使用します。これを実行すると、ほとんどのルーターでサポートされますが、多くのルーターはまだこれを実行していません。

したがって、まず最初に、外部IPとローカルIPを取得する方法が必要です。ローカルIPを取得するためのコードは次のとおりです。

// From http://stackoverflow.com/questions/6803073/get-local-ip-address
public string GetLocalIp()
{
    var Host = Dns.GetHostEntry(Dns.GetHostName());
    foreach (var ip in Host.AddressList)
    {
        if (ip.AddressFamily == AddressFamily.InterNetwork)
        {
            return ip.ToString();
        }
    }
    throw new Exception("Failed to get local IP");
}

そして、ここにあなたの外部IPを返すように設計されたいくつかのウェブサイトを試すことによってあなたの外部IPを取得するためのいくつかのコードがあります

public string GetExternalIp()
{
    for (int i = 0; i < 2; i++)
    {
        string res = GetExternalIpWithTimeout(400);
        if (res != "")
        {
            return res;
        }
    }
    throw new Exception("Failed to get external IP");
}
private static string GetExternalIpWithTimeout(int timeoutMillis)
{
    string[] sites = new string[] {
      "http://ipinfo.io/ip",
      "http://icanhazip.com/",
      "http://ipof.in/txt",
      "http://ifconfig.me/ip",
      "http://ipecho.net/plain"
    };
    foreach (string site in sites)
    {
        try
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(site);
            request.Timeout = timeoutMillis;
            using (var webResponse = (HttpWebResponse)request.GetResponse())
            {
                using (Stream responseStream = webResponse.GetResponseStream())
                {
                    using (StreamReader responseReader = new System.IO.StreamReader(responseStream, Encoding.UTF8))
                    {
                        return responseReader.ReadToEnd().Trim();
                    }
                }
            }
        }
        catch
        {
            continue;
        }
    }

    return "";

}

次に、開いているポートを見つけて、外部ポートに転送する必要があります。上記のように、私は Open.NAT を使用しました。まず、 登録されたUDPポート を確認した後、アプリケーションが使用するのに妥当と思われるポートのリストをまとめます。次にいくつか例を示します。

public static int[] ports = new int[]
{
  5283,
  5284,
  5285,
  5286,
  5287,
  5288,
  5289,
  5290,
  5291,
  5292,
  5293,
  5294,
  5295,
  5296,
  5297
};

これで、それらをループして、ポート転送を使用するために使用されていないものを見つけることができます。

public UdpClient GetUDPClientFromPorts(out Socket portHolder, out string localIp, out int localPort, out string externalIp, out int externalPort)
{
  localIp = GetLocalIp();
  externalIp = GetExternalIp();

  var discoverer = new Open.Nat.NatDiscoverer();
  var device = discoverer.DiscoverDeviceAsync().Result;

  IPAddress localAddr = IPAddress.Parse(localIp);
  int workingPort = -1;
  for (int i = 0; i < ports.Length; i++)
  {
      try
      {
          // You can alternatively test tcp with  nc -vz externalip 5293 in linux and
          // udp with  nc -vz -u externalip 5293 in linux
          Socket tempServer = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
          tempServer.Bind(new IPEndPoint(localAddr, ports[i]));
          tempServer.Close();
          workingPort = ports[i];
          break;
      }
      catch
      {
        // Binding failed, port is in use, try next one
      }
  }


  if (workingPort == -1)
  {
      throw new Exception("Failed to connect to a port");
  }


  int localPort = workingPort;

  // You could try a different external port if the below code doesn't work
  externalPort = workingPort;

  // Mapping ports
  device.CreatePortMapAsync(new Open.Nat.Mapping(Open.Nat.Protocol.Udp, localPort, externalPort));

  // Bind a socket to our port to "claim" it or cry if someone else is now using it
  try
  {
      portHolder = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
      portHolder.Bind(new IPEndPoint(localAddr, localPort));
  }
  catch
  {
      throw new Exception("Failed, someone is now using local port: " + localPort);
  }


  // Make a UDP Client that will use that port
  UdpClient udpClient = new UdpClient(localPort);
  return udpClient;
}

ここで、PubNubリレーサーバーコードについて説明します(P2PPeerは後で定義します)。ここにはたくさんあるので、実際には説明しませんが、コードが明確で、何が起こっているのかを理解するのに役立つことを願っています

public delegate void NewPeerCallback(P2PPeer newPeer);
public event NewPeerCallback OnNewPeerConnection;

public Pubnub pubnub;
public string pubnubChannelName;
public string localIp;
public string externalIp;
public int localPort;
public int externalPort;
public UdpClient udpClient;
HashSet<string> uniqueIdsPubNubSeen;
object peerLock = new object();
Dictionary<string, P2PPeer> connectedPeers;
string myPeerDataString;

public void InitPubnub(string pubnubPublishKey, string pubnubSubscribeKey, string pubnubChannelName)
{
    uniqueIdsPubNubSeen = new HashSet<string>();
    connectedPeers = new Dictionary<string, P2PPeer>;
    pubnub = new Pubnub(pubnubPublishKey, pubnubSubscribeKey);
    myPeerDataString = localIp + " " + externalIp + " " + localPort + " " + externalPort + " " + pubnub.SessionUUID;
    this.pubnubChannelName = pubnubChannelName;
    pubnub.Subscribe<string>(
        pubnubChannelName,
        OnPubNubMessage,
        OnPubNubConnect,
        OnPubNubError);
    return pubnub;
}

//// Subscribe callbacks
void OnPubNubConnect(string res)
{
    pubnub.Publish<string>(pubnubChannelName, connectionDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);
}

void OnPubNubError(PubnubClientError clientError)
{
    throw new Exception("PubNub error on subscribe: " + clientError.Message);
}

void OnPubNubMessage(string message)
{
    // The message will be the string ["localIp externalIp localPort externalPort","messageId","channelName"]
    string[] splitMessage = message.Trim().Substring(1, message.Length - 2).Split(new char[] { ',' });
    string peerDataString = splitMessage[0].Trim().Substring(1, splitMessage[0].Trim().Length - 2);

    // If you want these, I don't need them
    //string peerMessageId = splitMessage[1].Trim().Substring(1, splitMessage[1].Trim().Length - 2);
    //string channelName = splitMessage[2].Trim().Substring(1, splitMessage[2].Trim().Length - 2);


    string[] pieces = peerDataString.Split(new char[] { ' ', '\t' });
    string peerLocalIp = pieces[0].Trim();
    string peerExternalIp = pieces[1].Trim();
    string peerLocalPort = int.Parse(pieces[2].Trim());
    string peerExternalPort = int.Parse(pieces[3].Trim());
    string peerPubnubUniqueId = pieces[4].Trim();

    pubNubUniqueId = pieces[4].Trim();

    // If you are on the same device then you have to do this for it to work idk why
    if (peerLocalIp == localIp && peerExternalIp == externalIp)
    {
        peerLocalIp = "127.0.0.1";
    }


    // From me, ignore
    if (peerPubnubUniqueId == pubnub.SessionUUID)
    {
        return;
    }

    // We haven't set up our connection yet, what are we doing
    if (udpClient == null)
    {
        return;
    }


    // From someone else


    IPEndPoint peerEndPoint = new IPEndPoint(IPAddress.Parse(peerExternalIp), peerExternalPort);
    IPEndPoint peerEndPointLocal = new IPEndPoint(IPAddress.Parse(peerLocalIp), peerLocalPort);

    // First time we have heard from them
    if (!uniqueIdsPubNubSeen.Contains(peerPubnubUniqueId))
    {
        uniqueIdsPubNubSeen.Add(peerPubnubUniqueId);

        // Dummy messages to do UDP hole punching, these may or may not go through and that is fine
        udpClient.Send(new byte[10], 10, peerEndPoint);
        udpClient.Send(new byte[10], 10, peerEndPointLocal); // This is if they are on a LAN, we will try both
        pubnub.Publish<string>(pubnubChannelName, myPeerDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);
    }
    // Second time we have heard from them, after then we don't care because we are connected
    else if (!connectedPeers.ContainsKey(peerPubnubUniqueId))
    {
        //bool isOnLan = IsOnLan(IPAddress.Parse(peerExternalIp)); TODO, this would be Nice to test for
        bool isOnLan = false; // For now we will just do things for both
        P2PPeer peer = new P2PPeer(peerLocalIp, peerExternalIp, peerLocalPort, peerExternalPort, this, isOnLan);
        lock (peerLock)
        {
            connectedPeers.Add(peerPubnubUniqueId, peer);
        }

        // More dummy messages because why not
        udpClient.Send(new byte[10], 10, peerEndPoint);
        udpClient.Send(new byte[10], 10, peerEndPointLocal);


        pubnub.Publish<string>(pubnubChannelName, connectionDataString, OnPubNubTheyGotMessage, OnPubNubMessageFailed);
        if (OnNewPeerConnection != null)
        {
            OnNewPeerConnection(peer);
        }
    }
}

//// Publish callbacks
void OnPubNubTheyGotMessage(object result)
{

}

void OnPubNubMessageFailed(PubnubClientError clientError)
{
    throw new Exception("PubNub error on publish: " + clientError.Message);
}

そしてここにP2PPeerがあります

public class P2PPeer
{
    public string localIp;
    public string externalIp;
    public int localPort;
    public int externalPort;
    public bool isOnLan;

    P2PClient client;

    public delegate void ReceivedBytesFromPeerCallback(byte[] bytes);

    public event ReceivedBytesFromPeerCallback OnReceivedBytesFromPeer;


    public P2PPeer(string localIp, string externalIp, int localPort, int externalPort, P2PClient client, bool isOnLan)
    {
        this.localIp = localIp;
        this.externalIp = externalIp;
        this.localPort = localPort;
        this.externalPort = externalPort;
        this.client = client;
        this.isOnLan = isOnLan;



        if (isOnLan)
        {
            IPEndPoint endPointLocal = new IPEndPoint(IPAddress.Parse(localIp), localPort);
            Thread localListener = new Thread(() => ReceiveMessage(endPointLocal));
            localListener.IsBackground = true;
            localListener.Start();
        }

        else
        {
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(externalIp), externalPort);
            Thread externalListener = new Thread(() => ReceiveMessage(endPoint));
            externalListener.IsBackground = true;
            externalListener.Start();
        }
    }

    public void SendBytes(byte[] data)
    {
        if (client.udpClient == null)
        {
            throw new Exception("P2PClient doesn't have a udpSocket open anymore");
        }
        //if (isOnLan) // This would work but I'm not sure how to test if they are on LAN so I'll just use both for now
        {
            client.udpClient.Send(data, data.Length, new IPEndPoint(IPAddress.Parse(localIp), localPort));
        }
        //else
        {
            client.udpClient.Send(data, data.Length, new IPEndPoint(IPAddress.Parse(externalIp), externalPort));
        }
    }

    // Encoded in UTF8
    public void SendString(string str)
    {
        SendBytes(System.Text.Encoding.UTF8.GetBytes(str));
    }


    void ReceiveMessage(IPEndPoint endPoint)
    {
        while (client.udpClient != null)
        {
            byte[] message = client.udpClient.Receive(ref endPoint);
            if (OnReceivedBytesFromPeer != null)
            {
                OnReceivedBytesFromPeer(message);
            }
            //string receiveString = Encoding.UTF8.GetString(message);
            //Console.Log("got: " + receiveString);
        }
    }
}

最後に、これが私のすべての使用法です:

using PubNubMessaging.Core; // Get from PubNub GitHub for C#, I used the Unity3D library
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

私はコメントや質問を受け付けています。ここで何かが悪い習慣であるか、うまくいかない場合は、遠慮なくフィードバックを送ってください。私のコードからの翻訳でいくつかのバグが導入され、最終的にここで修正しますが、これにより、少なくとも何をすべきかがわかるはずです。

5
Phylliida

非同期関数を使用してみましたか?これは、それを機能させる方法の例です。100%機能させるには少し作業が必要になる場合があります。

    public void HolePunch(String ServerIp, Int32 Port)
    {
        IPEndPoint LocalPt = new IPEndPoint(Dns.GetHostEntry(Dns.GetHostName()).AddressList[0], Port);
        UdpClient Client = new UdpClient();
        Client.ExclusiveAddressUse = false;
        Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        Client.Client.Bind(LocalPt);

        IPEndPoint RemotePt = new IPEndPoint(IPAddress.Parse(ServerIp), Port);

        // This Part Sends your local endpoint to the server so if the two peers are on the same nat they can bypass it, you can omit this if you wish to just use the remote endpoint.
        byte[] IPBuffer = System.Text.Encoding.UTF8.GetBytes(Dns.GetHostEntry(Dns.GetHostName()).AddressList[0].ToString());
        byte[] LengthBuffer = BitConverter.GetBytes(IPBuffer.Length);
        byte[] PortBuffer = BitConverter.GetBytes(Port);
        byte[] Buffer = new byte[IPBuffer.Length + LengthBuffer.Length + PortBuffer.Length];
        LengthBuffer.CopyTo(Buffer,0);
        IPBuffer.CopyTo(Buffer, LengthBuffer.Length);
        PortBuffer.CopyTo(Buffer, IPBuffer.Length + LengthBuffer.Length);
        Client.BeginSend(Buffer, Buffer.Length, RemotePt, new AsyncCallback(SendCallback), Client);

        // Wait to receve something
        BeginReceive(Client, Port);

        // you may want to use a auto or manual ResetEvent here and have the server send back a confirmation, the server should have now stored your local (you sent it) and remote endpoint.

        // you now need to work out who you need to connect to then ask the server for there remote and local end point then need to try to connect to the local first then the remote.
        // if the server knows who you need to connect to you could just have it send you the endpoints as the confirmation.

        // you may also need to keep this open with a keepalive packet untill it is time to connect to the peer or peers.

        // once you have the endpoints of the peer you can close this connection unless you need to keep asking the server for other endpoints

        Client.Close();
    }

    public void ConnectToPeer(String PeerIp, Int32 Port)
    {
        IPEndPoint LocalPt = new IPEndPoint(Dns.GetHostEntry(Dns.GetHostName()).AddressList[0], Port);
        UdpClient Client = new UdpClient();
        Client.ExclusiveAddressUse = false;
        Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        Client.Client.Bind(LocalPt);
        IPEndPoint RemotePt = new IPEndPoint(IPAddress.Parse(PeerIp), Port);
        Client.Connect(RemotePt);
        //you may want to keep the peer client connections in a list.

        BeginReceive(Client, Port);
    }

    public void SendCallback(IAsyncResult ar)
    {
        UdpClient Client = (UdpClient)ar.AsyncState;
        Client.EndSend(ar);
    }

    public void BeginReceive(UdpClient Client, Int32 Port)
    {
        IPEndPoint ListenPt = new IPEndPoint(IPAddress.Any, Port);

        Object[] State = new Object[] { Client, ListenPt };

        Client.BeginReceive(new AsyncCallback(ReceiveCallback), State);
    }

    public void ReceiveCallback(IAsyncResult ar)
    {
        UdpClient Client = (UdpClient)((Object[])ar.AsyncState)[0];
        IPEndPoint ListenPt = (IPEndPoint)((Object[])ar.AsyncState)[0];

        Byte[] receiveBytes = Client.EndReceive(ar, ref ListenPt);
    }

これがお役に立てば幸いです。

3
Alex L

このような巨大なコードをアップロードして申し訳ありませんが、これは物事がどのように機能するかを非常に明確に説明しており、本当に役立つかもしれません。このコードで問題が発生する場合は、お知らせください。

注:

  1. これは単なるドラフトです
  2. (重要)ローカルエンドポイントをサーバーに通知する必要があります。そうしないと、サーバーがNATに接続されていなくても、1つのNAT(たとえば、1つのローカルマシン上)の背後にある2つのピア間で通信できなくなります。
  3. 「パンチャー」クライアントを閉じる必要があります(少なくとも、受信するまでパケットを受信できませんでした)。後で、他のUdpClientを使用してサーバーと通信できるようになります
  4. 当然のことながら、対称NATでは機能しません
  5. このコードの何かが「ひどい習慣」であることがわかったら、教えてください、私はネットワークの専門家ではありません:)

Server.cs

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using HolePunching.Common;

namespace HolePunching.Server
{
    class Server
    {
        private static bool _isRunning;
        private static UdpClient _udpClient;
        private static readonly Dictionary<byte, PeerContext> Contexts = new Dictionary<byte, PeerContext>();

        private static readonly Dictionary<byte, byte> Mappings = new Dictionary<byte, byte>
        {
            {1, 2},
            {2, 1},
        };

        static void Main()
        {
            _udpClient = new UdpClient( Consts.UdpPort );
            ListenUdp();

            Console.ReadLine();
            _isRunning = false;
        }

        private static async void ListenUdp()
        {
            _isRunning = true;

            while ( _isRunning )
            {
                try
                {
                    var receivedResults = await _udpClient.ReceiveAsync();

                    if ( !_isRunning )
                    {
                        break;
                    }

                    ProcessUdpMessage( receivedResults.Buffer, receivedResults.RemoteEndPoint );
                }
                catch ( Exception ex )
                {
                    Console.WriteLine( $"Error: {ex.Message}" );
                }
            }
        }

        private static void ProcessUdpMessage( byte[] buffer, IPEndPoint remoteEndPoint )
        {
            if ( !UdpProtocol.UdpInfoMessage.TryParse( buffer, out UdpProtocol.UdpInfoMessage message ) )
            {
                Console.WriteLine( $" >>> Got shitty UDP [ {remoteEndPoint.Address} : {remoteEndPoint.Port} ]" );
                _udpClient.Send( new byte[] { 1 }, 1, remoteEndPoint );
                return;
            }

            Console.WriteLine( $" >>> Got UDP from {message.Id}. [ {remoteEndPoint.Address} : {remoteEndPoint.Port} ]" );

            if ( !Contexts.TryGetValue( message.Id, out PeerContext context ) )
            {
                context = new PeerContext
                {
                    PeerId = message.Id,
                    PublicUdpEndPoint = remoteEndPoint,
                    LocalUdpEndPoint = new IPEndPoint( message.LocalIp, message.LocalPort ),
                };

                Contexts.Add( context.PeerId, context );
            }

            byte partnerId = Mappings[context.PeerId];
            if ( !Contexts.TryGetValue( partnerId, out context ) )
            {
                _udpClient.Send( new byte[] { 1 }, 1, remoteEndPoint );
                return;
            }

            var response = UdpProtocol.PeerAddressMessage.GetMessage(
                partnerId,
                context.PublicUdpEndPoint.Address,
                context.PublicUdpEndPoint.Port,
                context.LocalUdpEndPoint.Address,
                context.LocalUdpEndPoint.Port );

            _udpClient.Send( response.Data, response.Data.Length, remoteEndPoint );

            Console.WriteLine( $" <<< Responsed to {message.Id}" );
        }
    }

    public class PeerContext
    {
        public byte PeerId { get; set; }
        public IPEndPoint PublicUdpEndPoint { get; set; }
        public IPEndPoint LocalUdpEndPoint { get; set; }
    }
}

Client.cs

using System;

namespace HolePunching.Client
{
    class Client
    {
        public const string ServerIp = "your.server.public.address";

        static void Main()
        {
            byte id = ReadIdFromConsole();

            // you need some smarter :)
            int localPort = id == 1 ? 61043 : 59912;
            var x = new Demo( ServerIp, id, localPort );
            x.Start();
        }

        private static byte ReadIdFromConsole()
        {
            Console.Write( "Peer id (1 or 2): " );

            var id = byte.Parse( Console.ReadLine() );

            Console.Title = $"Peer {id}";

            return id;
        }
    }
}

Demo.cs

using HolePunching.Common;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace HolePunching.Client
{
    public class Demo
    {
        private static bool _isRunning;

        private static UdpClient _udpPuncher;
        private static UdpClient _udpClient;
        private static UdpClient _extraUdpClient;
        private static bool _extraUdpClientConnected;

        private static byte _id;

        private static IPEndPoint _localEndPoint;
        private static IPEndPoint _serverUdpEndPoint;
        private static IPEndPoint _partnerPublicUdpEndPoint;
        private static IPEndPoint _partnerLocalUdpEndPoint;

        private static string GetLocalIp()
        {
            var Host = Dns.GetHostEntry( Dns.GetHostName() );
            foreach ( var ip in Host.AddressList )
            {
                if ( ip.AddressFamily == AddressFamily.InterNetwork )
                {
                    return ip.ToString();
                }
            }
            throw new Exception( "Failed to get local IP" );
        }

        public Demo( string serverIp, byte id, int localPort )
        {
            _serverUdpEndPoint = new IPEndPoint( IPAddress.Parse( serverIp ), Consts.UdpPort );
            _id = id;

            // we have to bind all our UdpClients to this endpoint
            _localEndPoint = new IPEndPoint( IPAddress.Parse( GetLocalIp() ), localPort );
        }

        public void Start(  )
        {
            _udpPuncher = new UdpClient(); // this guy is just for punching
            _udpClient = new UdpClient(); // this will keep hole alive, and also can send data
            _extraUdpClient = new UdpClient(); // i think, this guy is the best option for sending data (explained below)

            InitUdpClients( new[] { _udpPuncher, _udpClient, _extraUdpClient }, _localEndPoint );

            Task.Run( (Action) SendUdpMessages );
            Task.Run( (Action) ListenUdp );

            Console.ReadLine();
            _isRunning = false;
        }

        private void InitUdpClients(IEnumerable<UdpClient> clients, EndPoint localEndPoint)
        {
            // if you don't want to use explicit localPort, you should create here one more UdpClient (X) and send something to server (it will automatically bind X to free port). then bind all clients to this port and close X

            foreach ( var udpClient in clients )
            {
                udpClient.ExclusiveAddressUse = false;
                udpClient.Client.SetSocketOption( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true );
                udpClient.Client.Bind( localEndPoint );
            }
        }

        private void SendUdpMessages()
        {
            _isRunning = true;

            var messageToServer = UdpProtocol.UdpInfoMessage.GetMessage( _id, _localEndPoint.Address, _localEndPoint.Port );
            var messageToPeer = UdpProtocol.P2PKeepAliveMessage.GetMessage();

            while ( _isRunning )
            {
                // while we dont have partner's address, we will send messages to server
                if ( _partnerPublicUdpEndPoint == null && _partnerLocalUdpEndPoint == null )
                {
                    _udpPuncher.Send( messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint );
                    Console.WriteLine( $" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]" );
                }
                else
                {
                    // you can skip it. just demonstration, that you still can send messages to server
                    _udpClient.Send( messageToServer.Data, messageToServer.Data.Length, _serverUdpEndPoint );
                    Console.WriteLine( $" >>> Sent UDP to server [ {_serverUdpEndPoint.Address} : {_serverUdpEndPoint.Port} ]" );

                    // THIS is how we punching hole! very first this message should be dropped by partner's NAT, but it's ok.
                    // i suppose that this is good idea to send this "keep-alive" messages to peer even if you are connected already,
                    // because AFAIK "hole" for UDP lives ~2 minutes on NAT. so "we will let it die? NEVER!" (c)
                    _udpClient.Send( messageToPeer.Data, messageToPeer.Data.Length, _partnerPublicUdpEndPoint );
                    _udpClient.Send( messageToPeer.Data, messageToPeer.Data.Length, _partnerLocalUdpEndPoint );
                    Console.WriteLine( $" >>> Sent UDP to peer.public [ {_partnerPublicUdpEndPoint.Address} : {_partnerPublicUdpEndPoint.Port} ]" );
                    Console.WriteLine( $" >>> Sent UDP to peer.local [ {_partnerLocalUdpEndPoint.Address} : {_partnerLocalUdpEndPoint.Port} ]" );

                    // "connected" UdpClient sends data much faster, 
                    // so if you have something that your partner cant wait for (voice, for example), send it this way
                    if ( _extraUdpClientConnected )
                    {
                        _extraUdpClient.Send( messageToPeer.Data, messageToPeer.Data.Length );
                        Console.WriteLine( $" >>> Sent UDP to peer.received EP" );
                    }
                }

                Thread.Sleep( 3000 );
            }
        }

        private async void ListenUdp()
        {
            _isRunning = true;

            while ( _isRunning )
            {
                try
                {
                    // also important thing!
                    // when you did not punched hole yet, you must listen incoming packets using "puncher" (later we will close it).
                    // where you already have p2p connection (and "puncher" closed), use "non-puncher"
                    UdpClient udpClient = _partnerPublicUdpEndPoint == null ? _udpPuncher : _udpClient;

                    var receivedResults = await udpClient.ReceiveAsync();

                    if ( !_isRunning )
                    {
                        break;
                    }

                    ProcessUdpMessage( receivedResults.Buffer, receivedResults.RemoteEndPoint );
                }
                catch ( SocketException ex )
                {
                    // do something here...
                }
                catch ( Exception ex )
                {
                    Console.WriteLine( $"Error: {ex.Message}" );
                }
            }
        }

        private static void ProcessUdpMessage( byte[] buffer, IPEndPoint remoteEndPoint )
        {
            // if server sent partner's endpoinps, we will store it and (IMPORTANT) close "puncher"
            if ( UdpProtocol.PeerAddressMessage.TryParse( buffer, out UdpProtocol.PeerAddressMessage peerAddressMessage ) )
            {
                Console.WriteLine( " <<< Got response from server" );
                _partnerPublicUdpEndPoint = new IPEndPoint( peerAddressMessage.PublicIp, peerAddressMessage.PublicPort );
                _partnerLocalUdpEndPoint = new IPEndPoint( peerAddressMessage.LocalIp, peerAddressMessage.LocalPort );

                _udpPuncher.Close();
            }
            // since we got this message we know partner's endpoint for sure, 
            // and we can "connect" UdpClient to it, so it will work faster
            else if ( UdpProtocol.P2PKeepAliveMessage.TryParse( buffer ) )
            {
                Console.WriteLine( $"           IT WORKS!!! WOW!!!  [ {remoteEndPoint.Address} : {remoteEndPoint.Port} ]" );

                _extraUdpClientConnected = true;
                _extraUdpClient.Connect( remoteEndPoint );
            }
            else
            {
                Console.WriteLine( "???" );
            }
        }
    }
}

Protocol.cs

このアプローチがどれほど優れているかはわかりません。おそらくprotobufのようなものがそれをより良くすることができます

using System;
using System.Linq;
using System.Net;
using System.Text;

namespace HolePunching.Common
{
    public static class UdpProtocol
    {
        public static readonly int GuidLength = 16;
        public static readonly int PeerIdLength = 1;
        public static readonly int IpLength = 4;
        public static readonly int IntLength = 4;

        public static readonly byte[] Prefix = { 12, 23, 34, 45 };

        private static byte[] JoinBytes( params byte[][] bytes )
        {
            var result = new byte[bytes.Sum( x => x.Length )];
            int pos = 0;

            for ( int i = 0; i < bytes.Length; i++ )
            {
                for ( int j = 0; j < bytes[i].Length; j++, pos++ )
                {
                    result[pos] = bytes[i][j];
                }
            }

            return result;
        }

        #region Helper extensions

        private static bool StartsWith( this byte[] @this, byte[] value, int offset = 0 )
        {
            if ( @this == null || value == null || @this.Length < offset + value.Length )
            {
                return false;
            }

            for ( int i = 0; i < value.Length; i++ )
            {
                if ( @this[i + offset] < value[i] )
                {
                    return false;
                }
            }

            return true;
        }

        private static byte[] ToUnicodeBytes( this string @this )
        {
            return Encoding.Unicode.GetBytes( @this );
        }

        private static byte[] Take( this byte[] @this, int offset, int length )
        {
            return @this.Skip( offset ).Take( length ).ToArray();
        }

        public static bool IsSuitableUdpMessage( this byte[] @this )
        {
            return @this.StartsWith( Prefix );
        }

        public static int GetInt( this byte[] @this )
        {
            if ( @this.Length != 4 )
                throw new ArgumentException( "Byte array must be exactly 4 bytes to be convertible to uint." );

            return ( ( ( @this[0] << 8 ) + @this[1] << 8 ) + @this[2] << 8 ) + @this[3];
        }

        public static byte[] ToByteArray( this int value )
        {
            return new[]
            {
                (byte)(value >> 24),
                (byte)(value >> 16),
                (byte)(value >> 8),
                (byte)value
            };
        }

        #endregion

        #region Messages

        public abstract class UdpMessage
        {
            public byte[] Data { get; }

            protected UdpMessage( byte[] data )
            {
                Data = data;
            }
        }

        public class UdpInfoMessage : UdpMessage
        {
            private static readonly byte[] MessagePrefix = { 41, 57 };
            private static readonly int MessageLength = Prefix.Length + MessagePrefix.Length + PeerIdLength + IpLength + IntLength;

            public byte Id { get; }
            public IPAddress LocalIp { get; }
            public int LocalPort { get; }

            private UdpInfoMessage( byte[] data, byte id, IPAddress localIp, int localPort )
                : base( data )
            {
                Id = id;
                LocalIp = localIp;
                LocalPort = localPort;
            }

            public static UdpInfoMessage GetMessage( byte id, IPAddress localIp, int localPort )
            {
                var data = JoinBytes( Prefix, MessagePrefix, new[] { id }, localIp.GetAddressBytes(), localPort.ToByteArray() );

                return new UdpInfoMessage( data, id, localIp, localPort );
            }

            public static bool TryParse( byte[] data, out UdpInfoMessage message )
            {
                message = null;

                if ( !data.StartsWith( Prefix ) )
                    return false;
                if ( !data.StartsWith( MessagePrefix, Prefix.Length ) )
                    return false;
                if ( data.Length != MessageLength )
                    return false;

                int index = Prefix.Length + MessagePrefix.Length;
                byte id = data[index];

                index += PeerIdLength;
                byte[] localIpBytes = data.Take( index, IpLength );
                var localIp = new IPAddress( localIpBytes );

                index += IpLength;
                byte[] localPortBytes = data.Take( index, IntLength );
                int localPort = localPortBytes.GetInt();

                message = new UdpInfoMessage( data, id, localIp, localPort );

                return true;
            }
        }

        public class PeerAddressMessage : UdpMessage
        {
            private static readonly byte[] MessagePrefix = { 36, 49 };
            private static readonly int MessageLength = Prefix.Length + MessagePrefix.Length + PeerIdLength + ( IpLength + IntLength ) * 2;

            public byte Id { get; }
            public IPAddress PublicIp { get; }
            public int PublicPort { get; }
            public IPAddress LocalIp { get; }
            public int LocalPort { get; }

            private PeerAddressMessage( byte[] data, byte id, IPAddress publicIp, int publicPort, IPAddress localIp, int localPort )
                : base( data )
            {
                Id = id;
                PublicIp = publicIp;
                PublicPort = publicPort;
                LocalIp = localIp;
                LocalPort = localPort;
            }

            public static PeerAddressMessage GetMessage( byte id, IPAddress publicIp, int publicPort, IPAddress localIp, int localPort )
            {
                var data = JoinBytes( Prefix, MessagePrefix, new[] { id }, 
                    publicIp.GetAddressBytes(), publicPort.ToByteArray(),
                    localIp.GetAddressBytes(), localPort.ToByteArray() );

                return new PeerAddressMessage( data, id, publicIp, publicPort, localIp, localPort );
            }

            public static bool TryParse( byte[] data, out PeerAddressMessage message )
            {
                message = null;

                if ( !data.StartsWith( Prefix ) )
                    return false;
                if ( !data.StartsWith( MessagePrefix, Prefix.Length ) )
                    return false;
                if ( data.Length != MessageLength )
                    return false;

                int index = Prefix.Length + MessagePrefix.Length;
                byte id = data[index];

                index += PeerIdLength;
                byte[] publicIpBytes = data.Take( index, IpLength );
                var publicIp = new IPAddress( publicIpBytes );

                index += IpLength;
                byte[] publicPortBytes = data.Take( index, IntLength );
                int publicPort = publicPortBytes.GetInt();

                index += IntLength;
                byte[] localIpBytes = data.Take( index, IpLength );
                var localIp = new IPAddress( localIpBytes );

                index += IpLength;
                byte[] localPortBytes = data.Take( index, IntLength );
                int localPort = localPortBytes.GetInt();

                message = new PeerAddressMessage( data, id, publicIp, publicPort, localIp, localPort );

                return true;
            }
        }

        public class P2PKeepAliveMessage : UdpMessage
        {
            private static readonly byte[] MessagePrefix = { 11, 19 };
            private static P2PKeepAliveMessage _message;

            private P2PKeepAliveMessage( byte[] data )
                : base( data )
            {

            }

            public static bool TryParse( byte[] data )
            {
                if ( !data.StartsWith( Prefix ) )
                    return false;
                if ( !data.StartsWith( MessagePrefix, Prefix.Length ) )
                    return false;

                return true;
            }

            public static P2PKeepAliveMessage GetMessage()
            {
                if ( _message == null )
                {
                    var data = JoinBytes( Prefix, MessagePrefix );
                    _message = new P2PKeepAliveMessage( data );
                }

                return _message;
            }
        }

        #endregion
    }
}
1
Sam Sch

更新:

UdpClientのどちらが最初にバインドするかは、Windowsによって着信パケットが送信されるものです。あなたの例では、リスニングスレッドを設定するコードブロックを一番上に移動してみてください。

問題は、受信スレッドが単一の受信を処理するためだけに記述されていることだけではないことを確認しますか?受信スレッドを以下のように置き換えてみてください。

ThreadPool.QueueUserWorkItem(delegate
{
    UdpClient udpServer = new UdpClient();
    udpServer.ExclusiveAddressUse = false;
    udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    udpServer.Client.Bind(localpt);

    IPEndPoint inEndPoint = new IPEndPoint(IPAddress.Any, 0);
    Console.WriteLine("Listening on " + localpt + ".");

    while (inEndPoint != null)
    {
        byte[] buffer = udpServer.Receive(ref inEndPoint);
        Console.WriteLine("Bytes received from " + inEndPoint + " " + Encoding.ASCII.GetString(buffer) + ".");
    }
});
1
sipwiz