UDP打洞实现

发布于 2025-01-02 00:32:35 字数 3710 浏览 2 评论 0原文

我正在尝试完成UDP打洞。我的理论基于这篇文章和这篇WIKI 页面,但我在 C# 编码方面遇到了一些问题。这是我的问题:

使用此处发布的代码 我现在能够连接到远程计算机并在同一端口上侦听传入连接(将 2 个 UDP 客户端绑定到同一端口)。

由于某种原因,到同一端口的两个绑定彼此阻止接收任何数据。 我有一个 UDP 服务器来响应我的连接,因此如果我在将任何其他客户端绑定到该端口之前先连接到它,我会收到它的响应。

如果我将另一个客户端绑定到该端口,则任何一个客户端都不会收到数据。

以下是显示我的问题的 2 个代码片段。第一个连接到远程服务器以在 NAT 设备上创建规则,然后在不同的线程上启动侦听器以捕获传入数据包。然后,代码将数据包发送到本地 IP,以便侦听器能够获取它。第二个仅将数据包发送到本地 IP 以确保其正常工作。我知道这不是真正的打洞,因为我根本不使用 NAT 设备,而是将数据包发送给自己。我现在面临一个问题,如果我使用 NAT 设备外部的计算机进行连接,我认为这不会有任何不同。

[编辑]2012 年 2 月 4 日 我尝试使用网络上的另一台计算机和 WireShark(数据包嗅探器)来测试侦听器。我看到从另一台计算机传入的数据包,但侦听器 UDP 客户端 (udpServer) 或发送者 UDP 客户端 (client) 没有接收到。

[编辑]2010 年 2 月 5 日 我现在添加了一个函数调用,用于在初始发送和接收数据包后关闭第一个 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();
}

I am trying to accomplish UDP hole punching. I am basing my theory on this article and this WIKI page, but I am facing some issues with the C# coding of it. Here is my problem:

Using the code that was posted here I am now able to connect to a remote machine and listen on the same port for incoming connections (Bind 2 UDP clients to the same port).

For some reason the two bindings to the same port block each other from receiving any data.
I have a UDP server that responds to my connection so if I connect to it first before binding any other client to the port I get its responses back.

If I bind another client to the port no data will be received on either clients.

Following are 2 code pieces that show my problem. The first connects to a remote server to create the rule on the NAT device and then a listener is started on a different thread to capture the incoming packets. The code then sends packets to the local IP so that the listener will get it. The second only sends packets to the local IP to make sure this works. I know this is not the actual hole punching as I am sending the packets to myself without living the NAT device at all. I am facing a problem at this point, and I don't imagine this will be any different if I use a computer out side the NAT device to connect.

[EDIT] 2/4/2012
I tried using another computer on my network and WireShark (packet sniffer) to test the listener. I see the packets incoming from the other computer but are not received by the listener UDP client (udpServer) or the sender UDP client (client).

[EDIT] 2/5/2010
I have now added a function call to close the first UDP client after the initial sending and receiving of packets only living the second UDP client to listen on the port. This works and I can receive packets from inside the network on that port. I will now try to send and receive packets from outside the network. I will post my findings as soon as I find something.

Using this code I get data on the listening client:

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();
}

If I use the following code, after the connection and data transfer between my client and server, the listening UDP client will not receive anything:

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();
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

蓝礼 2025-01-09 00:32:35

如果我理解正确的话,您正在尝试使用中介服务器进行打洞,在两个客户端之间进行点对点通信,每个客户端都位于不同的 NAT 后面?

几年前,我在 C# 中做了完全相同的事情,我还没有找到代码,但如果您愿意,我会给您一些指示:

首先,我不会在 udpclient 上使用 Connect() 函数,因为 UDP 是一个无连接协议,这个函数真正做的就是隐藏 UDP 套接字的功能。

