golang实现p2p之UDP打洞

2023-11-05

 

当今互联网到处存在着一些中间件(MIddleBoxes),如NAT和防火墙,导致两个(不在同一内网)中的客户端无法直接通信。 这些问题即便是到了IPV6时代也会存在,因为即使不需要NAT,但还有其他中间件如防火墙阻挡了链接的建立。 目前部署的中间件多都是在C/S架构上设计的,其中相对隐匿的客户机主动向周知的服务端(拥有静态IP地址和DNS名称)发起链接请求。 大多数中间件实现了一种非对称的通讯模型,即内网中的主机可以初始化对外的链接,而外网的主机却不能初始化对内网的链接, 除非经过中间件管理员特殊配置。

在中间件为常见的NAPT的情况下(也是本文主要讨论的),内网中的客户端没有单独的公网IP地址, 而是通过NAPT转换,和其他同一内网用户共享一个公网IP。这种内网主机隐藏在中间件后的不可访问性对于一些客户端软件如浏览器来说 并不是一个问题,因为其只需要初始化对外的链接,从某方面来看反而还对隐私保护有好处。然而在P2P应用中, 内网主机(客户端)需要对另外的终端(Peer)直接建立链接,但是发起者和响应者可能在不同的中间件后面, 两者都没有公网IP地址。而外部对NAT公网IP和端口主动的链接或数据都会因内网未请求被丢弃掉。本文讨论的就是如何跨越NAT实现内网主机直接通讯的问题。

网络模型

假设客户端A和客户端B的地址都是内网地址,且在不同的NAT后面。A、B上运行的P2P应用程序和服务器S都使用了UDP端口9982,A和B分别初始化了 与Server的UDP通信,地址映射如图所示:

                        Server S
                    207.148.70.129:9981
                           |
                           |
    +----------------------|----------------------+
    |                                             |
  NAT A                                         NAT B
120.27.209.161:6000                            120.26.10.118:3000
    |                                             |
    |                                             |
 Client A                                      Client B
  10.0.0.1:9982                                 192.168.0.1:9982

现在假设客户端A打算与客户端B直接建立一个UDP通信会话。如果A直接给B的公网地址120.26.10.118:3000发送UDP数据,NAT B将很可能会无视进入的 数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,而最初只与S建立过会话。B往A直接发信息也类似。

假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息。A往B的输出信息会导致NAT A打开 一个A的内网地址与与B的外网地址之间的新通讯会话,B往A亦然。一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯, 而无须再通过引导服务器S了。

UDP打洞技术有许多有用的性质。一旦一个的P2P链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞, 极大减少了服务器的负载。应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下 也一样能建立通信链路。

打洞流程

假设A现在希望建立一条到B的udp会话,那么这个建立基本流程是:

  1. A,B分别建立到Server S的udp会话,那么Server S此时是知道A,B各自的外网ip+端口
  2. Server S在和B的udp会话里告诉A的地址(外网ip+端口: 120.27.209.161:6000),同理把B的地址(120.26.10.118:3000)告诉A
  3. B向A地址(120.27.209.161:6000)发送一个"握手"udp包,打通A->B的udp链路
  4. 此时A可以向B(120.26.10.118:3000)发送udp包,A->B的会话建立成功

 

先决条件

能够完成打洞有几个先决条件:

  1. A,B所在的nat网络类型(Full cone, Restricted cone, Port-restricted cone, Symmetric NAT)
  2. 在一次udp会话期间,nat设备(路由器)会保持内网进程 inner_ip:inner_port <-> share_public_ip:share_port的映射关系,一般根据具体路由器实现,这个映射关系可以维持几分钟到几个小时不等
  3. 流程中第3步,nat A收到这个握手包后并不会转发给A,因为它发现自己的没有保存过B的地址,认为这是一个来历不明的包而直接丢弃,然而这个包的作用在于在nat B留下了A的记录,使得nat B认为A是可达或者说可通过了,这样当A->B再发送udp包时就可以真正到达B了。所以这个"握手"包的作用是可以打通A->B的通路,是必要的

 

源码示例

使用三台设备模拟,外网设备207.148.70.129模拟Server S,执行server.go代码:

