自由通讯的魔法:Go从零实现UDP/P2P 聊天工具

嘿,各位网络巫师们!今天我们要揭秘一个神奇的魔法——如何让两个不同网络下的客户端,绕过所有阻碍,直接进行秘密通信!这个项目将带你探索 Go 语言实现的 UDP 模式下的p2p技术,让你也能成为网络通信的魔法师!

项目概述

这是一个简单但强大的 P2P 通信系统,由两位得力助手组成:

  1. 红娘服务器:扮演月老角色,为两个素未谋面的客户端牵线搭桥
  2. 通信客户端:一旦拿到对方的联系方式,就能甩开红娘,直接私聊(坏笑)

技术魔法原理

P2P的秘密

想象一下:两个客户端就像住在两个封闭小区里的人,小区门口有保安(NAT 设备)严格检查访客。直接敲门是行不通的,但我们可以用一个巧妙的方法——

  1. 两个人都先给小区外的红娘服务器打电话:"我想认识新朋友!"
  2. 红娘记下了两人的家庭住址(公网 IP 和端口)
  3. 红娘告诉双方:"对方住在某某小区几号门"
  4. 两人同时尝试给对方打电话(虽然可能都打不通)
  5. 关键来了!这次拨号虽然失败,但会在各自小区门口留下记录:"这个人可以接来自某某小区的电话"
  6. 当他们再次尝试联系时——奇迹发生了!电话居然接通了!
  7. 从此,两人可以自由聊天,再也不需要红娘从中传话了!

代码实现分析

红娘服务器实现(幕后牵线人)

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
        }
    }
}

服务器端的主要功能:

  1. 在 UDP 端口 9981 上监听连接
  2. 维护一个 peers 数组,最多存储两个客户端的地址信息
  3. 当有客户端发送数据时,将其地址添加到 peers 数组中
  4. peers 数组中有两个客户端时,执行以下操作:
    • 将第一个客户端的地址信息发送给第二个客户端
    • 将第二个客户端的地址信息发送给第一个客户端
    • 等待 8 秒,确保两个客户端有足够的时间完成穿透
    • 服务器退出,但不影响两个客户端之间已建立的 P2P 连接

客户端实现(想要交友的小伙伴)

package main

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

var tag string  // 我的昵称

const HAND_SHAKE_MSG = "我是打招呼消息"  // 破冰开场白

func main() {
    // 当前进程标记字符串,便于显示
    tag = os.Args[1]
    srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 7777} // 注意端口必须固定,这是我的固定电话
    dstAddr := &net.UDPAddr{IP: net.ParseIP("*.*.*.*"), Port: 9981} // 红娘服务器地址(已隐藏真实IP)
    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])
        }
    }
}

客户端的主要功能:

  1. 从命令行参数获取标签(tag),用于标识自己
  2. 在固定端口 7777 上创建 UDP 连接(注意:端口必须固定,程序自定义的端口
  3. 连接到服务器(...:9981)并发送自己的标识信息
  4. 从服务器接收另一个客户端的地址信息
  5. 关闭与服务器的连接
  6. 调用 bidirectionHole 函数进行双向通讯:
    • 创建一个连接到另一个客户端的 UDP 连接
    • 发送一条握手消息,虽然可能被对方的 NAT 丢弃,但会在自己的 NAT 上打开一条通道
    • 启动一个 goroutine,每 10 秒向对方发送一条消息
    • 主 goroutine 持续监听并打印来自对方的消息

使用魔法指南

如何启动红娘服务器

  1. 在一台有公网 IP 的服务器上,施展以下咒语:
go build -o server.exe main.go  # 铸造魔法道具
./server.exe  # 启动红娘服务

如何让两个客户端建立私聊

  1. 在两个不同的网络环境(比如不同的家庭网络)中,分别启动客户端:
go build -o client.exe main.go  # 铸造魔法道具
./client.exe 小明  # 第一个客户端,名为小明
./client.exe 小红  # 第二个客户端,名为小红
  1. 接下来,魔法就会自动发生!两个客户端会自动联系红娘,获取对方的地址,完成 P2P,然后就可以直接聊天啦!

魔法注意事项

⚠️ 重要提示:以下是施展魔法的关键要点! ⚠️

  1. 客户端的端口必须固定的!这就像是魔法通讯的专用频道,一旦改变,魔法就会失效!
  2. 你需要在客户端代码中填入真实的红娘服务器 IP 地址,替换掉 *.*.*.*
  3. 红娘服务器是个热心人,但她只在介绍阶段工作。一旦两位客户端建立了私聊,红娘就会功成身退,而私聊不会受到影响
  4. 记得在实际部署时修改服务器 IP 地址,代码中的 *.*.*.* 只是个占位符

魔法升级建议

如果你想让这个魔法更加强大,可以考虑以下升级路径:

  1. 添加错误重试魔法,让连接更加稳定可靠
  2. 添加心跳检测咒语,及时发现魔法连接是否中断
  3. 让红娘服务器能够同时介绍多对朋友,而不仅仅是一对
  4. 添加通信加密魔法,保护私聊内容不被偷听

魔法师总结

这个项目用简单的 Go 代码展示了 UDP 穿透这个网络魔法的实现方法。通过这个魔法,我们能够让两个被防火墙隔离的客户端建立直接的秘密通信通道!

这种魔法在很多地方都有应用:

  • 在线游戏中,玩家之间的低延迟通信
  • 视频通话软件,让通话更加流畅清晰
  • 文件共享工具,实现点对点高速传输

虽然示例代码看起来简单,但它包含了 UDP 穿透魔法的核心秘密。掌握了这个魔法,你就可以进一步探索更复杂的 P2P 通信系统,成为真正的网络魔法师!

现在,快去施展你的魔法吧!🧙♂️✨

往期部分文章列表

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容