您应该执行以下步骤:

  1. 在服务器上的特定端口上打开一个 UDP 套接字,其端口未被防火墙阻止(例如将此套接字绑定到所选端口,例如 23000)
  2. 创建一个 UDP第一个客户端上的套接字,并在 23000 处向服务器发送一些内容。不要绑定此套接字。当使用 udp 发送数据包时,Windows 将自动为套接字分配一个空闲端口
  3. 从其他客户端执行相同的操作
  4. 现在,服务器已从 2 个不同地址、2 个不同端口的 2 个客户端接收到 2 个数据包。测试服务器是否可以在相同的地址和端口上发送回数据包。 (如果这不起作用,你做错了什么或者你的 NAT 不起作用。如果你可以在不打开端口的情况下玩游戏,你就知道它正在工作:D)
  5. 服务器现在应该将其他客户端的地址和端口发送到每个连接的客户端客户。
  6. 客户端现在应该能够使用 UDP 向从服务器接收到的地址发送数据包。

您应该注意,nat 上使用的端口可能与您客户端电脑上使用的端口不同!服务器应将此外部端口分配给客户端。 您必须使用外部地址和外部端口来发送!

另请注意,您的 NAT 可能不支持这种端口转发。有些 NAT 将指定端口上的所有传入流量转发给您的客户端,这正是您想要的。但有些 NAT 会对传入数据包地址进行过滤,因此可能会阻止其他客户端数据包。但在使用标准个人用户路由器时,这种情况不太可能发生。

If i understand correctly, you are trying to communicate peer-to-peer between 2 clients each behind a different NAT, using a mediation server for hole punching?

Few years ago i did the exact same thing in c#, i haven't found the code yet, but ill give you some pointers if you like:

First, I wouldn't use the Connect() function on the udpclient, since UDP is a connectionless protocol, all this function really does is hide the functionality of a UDP socket.

You should perfrom the following steps:

  1. Open a UDP socket on a server with it's ports not blocked by a firewall, at a specific port (eg Bind this socket to a chosen port for example 23000)
  2. Create a UDP socket on the first client, and send something to the server at 23000. Do not bind this socket. When a udp is used to send a packet, windows will automatically assign a free port to the socket
  3. Do the same from the other client
  4. The server has now received 2 packets from 2 clients at 2 different adresses with 2 different ports. Test if the server can send packets back on the same address and port. (If this doesn't work you did something wrong or your NAT isn't working. You know its working if you can play games without opening ports :D)
  5. The server should now send the address and port of the other clients to each connected client.
  6. A client should now be able to send packets using UDP to the adresses received from the server.

You should note that the port used on the nat is probably not the same port as on your client pc!! The server should distribute this external port to clients. You must use the external adresses and the external ports to send to!

Also note that your NAT might not support this kind of port forwarding. Some NAT's forward all incoming traffic on a assigned port to you client, which is what you want. But some nats do filtering on the incoming packets adresses so it might block the other clients packets. This is unlikely though when using a standard personal user router.

霓裳挽歌倾城醉 2025-01-09 00:32:35

编辑:经过更多测试后,除非我启用 UPnP,否则这似乎对我根本不起作用。因此,我在这里写的很多内容可能对您有用,但许多人没有启用 UPnP(因为这是一个安全风险),因此它对他们不起作用。

这是一些使用 PubNub 作为中继服务器的代码:)。我不建议在未经测试的情况下使用此代码,因为它并不完美(我不确定它是否安全或正确的做事方式?idk我不是网络专家)但它应该给你一个想法做什么。到目前为止,它至少在我的业余爱好项目中对我有用。它缺少的东西是:

  • 测试客户端是否在您的 LAN 上。我只是发送到适用于您的 LAN 的设备和另一个网络上的设备,但效率非常低。
  • 测试客户端何时停止监听,例如,如果他们关闭了程序。因为这是 UDP,所以它是无状态的,所以我们是否将消息发送到虚空并不重要,但如果没有人收到它们,我们可能不应该这样做,
  • 我使用 Open.NAT 以编程方式进行端口转发,但这可能不适用于某些设备。具体来说,它使用的UPnP有点不安全,需要手动转发UDP端口1900。一旦他们这样做了,大多数路由器都支持它,但许多路由器还没有这样做。

因此,首先,您需要一种获取外部和本地 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;

我愿意接受评论和问题,如果这里的某些做法不好或不起作用,请随时提供反馈。我的代码在翻译过程中引入了一些错误,我最终会在这里修复这些错误,但这至少应该让您知道该怎么做。