server.go
package main
​
import (
    "fmt"
    "log"
    "net"
    "time"
)func main() {
    listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9981})
    if err != nil {
        fmt.Println(err)
        return
    }
    log.Printf("本地地址: <%s> \n", listener.LocalAddr().String())
    peers := make([]net.UDPAddr, 0, 2)
    data := make([]byte, 1024)
    for {
        n, remoteAddr, err := listener.ReadFromUDP(data)
        if err != nil {
            fmt.Printf("error during read: %s", err)
        }
        log.Printf("<%s> %s\n", remoteAddr.String(), data[:n])
        peers = append(peers, *remoteAddr)
        if len(peers) == 2 {
​
            log.Printf("进行UDP打洞,建立 %s <--> %s 的连接\n", peers[0].String(), peers[1].String())
            listener.WriteToUDP([]byte(peers[1].String()), &peers[0])
            listener.WriteToUDP([]byte(peers[0].String()), &peers[1])
            time.Sleep(time.Second * 8)
            log.Println("中转服务器退出,仍不影响peers间通信")
            return
        }
    }
}

另外两台分别位于不同内网后的设备,均运行相同代码peer.go:
package main
​
import (
    "fmt"
    "log"
    "net"
    "os"
    "strconv"
    "strings"
    "time"
)var tag stringconst HAND_SHAKE_MSG = "我是打洞消息"func main() {
    // 当前进程标记字符串,便于显示
    tag = os.Args[1]
    srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 9982} // 注意端口必须固定
    dstAddr := &net.UDPAddr{IP: net.ParseIP("207.148.70.129"), Port: 9981}
    conn, err := net.DialUDP("udp", srcAddr, dstAddr)
    if err != nil {
        fmt.Println(err)
    }
    if _, err = conn.Write([]byte("hello, I'm new peer:" + tag)); err != nil {
        log.Panic(err)
    }
    data := make([]byte, 1024)
    n, remoteAddr, err := conn.ReadFromUDP(data)
    if err != nil {
        fmt.Printf("error during read: %s", err)
    }
    conn.Close()
    anotherPeer := parseAddr(string(data[:n]))
    fmt.Printf("local:%s server:%s another:%s\n", srcAddr, remoteAddr, anotherPeer.String())// 开始打洞
    bidirectionHole(srcAddr, &anotherPeer)}func parseAddr(addr string) net.UDPAddr {
    t := strings.Split(addr, ":")
    port, _ := strconv.Atoi(t[1])
    return net.UDPAddr{
        IP:   net.ParseIP(t[0]),
        Port: port,
    }
}func bidirectionHole(srcAddr *net.UDPAddr, anotherAddr *net.UDPAddr) {
    conn, err := net.DialUDP("udp", srcAddr, anotherAddr)
    if err != nil {
        fmt.Println(err)
    }
    defer conn.Close()
    // 向另一个peer发送一条udp消息(对方peer的nat设备会丢弃该消息,非法来源),用意是在自身的nat设备打开一条可进入的通道,这样对方peer就可以发过来udp消息
    if _, err = conn.Write([]byte(HAND_SHAKE_MSG)); err != nil {
        log.Println("send handshake:", err)
    }
    go func() {
        for {
            time.Sleep(10 * time.Second)
            if _, err = conn.Write([]byte("from [" + tag + "]")); err != nil {
                log.Println("send msg fail", err)
            }
        }
    }()
    for {
        data := make([]byte, 1024)
        n, _, err := conn.ReadFromUDP(data)
        if err != nil {
            log.Printf("error during read: %s\n", err)
        } else {
            log.Printf("收到数据:%s\n", data[:n])
        }
    }
}

注意代码仅模拟打洞基础流程,如果读者测试网络情况较差发生udp丢包,可能看不到预期结果,此时简单重启server,peer即可.

完整代码参考github

udp打洞转tcp通信

通常,由于udp打洞实现简单,p2p的实现采用udp打洞较多,然而当通路建立起来后使用tcp进行节点间通信可以获取更好的通信效果。因为udp打洞完成后形成的nat映射是和tcp/udp无关的,所以此时可以转为使用tcp建立连接,达到最终的p2p的tcp通信.由于代码较简单,这里就不给出示例了。

