UDP打孔实现

我正在尝试完成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(); } 

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

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

几年前我在c#中做了完全相同的事情,我还没有找到代码,但是如果你喜欢的话会给你一些指示:

首先,我不会在udpclient上使用Connect()函数,因为UDP是一个无连接协议,所有这个function确实隐藏了UDP套接字的function。

您应该执行以下步骤:

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

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

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

您是否尝试过使用Async函数,以下是如何使其工作的示例,可能需要一些工作才能使其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); } 

我希望这有帮助。

编辑:经过更多测试后,除非我启用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 uniqueIdsPubNubSeen; object peerLock = new object(); Dictionary connectedPeers; string myPeerDataString; public void InitPubnub(string pubnubPublishKey, string pubnubSubscribeKey, string pubnubChannelName) { uniqueIdsPubNubSeen = new HashSet(); connectedPeers = new Dictionary; pubnub = new Pubnub(pubnubPublishKey, pubnubSubscribeKey); myPeerDataString = localIp + " " + externalIp + " " + localPort + " " + externalPort + " " + pubnub.SessionUUID; this.pubnubChannelName = pubnubChannelName; pubnub.Subscribe( pubnubChannelName, OnPubNubMessage, OnPubNubConnect, OnPubNubError); return pubnub; } //// Subscribe callbacks void OnPubNubConnect(string res) { pubnub.Publish(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(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(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; 

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

更新:

无论哪个UdpClients首先绑定是由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) + "."); } });