├── .gitignore ├── model ├── client.go └── msg.go ├── cloud └── main.go ├── client └── main.go ├── Readme.md ├── go.mod ├── server ├── msg_type.go ├── msg.go ├── name.go ├── cloud_server.go └── client.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /model/client.go: -------------------------------------------------------------------------------- 1 | package model 2 | -------------------------------------------------------------------------------- /model/msg.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | -------------------------------------------------------------------------------- /cloud/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/xhyonline/nat3p2p/server" 5 | ) 6 | 7 | func main() { 8 | cloud := server.NewCloud("0.0.0.0:7709") 9 | cloud.Run() 10 | } 11 | -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/gogf/gf/util/grand" 7 | "github.com/xhyonline/nat3p2p/server" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | const cloud = "此处请填写你的云端地址" // 示例:120.120.120.120:7709 13 | 14 | func main() { 15 | fmt.Printf("请输入昵称:") 16 | inputReader := bufio.NewReader(os.Stdin) 17 | nickname, err := inputReader.ReadString('\n') 18 | if err != nil { 19 | panic(err) 20 | } 21 | nickname = strings.TrimSuffix(nickname, "\n") 22 | port := grand.N(10000, 20000) 23 | localAddress := fmt.Sprintf("0.0.0.0:%d", port) 24 | client := server.NewClient(cloud, localAddress, nickname) 25 | client.Run() 26 | } 27 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## bilibili 乐享互联 2 | 3 | ## 基于 TCP NAT3 打洞实现的 P2P 聊天室简易 Demo 4 | 5 | B站视频: 6 | ``` 7 | https://www.bilibili.com/video/BV1mi4y1W7cb/?spm_id_from=333.999.0.0&vd_source=2d0c706e18e52ecf183100ed5009fe51 8 | ``` 9 | 10 | ### 项目说明 11 | 12 | 本项目是Up主基于Golang编写的TCP打洞实现的 P2P 聊天室简易 Demo。 13 | 14 | 已经支持 NAT1、2、3 三种打洞,并未实现Stun探测NAT类型服务,只是作为一个简易Demo使用。 15 | 16 | 部署后,请将客户端发送给你的好朋友进行测试。关闭云端服务后,你仍然可以和对等节点 Peer(你的朋友) 进行聊天通信。 17 | 18 | 19 | ### 部署说明 20 | 21 | 本demo基于`Golang`开发,因此请确保有Go环境 22 | 23 | #### 一、编译启动云端 24 | 编译 25 | ``` 26 | cd cloud 27 | go build -o cloud main.go 28 | ``` 29 | 请将编译后的`cloud`文件放置云端然后启动,启动后,云端会监听在 7709 端口 30 | ``` 31 | ./cloud 32 | ``` 33 | 34 | #### 二、启动客户端 35 | 36 | 启动客户端前,请打开 `client/main.go` 文件将 cloud 常量改写为你自己的云端地址 37 | 38 | 编译客户端 39 | ``` 40 | cd client 41 | go build -o client main.go 42 | ``` 43 | 启动客户端 44 | ``` 45 | ./client.exe 46 | ``` 47 | 并且将客户端发送一份至你的朋友共同启动,双方将会自动进行打洞。 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xhyonline/nat3p2p 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/gogf/gf v1.16.3 7 | github.com/gookit/color v1.5.4 8 | github.com/libp2p/go-reuseport v0.4.0 9 | ) 10 | 11 | require ( 12 | github.com/BurntSushi/toml v0.3.1 // indirect 13 | github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 // indirect 14 | github.com/fsnotify/fsnotify v1.4.9 // indirect 15 | github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579 // indirect 16 | github.com/gomodule/redigo v2.0.0+incompatible // indirect 17 | github.com/gorilla/websocket v1.4.1 // indirect 18 | github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf // indirect 19 | github.com/mattn/go-runewidth v0.0.10 // indirect 20 | github.com/olekukonko/tablewriter v0.0.5 // indirect 21 | github.com/rivo/uniseg v0.1.0 // indirect 22 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 23 | go.opentelemetry.io/otel v0.19.0 // indirect 24 | go.opentelemetry.io/otel/metric v0.19.0 // indirect 25 | go.opentelemetry.io/otel/trace v0.19.0 // indirect 26 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect 27 | golang.org/x/sys v0.10.0 // indirect 28 | golang.org/x/text v0.3.4 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /server/msg_type.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | const ( 4 | No = iota 5 | Yes 6 | ) 7 | 8 | const ( 9 | Success = iota 10 | Error 11 | ) 12 | 13 | const ( 14 | // 云端消息发送给客户端的类型 15 | CloudBroadcastClientOnline = iota + 1 // 客户端上线广播 16 | CloudBroadcastOffline // 客户端下线广播 17 | CloudNotifyClientRegisterRes // 云端通知客户端注册结果 18 | CloudNotifyClientHolePunching // 云端通知客户端,向来源打洞 19 | ClientNotifyCloudHolePunching // 客户端通知云端,让对方打洞 20 | ClientGetFriendList // 云端提供在线好友列表 21 | // 客户端告知云端的消息类型 22 | ClientRegister // 客户端向云端进行注册 23 | PeerMsgSay // 消息 24 | ) 25 | 26 | type CommonResult struct { 27 | Code int `json:"code"` 28 | Msg string `json:"msg"` 29 | Data interface{} `json:"data"` 30 | } 31 | 32 | // ClientRegisterMsg 客户端注册事件 33 | type ClientRegisterMsg struct { 34 | NickName string `json:"nickname"` 35 | } 36 | 37 | type ClientMsg struct { 38 | MsgType int `json:"msg_type"` 39 | Body interface{} `json:"body"` 40 | } 41 | 42 | // ClientMsgResult 客户端注册结果 43 | type ClientMsgResult struct { 44 | MsgType int `json:"msg_type"` 45 | Code int `json:"code"` 46 | Msg string `json:"msg"` 47 | Body interface{} `json:"body"` 48 | } 49 | 50 | // CloudBroadcastRegisterMsg 云端广播注册事件 51 | type CloudBroadcastRegisterMsg struct { 52 | NickName string `json:"nickname"` 53 | RemoteAddr string `json:"remote_addr"` 54 | } 55 | 56 | // CloudBroadcastOfflineMsg 广播通知用户下线 57 | type CloudBroadcastOfflineMsg struct { 58 | } 59 | 60 | type Friend struct { 61 | NickName string `json:"nickname"` 62 | RemoteAddr string `json:"remote_addr"` 63 | } 64 | -------------------------------------------------------------------------------- /server/msg.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "encoding/json" 8 | "net" 9 | ) 10 | 11 | // Message 是一条标准消息的实现 12 | type Message struct { 13 | Body string `json:"body"` 14 | } 15 | 16 | // Encode 消息编码 17 | func (m *Message) Encode() []byte { 18 | // 序列化为 json 19 | message, _ := json.Marshal(m) 20 | // 读取该 json 的长度 21 | var length = int32(len(message)) 22 | var pkg = new(bytes.Buffer) 23 | // 写入消息头 24 | err := binary.Write(pkg, binary.BigEndian, length) 25 | if err != nil { 26 | return nil 27 | } 28 | // 写入消息实体 29 | err = binary.Write(pkg, binary.BigEndian, message) 30 | if err != nil { 31 | return nil 32 | } 33 | return pkg.Bytes() 34 | } 35 | 36 | // NewMsg 实例化消息实体 37 | func NewMsg(msg interface{}) *Message { 38 | body, _ := json.Marshal(msg) 39 | return &Message{Body: string(body)} 40 | } 41 | 42 | // Ctx 消息上下文 43 | type Ctx struct { 44 | RemoteAddr net.Addr 45 | Body []byte 46 | BodyString string 47 | FromConn net.Conn 48 | } 49 | 50 | // BizStandardMsg 业务标准消息 51 | type BizStandardMsg struct { 52 | MsgType int `json:"msg_type"` 53 | MsgBody interface{} `json:"msg_body"` 54 | } 55 | 56 | // OnMsg 当消息事件发生时 57 | func OnMsg(conn net.Conn, handelFunc func(Ctx), handelError func(conn net.Conn, err error)) { 58 | reader := bufio.NewReader(conn) 59 | for { 60 | // 前4个字节表示数据长度 61 | // 此外 Peek 方法并不会减少 reader 中的实际数据量 62 | peek, err := reader.Peek(4) 63 | if err != nil { 64 | handelError(conn, err) 65 | return 66 | } 67 | buffer := bytes.NewBuffer(peek) 68 | var length int32 69 | // 读取缓冲区前4位,代表消息实体的数据长度,赋予 length 变量 70 | err = binary.Read(buffer, binary.BigEndian, &length) 71 | if err != nil { 72 | handelError(conn, err) 73 | return 74 | } 75 | // reader.Buffered() 返回缓存中未读取的数据的长度, 76 | // 如果缓存区的数据小于总长度,则意味着数据不完整,很可能是内核态没有完全拷贝数据到用户态中 77 | // 因此下一轮就齐活了 78 | if int32(reader.Buffered()) < length+4 { 79 | continue 80 | } 81 | //从缓存区读取大小为数据长度的数据 82 | data := make([]byte, length+4) 83 | _, err = reader.Read(data) 84 | if err != nil { 85 | handelError(conn, err) 86 | return 87 | } 88 | m := new(Message) 89 | _ = json.Unmarshal(data[4:], m) 90 | handelFunc(Ctx{ 91 | RemoteAddr: conn.RemoteAddr(), 92 | Body: data[4:], 93 | BodyString: m.Body, 94 | FromConn: conn, 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /server/name.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | var lastName = []string{ 10 | "赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "褚", "卫", "蒋", 11 | "沈", "韩", "杨", "朱", "秦", "尤", "许", "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏", 12 | "陶", "姜", "戚", "谢", "邹", "喻", "柏", "水", "窦", "章", "云", "苏", "潘", "葛", "奚", "范", "彭", 13 | "郎", "鲁", "韦", "昌", "马", "苗", "凤", "花", "方", "任", "袁", "柳", "鲍", "史", "唐", "费", "薛", 14 | "雷", "贺", "倪", "汤", "滕", "殷", "罗", "毕", "郝", "安", "常", "傅", "卞", "齐", "元", "顾", "孟", 15 | "平", "黄", "穆", "萧", "尹", "姚", "邵", "湛", "汪", "祁", "毛", "狄", "米", "伏", "成", "戴", "谈", 16 | "宋", "茅", "庞", "熊", "纪", "舒", "屈", "项", "祝", "董", "梁", "杜", "阮", "蓝", "闵", "季", "贾", 17 | "路", "娄", "江", "童", "颜", "郭", "梅", "盛", "林", "钟", "徐", "邱", "骆", "高", "夏", "蔡", "田", 18 | "樊", "胡", "凌", "霍", "虞", "万", "支", "柯", "管", "卢", "莫", "柯", "房", "裘", "缪", "解", "应", 19 | "宗", "丁", "宣", "邓", "单", "杭", "洪", "包", "诸", "左", "石", "崔", "吉", "龚", "程", "嵇", "邢", 20 | "裴", "陆", "荣", "翁", "荀", "于", "惠", "甄", "曲", "封", "储", "仲", "伊", "宁", "仇", "甘", "武", 21 | "符", "刘", "景", "詹", "龙", "叶", "幸", "司", "黎", "溥", "印", "怀", "蒲", "邰", "从", "索", "赖", 22 | "卓", "屠", "池", "乔", "胥", "闻", "莘", "党", "翟", "谭", "贡", "劳", "逄", "姬", "申", "扶", "堵", 23 | "冉", "宰", "雍", "桑", "寿", "通", "燕", "浦", "尚", "农", "温", "别", "庄", "晏", "柴", "瞿", "阎", 24 | "连", "习", "容", "向", "古", "易", "廖", "庾", "终", "步", "都", "耿", "满", "弘", "匡", "国", "文", 25 | "寇", "广", "禄", "阙", "东", "欧", "利", "师", "巩", "聂", "关", "荆", "司马", "上官", "欧阳", "夏侯", 26 | "诸葛", "闻人", "东方", "赫连", "皇甫", "尉迟", "公羊", "澹台", "公冶", "宗政", "濮阳", "淳于", "单于", 27 | "太叔", "申屠", "公孙", "仲孙", "轩辕", "令狐", "徐离", "宇文", "长孙", "慕容", "司徒", "司空"} 28 | var firstName = []string{ 29 | "伟", "刚", "勇", "毅", "俊", "峰", "强", "军", "平", "保", "东", "文", "辉", "力", "明", "永", "健", "世", "广", "志", "义", 30 | "兴", "良", "海", "山", "仁", "波", "宁", "贵", "福", "生", "龙", "元", "全", "国", "胜", "学", "祥", "才", "发", "武", "新", 31 | "利", "清", "飞", "彬", "富", "顺", "信", "子", "杰", "涛", "昌", "成", "康", "星", "光", "天", "达", "安", "岩", "中", "茂", 32 | "进", "林", "有", "坚", "和", "彪", "博", "诚", "先", "敬", "震", "振", "壮", "会", "思", "群", "豪", "心", "邦", "承", "乐", 33 | "绍", "功", "松", "善", "厚", "庆", "磊", "民", "友", "裕", "河", "哲", "江", "超", "浩", "亮", "政", "谦", "亨", "奇", "固", 34 | "之", "轮", "翰", "朗", "伯", "宏", "言", "若", "鸣", "朋", "斌", "梁", "栋", "维", "启", "克", "伦", "翔", "旭", "鹏", "泽", 35 | "晨", "辰", "士", "以", "建", "家", "致", "树", "炎", "德", "行", "时", "泰", "盛", "雄", "琛", "钧", "冠", "策", "腾", "楠", 36 | "榕", "风", "航", "弘", "秀", "娟", "英", "华", "慧", "巧", "美", "娜", "静", "淑", "惠", "珠", "翠", "雅", "芝", "玉", "萍", 37 | "红", "娥", "玲", "芬", "芳", "燕", "彩", "春", "菊", "兰", "凤", "洁", "梅", "琳", "素", "云", "莲", "真", "环", "雪", "荣", 38 | "爱", "妹", "霞", "香", "月", "莺", "媛", "艳", "瑞", "凡", "佳", "嘉", "琼", "勤", "珍", "贞", "莉", "桂", "娣", "叶", "璧", 39 | "璐", "娅", "琦", "晶", "妍", "茜", "秋", "珊", "莎", "锦", "黛", "青", "倩", "婷", "姣", "婉", "娴", "瑾", "颖", "露", "瑶", 40 | "怡", "婵", "雁", "蓓", "纨", "仪", "荷", "丹", "蓉", "眉", "君", "琴", "蕊", "薇", "菁", "梦", "岚", "苑", "婕", "馨", "瑗", 41 | "琰", "韵", "融", "园", "艺", "咏", "卿", "聪", "澜", "纯", "毓", "悦", "昭", "冰", "爽", "琬", "茗", "羽", "希", "欣", "飘", 42 | "育", "滢", "馥", "筠", "柔", "竹", "霭", "凝", "晓", "欢", "霄", "枫", "芸", "菲", "寒", "伊", "亚", "宜", "可", "姬", "舒", 43 | "影", "荔", "枝", "丽", "阳", "妮", "宝", "贝", "初", "程", "梵", "罡", "恒", "鸿", "桦", "骅", "剑", "娇", "纪", "宽", "苛", 44 | "灵", "玛", "媚", "琪", "晴", "容", "睿", "烁", "堂", "唯", "威", "韦", "雯", "苇", "萱", "阅", "彦", "宇", "雨", "洋", "忠", 45 | "宗", "曼", "紫", "逸", "贤", "蝶", "菡", "绿", "蓝", "儿", "翠", "烟", "小", "轩"} 46 | var lastNameLen = len(lastName) 47 | var firstNameLen = len(firstName) 48 | 49 | func GetFullName() string { 50 | rand.Seed(time.Now().UnixNano()) //设置随机数种子 51 | var first string //名 52 | for i := 0; i <= rand.Intn(2); i++ { //随机产生2位或者3位的名 53 | first = fmt.Sprint(firstName[rand.Intn(firstNameLen-1)]) 54 | } 55 | //返回姓名 56 | return fmt.Sprintf("%s%s", fmt.Sprint(lastName[rand.Intn(lastNameLen-1)]), first) 57 | } 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 h1:LdXxtjzvZYhhUaonAaAKArG3pyC67kGL3YY+6hGG8G4= 4 | github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 8 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 9 | github.com/gogf/gf v1.16.3 h1:zz4qBRPchiyzLq3EH0rzVb1SBkax418ifC+4ABI6Ylc= 10 | github.com/gogf/gf v1.16.3/go.mod h1:EjnxZXddTwfFoLPofDE3NokFWx+immofINtSyFCj280= 11 | github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579 h1:pP/uEy52biKDytlgK/ug8kiYPAiYu6KajKVUHfGrtyw= 12 | github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579/go.mod h1:52e6mXyNnHAsFrXrSnj5JPRSKsZKpHylVtA3j4AtMz8= 13 | github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= 14 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 15 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 16 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 17 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 18 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 19 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 20 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 21 | github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf h1:wIOAyJMMen0ELGiFzlmqxdcV1yGbkyHBAB6PolcNbLA= 22 | github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= 23 | github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= 24 | github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= 25 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 26 | github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= 27 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 28 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 29 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 33 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 37 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= 38 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 39 | go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng= 40 | go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= 41 | go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg= 42 | go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= 43 | go.opentelemetry.io/otel/oteltest v0.19.0 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1rU+ip2v9Q= 44 | go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= 45 | go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= 46 | go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 49 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= 51 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 57 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 60 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 61 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 62 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 63 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 68 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 69 | -------------------------------------------------------------------------------- /server/cloud_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gogf/gf/container/gmap" 5 | "github.com/gogf/gf/encoding/gjson" 6 | "github.com/gogf/gf/frame/g" 7 | "github.com/gogf/gf/os/glog" 8 | "github.com/gogf/gf/util/gconv" 9 | reuse "github.com/libp2p/go-reuseport" 10 | "net" 11 | ) 12 | 13 | type Cloud struct { 14 | // 好友列表,key 为用户昵称 v 为 conn 连接句柄 15 | FriendList *gmap.Map 16 | // 链接映射用户 17 | ConnRemapUserNickname *gmap.Map 18 | // 云端监听的本地地址 19 | Local string 20 | // 云端监听地址 21 | listener net.Listener 22 | } 23 | 24 | // NewCloud 创建一个云端地址 25 | func NewCloud(local string) *Cloud { 26 | return &Cloud{ 27 | FriendList: gmap.New(true), 28 | ConnRemapUserNickname: gmap.New(true), 29 | Local: local, 30 | } 31 | } 32 | 33 | // Run 运行云端 34 | func (s *Cloud) Run() { 35 | listener, err := reuse.Listen("tcp", s.Local) 36 | if err != nil { 37 | glog.Fatalf("云端监听地址失败,%s", err) 38 | } 39 | s.listener = listener 40 | go s.accept() 41 | select {} 42 | } 43 | 44 | // accept 接受客户端 45 | func (s *Cloud) accept() { 46 | for { 47 | conn, err := s.listener.Accept() 48 | if err != nil { 49 | glog.Fatalf("云端监听发生错误:%s", err) 50 | } 51 | go OnMsg(conn, s.OnMsg, s.OnMsgError) 52 | } 53 | } 54 | 55 | // OnMsg 读取到消息时 56 | func (s *Cloud) OnMsg(ctx Ctx) { 57 | parseJSON, err := gjson.LoadContent(ctx.BodyString) 58 | if err != nil { 59 | glog.Fatal(err) 60 | } 61 | msgType := parseJSON.GetInt("msg_type") 62 | switch msgType { 63 | case ClientRegister: 64 | s.ClientOnline(ctx.FromConn, parseJSON) 65 | case ClientGetFriendList: // 获取在线的用户列表 66 | s.ClientGetFriendList(ctx.FromConn, parseJSON) 67 | case ClientNotifyCloudHolePunching: // 客户端通知云端向另一端发送打洞消息 68 | s.CloudNotifyHolePunching(ctx.FromConn, parseJSON) 69 | } 70 | } 71 | 72 | // ClientNotifyCloudHolePunching 云端通知对端开始进行打洞 73 | func (s *Cloud) CloudNotifyHolePunching(src net.Conn, parseJSON *gjson.Json) { 74 | srcNickname := parseJSON.GetString("body.my_nickname") 75 | distNickname := parseJSON.GetString("body.dst_nickname") 76 | srcUser := s.FriendList.GetVar(srcNickname) 77 | dstUser := s.FriendList.GetVar(distNickname) 78 | if dstUser.IsEmpty() { 79 | glog.Error("云端通知打洞失败 dst empty") 80 | return 81 | } 82 | if srcUser.IsEmpty() { 83 | glog.Error("云端通知打洞失败 src empty") 84 | return 85 | } 86 | // 通知另一端进行打洞 87 | dstConn := dstUser.Interface().(net.Conn) 88 | srcConn := srcUser.Interface().(net.Conn) 89 | go func() { 90 | _, _ = dstConn.Write(NewMsg(&ClientMsg{ 91 | MsgType: CloudNotifyClientHolePunching, 92 | Body: g.Map{ 93 | "src_nickname": srcNickname, 94 | "src_remote_addr": src.RemoteAddr().String(), 95 | }, 96 | }).Encode()) 97 | }() 98 | go func() { 99 | _, _ = srcConn.Write(NewMsg(&ClientMsg{ 100 | MsgType: CloudNotifyClientHolePunching, 101 | Body: g.Map{ 102 | "src_nickname": distNickname, 103 | "src_remote_addr": dstConn.RemoteAddr().String(), 104 | }, 105 | }).Encode()) 106 | }() 107 | } 108 | 109 | // ClientOffline 客户端下线事件通知 110 | func (s *Cloud) ClientOffline(offlineNickname string) { 111 | conn := s.FriendList.Get(offlineNickname).(net.Conn) 112 | s.FriendList.Remove(offlineNickname) // 删除该用户 113 | s.ConnRemapUserNickname.Remove(conn.RemoteAddr().String()) // 删除该用户 114 | tmp := &ClientMsg{ 115 | MsgType: CloudBroadcastOffline, 116 | Body: g.Map{ 117 | "nickname": offlineNickname, 118 | }, 119 | } 120 | msg := NewMsg(tmp).Encode() 121 | // 全体用户发通知,告知该用户下线了 122 | s.FriendList.Iterator(func(nicknameKey interface{}, connIface interface{}) bool { 123 | conn := connIface.(net.Conn) 124 | _, _ = conn.Write(msg) 125 | return true 126 | }) 127 | glog.Infof("用户:%s已下线", offlineNickname) 128 | } 129 | 130 | // ClientGetFriendList 获取云端的好友列表 131 | func (s *Cloud) ClientGetFriendList(conn net.Conn, _ *gjson.Json) { 132 | friends := make([]*Friend, 0) 133 | s.FriendList.Iterator(func(nickname interface{}, connIface interface{}) bool { 134 | remoteAddr := connIface.(net.Conn).RemoteAddr().String() 135 | friends = append(friends, &Friend{ 136 | NickName: gconv.String(nickname), 137 | RemoteAddr: remoteAddr, 138 | }) 139 | return true 140 | }) 141 | _, _ = conn.Write(NewMsg(&ClientMsgResult{ 142 | MsgType: ClientGetFriendList, 143 | Body: g.Map{ 144 | "list": friends, 145 | }, 146 | }).Encode()) 147 | } 148 | 149 | // ClientOnline 客户端上线通知 150 | func (s *Cloud) ClientOnline(conn net.Conn, parseJSON *gjson.Json) { 151 | nickname := parseJSON.GetString("body.nickname") 152 | gvar := s.FriendList.GetVar(nickname) 153 | if !gvar.IsEmpty() { 154 | clientMsg := &ClientMsgResult{ 155 | MsgType: CloudNotifyClientRegisterRes, 156 | Code: Error, 157 | Msg: "聊天室已有和您相同的昵称,请重新注册一个昵称", 158 | } 159 | if _, err := conn.Write(NewMsg(clientMsg).Encode()); err != nil { 160 | glog.Error(err) 161 | } 162 | _ = conn.Close() 163 | return 164 | } 165 | clientMsg := &ClientMsgResult{ 166 | MsgType: CloudNotifyClientRegisterRes, 167 | Code: Success, 168 | Msg: "success", 169 | } 170 | if _, err := conn.Write(NewMsg(clientMsg).Encode()); err != nil { 171 | glog.Error(err) 172 | } 173 | // 写入用户列表 174 | s.FriendList.Set(nickname, conn) 175 | s.ConnRemapUserNickname.Set(conn.RemoteAddr().String(), nickname) 176 | // 广播事件,有用户上线了 177 | s.CloudBroadcast(&ClientMsgResult{ 178 | MsgType: CloudBroadcastClientOnline, 179 | Body: g.Map{ 180 | "nickname": nickname, 181 | "remote_addr": conn.RemoteAddr().String(), 182 | }, 183 | }) 184 | glog.Infof("用户:%s已上线 边缘节点地址为:%s", nickname, conn.RemoteAddr().String()) 185 | } 186 | 187 | // CloudBroadcast 云端广播消息 188 | func (s *Cloud) CloudBroadcast(msg interface{}) { 189 | waitSendMsg := NewMsg(msg).Encode() 190 | s.FriendList.Iterator(func(nickname interface{}, connIface interface{}) bool { 191 | go func() { 192 | conn := connIface.(net.Conn) 193 | _, _ = conn.Write(waitSendMsg) 194 | }() 195 | return true 196 | }) 197 | } 198 | 199 | // OnMsgError 当发生错误时 200 | func (s *Cloud) OnMsgError(conn net.Conn, _ error) { 201 | var nickname string 202 | s.FriendList.Iterator(func(nicknameKey interface{}, connIface interface{}) bool { 203 | listConn := connIface.(net.Conn) 204 | if listConn.RemoteAddr().String() == conn.RemoteAddr().String() { 205 | nickname = gconv.String(nicknameKey) 206 | return false // 退出循环 207 | } 208 | return true 209 | }) 210 | if nickname != "" { 211 | s.ClientOffline(nickname) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /server/client.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/gogf/gf/container/gmap" 7 | "github.com/gogf/gf/encoding/gjson" 8 | "github.com/gogf/gf/frame/g" 9 | "github.com/gogf/gf/os/glog" 10 | "github.com/gogf/gf/os/gtime" 11 | "github.com/gogf/gf/util/gconv" 12 | "github.com/gookit/color" 13 | reuse "github.com/libp2p/go-reuseport" 14 | "net" 15 | "os" 16 | "strings" 17 | "sync" 18 | "sync/atomic" 19 | "time" 20 | ) 21 | 22 | // TmpPeerConn 临时对等节点 23 | type TmpPeerConn struct { 24 | nickname string 25 | Timestamp int64 26 | } 27 | 28 | // WaitHandShake 29 | type WaitHandShake struct { 30 | PeerConn net.Conn // 对等节点 31 | UUID string 32 | Timestamp int64 33 | } 34 | 35 | type Client struct { 36 | nickname string 37 | // 本地监听地址 38 | Local string 39 | // 云端地址 40 | Cloud string 41 | // 聊天处理函数 42 | HandleMsg func(ctx Ctx) 43 | // 云端连接 44 | CloudConn net.Conn 45 | // 地址转昵称 46 | RemoteAddrToNickname *gmap.Map 47 | // 好友列表 key:nickname value:remoteAddr 48 | FriendList *gmap.Map 49 | // 等待连接对等节点 key 对等节点 nickname 50 | WaitConnectPeer *gmap.Map 51 | // 自己是否注册成功 52 | registerInit *int32 53 | // 对等节点连接集合,当云端通知用户下线时,此处会自动剔除对等连接 54 | // key:nickname,value:net.Conn 55 | PeersConn *gmap.Map 56 | // 监听句柄 57 | listener net.Listener 58 | // key:nickname value:int32 原子性操作 59 | nicknameConnectLock *gmap.Map 60 | // 是否首次刷新用户列表 61 | isFirstRefreshUserList *int32 62 | // 头行管道 63 | headline chan struct{} 64 | } 65 | 66 | // NewClient 实例化客户端 67 | func NewClient(cloud, local, nickname string) *Client { 68 | ins := &Client{ 69 | nickname: nickname, 70 | Local: local, 71 | Cloud: cloud, 72 | FriendList: gmap.New(true), 73 | RemoteAddrToNickname: gmap.New(true), 74 | PeersConn: gmap.New(true), 75 | nicknameConnectLock: gmap.New(true), 76 | WaitConnectPeer: gmap.New(true), 77 | registerInit: new(int32), 78 | isFirstRefreshUserList: new(int32), 79 | headline: make(chan struct{}, 3), 80 | } 81 | return ins 82 | } 83 | 84 | // OnMessage 注册消息发送时触发的事件函数 85 | func (s *Client) On1Message(handel func(ctx Ctx)) { 86 | s.HandleMsg = handel 87 | } 88 | 89 | // cloudDail 云端链接 90 | func (s *Client) cloudDail(nickname string) { 91 | cloudConn, err := reuse.Dial("tcp", s.Local, s.Cloud) 92 | if err != nil { 93 | glog.Fatal(err) 94 | } 95 | s.CloudConn = cloudConn 96 | s.RegisterUser(nickname) 97 | // 持续监听云端事件,包括客户端注册事件 98 | go OnMsg(cloudConn, s.onCloudMsg, s.onCloudMsgError) 99 | } 100 | 101 | // onCloudMsgError 当读取消息失败时 102 | func (s *Client) onCloudMsgError(conn net.Conn, err error) { 103 | color.LightBlue.Printf("\n云端链接断开\n") 104 | _ = conn.Close() 105 | } 106 | 107 | // RegisterUser 发送注册消息,告诉云端我上线了 108 | func (s *Client) RegisterUser(nickname string) { 109 | registerMsg := &ClientMsg{ 110 | MsgType: ClientRegister, 111 | Body: g.Map{ 112 | "nickname": nickname, 113 | }, 114 | } 115 | msg := NewMsg(registerMsg).Encode() 116 | if _, err := s.CloudConn.Write(msg); err != nil { 117 | glog.Fatalf("向云端发送注册消息失败,错误:%s", err) 118 | } 119 | } 120 | 121 | // onCloudMsg 获取到云端的消息时需要执行的方法 122 | func (s *Client) onCloudMsg(ctx Ctx) { 123 | parseJSON, err := gjson.LoadContent(ctx.BodyString) 124 | if err != nil { 125 | glog.Fatalf("无法解析云端数据:%s", ctx.BodyString) 126 | } 127 | msgType := parseJSON.GetInt("msg_type") 128 | if atomic.LoadInt32(s.registerInit) == No && msgType != CloudNotifyClientRegisterRes { 129 | return 130 | } 131 | // 获取消息类型 132 | switch msgType { 133 | case CloudNotifyClientRegisterRes: // 获取注册结果 134 | s.HandleRegisterRes(parseJSON) 135 | case CloudBroadcastClientOnline: // 客户端上线事件 136 | s.HandleClientOnline(parseJSON) 137 | case CloudBroadcastOffline: // 客户端下线事件 138 | s.HandleClientOffline(parseJSON) 139 | case ClientGetFriendList: // 云端提供在线好友列表事件 140 | s.HandleFriendList(parseJSON) 141 | case CloudNotifyClientHolePunching: // 云端通知客户端向对等节点进行打洞 142 | s.ClientHolePunching(parseJSON) 143 | } 144 | } 145 | 146 | // CloudBroadcastClientOnline 客户端注册事件 147 | func (s *Client) HandleClientOnline(msgJSON *gjson.Json) { 148 | nickname := msgJSON.GetString("body.nickname") 149 | remoteAddr := msgJSON.GetString("body.remote_addr") 150 | s.nicknameConnectLock.Set(nickname, new(int32)) 151 | s.FriendList.Set(nickname, remoteAddr) 152 | s.RemoteAddrToNickname.Set(remoteAddr, nickname) 153 | color.Cyan.Printf("\n云端全体广播事件,用户:%s 地址:%s 在聊天室中已上线\n", nickname, remoteAddr) 154 | } 155 | 156 | // CloudBroadcastOffline 客户端离线事件 157 | func (s *Client) HandleClientOffline(msgJSON *gjson.Json) { 158 | nickname := msgJSON.GetString("body.nickname") 159 | color.Cyan.Printf("\n云端全体广播事件,用户:%s 在聊天室中下线\n", nickname) 160 | s.removeUser(nickname) 161 | } 162 | 163 | // HandleRegisterRes 处理注册结果 164 | func (s *Client) HandleRegisterRes(msgJSON *gjson.Json) { 165 | if msgJSON.GetInt("code") != Success { 166 | glog.Fatalf("加入聊天室注册失败,原因:%s", msgJSON.GetString("msg")) 167 | } 168 | atomic.AddInt32(s.registerInit, 1) // 从此刻起,用户可以接收所有数据 169 | // 向云端获取全部在线的好友列表 170 | s.RefreshOnlineFriendList() 171 | } 172 | 173 | // RefreshOnlineFriendList 向云端获取好友列表 174 | func (s *Client) RefreshOnlineFriendList() { 175 | clientMsg := &ClientMsg{ 176 | MsgType: ClientGetFriendList, 177 | } 178 | msg := NewMsg(clientMsg).Encode() 179 | if _, err := s.CloudConn.Write(msg); err != nil { 180 | glog.Fatalf("向云端发送注册消息失败,错误:%s", err) 181 | } 182 | } 183 | 184 | // HandleFriendList 获取云端提供的好友列表 185 | func (s *Client) HandleFriendList(msgJSON *gjson.Json) { 186 | if msgJSON.GetInt("code") != Success { 187 | glog.Error("获取云端好友列表失败,错误:%s", msgJSON.GetString("msg")) 188 | return 189 | } 190 | var printResult = new(strings.Builder) 191 | friends := msgJSON.GetJsons("body.list") 192 | printResult.WriteString("\n当前在线的好友列表如下\n") 193 | var initNicknameConnLock bool 194 | if atomic.AddInt32(s.isFirstRefreshUserList, 1) == 1 { 195 | initNicknameConnLock = true 196 | } 197 | for _, v := range friends { 198 | if initNicknameConnLock { 199 | s.nicknameConnectLock.Set(v.GetString("nickname"), new(int32)) 200 | } 201 | res := fmt.Sprintf("昵称:%s 通讯地址:%s\n", v.GetString("nickname"), 202 | v.GetString("remote_addr")) 203 | printResult.WriteString(res) 204 | s.RemoteAddrToNickname.Set(v.GetString("remote_addr"), v.GetString("nickname")) 205 | s.FriendList.Set(v.GetString("nickname"), v.GetString("remote_addr")) 206 | } 207 | fmt.Println(printResult.String()) 208 | s.headline <- struct{}{} 209 | } 210 | 211 | // tryConnPeer 本地客户端不停尝试连接对等节点 212 | func (s *Client) tryConnPeer() { 213 | for { 214 | wg := new(sync.WaitGroup) 215 | s.FriendList.Iterator(func(nickname interface{}, remoteAddrString interface{}) bool { 216 | // 自己不能向自己链接 217 | if gconv.String(nickname) == s.nickname { 218 | return true 219 | } 220 | peerConnAddr := s.PeersConn.GetVar(nickname) 221 | // 已经连接上的用户跳过 222 | if !peerConnAddr.IsEmpty() { 223 | return true 224 | } 225 | // 等待连接的跳过 226 | if !s.WaitConnectPeer.GetVar(nickname).IsEmpty() { 227 | return true 228 | } 229 | wg.Add(1) 230 | go s.ConnPeerAndNotifyCloud(wg, gconv.String(nickname)) 231 | return true 232 | }) 233 | wg.Wait() 234 | time.Sleep(time.Millisecond * 200) 235 | } 236 | } 237 | 238 | // removeUser 移除用户 239 | func (s *Client) removeUser(nickname string) { 240 | friendAddr := s.FriendList.GetVar(nickname) 241 | if !friendAddr.IsEmpty() { 242 | s.RemoteAddrToNickname.Remove(friendAddr.String()) 243 | } 244 | s.FriendList.Remove(nickname) 245 | s.PeersConn.Remove(nickname) 246 | s.nicknameConnectLock.Remove(nickname) 247 | s.WaitConnectPeer.Remove(nickname) 248 | } 249 | 250 | // ConnPeerAndNotifyCloud 通知云端 251 | func (s *Client) ConnPeerAndNotifyCloud(wg *sync.WaitGroup, nickname string) { 252 | defer wg.Done() 253 | s.WaitConnectPeer.Set(nickname, "") 254 | _, _ = s.CloudConn.Write(NewMsg(&ClientMsg{ 255 | MsgType: ClientNotifyCloudHolePunching, 256 | Body: g.Map{ 257 | "my_nickname": s.nickname, 258 | "dst_nickname": nickname, 259 | }, 260 | }).Encode()) 261 | } 262 | 263 | // peerJoin 加入对等节点 264 | func (s *Client) peerJoin(nickname string, conn net.Conn) { 265 | s.PeersConn.Set(nickname, conn) 266 | color.Green.Printf("\n昵称:%s,加入聊天,对端远程地址:%s\n", nickname, conn.RemoteAddr().String()) 267 | } 268 | 269 | // accept 监听对等节点连接事件 270 | func (s *Client) accept() { 271 | for { 272 | conn, err := s.listener.Accept() 273 | if err != nil { 274 | glog.Fatal(err) 275 | } 276 | nickname := s.RemoteAddrToNickname.GetVar(conn.RemoteAddr().String()).String() 277 | color.LightBlue.Printf("\n昵称:%s,加入聊天,对端远程地址:%s\n", nickname, conn.RemoteAddr().String()) 278 | go OnMsg(conn, s.OnPeerMsg, s.OnPeerMsgError) 279 | } 280 | } 281 | 282 | // OnPeerMsg 当对等节点发来连接消息的请求 283 | func (s *Client) OnPeerMsg(ctx Ctx) { 284 | parseJSON, err := gjson.LoadContent(ctx.BodyString) 285 | if err != nil { 286 | glog.Fatal(err) 287 | } 288 | msgType := parseJSON.GetInt("msg_type") 289 | switch msgType { 290 | case PeerMsgSay: 291 | s.OnPeerMsgSay(ctx, parseJSON) 292 | default: 293 | glog.Infof("对等节点收到未知消息:%s", ctx.BodyString) 294 | } 295 | } 296 | 297 | // OnPeerMsgSay 298 | func (s *Client) OnPeerMsgSay(ctx Ctx, parseJSON *gjson.Json) { 299 | nickname := parseJSON.GetString("body.nickname") 300 | say := parseJSON.GetString("body.say") 301 | date := gtime.Now().Format("Y-m-d H:i:s") 302 | color.LightBlue.Printf("\n%s 来自%s:%s的消息:%s", date, ctx.FromConn.RemoteAddr().String(), nickname, say) 303 | s.headline <- struct{}{} 304 | } 305 | 306 | // ClientHolePunching 客户端 307 | func (s *Client) ClientHolePunching(parseJSON *gjson.Json) { 308 | nickname := parseJSON.GetString("body.src_nickname") 309 | remoteAddr := parseJSON.GetString("body.src_remote_addr") 310 | lock := s.nicknameConnectLock.GetVar(nickname) 311 | // 避免同时打洞,此处原子性保证 312 | if lock.IsEmpty() { 313 | var lockInt int32 = 1 314 | s.nicknameConnectLock.Set(nickname, &lockInt) 315 | } else if atomic.AddInt32(lock.Interface().(*int32), 1) != 1 { 316 | return 317 | } 318 | go func() { 319 | FOR: 320 | for { 321 | select { 322 | case <-time.After(time.Minute * 2): 323 | break FOR 324 | default: 325 | } 326 | // 打洞也需要检查该好友是否在线 327 | if s.FriendList.GetVar(nickname).IsEmpty() { 328 | break 329 | } 330 | conn, err := reuse.Dial("tcp", s.Local, remoteAddr) 331 | if err == nil { 332 | s.peerJoin(nickname, conn) 333 | go OnMsg(conn, s.OnPeerMsg, s.OnPeerMsgError) 334 | break 335 | } 336 | time.Sleep(time.Millisecond * 100) 337 | } 338 | s.WaitConnectPeer.Remove(nickname) 339 | }() 340 | } 341 | 342 | // OnPeerMsgError 当对等节点失败 343 | func (s *Client) OnPeerMsgError(conn net.Conn, err error) { 344 | var nickname string 345 | s.PeersConn.Iterator(func(nicknameIface interface{}, connIface interface{}) bool { 346 | remoteAddr := connIface.(net.Conn).RemoteAddr().String() 347 | if remoteAddr == conn.RemoteAddr().String() { 348 | nickname = gconv.String(nicknameIface) 349 | return false 350 | } 351 | return true 352 | }) 353 | _ = conn.Close() 354 | if nickname != "" { 355 | color.LightBlue.Printf("\n用户:%s 断开连接 离线\n", nickname) 356 | s.removeUser(nickname) 357 | } 358 | } 359 | 360 | // ReadStdinSend 读取标准输入并发送 361 | func (s *Client) ReadStdinSend() { 362 | for { 363 | inputReader := bufio.NewReader(os.Stdin) 364 | input, err := inputReader.ReadString('\n') 365 | if err != nil { 366 | fmt.Println("您发送的有误:", err.Error()) 367 | continue 368 | } 369 | if strings.TrimSpace(input) == "" { 370 | s.headline <- struct{}{} 371 | continue 372 | } 373 | s.headline <- struct{}{} 374 | s.PeersConn.Iterator(func(k interface{}, v interface{}) bool { 375 | peerConn := v.(net.Conn) 376 | _, _ = peerConn.Write(NewMsg(&ClientMsg{ 377 | MsgType: PeerMsgSay, 378 | Body: g.Map{ 379 | "nickname": s.nickname, 380 | "say": input, 381 | }, 382 | }).Encode()) 383 | return true 384 | }) 385 | } 386 | } 387 | 388 | // inputHeadLine 输出头行 389 | func (s *Client) inputHeadLine() { 390 | for { 391 | select { 392 | case <-s.headline: 393 | color.Red.Printf("请说点什么:") 394 | } 395 | } 396 | } 397 | 398 | // Run 执行启动函数 399 | func (s *Client) Run() { 400 | s.cloudDail(s.nickname) 401 | listener, err := reuse.Listen("tcp", s.Local) 402 | if err != nil { 403 | glog.Fatal(err) 404 | } 405 | s.listener = listener 406 | go s.accept() 407 | go s.tryConnPeer() 408 | go s.ReadStdinSend() 409 | go s.inputHeadLine() 410 | select {} 411 | } 412 | --------------------------------------------------------------------------------