Edit: After a lot more testing this doesn't seem to work at all for me unless I enable UPnP. So a lot of the things I wrote here you may find useful but many people don't have UPnP enabled (because it is a security risk) so it will not work for them.

Here is some code using PubNub as a relay server :). I don't recommend using this code without testing because it is not perfect (I'm not sure if it is even secure or the right way to do things? idk I'm not a networking expert) but it should give you an idea of what to do. It at least has worked for me so far in a hobby project. The things it is missing are:

  • Testing if the client is on your LAN. I just send to both which works for your LAN and a device on another network but that is very inefficient.
  • Testing when the client stops listening if, for example, they closed the program. Because this is UDP it is stateless so it doesn't matter if we are sending messages into the void but we probably shouldn't do that if noone is getting them
  • I use Open.NAT to do port forwarding programatically but this might not work on some devices. Specifically, it uses UPnP which is a little insecure and requires UDP port 1900 to be port forwarded manually. Once they do this it is supported on most routers but many have not done this yet.

So first of all, you need a way to get your external and local IPs. Here is code for getting your local 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");
}

And here is some code for getting your external IP via trying a few websites that are designed to return your external 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 "";

}

Now we need to find an open port and forward it to an external port. As mentioned above I used Open.NAT. First, you put together a list of ports that you think would be reasonable for your application to use after looking at registered UDP ports. Here are a few for example:

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

Now we can loop through them and hopefully find one that is not in use to use port forwarding on:

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;
}

Now for the PubNub relay server code (P2PPeer will be defined later below). There is a lot here so I'm not really gonna explain it but hopefully the code is clear enough to help you understand what is going on

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);
}

And here is a 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);
        }
    }
}

Finally, here are all my usings:

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;

I'm open to comments and questions, feel free to give feedback if something here is bad practice or doesn't work. A few bugs were introduced in translation from my code that I'll fix here eventually but this should at least give you the idea of what to do.

玩世 2025-01-09 00:32:35

您是否尝试过使用异步函数,这里是一个如何让它工作的示例,它可能需要一些工作才能使其 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);
    }

我希望这会有所帮助。

Have you tried using the Async functions, here is a example of how you might get it to work it may need a bit of work to make it 100% functional:

    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);
    }

I hope this helps.

π浅易 2025-01-09 00:32:35

更新:

第一个绑定的 UdpClient 就是 Windows 将发送传入数据包的 UdpClient。在您的示例中,尝试将设置侦听线程的代码块移至顶部。

您确定问题不仅仅是接收线程仅为处理单个接收而编写吗?尝试将接收线程替换为如下所示。

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) + ".");
    }
});

Update:

Whichever of the UdpClients binds first is the one that will be sent incoming packets by Windows. In your example try moving the code block that sets up the listening thread to the top.

Are you sure the problem is not just that the receive thread is only written to handle a single receive? Try replacing the receive thread with as below.

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) + ".");
    }
});
GRAY°灰色天空 2025-01-09 00:32:35

抱歉上传这么大的代码,但我想这非常清楚地解释了事情是如何工作的,并且可能非常有用。如果您对此代码有疑问,请告诉我。

注意:

  1. 这只是一个草稿
  2. (重要),您必须使用本地端点通知服务器。如果你不这样做,你将无法在一个 NAT 后面的两个对等点之间进行通信(例如,在一台本地计算机上),即使服务器不在 NAT 范围内,
  3. 你也必须关闭“puncher”客户端(至少,我没有这样做)设法接收任何数据包,直到我这样做)。稍后您将能够使用其他 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< /em>

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
    }
}

Sorry for uploading such a huge piece of code, but i guess this is very clearly explains how things work, and may be really useful. If you will have issues with this code, please let me know.

Note:

  1. this is just a draft
  2. (important) you MUST inform server with your local endpoint. if you will not do it you will not be able to communicate between two peers behind one NAT (for example, on one local machine) even if the server is out of NAT
  3. you must close "puncher" client (at least, i did not manage to receive any packets until i did it). later you will be able to communicate with server using other UdpClient's
  4. of cource it will not work with symmetric NAT
  5. if you find that something in this code is "terrible practice", please tell me, i'm not network expert :)

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

I'm not sure how good this approach, maybe something like protobuf can do it better

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
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文