参考文献

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

golang实现p2p之UDP打洞 的相关文章

  • C++ 反序列化通过 UDP 从 C# 应用程序发送的对象

    我有一个申请c 它连接到另一个应用程序 编写为c 通过UDP 我更喜欢高性能的解决方案 因为我希望测量事件客户端 某些处理服务器端和客户端处理完成的响应之间的时间 本质上是 往返延迟 我有一个 C 对象 例如 public class Pa
  • HTML5 Websockets 可以在不使用服务器的情况下直接连接 2 个客户端(浏览器)吗? (P2P)[重复]

    这个问题在这里已经有答案了 使用 HTML5 Websockets 我可以直接在 2 个客户端 浏览器 之间打开套接字 假设我知道它们的 IP 地址并且不存在 NAT 遍历问题 我想将一些数据直接从一个浏览器发送到另一个浏览器 本质上是创建
  • HTTP 是否使用 UDP?

    这可能是一个愚蠢的问题 HTTP 是否使用过用户数据报协议 例如 如果使用 HTTP 传输 MP3 或视频 它内部是否使用 UDP 进行传输 From RFC 2616 http www ietf org rfc rfc2616 txt 通
  • 具有 3 个用户连接的 WebRTC

    我现在正在实施源代码WebRTC 示例 https github com webrtc samples tree gh pages src content peerconnection audio通过使用网状拓扑成为 3 个用户连接 但是
  • UDP sendto 上的 ECONNREFUSED 错误

    我在使用正在写入的应用程序时遇到一些无法解释的行为 使用 sendto 向多个端口发送 UDP 数据 所有端口均使用套接字 PF INET SOCK DGRAM 0 为了一组客户端读取进程的利益 这些 sendto 偶尔会不可预测地触发经济
  • 如何监听任意端口的广播包?

    使用 NET 如何在任何端口上侦听发送到 255的udp广播数据包 而不需要绑定到特定端口 我自己找到了办法 它是这样工作的 mainSocket new Socket AddressFamily InterNetwork SocketTy
  • iOS 14 在进行本地网络广播时给出“操作系统错误:错误的文件描述符,errno = 9”

    做一点Jeopardy 风格问答 https stackoverflow blog 2011 07 01 its ok to ask and answer your own questions here 我正在 Flutter 中开发一个应
  • 同步通过 LAN 电缆连接的两台 Windows 7 计算机之间的时间

    我有许多笔记本电脑 它们运行我们的应用程序 同时通过以太网电缆成对连接 但未连接到任何外部网络或互联网 时间 我需要连接对来同步其系统时间 但由于每台计算机都需要能够与任何其他计算机同步 因此我无法将一台计算机定义为时间服务器 而另一台计算
  • 提高UDP可靠性

    我正在构建一个基于 UDP 的小型服务器 服务器基于 Net并使用它自己的Socket类 我通过 ReceiveMessageFromAsync 和异步发送使用完成端口 我的问题是我失去了大约 5 10 的流量 现在我明白这是正常的 但是有
  • F1 2019 UDP解码

    我目前正在为 F1 方向盘开发自己的显示器 F1 2019 由codemasters提供 通过UDP发送数据 该数据存储在字节数组中 我在解码返回的数组时遇到一些问题 问题是我得到了很多信息 但我不知道如何处理它们 我将向您介绍我所尝试过的
  • 我应该害怕使用 UDP 进行客户端/服务器广播通话吗?

    我在过去的两天里阅读了每一篇StackOverflow问题和答案 以及googling当然 关于印地TCP and UDP协议 以便决定在我的用户应用程序和 Windows 服务之间的通信方法中应该使用哪一种 从我目前所看到的来看 UDP是
  • 为什么 UDP 服务器中只有一个套接字?

    我正在准备考试 发现了这个问题 典型的 UDP 服务器可以使用单个套接字来实现 解释一下为什么 对于 TCP 驱动的服务器 我发现创建了两个套接字 一个用于所有客户端访问服务器 另一个用于每个客户端的特定 套接字 用于服务器和客户端之间的进
  • 智能手机可以通过 3G/4G 进行点对点通信吗?

    我正在尝试编写一个应用程序 将数据从一个 Android 设备传输到另一个 Android 设备 但这些设备很可能位于城市 州或国家的不同部分 直接的方法是拥有一台中央服务器 或任何类型的服务器 但我试图避免使用中央服务器 我试图传递的数据
  • 如何使用 Nmap 检索 TCP 和 UDP 端口? [关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我需要在使用 Nmap 的同一扫描中以尽可能最快的方式检索 TCP 和 UDP 端口 我会尽力解释得更好 如果我使用最常用的命令 nmap 192 1
  • 错误号:11,资源暂时不可用

    我正在使用 c 套接字来实现可靠的 UDP 协议 我正在使用以下代码在等待确认的套接字上设置超时 我不确定为什么会收到 errno 11 资源暂时不可用 set timer for recv socket struct timeval tv
  • 为什么WebRTC需要ICE协议才能运行?

    据我了解 ICE协议用于发现从最终用户设备到 外部 的节点 设备 我不明白为什么需要它 数据包路由不是由路由器和交换机等网络设备负责吗 他们应该找到从网关到最终用户设备的最短路径 实际上 路由器会记住他们之前发现的那些路由 此外 NAT 协
  • 用 C 语言进行非阻塞 udp 套接字编程:我能得到什么?

    我在理解从非阻塞 UDP 套接字返回什么recv recvfrom 时遇到问题 与 TCP 相比更具体一点 如果我错了 请纠正我 阻塞套接字 TCP 或 UDP 在缓冲区中有一些数据之前不会从 recv 返回 这可以是一定数量的字节 TCP
  • 写入 UDP 套接字会被阻塞吗?

    如果是的话 在什么条件下 或者 换句话说 在twisted 中运行此代码是否安全 class StatsdClient AbstractStatsdClient def init self host port super StatsdCli
  • 如何从 DatagramPacket 中检索字符串[重复]

    这个问题在这里已经有答案了 下面的代码打印 B 40545a60 B 40545a60abc exp 但我想打印abc 以便我可以从接收系统检索正确的消息 public class Operation InetAddress ip Data
  • SO_BINDTODEVICE Linux 套接字选项的问题

    我有一台带有两个网卡的电脑 一 eth0 用于 LAN 互联网 另一个用于与一个微控制器设备进行 UDP 通信 微控制器有一个 IP 192 168 7 2 和一个 MAC 地址 第二个电脑网络适配器 eth1 有 192 168 7 1

随机推荐

  • 【pandas】(六)增删改查

    文章目录 一 增加数据 1 1 增加一行 1 2 增加一列 1 3 pd concat 拼接数据 1 objs Series DataFrame或Panel对象的 序列或映射 2 axis 0 1 默认为0 纵向拼接 3 join inne
  • IOS技术分享

    前言 最近对 WebRTC iOS 端源码进行了下载和编译 网上针对 WebRTC iOS 端的编译文章基本都是几年前的 有些地方已经不适用于最新版的 WebRTC 的编译 简单记录下载 编译的过程 以 M93 版本为例 编译环境 硬件 M
  • Android购物车效果实现(RecyclerView悬浮头部实现)

    刚开始看购物车效果觉得挺复杂 但是把这个功能拆开来一步一步实现会发现并不难 其实就涉及到 ItemDecoration的绘制 recyclerview的滑动监听 贝塞尔曲线和属性动画相关内容 剩下的就是RecyclerView滑动和点击时左
  • Xshell6和Xftp提示“要继续使用此程序,您必须应用最新的更新或使用新版本“

    Xshell6和Xftp提示 要继续使用此程序 您必须应用最新的更新或使用新版本 使用二进制编辑器修改Xshell和Xftp的nslicense dll文件 如sublime Txt编辑器等 1 分别进入Xshell和Xftp的安装路径下
  • torch.autograd.grad求二阶导数

    1 用法介绍 pytorch中torch autograd grad函数主要用于计算并返回输出相对于输入的梯度总和 具体的参数作用如下所示 torch tril input diagonal 0 out None longrightarro
  • C语言:求斐波那契数列前n项的和

    include
  • 经典卷积神经网络——resnet

    resnet 前言 一 resnet 二 resnet网络结构 三 resnet18 1 导包 2 残差模块 2 通道数翻倍残差模块 3 rensnet18模块 4 数据测试 5 损失函数 优化器 6 加载数据集 数据增强 7 训练数据 8
  • linux安装python3以及pip过程,遇到的错误处理

    需要自己搭建环境 没想到的是基本的安装python3过程过程就踩了很多坑 希望对别人有帮助 参考 https www centoschina cn course config 11027 html 1 下载相应python3 7解释器htt
  • Unity—3D数学基础

    今天用了小半天时间初步了解3D数学基础 明天开始进入unity游戏脚本编写 每日一句 人间骄阳正好 风过林梢 彼时我们正当年少 目录 3D坐标系 全局 世界 坐标系 局部 模型 物体 坐标系 相机坐标系 屏幕坐标系 向量 向量的运算 Vec
  • div滚动到顶部或者底部触发分页查询方法

    如聊天界面 滚动到顶部触发分页 div标签里添加滚动事件 scroll passive getScrollUser event 方法 getScroll event if event target scrollTop 0 this chat
  • 6.824分布式

    MapReduce 例子加深理解 你的工作是实现一个分布式的MapReduce 包括两个程序 master 和 worker 只有一个master进程 以及 一个或多个worker进程并行执行 在真实的系统中 工作人员将在许多不同的机器上运
  • 【安全工具】Web漏洞扫描十大工具

    Web漏洞扫描十大工具 Acunetix Web Vulnerability Scanner 简称AwVS AwVS是一款知名的Web网络漏洞扫描工具 它通过网络爬虫测试你的网站安全 检测流行安全漏洞 a 自动的客户端脚本分析器 允许对Aj
  • 关于springBoot如何配置双数据源

    前言 本文采用springBoot 配置类的方式简单配置Mysql PostgreSql双数据源 1 首先导入需要的pom依赖
  • 配置测试

    1 配置测试 configuration testing 配置测试是指使用各种硬件来测试软件运行的过程 2 基于Windows的PC机包括 个人计算机 部件 系统主板 内部板卡和其他内部设备 外设 接口 可选项和内存 设备驱动程序 3 配置
  • 轻松理解转置卷积(transposed convolution)或反卷积(deconvolution)

    本译文很大程度上保留了原文原貌 并添加了细节便于理解 各种指代 在CNN中 转置卷积是一种上采样 up sampling 的常见方法 如果你不清楚转置卷积是怎么操作的 那么就来读读这篇文章吧 本文的notebook代码在Github 上采样
  • Redis——数据结构介绍

    Redis是一个key value的数据库 key一般是String类型 不过value的类型是多样的 String hello word Hash name Jack age 21 List A gt B gt C gt D Set A
  • SaltStack 自动化运维详解

    一 自动化运维工具对比 使用所需软件配置单个服务器是一项相当简单的任务 但是 如果许多服务器需要安装相同或相似的软件和配置 则该过程将需要大量的工时才能完成 这会耗尽您本已紧张的资源 如果没有某种形式的自动化 这项任务几乎无法完成 考虑到这
  • 关于SpringMVC返回date的格式问题

    Spring 项目中 java的util date和java time类型的日期 返回到前端的时候 默认的序列化方式显示的是标准格式 为了能够正确的显示想要的时间 可以使用jackson指定时间的格式 访问我的个人网站获取更多文章 数据库的
  • 汽车的操作系统AUTOSAR

    汽车软件开发autosar 01汽车相关知识 汽车发展三大趋势 电动化 智能化 网联化 1 电动化 底层支撑 网联化的驱动力 2 智能化 人工智能借助软硬融合带来功能升级 体验升级 安全升级 3 网联化 5G的应用场景 让汽车与人 车 物的
  • golang实现p2p之UDP打洞

    当今互联网到处存在着一些中间件 MIddleBoxes 如NAT和防火墙 导致两个 不在同一内网 中的客户端无法直接通信 这些问题即便是到了IPV6时代也会存在 因为即使不需要NAT 但还有其他中间件如防火墙阻挡了链接的建立 目前部署的中间