UDP打孔实现

bro*_*ooc 21 c# udp traversal nat hole-punching

我正在尝试完成UDP打孔.我的理论基于这篇文章WIKI页面,但我面临着C#编码的一些问题.这是我的问题:

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

由于某种原因,对同一端口的两个绑定相互阻止接收任何数据.我有一个响应我的连接的UDP服务器,所以如果我在将任何其他客户端绑定到端口之前先连接到它,我会得到它的响应.

如果我将另一个客户端绑定到该端口,则不会在任一客户端上接收数据.

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

[编辑] 2/4/2012我尝试在我的网络上使用另一台计算机和WireShark(数据包嗅探器)来测试监听器.我看到从其他计算机传入的数据包但是没有被侦听器UDP客户端(udpServer)或发送方UDP客户端(客户端)接收.

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

如果我使用以下代码,在客户端和服务器之间的连接和数据传输之后,侦听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();
}
Run Code Online (Sandbox Code Playgroud)

MHG*_*ork 13

如果我理解正确,你试图在两个客户端之间进行点对点通信,每个客户端位于不同的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上使用的端口可能与客户端PC上的端口不同!! 服务器应将此外部端口分发给客户端.您必须使用外部地址和外部端口发送到!

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

  • 好的,让我知道你是否正常工作:)另一件事,我是否正确理解你将多个套接字绑定到同一个端口?在大多数情况下,每个端口只应使用一个套接字,我不知道是否可以绑定多个套接字:D (2认同)

Phy*_*ida 5

编辑:经过大量测试后,除非我启用 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");
}
Run Code Online (Sandbox Code Playgroud)

这是一些通过尝试一些旨在返回您的外部 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 "";

}
Run Code Online (Sandbox Code Playgroud)

现在我们需要找到一个开放的端口并将其转发到外部端口。如上所述,我使用了Open.NAT。首先,在查看注册的 UDP 端口后,您将您认为适合您的应用程序使用的端口列表放在一起。举几个例子:

public static int[] ports = new int[]
{
  5283,
  5284,
  5285,
  5286,
  5287,
  5288,
  5289,
  5290,
  5291,
  5292,
  5293,
  5294,
  5295,
  5296,
  5297
};
Run Code Online (Sandbox Code Playgroud)

现在我们可以遍历它们并希望找到一个未用于使用端口转发的:

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;
}
Run Code Online (Sandbox Code Playgroud)

现在为 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);
}
Run Code Online (Sandbox Code Playgroud)

这是一个 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,这里是我所有的用途:

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;
Run Code Online (Sandbox Code Playgroud)

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


sip*_*wiz 1

更新:

第一个绑定的 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) + ".");
    }
});
Run Code Online (Sandbox Code Playgroud)