整合功能:
map存储在线客户端,key 为客户端ip:port- 客户端结构体
Client内置发送管道、用户名、地址 - 全局消息管道统一广播,独立协程分发消息
- 客户端上线广播、下线 / 超时自动清在线用户
- 指令:
who查询在线列表、rename|新名字修改昵称 - 全并发读写,每个连接独立收发协程
- 60 秒无数据自动超时踢下线
go
package main import ( "fmt" "net" "strings" "time" ) // Client 在线客户端结构体 type Client struct { C chan string // 接收广播消息的管道 Name string // 用户昵称 Addr string // 客户端地址 ip:port,和map的key一致 } var ( onlineMap map[string]Client // 在线用户集合 key:客户端地址 message chan string // 全局消息管道,所有待广播消息丢这里 ) // Manager 全局消息管理协程:收到消息后广播给所有在线用户 func Manager() { onlineMap = make(map[string]Client) // 初始化在线map for { // 阻塞等待消息 msg := <-message // 遍历所有在线用户,推送消息 for _, cli := range onlineMap { cli.C <- msg } } } // MakeMsg 格式化广播消息 [地址]昵称: 内容 func MakeMsg(cli Client, content string) string { return fmt.Sprintf("[%s]%s: %s", cli.Addr, cli.Name, content) } // WriteMsgToClient 单独协程:持续从cli.C读消息,写给客户端tcp连接 func WriteMsgToClient(cli Client, conn net.Conn, quitChan chan bool) { for { select { case <-quitChan: // 收到退出信号,关闭写协程 return case msg := <-cli.C: _, err := conn.Write([]byte(msg + "\n")) if err != nil { return } } } } // HandleConn 处理单个客户端连接 func HandleConn(conn net.Conn) { cliAddr := conn.RemoteAddr().String() fmt.Printf("新客户端连接:%s\n", cliAddr) // 初始化客户端,默认昵称=地址 cli := Client{ C: make(chan string), Name: cliAddr, Addr: cliAddr, } onlineMap[cliAddr] = cli quitChan := make(chan bool) // 标记客户端下线,通知写协程退出 // 开启写协程,专门给当前客户端推送广播消息 go WriteMsgToClient(cli, conn, quitChan) // 广播:某某用户上线 message <- MakeMsg(cli, "上线了") // 开启读数据协程:持续读取客户端发送的内容 go func() { buf := make([]byte, 2048) for { // 60秒超时,无数据自动踢下线 readChan := make(chan bool) go func() { n, err := conn.Read(buf) if n == 0 || err != nil { readChan <- false return } readChan <- true }() select { case <-time.After(60 * time.Second): // 超时下线 delete(onlineMap, cliAddr) message <- MakeMsg(cli, "60秒无消息,超时离线") quitChan <- true conn.Close() return case hasData := <-readChan: if !hasData { // 客户端主动断开连接 delete(onlineMap, cliAddr) message <- MakeMsg(cli, "主动下线") quitChan <- true conn.Close() return } // 去除换行符(windows nc会多一个\n) msg := string(buf[:len(buf)-1]) // 指令1:who 查询在线用户列表 if len(msg) == 3 && msg == "who" { _, _ = conn.Write([]byte("=====在线用户列表=====\n")) for _, tmp := range onlineMap { info := fmt.Sprintf("%s : %s\n", tmp.Addr, tmp.Name) _, _ = conn.Write([]byte(info)) } _, _ = conn.Write([]byte("=====================\n")) continue } // 指令2:rename|xxx 修改昵称 if len(msg) >= 6 && msg[:6] == "rename" { parts := strings.Split(msg, "|") if len(parts) >= 2 { newName := parts[1] cli.Name = newName onlineMap[cliAddr] = cli // 更新map内的客户端 _, _ = conn.Write([]byte("rename ok,新昵称:" + newName + "\n")) message <- MakeMsg(cli, "修改昵称为 "+newName) } continue } // 普通聊天消息,丢入全局管道广播所有人 message <- MakeMsg(cli, msg) } } }() } func main() { // 初始化全局消息管道 message = make(chan string) // 启动全局消息广播协程 go Manager() // 监听8000端口tcp listener, err := net.Listen("tcp", ":8000") if err != nil { fmt.Println("监听失败 err = ", err) return } defer listener.Close() fmt.Println("聊天服务器启动成功,监听 :8000") // 循环等待客户端接入 for { conn, err := listener.Accept() if err != nil { fmt.Println("接收连接失败 err = ", err) continue } // 每一个客户端开一个协程处理 go HandleConn(conn) } }一、核心知识点整理(对应你笔记)
1. 客户端结构体 Client
go
type Client struct { C chan string // 管道:接收全局广播消息,每个客户端独立管道 Name string // 用户昵称 Addr string // ip:port,作为onlineMap的唯一key }onlineMap map[string]Client:key =conn.RemoteAddr().String(),天然唯一,区分不同客户端
2. 两大核心协程
① Manager 全局广播协程(单例,main 启动)
- 初始化
onlineMap - 死循环阻塞读取全局
message管道 - 遍历所有在线客户端,把消息推入每个客户端的
cli.C管道,实现全员广播
② 每个客户端 2 个独立协程(新连接就创建)
- WriteMsgToClient 写协程
- 单独负责把广播消息发送给当前 TCP 客户端
- 监听
quitChan,下线时安全退出
- 匿名读协程
- 循环读取客户端发来的数据
- 内置 60 秒超时检测
- 解析指令、普通消息,分发逻辑
3. 支持指令功能
who:查询全部在线用户,仅自己可见列表rename|Mike:修改自己昵称,广播给所有人- 普通文字:全员广播聊天消息
4. 上下线 & 超时处理
- 上线:客户端连接成功,存入
onlineMap,广播「xxx 上线」 - 主动下线:客户端断开 TCP 连接,删除 map 数据,广播「xxx 离线」
- 超时下线:60 秒无任何消息,自动踢出,广播超时提示
5. 并发设计优势
- 读写分离协程:读、写互不阻塞
- 消息统一中转:全部消息只丢给全局
message管道,由 Manager 统一分发 - 管道通信代替共享锁:利用 channel 实现协程安全,不用 mutex 操作 map(仅增删时串行安全)
二、测试方法
- 运行服务:
go run main.go - 多开终端使用 nc 连接:
nc 127.0.0.1 8000 - 输入指令测试:
who查看在线列表rename|小明修改昵称- 随便打字,所有客户端都会收到广播
- 闲置 60 秒会自动被服务器踢下线