嘿,各位网络巫师们!今天我们要揭秘一个神奇的魔法------如何让两个不同网络下的客户端,绕过所有阻碍,直接进行秘密通信!这个项目将带你探索 Go 语言实现的 UDP 模式下的p2p技术,让你也能成为网络通信的魔法师!
项目概述
这是一个简单但强大的 P2P 通信系统,由两位得力助手组成:
- 红娘服务器:扮演月老角色,为两个素未谋面的客户端牵线搭桥
- 通信客户端:一旦拿到对方的联系方式,就能甩开红娘,直接私聊(坏笑)
技术魔法原理
P2P的秘密
想象一下:两个客户端就像住在两个封闭小区里的人,小区门口有保安(NAT 设备)严格检查访客。直接敲门是行不通的,但我们可以用一个巧妙的方法------
- 两个人都先给小区外的红娘服务器打电话:"我想认识新朋友!"
- 红娘记下了两人的家庭住址(公网 IP 和端口)
- 红娘告诉双方:"对方住在某某小区几号门"
- 两人同时尝试给对方打电话(虽然可能都打不通)
- 关键来了!这次拨号虽然失败,但会在各自小区门口留下记录:"这个人可以接来自某某小区的电话"
- 当他们再次尝试联系时------奇迹发生了!电话居然接通了!
- 从此,两人可以自由聊天,再也不需要红娘从中传话了!
代码实现分析
红娘服务器实现(幕后牵线人)
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
}
}
}
服务器端的主要功能:
- 在 UDP 端口 9981 上监听连接
- 维护一个
peers数组,最多存储两个客户端的地址信息 - 当有客户端发送数据时,将其地址添加到
peers数组中 - 当
peers数组中有两个客户端时,执行以下操作:- 将第一个客户端的地址信息发送给第二个客户端
- 将第二个客户端的地址信息发送给第一个客户端
- 等待 8 秒,确保两个客户端有足够的时间完成穿透
- 服务器退出,但不影响两个客户端之间已建立的 P2P 连接
客户端实现(想要交友的小伙伴)
go
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])
}
}
}
客户端的主要功能:
- 从命令行参数获取标签(tag),用于标识自己
- 在固定端口 7777 上创建 UDP 连接(注意:端口必须固定,程序自定义的端口)
- 连接到服务器(. ..:9981)并发送自己的标识信息
- 从服务器接收另一个客户端的地址信息
- 关闭与服务器的连接
- 调用
bidirectionHole函数进行双向通讯:- 创建一个连接到另一个客户端的 UDP 连接
- 发送一条握手消息,虽然可能被对方的 NAT 丢弃,但会在自己的 NAT 上打开一条通道
- 启动一个 goroutine,每 10 秒向对方发送一条消息
- 主 goroutine 持续监听并打印来自对方的消息
使用魔法指南
如何启动红娘服务器
- 在一台有公网 IP 的服务器上,施展以下咒语:
bash
go build -o server.exe main.go # 铸造魔法道具
./server.exe # 启动红娘服务
如何让两个客户端建立私聊
- 在两个不同的网络环境(比如不同的家庭网络)中,分别启动客户端:
bash
go build -o client.exe main.go # 铸造魔法道具
./client.exe 小明 # 第一个客户端,名为小明
./client.exe 小红 # 第二个客户端,名为小红
- 接下来,魔法就会自动发生!两个客户端会自动联系红娘,获取对方的地址,完成 P2P,然后就可以直接聊天啦!
魔法注意事项
⚠️ 重要提示:以下是施展魔法的关键要点! ⚠️
- 客户端的端口必须固定的!这就像是魔法通讯的专用频道,一旦改变,魔法就会失效!
- 你需要在客户端代码中填入真实的红娘服务器 IP 地址,替换掉
*.*.*.* - 红娘服务器是个热心人,但她只在介绍阶段工作。一旦两位客户端建立了私聊,红娘就会功成身退,而私聊不会受到影响
- 记得在实际部署时修改服务器 IP 地址,代码中的
*.*.*.*只是个占位符
魔法升级建议
如果你想让这个魔法更加强大,可以考虑以下升级路径:
- 添加错误重试魔法,让连接更加稳定可靠
- 添加心跳检测咒语,及时发现魔法连接是否中断
- 让红娘服务器能够同时介绍多对朋友,而不仅仅是一对
- 添加通信加密魔法,保护私聊内容不被偷听
魔法师总结
这个项目用简单的 Go 代码展示了 UDP 穿透这个网络魔法的实现方法。通过这个魔法,我们能够让两个被防火墙隔离的客户端建立直接的秘密通信通道!
这种魔法在很多地方都有应用:
- 在线游戏中,玩家之间的低延迟通信
- 视频通话软件,让通话更加流畅清晰
- 文件共享工具,实现点对点高速传输
虽然示例代码看起来简单,但它包含了 UDP 穿透魔法的核心秘密。掌握了这个魔法,你就可以进一步探索更复杂的 P2P 通信系统,成为真正的网络魔法师!
现在,快去施展你的魔法吧!🧙♂️✨
往期部分文章列表
- Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
- 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
- 验证码识别API:告别收费接口,迎接免费午餐
- 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
- 无奈!我用go写了个MySQL服务
- 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
- 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
- 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
- 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载