什么是内网穿透?

本文将详细讲解内网穿透的基本原理,尤其是使用 Go 语言实现的基于信令服务器的 UDP 打洞(NAT 穿透)方法。通过本文你将了解每个步骤的原理,并获得完整可运行的示例代码,助你轻松构建点对点通信通道。


一、什么是内网穿透?

内网穿透(NAT Traversal)是指在没有公网 IP 的情况下,让处于不同局域网的设备能够直接通信的技术。

通常,家庭或企业网络的设备都在 NAT(网络地址转换)路由器之后,这样外部设备无法主动访问这些设备。

为了让这些设备之间能够 P2P 通信,需要借助一些策略进行 NAT 穿透。


二、常见内网穿透方式

  1. 中继服务器(TURN):借助公网中继服务器转发数据,稳定但耗带宽。
  2. 打洞(UDP Hole Punching):通过 NAT 的端口映射机制建立直接连接,是最常见的高效方案。
  3. 端口映射(UPnP):需要路由器支持,一般用于家庭设备。

本文重点介绍打洞方式。


三、UDP 打洞的原理

参与角色

角色描述
A/B两个客户端,处于不同的局域网
A-NAT/B-NAT各自的 NAT 路由器
Signaling Server有公网 IP 的信令服务器,用于协调通信

步骤详解

1. 注册自身地址
  • A 和 B 向信令服务器发送注册请求,NAT 路由器自动为它们分配映射的公网地址和端口。
2. 获取对方地址
  • 信令服务器返回对方的公网地址和端口。
3. 打洞
  • A 和 B 开始向对方的地址发送 UDP 包。
  • NAT 记录了“曾向该地址发出过请求”,从而允许外部数据包进入。
4. 成功建立 P2P 通信
  • 经过多次尝试后,双方成功建立直连通道,之后的数据不再经过信令服务器。

小贴士

  • 信令服务器必须有公网 IP。
  • 若遇到对称 NAT,可能打洞失败,需使用 TURN 中继。

四、基于 Go 实现 UDP 打洞

我们使用 Go 实现一个简单的系统,包括:

  • 信令服务器 signaling_server.go
  • 客户端 client.go

1. 信令服务器

// signaling_server.go
package main

import (
    "fmt"
    "net"
    "strings"
    "sync"
)

var peers = make(map[string]*net.UDPAddr)
var mu sync.Mutex

func main() {
    addr, _ := net.ResolveUDPAddr("udp", ":9999")
    conn, _ := net.ListenUDP("udp", addr)
    defer conn.Close()

    fmt.Println("Signaling server listening on port 9999")

    buf := make([]byte, 1024)
    for {
        n, clientAddr, _ := conn.ReadFromUDP(buf)
        msg := string(buf[:n])
        tokens := strings.Split(msg, " ")

        if tokens[0] == "REGISTER" && len(tokens) == 2 {
            name := tokens[1]
            mu.Lock()
            peers[name] = clientAddr
            mu.Unlock()
            fmt.Printf("Registered %s: %s\n", name, clientAddr.String())
        } else if tokens[0] == "GET" && len(tokens) == 2 {
            target := tokens[1]
            mu.Lock()
            targetAddr, ok := peers[target]
            mu.Unlock()
            if ok {
                response := fmt.Sprintf("PEER %s %s", target, targetAddr.String())
                conn.WriteToUDP([]byte(response), clientAddr)
            }
        }
    }
}

2.信令服务器

// client.go
package main

import (
    "fmt"
    "net"
    "os"
    "strings"
    "time"
)

func main() {
    if len(os.Args) != 4 {
        fmt.Println("Usage: go run client.go <your-name> <peer-name> <signaling-server-ip>")
        return
    }

    selfName := os.Args[1]
    peerName := os.Args[2]
    serverIP := os.Args[3]

    localAddr, _ := net.ResolveUDPAddr("udp", ":0")
    conn, _ := net.ListenUDP("udp", localAddr)
    defer conn.Close()

    serverAddr, _ := net.ResolveUDPAddr("udp", serverIP+":9999")

    regMsg := fmt.Sprintf("REGISTER %s", selfName)
    conn.WriteToUDP([]byte(regMsg), serverAddr)

    time.Sleep(1 * time.Second)
    getMsg := fmt.Sprintf("GET %s", peerName)
    conn.WriteToUDP([]byte(getMsg), serverAddr)

    var peerAddr *net.UDPAddr
    buf := make([]byte, 1024)

    go func() {
        for {
            n, addr, _ := conn.ReadFromUDP(buf)
            msg := string(buf[:n])
            if strings.HasPrefix(msg, "PEER") {
                parts := strings.Split(msg, " ")
                if len(parts) == 3 {
                    peerAddr, _ = net.ResolveUDPAddr("udp", parts[2])
                    fmt.Printf("Peer address: %s\n", peerAddr.String())
                }
            } else {
                fmt.Printf("Received from %s: %s\n", addr.String(), msg)
            }
        }
    }()

    for peerAddr == nil {
        time.Sleep(500 * time.Millisecond)
    }

    go func() {
        for {
            msg := fmt.Sprintf("HELLO from %s", selfName)
            conn.WriteToUDP([]byte(msg), peerAddr)
            time.Sleep(1 * time.Second)
        }
    }()

    select {}
}

3. 启动方式

# 启动服务器
$ go run signaling_server.go

# 启动 A 客户端
$ go run client.go A B 服务器IP

# 启动 B 客户端
$ go run client.go B A 服务器IP

五、总结

使用UDP 实现NAT 打洞,本质上就是通过同时发送请求来欺骗NAT 服务器,让他误认为外部的请求是局域网内设备发送请求的响应。同时UDP 打洞只能支持UDP协议发送数据, 如果想是实现TCP打洞会更具备挑战。因为 TCP 是连接导向的协议,不像 UDP 那样无状态。TCP 建立连接时需要 3 次握手(SYN → SYN-ACK → ACK)。如果你主动向对方发起连接,而对方没有监听,就会被目标 NAT 拒绝更麻烦的是,大多数 NAT 对 TCP 的行为更严格,且 NAT 不会帮你中继 TCP SYN 包,所以TCP 打洞比 UDP 更麻烦,需要双方同时发起 TCP 连接请求才能“打洞成功”。更常见的做法是使用UDP 打洞,然后在用户层实现类似于TCP协议的可靠性。(如 QUIC / 自定义协议)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

指针指指针

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值