Golang 即时通信系统 9 个版本迭代 本文按照项目真实迭代顺序讲解:
版本一:基础 TCP Server 版本二:用户上线 版本三:消息广播 版本四:用户业务封装 版本五:在线用户查询 版本六:修改用户名 版本七:超时强踢 版本八:私聊 版本九:客户端实现 每个版本都包含两部分:
改动目标(为什么改) 关键代码(怎么改) 为了让你能边看边验证,全文额外补充了三类信息:
设计动机:为什么这一版必须做,不做会出现什么问题。 关键细节:并发、锁、通道、协议解析这些最容易踩坑的点。 可验证步骤:你可以直接在终端输入什么命令、预期看到什么结果。 先看懂架构图:系统分层与数据流 这个项目的架构图可以简化成 4 层:
客户端层:client.go 负责菜单、命令输入、消息展示。 连接层:基于 TCP 长连接,每个客户端和服务端之间都有一个 conn。 服务调度层:server.go 负责监听端口、接入连接、广播分发、超时控制。 用户业务层:user.go 负责解析命令并执行业务(who、rename|、to|)。 可以把整条消息链路理解为:
Client 输入 -> conn.Write -> Server.Handler 读取 -> User.DoMessage 解析 -> 广播/私聊 -> 目标用户 conn.Write 输出
为什么要这样分层:
server.go 专注“调度连接”,避免和具体业务耦合。user.go 专注“处理命令”,后续扩展指令更容易。client.go 专注“交互体验”,和服务端实现解耦。网络基础扫盲:net 和 conn 是什么 1) net 是什么 net 是 Go 标准库的网络包,TCP 聊天室最常见用法是:
net.Listen("tcp", "127.0.0.1:8888"):服务端开启监听。listener.Accept():阻塞等待一个新客户端连接。net.Dial("tcp", "127.0.0.1:8888"):客户端主动连接服务端。2) conn 是什么 conn 是 net.Conn 接口,代表“一条双向连接”。你可以把它当成电话线:
conn.Read([]byte):从连接里读对方发来的字节。conn.Write([]byte):往连接里写字节给对方。conn.Close():关闭这条连接。3) 最小通信示例(帮助你建立直觉) 服务端:
1 2 3 4 5 6 listener, _ := net.Listen("tcp" , "127.0.0.1:8888" ) conn, _ := listener.Accept() buf := make ([]byte , 1024 ) n, _ := conn.Read(buf) fmt.Println("收到:" , string (buf[:n])) conn.Write([]byte ("收到你的消息了\n" ))
客户端:
1 2 3 4 5 conn, _ := net.Dial("tcp" , "127.0.0.1:8888" ) conn.Write([]byte ("你好,服务端\n" )) buf := make ([]byte , 1024 ) n, _ := conn.Read(buf) fmt.Println("服务端回复:" , string (buf[:n]))
在 IM 项目里,本质上就是把“1 个 conn 的收发”扩展为“多个 conn 并发收发 + 命令分发”。
代码前必须知道的 5 个概念 goroutine:每个连接通常单独开协程处理,不会互相阻塞。channel:协程之间传消息,广播时通过全局消息通道分发。OnlineMap:在线用户表,key 一般是用户名,value 是 *User。命令协议:约定文本格式,例如 who、rename|张三、to|李四|你好。 连接生命周期:连接建立 -> 上线 -> 收发消息 -> 下线或超时关闭。 带着这 5 个概念再看版本一到版本九,代码会顺很多。
版本一:构建基础 Server 改动目标 先把最小可运行版本搭起来:
定义 Server 结构体 启动 TCP 监听 接受连接并交给 Handler 为什么版本一非常重要:
它是后续所有功能的地基,如果监听和连接生命周期不稳定,后续广播、私聊都无法成立。 先做“空 Handler”是典型的增量开发思路:先打通主链路,再逐层加能力。 这一版的验收标准只有一个:客户端能连上,服务端能稳定接收连接并创建 goroutine。 版本一结构图
图中重点可以先抓 3 条线:
main.go 只做入口,创建并启动 Server。server.go 负责监听端口、接收连接、把连接交给 Handler。本版本 Handler 先做占位,目标是先打通“能接入连接”的最小闭环。 关键代码 1 2 3 4 5 6 7 package mainfunc main () { server := NewServer("127.0.0.1" , 8888 ) server.Start() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 type Server struct { Ip string Port int } func NewServer (ip string , port int ) *Server { return &Server{Ip: ip, Port: port} } func (s *Server) Handler(conn net.Conn) { conn.Close() } func (s *Server) Start() { address := fmt.Sprintf("%s:%d" , s.Ip, s.Port) listener, err := net.Listen("tcp" , address) if err != nil { fmt.Println("net.Listen err:" , err) return } defer listener.Close() for { conn, err := listener.Accept() if err != nil { fmt.Println("listener.Accept err:" , err) continue } go s.Handler(conn) } }
快速验证(版本一):
1 2 3 4 5 go run ./server telnet 127.0.0.1 8888
预期:服务端无 panic,连接建立后会被立即关闭(因为 Handler 目前是占位实现)。
版本二:用户上线功能 改动目标 引入用户概念,维护在线用户集合,并能广播“某用户上线”。
设计动机:
版本一只有连接,没有“用户身份”,所以无法做在线状态和消息路由。 把 conn 包装成 User 后,后续命令(who/rename|/to|)都能围绕用户对象扩展。 OnlineMap + RWMutex 是这个项目并发安全的核心,必须在这个版本定好。版本二结构图
版本二比版本一多了“用户状态管理”主线:
User 被正式引入,连接不再只是裸 conn。OnlineMap 记录在线用户,mapLock 保证并发安全。Message 作为广播通道,后续版本会扩展成完整广播机制。关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type Server struct { Ip string Port int OnlineMap map [string ]*User mapLock sync.RWMutex Message chan string } func NewServer (ip string , port int ) *Server { return &Server{ Ip: ip, Port: port, OnlineMap: make (map [string ]*User), Message: make (chan string ), } } func (s *Server) BroadCast(user *User, msg string ) { sendMsg := fmt.Sprintf("[%s]%s: %s" , user.Addr, user.Name, msg) s.Message <- sendMsg }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 type User struct { Name string Addr string C chan string conn net.Conn server *Server } func NewUser (conn net.Conn, server *Server) *User { userAddr := conn.RemoteAddr().String() user := &User{ Name: userAddr, Addr: userAddr, C: make (chan string ), conn: conn, server: server, } go user.ListenMessage() return user } func (u *User) Online() { u.server.mapLock.Lock() u.server.OnlineMap[u.Name] = u u.server.mapLock.Unlock() u.server.BroadCast(u, "已上线" ) }
版本三:用户消息广播机制 改动目标 让每个用户发言都能被全体在线用户收到。
实现重点:
广播采用“生产者-消费者”模型:BroadCast 只负责写通道,ListenMessage 专门做分发。 这样的解耦能减少锁持有时间,避免在业务协程里直接遍历全体用户。 后续私聊不会走这条链路,广播和私聊职责会自然分开。 关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (s *Server) ListenMessage() { for { msg := <-s.Message s.mapLock.RLock() for _, user := range s.OnlineMap { user.C <- msg } s.mapLock.RUnlock() } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func (s *Server) Handler(conn net.Conn) { user := NewUser(conn, s) user.Online() buf := make ([]byte , 4096 ) for { n, err := conn.Read(buf) if n == 0 { user.Offline() return } if err != nil { fmt.Println("conn.Read err:" , err) return } msg := strings.TrimSpace(string (buf[:n])) s.BroadCast(user, msg) } }
版本四:用户业务层封装 改动目标 把“解析消息、执行业务”收敛到 User,Server.Handler 只负责生命周期调度。
这样拆分后的好处:
server.go 关注连接管理(接入、读取、超时、断开)。user.go 关注业务命令(who / rename / to / 默认广播)。后续加命令时不用改连接层,降低回归风险。 关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (u *User) Offline() { u.server.mapLock.Lock() delete (u.server.OnlineMap, u.Name) u.server.mapLock.Unlock() u.server.BroadCast(u, "下线" ) } func (u *User) SendMsg(msg string ) { _, _ = u.conn.Write([]byte (msg)) } func (u *User) DoMessage(msg string ) { u.server.BroadCast(u, msg) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func (s *Server) Handler(conn net.Conn) { user := NewUser(conn, s) user.Online() buf := make ([]byte , 4096 ) for { n, err := conn.Read(buf) if n == 0 { user.Offline() return } if err != nil { fmt.Println("conn.Read err:" , err) return } msg := strings.TrimSpace(string (buf[:n])) user.DoMessage(msg) } }
版本五:在线用户查询(who) 改动目标 支持客户端发送 who,查看当前在线用户列表。
实现要点:
who 是只读命令,读 OnlineMap 用读锁,不阻塞其他读请求。返回给发起者本人,不走全局广播,避免污染聊天频道。 输出格式保持稳定,客户端可以据此做自动解析(如果后续要升级 UI)。 关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func (u *User) DoMessage(msg string ) { if msg == "who" { u.server.mapLock.RLock() for _, user := range u.server.OnlineMap { onlineMsg := "[" + user.Addr + "]" + user.Name + ": 在线\n" u.SendMsg(onlineMsg) } u.server.mapLock.RUnlock() return } u.server.BroadCast(u, msg) }
版本六:修改用户名(rename|新名字) 改动目标 支持动态改名,并处理用户名冲突。
实现要点:
改名是“读 + 写”组合操作,必须放在同一段写锁内完成,避免竞态条件。 名称冲突必须先判断再修改 map,否则会覆盖已有在线用户。 采用 rename|新名字 文本协议,简单可读,便于后续升级成结构化协议。 关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 func (u *User) DoMessage(msg string ) { if msg == "who" { u.server.mapLock.RLock() for _, user := range u.server.OnlineMap { onlineMsg := "[" + user.Addr + "]" + user.Name + ": 在线\n" u.SendMsg(onlineMsg) } u.server.mapLock.RUnlock() return } if strings.HasPrefix(msg, "rename|" ) { parts := strings.SplitN(msg, "|" , 2 ) if len (parts) != 2 || parts[1 ] == "" { u.SendMsg("格式错误,请使用 rename|新用户名\n" ) return } newName := parts[1 ] u.server.mapLock.Lock() if _, ok := u.server.OnlineMap[newName]; ok { u.server.mapLock.Unlock() u.SendMsg("当前用户名已被使用\n" ) return } delete (u.server.OnlineMap, u.Name) u.server.OnlineMap[newName] = u u.Name = newName u.server.mapLock.Unlock() u.SendMsg("用户名修改成功: " + u.Name + "\n" ) return } u.server.BroadCast(u, msg) }
版本七:超时强踢 改动目标 用户长时间无输入视为不活跃,服务端主动断开连接。
实现要点:
连接读取协程负责“上报活跃信号”到 isLive。 主协程用 select + timer 统一处理“活跃重置”与“超时踢出”。 timer.Stop 返回值必须处理,否则可能出现定时器通道残留导致误踢。关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 func (s *Server) Handler(conn net.Conn) { user := NewUser(conn, s) user.Online() isLive := make (chan struct {}) go func () { buf := make ([]byte , 4096 ) for { n, err := conn.Read(buf) if n == 0 { user.Offline() return } if err != nil { fmt.Println("conn.Read err:" , err) return } msg := strings.TrimSpace(string (buf[:n])) user.DoMessage(msg) isLive <- struct {}{} } }() timer := time.NewTimer(300 * time.Second) defer timer.Stop() for { select { case <-isLive: if !timer.Stop() { <-timer.C } timer.Reset(300 * time.Second) case <-timer.C: user.SendMsg("你被踢了\n" ) close (user.C) _ = conn.Close() return } } }
版本八:私聊(to|用户名|内容) 改动目标 支持点对点消息,不走全局广播。
实现要点:
协议格式:to|用户名|内容,必须严格校验三段。 找目标用户只需要读锁;找到后直接 remoteUser.SendMsg,不进入广播通道。 私聊失败(用户不存在/离线)要明确回执,避免客户端误判为发送成功。 关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 func (u *User) DoMessage(msg string ) { if msg == "who" { u.server.mapLock.RLock() for _, user := range u.server.OnlineMap { onlineMsg := "[" + user.Addr + "]" + user.Name + ": 在线\n" u.SendMsg(onlineMsg) } u.server.mapLock.RUnlock() return } if strings.HasPrefix(msg, "rename|" ) { parts := strings.SplitN(msg, "|" , 2 ) if len (parts) != 2 || parts[1 ] == "" { u.SendMsg("格式错误,请使用 rename|新用户名\n" ) return } newName := parts[1 ] u.server.mapLock.Lock() if _, ok := u.server.OnlineMap[newName]; ok { u.server.mapLock.Unlock() u.SendMsg("当前用户名已被使用\n" ) return } delete (u.server.OnlineMap, u.Name) u.server.OnlineMap[newName] = u u.Name = newName u.server.mapLock.Unlock() u.SendMsg("用户名修改成功: " + u.Name + "\n" ) return } if strings.HasPrefix(msg, "to|" ) { parts := strings.SplitN(msg, "|" , 3 ) if len (parts) != 3 || parts[1 ] == "" || parts[2 ] == "" { u.SendMsg("格式错误,请使用 to|用户名|消息内容\n" ) return } remoteName := parts[1 ] content := parts[2 ] u.server.mapLock.RLock() remoteUser, ok := u.server.OnlineMap[remoteName] u.server.mapLock.RUnlock() if !ok { u.SendMsg("用户不存在或不在线\n" ) return } remoteUser.SendMsg("[私聊]" + u.Name + ": " + content + "\n" ) return } u.server.BroadCast(u, msg) }
版本九:客户端实现 改动目标 实现命令行客户端,支持:
实现要点:
客户端和服务端通过纯文本协议交互,所有命令最终都是 conn.Write。 DealResponse 持续把服务端消息打印到终端,保证“收消息”和“发命令”并行。菜单模式本质上是一个状态机:选择模式 -> 执行业务 -> 返回菜单。 关键代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 type Client struct { ServerIp string ServerPort int Name string conn net.Conn flag int } func (c *Client) SelectUsers() { _, _ = c.conn.Write([]byte ("who\n" )) } func (c *Client) UpdateName() { fmt.Println(">> 请输入新用户名:" ) fmt.Scanln(&c.Name) _, _ = c.conn.Write([]byte ("rename|" + c.Name + "\n" )) } func (c *Client) PrivateChat() { var remoteName string var chatMsg string c.SelectUsers() fmt.Println(">> 请输入聊天对象(exit 退出):" ) fmt.Scanln(&remoteName) for remoteName != "exit" { fmt.Println(">> 请输入消息(exit 结束当前会话):" ) fmt.Scanln(&chatMsg) for chatMsg != "exit" { if chatMsg != "" { sendMsg := "to|" + remoteName + "|" + chatMsg + "\n" _, _ = c.conn.Write([]byte (sendMsg)) } fmt.Scanln(&chatMsg) } c.SelectUsers() fmt.Println(">> 请输入聊天对象(exit 退出):" ) fmt.Scanln(&remoteName) } }
项目目录结构 1 2 3 4 5 6 7 8 golang-im-system/ ├── go.mod ├── server/ │ ├── main.go │ ├── server.go │ └── user.go └── client/ └── client.go
完整代码 下面是可直接运行的完整版本。我把注释强化为三层:
做什么:当前代码块的职责。 为什么:为什么选这种写法。 风险点:如果忽略这个细节会出什么 bug。 go.mod1 2 3 module golang-im-system go 1.20
server/main.go1 2 3 4 5 6 package mainfunc main () { server := NewServer("127.0.0.1" , 8888 ) server.Start() }
server/server.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 package mainimport ( "fmt" "net" "strings" "sync" "time" ) type Server struct { Ip string Port int OnlineMap map [string ]*User mapLock sync.RWMutex Message chan string } func NewServer (ip string , port int ) *Server { return &Server{ Ip: ip, Port: port, OnlineMap: make (map [string ]*User), Message: make (chan string ), } } func (s *Server) ListenMessage() { for { msg := <-s.Message s.mapLock.RLock() for _, user := range s.OnlineMap { user.C <- msg } s.mapLock.RUnlock() } } func (s *Server) BroadCast(user *User, msg string ) { sendMsg := fmt.Sprintf("[%s]%s: %s" , user.Addr, user.Name, msg) s.Message <- sendMsg } func (s *Server) Handler(conn net.Conn) { user := NewUser(conn, s) user.Online() isLive := make (chan struct {}) go func () { buf := make ([]byte , 4096 ) for { n, err := conn.Read(buf) if n == 0 { user.Offline() return } if err != nil { fmt.Println("conn.Read err:" , err) return } msg := strings.TrimSpace(string (buf[:n])) if msg != "" { user.DoMessage(msg) } isLive <- struct {}{} } }() timer := time.NewTimer(300 * time.Second) defer timer.Stop() for { select { case <-isLive: if !timer.Stop() { <-timer.C } timer.Reset(300 * time.Second) case <-timer.C: user.SendMsg("你被踢了\n" ) close (user.C) _ = conn.Close() return } } } func (s *Server) Start() { address := fmt.Sprintf("%s:%d" , s.Ip, s.Port) listener, err := net.Listen("tcp" , address) if err != nil { fmt.Println("net.Listen err:" , err) return } defer listener.Close() go s.ListenMessage() fmt.Println(">>> 服务器启动成功,监听" , address, "<<<" ) for { conn, err := listener.Accept() if err != nil { fmt.Println("listener.Accept err:" , err) continue } go s.Handler(conn) } }
server/user.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 package mainimport ( "net" "strings" ) type User struct { Name string Addr string C chan string conn net.Conn server *Server } func NewUser (conn net.Conn, server *Server) *User { userAddr := conn.RemoteAddr().String() user := &User{ Name: userAddr, Addr: userAddr, C: make (chan string ), conn: conn, server: server, } go user.ListenMessage() return user } func (u *User) ListenMessage() { for msg := range u.C { _, _ = u.conn.Write([]byte (msg + "\n" )) } } func (u *User) Online() { u.server.mapLock.Lock() u.server.OnlineMap[u.Name] = u u.server.mapLock.Unlock() u.server.BroadCast(u, "已上线" ) } func (u *User) Offline() { u.server.mapLock.Lock() delete (u.server.OnlineMap, u.Name) u.server.mapLock.Unlock() u.server.BroadCast(u, "下线" ) } func (u *User) SendMsg(msg string ) { _, _ = u.conn.Write([]byte (msg)) } func (u *User) DoMessage(msg string ) { if msg == "who" { u.server.mapLock.RLock() for _, user := range u.server.OnlineMap { onlineMsg := "[" + user.Addr + "]" + user.Name + ": 在线\n" u.SendMsg(onlineMsg) } u.server.mapLock.RUnlock() return } if strings.HasPrefix(msg, "rename|" ) { parts := strings.SplitN(msg, "|" , 2 ) if len (parts) != 2 || parts[1 ] == "" { u.SendMsg("格式错误,请使用 rename|新用户名\n" ) return } newName := parts[1 ] u.server.mapLock.Lock() if _, ok := u.server.OnlineMap[newName]; ok { u.server.mapLock.Unlock() u.SendMsg("当前用户名已被使用\n" ) return } delete (u.server.OnlineMap, u.Name) u.server.OnlineMap[newName] = u u.Name = newName u.server.mapLock.Unlock() u.SendMsg("用户名修改成功: " + u.Name + "\n" ) return } if strings.HasPrefix(msg, "to|" ) { parts := strings.SplitN(msg, "|" , 3 ) if len (parts) != 3 || parts[1 ] == "" || parts[2 ] == "" { u.SendMsg("格式错误,请使用 to|用户名|消息内容\n" ) return } remoteName := parts[1 ] content := parts[2 ] u.server.mapLock.RLock() remoteUser, ok := u.server.OnlineMap[remoteName] u.server.mapLock.RUnlock() if !ok { u.SendMsg("用户不存在或不在线\n" ) return } remoteUser.SendMsg("[私聊]" + u.Name + ": " + content + "\n" ) return } u.server.BroadCast(u, msg) }
client/client.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 package mainimport ( "flag" "fmt" "io" "net" "os" ) type Client struct { ServerIp string ServerPort int Name string conn net.Conn flag int } func NewClient (serverIp string , serverPort int ) *Client { client := &Client{ ServerIp: serverIp, ServerPort: serverPort, flag: -1 , } conn, err := net.Dial("tcp" , fmt.Sprintf("%s:%d" , serverIp, serverPort)) if err != nil { fmt.Println("net.Dial err:" , err) return nil } client.conn = conn return client } func (c *Client) DealResponse() { _, _ = io.Copy(os.Stdout, c.conn) } func (c *Client) menu() bool { var userChoice int fmt.Println("====================" ) fmt.Println("1. 公聊模式" ) fmt.Println("2. 私聊模式" ) fmt.Println("3. 修改用户名" ) fmt.Println("0. 退出" ) fmt.Println("====================" ) fmt.Scanln(&userChoice) if userChoice < 0 || userChoice > 3 { fmt.Println(">> 输入不合法" ) return false } c.flag = userChoice return true } func (c *Client) UpdateName() { fmt.Println(">> 请输入新用户名:" ) fmt.Scanln(&c.Name) _, _ = c.conn.Write([]byte ("rename|" + c.Name + "\n" )) } func (c *Client) PublicChat() { var chatMsg string fmt.Println(">> 公聊模式,输入 exit 退出" ) fmt.Scanln(&chatMsg) for chatMsg != "exit" { if chatMsg != "" { _, _ = c.conn.Write([]byte (chatMsg + "\n" )) } fmt.Scanln(&chatMsg) } } func (c *Client) SelectUsers() { _, _ = c.conn.Write([]byte ("who\n" )) } func (c *Client) PrivateChat() { var remoteName string var chatMsg string c.SelectUsers() fmt.Println(">> 请输入聊天对象(exit 退出):" ) fmt.Scanln(&remoteName) for remoteName != "exit" { fmt.Println(">> 请输入消息(exit 结束当前会话):" ) fmt.Scanln(&chatMsg) for chatMsg != "exit" { if chatMsg != "" { sendMsg := "to|" + remoteName + "|" + chatMsg + "\n" _, _ = c.conn.Write([]byte (sendMsg)) } fmt.Scanln(&chatMsg) } c.SelectUsers() fmt.Println(">> 请输入聊天对象(exit 退出):" ) fmt.Scanln(&remoteName) } } func (c *Client) Run() { for c.flag != 0 { for !c.menu() { } switch c.flag { case 1 : c.PublicChat() case 2 : c.PrivateChat() case 3 : c.UpdateName() } } } var serverIp string var serverPort int func init () { flag.StringVar(&serverIp, "ip" , "127.0.0.1" , "设置服务器 IP" ) flag.IntVar(&serverPort, "port" , 8888 , "设置服务器端口" ) } func main () { flag.Parse() client := NewClient(serverIp, serverPort) if client == nil { fmt.Println(">> 连接服务器失败" ) return } fmt.Println(">> 连接服务器成功" ) go client.DealResponse() client.Run() }