草庐IT

c# - UDP 打洞。让服务器与客户端交谈

coder 2024-05-20 原文

我已经阅读了很多关于如何实现 UDP 打洞的书,但由于某种原因我无法让它工作。

对于那些不熟悉什么是udp打洞的人,这里是我自己的定义:

目标是能够在两个客户端(客户端 A
和客户端 B) 在服务器的帮助下。因此客户端 A 连接到服务器并发送其信息。客户端 B 也这样做。服务器具有必要的信息,以便客户端 A 能够向客户端 B 发送数据,反之亦然。因此,服务器将该信息提供给两个客户端。一旦两个客户端都获得了彼此的信息,就可以在没有服务器帮助的情况下开始在这些客户端之间发送和接收数据。

我的目标是能够做我刚刚描述的事情(udp 打洞)。 在这样做之前,我认为能够从服务器连接到客户端会很有帮助 .为此,我计划向服务器发送有关客户端的信息。服务器收到该信息后,尝试从头开始连接到客户端。一旦我能够执行,我应该拥有开始实现真正的 udp 打洞所需的一切。

这是我如何设置:



顶部路由器将服务器和底部路由器连接到 LAN 端口。底部路由器 (NAT) 通过其 WAN 端口连接到顶部路由器。客户端计算机连接到底部路由器到其 LAN 端口之一。

因此,在该连接中,客户端能够看到服务器,但服务器无法看到客户端。

