GO_网络编程---并发聊天服务器

发布时间:2026/7/24 18:34:49
GO_网络编程---并发聊天服务器 整合功能map存储在线客户端key 为客户端ip:port客户端结构体Client内置发送管道、用户名、地址全局消息管道统一广播独立协程分发消息客户端上线广播、下线 / 超时自动清在线用户指令who查询在线列表、rename|新名字修改昵称全并发读写每个连接独立收发协程60 秒无数据自动超时踢下线gopackage 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]) // 指令1who 查询在线用户列表 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 } // 指令2rename|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. 客户端结构体 Clientgotype Client struct { C chan string // 管道接收全局广播消息每个客户端独立管道 Name string // 用户昵称 Addr string // ip:port作为onlineMap的唯一key }onlineMap map[string]Clientkey 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 秒会自动被服务器踢下线