所以我用伪代码做的算法是:

  • 客户端连接到服务器。
  • 客户端向服务器发送一些 UDP 包,以便在 NAT 上打开一些端口
  • 将有关客户端正在监听的端口的信息发送到服务器。
  • 服务器收到该信息后,尝试从头开始连接到客户端。

  • 下面是代码中的实现:

    服务器:
    static void Main()
    {     
        /* Part 1 receive data from client */
        UdpClient listener = new UdpClient(11000);
        IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);
        string received_data;
        byte[] receive_byte_array = listener.Receive(ref groupEP);       
        received_data = Encoding.ASCII.GetString(receive_byte_array, 0, receive_byte_array.Length);
    
        // get info
        var ip = groupEP.Address.ToString();
        var port = groupEP.Port;
    
        /* Part 2 atempt to connect to client from scratch */
        // now atempt to send data to client from scratch once we have the info       
        Socket sendSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        IPEndPoint endPointClient = new IPEndPoint(IPAddress.Parse(ip), port);
        sendSocket.SendTo(Encoding.ASCII.GetBytes("Hello"), endPointClient);
    }
    

    客户:
    static void Main(string[] args)
    {
        /* Part 1 send info to server */
        Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram,  ProtocolType.Udp);
        IPAddress send_to_address = IPAddress.Parse("192.168.0.132");
        IPEndPoint sending_end_point = new IPEndPoint(send_to_address, 11000);
        sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);
    
        // get info
        var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];
    
        /* Part 2 receive data from server */
        IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
        byte[] buffer = new byte[1024];
        sending_socket.Receive(buffer);
    } 
    

    出于某种原因,它工作了几次! 当客户端成功接收数据上线时工作:sending_socket.Receive(buffer);
    注意事项:
    如果在第二部分的服务器上,我使用了实例变量 listner而不是创建新变量 sendSocket并通过该变量发送字节,客户端能够接收正在发送的数据。请记住,服务器的第二部分将由第二个客户端 B 实现,这就是我从头开始再次初始化变量的原因......

    编辑:

    这是看待同一问题的不同方式。 当我初始化一个新对象而不是使用相同的对象时,客户端不会收到响应。

    我有一个 UdpClient 类型的对象。我能够将带有该对象的数据发送给另一个对等方。如果我创建另一个具有相同属性的相同类型的对象并尝试发送数据,它将不起作用!我可能缺少初始化一些变量。我可以用反射设置私有(private)变量,所以我应该没有问题。无论如何这里是服务器代码:
    public static void Main()
    {
        // wait for client to send data
        UdpClient listener = new UdpClient(11000);
        IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);        
        byte[] receive_byte_array = listener.Receive(ref groupEP);
    
        // connect so that we are able to send data back
        listener.Connect(groupEP);
    
        byte[] dataToSend = new byte[] { 1, 2, 3, 4, 5 };
    
        // now let's atempt to reply back
    
        // this part does not work!
        UdpClient newClient = CopyUdpClient(listener, groupEP);
        newClient.Send(dataToSend, dataToSend.Length);
    
        // this part works!
        listener.Send(dataToSend, dataToSend.Length);
    }
    
    static UdpClient CopyUdpClient(UdpClient client, IPEndPoint groupEP)
    {
        var ip = groupEP.Address.ToString();
        var port = groupEP.Port;
        var newUdpClient = new UdpClient(ip, port);
        return newUdpClient;
    }
    

    客户端代码基本上将数据发送到服务器,然后等待响应:
        string ipOfServer = "192.168.0.132";
        int portServerIsListeningOn = 11000;
    
        // send data to server
        Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        IPAddress send_to_address = IPAddress.Parse(ipOfServer);
        IPEndPoint sending_end_point = new IPEndPoint(send_to_address, portServerIsListeningOn);
        sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);
    
        // get info
        var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];
    
        // now wait for server to send data back
        IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
        byte[] buffer = new byte[1024];
        sending_socket.Receive(buffer); // <----- keeps waiting in here :(
    

    请注意,客户端位于路由器 (NAT) 后面,否则我不会遇到此问题。 我想复制 udpClient 的原因是我可以将该变量发送到另一台计算机,使另一台计算机能够将数据发送到客户端。

    所以我的问题是 为什么是原始对象listener能够发送数据但 newClient是不是可以?客户端一直在线等待sending_socket.Receive(buffer);即使在服务器执行该行之后:newClient.Send(dataToSend, dataToSend.Length); .当监听器发送数据但未发送新客户端时,客户端成功接收数据。如果两个变量具有相同的目标 IP 和端口,为什么会这样?变量如何不同?

    笔记:
    如果服务器和客户端在同一个网络上,则复制有效且变量 newClient能够向客户端发送数据。要模拟此问题,客户端必须位于 NAT(路由器)后面。这种网络的一个例子可能由两个路由器组成。让我们称它们为路由器 X 和路由器 Y。您还需要一个服务器调用该 S. 和一个客户端 C。因此 S 可以连接到 X 的 LAN 端口之一。 C 可以连接到 Y 的 LAN 端口之一。最后将 Y 的 WAN 端口连接到 X 的 LAN 端口之一。

    最佳答案

    嗯,我认为你在这里混淆了几件事。一方面,它真的叫 UDP hole punching .让我尝试解释这应该如何工作。

    NAT路由器通常做port mapping将数据包从内部专用网络转发到外部 Internet 时。

    假设您在 NAT 后面的机器上创建了一个 UDP 套接字,并将数据报发送到某个外部 IP/端口。当携带该数据报的 IP 数据包离开发送机器时,其 IP 报头的源地址字段设置为本地不可全局路由的私有(private) IP 地址(如 192.168.1.15),UDP 报头的源端口字段设置为任何端口分配给套接字(通过绑定(bind)显式分配,或由操作系统从临时端口隐式选择)。我将把这个源端口号称为 P1 .

    然后当 NAT 路由器将该数据包发送到外部网络时,它会将源 IP 地址覆盖为自己的外部 IP 地址(否则无法将数据包路由回),并且经常将源 UDP 端口覆盖为其他值(也许因为专用网络上的某些其他主机使用相同的源端口,这会造成歧义)。原始源端口和新端口号(让我们将其标记为 P2 )之间的映射保留在路由器中以匹配返回数据包。此映射也可能特定于目标 IP 地址和目标 UDP 端口。

    所以现在你已经在路由器上“打了一个洞” - UDP 数据包发送回路由器到端口 P2被转发到 UDP 端口 P1 上的内部机器.同样,根据 NAT 的实现,这可能仅限于来自原始目标 IP 地址和目标 UDP 端口的数据包。

    对于客户端到客户端的通信,您必须通过服务器将一个的外部 IP/端口告诉另一个,希望 NAT 路由器将相同的内部源端口映射到相同的外部源端口。然后客户端将使用这些数据包相互发送。

    希望这可以帮助。

    关于c# - UDP 打洞。让服务器与客户端交谈,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12136031/

    有关c# - UDP 打洞。让服务器与客户端交谈的更多相关文章

    1. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

      我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

    2. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

      我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

    3. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

      最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

    4. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

      在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

    5. c# - 如何在 ruby​​ 中调用 C# dll? - 2

      如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

    6. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

      我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

    7. ruby-on-rails - 在 Rails 中调试生产服务器 - 2

      您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除

    8. C# 到 Ruby sha1 base64 编码 - 2

      我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

    9. 基于C#实现简易绘图工具【100010177】 - 2

      C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

    10. ruby - 我的 Ruby IRC 机器人没有连接到 IRC 服务器。我究竟做错了什么? - 2

      require"socket"server="irc.rizon.net"port="6667"nick="RubyIRCBot"channel="#0x40"s=TCPSocket.open(server,port)s.print("USERTesting",0)s.print("NICK#{nick}",0)s.print("JOIN#{channel}",0)这个IRC机器人没有连接到IRC服务器,我做错了什么? 最佳答案 失败并显示此消息::irc.shakeababy.net461*USER:Notenoughparame

    随机推荐