Golang网络聊天室案例

1年前未命名178
Golang网络聊天室案例 2019ab 于2023-01-16 22:00:00发布 563 收藏 分类专栏: GO 文章标签: 网络 golang 开发语言 GO 专栏收录该内容 19 篇文章 0 订阅 订阅专栏 1.聊天室设计分析 一. 概览

实现 个网络聊天室(群) 功能分析:

上线下线聊天,其他人,自己都可以看到聊天消息查询当前聊天室用户名字 who可以修改自己名字 rename | Duke超时踢出

技术点分析: 1 . sock tcp 编程 2 . map结构 (存储当前用户,map遍历,map删除) 3 . go程,channel 4 . select(超时退出,主动退出) 5 . timer定时器

二、实现基础

第一阶段: tcp socket,建立多个连接

package main import ( "fmt" "net" ) func main(){ // 创建服务器 listener,err := net.Listen("tcp",":8088") if err != nil{ fmt.Println("net.Listen err:",err) return } fmt.Println("服务器启动成功,监听中...") for { fmt.Println("==>主go程监听中......") // 监听 conn,err := listener.Accept() if err != nil{ fmt.Println("listener.Accept err:",err) return } // 建立连接 fmt.Println("建立连接成功!") // 启动处理业务的go程 go handler(conn) } } // 处理具体业务 func handler(conn net.Conn){ for{ fmt.Println("启动业务...") // TODO // 代表这里以后再具体实现 buf := make([]byte,1024) // 读取客户端发送来的数据 cnt,err := conn.Read(buf) if err != nil{ fmt.Println("listener.Read err:",err) return } fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt) } }

go run chatroom.go 启动nc nc下载地址

2、定义User/map结构 type User struct { // 名字 name string // 唯一 的 id id string // 管道 msg chan string } // 创建一个全局的map结构,用于保存所有的用户 var allUsers = make(map[string]User)

在Handler中调用

// 处理具体业务 func handler(conn net.Conn){ for{ fmt.Println("启动业务...") // // 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id clientAddr := conn.RemoteAddr().String() fmt.Println("clientAddr:",clientAddr) // 创建user newUser := User{ id:clientAddr,// id 我们不会修改,这个作为map中的key name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同 msg:make(chan string), // 注意需要make空间,否则无法写入数据 } // 添加user到map结构 allUsers[newUser.id] = newUser / buf := make([]byte,1024) // 读取客户端发送来的数据 cnt,err := conn.Read(buf) if err != nil{ fmt.Println("listener.Read err:",err) return } fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt) } 3.定义message管道

创建监听广播go程函数

// 向所有的用户广播消息,启动一个全局唯一的go程 func broadcast(){ fmt.Println("广播go程启动成功...") // 1. 从message中读取数据 info := <-message // 2. 将数据写入到每一个用户的msg管道中 for _,user := range allUsers{ user.msg <- info } }

启动,全局唯一 写入上线数据 当前整体源码

package main import ( "fmt" "net" ) type User struct { // 名字 name string // 唯一 的 id id string // 管道 msg chan string } // 创建一个全局的map结构,用于保存所有的用户 var allUsers = make(map[string]User) // 定义一个message全局通道,用于接收任何人发送过来消息 var message = make(chan string,10) func main(){ // 创建服务器 listener,err := net.Listen("tcp",":8087") if err != nil{ fmt.Println("net.Listen err:",err) return } // 启动全局唯一的go程,负责监听message通道,写给所有的用户 go broadcast() fmt.Println("服务器启动成功,监听中...") for { fmt.Println("==>主go程监听中......") // 监听 conn,err := listener.Accept() if err != nil{ fmt.Println("listener.Accept err:",err) return } // 建立连接 fmt.Println("建立连接成功!") // 启动处理业务的go程 go handler(conn) } } // 处理具体业务 func handler(conn net.Conn){ for{ fmt.Println("启动业务...") // 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id clientAddr := conn.RemoteAddr().String() fmt.Println("clientAddr:",clientAddr) // 创建user newUser := User{ id:clientAddr,// id 我们不会修改,这个作为map中的key name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同 msg:make(chan string,10), // 注意需要make空间,否则无法写入数据 } // 添加user到map结构 allUsers[newUser.id] = newUser // 向message写入数据,当我用户上线的消息,用于通知所有人(广播) loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name) message <- loginInfo buf := make([]byte,1024) // 读取客户端发送来的数据 cnt,err := conn.Read(buf) if err != nil{ fmt.Println("listener.Read err:",err) return } fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt) } } // 向所有的用户广播消息,启动一个全局唯一的go程 func broadcast(){ fmt.Println("广播go程启动成功...") defer fmt.Println("broadcast 程序退出!") for { // 1. 从message中读取数据 fmt.Println("broadcast监听message中...") info := <-message // 2. 将数据写入到每一个用户的msg管道中 for _,user := range allUsers{ // 如果msg是非缓冲,那么会在这里阻塞 user.msg <- info } } } 4.user监听通道go程

每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端

// 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端 func writeBackToClient(user *User,conn net.Conn){ fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name) for data := range user.msg{ fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data) // Write(b []byte)(n int,err error) _,_ = conn.Write([]byte(data)) } }

当前代码整体为

package main import ( "fmt" "net" ) type User struct { // 名字 name string // 唯一 的 id id string // 管道 msg chan string } // 创建一个全局的map结构,用于保存所有的用户 var allUsers = make(map[string]User) // 定义一个message全局通道,用于接收任何人发送过来消息 var message = make(chan string,10) func main(){ // 创建服务器 listener,err := net.Listen("tcp",":8087") if err != nil{ fmt.Println("net.Listen err:",err) return } // 启动全局唯一的go程,负责监听message通道,写给所有的用户 go broadcast() fmt.Println("服务器启动成功,监听中...") for { fmt.Println("==>主go程监听中......") // 监听 conn,err := listener.Accept() if err != nil{ fmt.Println("listener.Accept err:",err) return } // 建立连接 fmt.Println("建立连接成功!") // 启动处理业务的go程 go handler(conn) } } // 处理具体业务 func handler(conn net.Conn){ fmt.Println("启动业务...") // 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id clientAddr := conn.RemoteAddr().String() fmt.Println("clientAddr:",clientAddr) // 创建user newUser := User{ id:clientAddr,// id 我们不会修改,这个作为map中的key name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同 msg:make(chan string,10), // 注意需要make空间,否则无法写入数据 } // 添加user到map结构 allUsers[newUser.id] = newUser // 启动go程,负责将msg的信息返回给客户端 go writeBackToClient(&newUser,conn) // 向message写入数据,当我用户上线的消息,用于通知所有人(广播) loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name) message <- loginInfo for{ // 具体业务逻辑 buf := make([]byte,1024) // 读取客户端发送来的数据 cnt,err := conn.Read(buf) if err != nil{ fmt.Println("listener.Read err:",err) return } fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt) } } // 向所有的用户广播消息,启动一个全局唯一的go程 func broadcast(){ fmt.Println("广播go程启动成功...") defer fmt.Println("broadcast 程序退出!") for { // 1. 从message中读取数据 fmt.Println("broadcast监听message中...") info := <-message // 2. 将数据写入到每一个用户的msg管道中 for _,user := range allUsers{ // 如果msg是非缓冲,那么会在这里阻塞 user.msg <- info } } } // 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端 func writeBackToClient(user *User,conn net.Conn){ fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name) for data := range user.msg{ fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data) // Write(b []byte)(n int,err error) _,_ = conn.Write([]byte(data)) } } 三、增加功能 查询用户 查询命令:who==>将当前所有的登录的用户,展示出来,id,name,返回给当前用户 package main import ( "fmt" "net" "strings" ) type User struct { // 名字 name string // 唯一 的 id id string // 管道 msg chan string } // 创建一个全局的map结构,用于保存所有的用户 var allUsers = make(map[string]User) // 定义一个message全局通道,用于接收任何人发送过来消息 var message = make(chan string,10) func main(){ // 创建服务器 listener,err := net.Listen("tcp",":8087") if err != nil{ fmt.Println("net.Listen err:",err) return } // 启动全局唯一的go程,负责监听message通道,写给所有的用户 go broadcast() fmt.Println("服务器启动成功,监听中...") for { fmt.Println("==>主go程监听中......") // 监听 conn,err := listener.Accept() if err != nil{ fmt.Println("listener.Accept err:",err) return } // 建立连接 fmt.Println("建立连接成功!") // 启动处理业务的go程 go handler(conn) } } // 处理具体业务 func handler(conn net.Conn){ fmt.Println("启动业务...") // 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id clientAddr := conn.RemoteAddr().String() fmt.Println("clientAddr:",clientAddr) // 创建user newUser := User{ id:clientAddr,// id 我们不会修改,这个作为map中的key name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同 msg:make(chan string,10), // 注意需要make空间,否则无法写入数据 } // 添加user到map结构 allUsers[newUser.id] = newUser // 启动go程,负责将msg的信息返回给客户端 go writeBackToClient(&newUser,conn) // 向message写入数据,当我用户上线的消息,用于通知所有人(广播) loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!",newUser.id,newUser.name) message <- loginInfo for{ // 具体业务逻辑 buf := make([]byte,1024) // 读取客户端发送来的数据 cnt,err := conn.Read(buf) if err != nil{ fmt.Println("listener.Read err:",err) return } fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt) // -------------业务逻辑处理 开始------------- // 1.查询当前所有的用户 who // a. 先判断接收的数据是不是who ==》 长度&&字符串 userInput := string(buf[:cnt-1]) // 这是用户输入的数据,最后一个是回车,我们去掉他 if len(userInput) == 3 && userInput == "who"{ // b.遍历allUser这个map(key:userid value:user 本身)。将id和name拼接成一个字符,返回给客户端 fmt.Println("用户即将查询所有用户信息!") // 这个切片包含所有的用户信息 var userInfos []string for _,user := range allUsers{ userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name) userInfos = append(userInfos,userInfo) } // 最终写入到通道中,一定是一个字符串 r := strings.Join(userInfos,"\n") // 将数据返回到客户端 newUser.msg <- r }else{ // 如果不是用户输入的命令,只是聊天信息,那么只需要写到广播中即可,由其他的go程常规转发 message <- userInput } // -------------业务逻辑处理 结束------------- } } // 向所有的用户广播消息,启动一个全局唯一的go程 func broadcast(){ fmt.Println("广播go程启动成功...") defer fmt.Println("broadcast 程序退出!") for { // 1. 从message中读取数据 fmt.Println("broadcast监听message中...") info := <-message // 2. 将数据写入到每一个用户的msg管道中 for _,user := range allUsers{ // 如果msg是非缓冲,那么会在这里阻塞 user.msg <- info } } } // 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端 func writeBackToClient(user *User,conn net.Conn){ fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name) for data := range user.msg{ fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data) // Write(b []byte)(n int,err error) _,_ = conn.Write([]byte(data)) } }

重命名 规则:rename|Duke 获取数据判断长度7,判断字符是rename 使用|进行分割,获取|后面的部分,作为名字 更新用户名字newUser.name = Duke 通知客户端,更新成功

主动退出 每个用户都有自己的watch go程,仅负责监听退出信号

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理 func watch(user *User,conn net.Conn,isQuit <-chan bool){ fmt.Println("启动监听信号退出的go程...") defer fmt.Println("watch go程退出!") for{ select{ case <-isQuit: logoutInfo := fmt.Sprintf("%s exit already!",user.name) fmt.Println("删除当前用户:",user.name) delete(allUsers,user.id) message<-logoutInfo conn.Close() return } } } 在handler中启动go watch,同时传入相应信息: // 定义一个退出信号,用来监听client退出 var isQuit = make(chan bool) // 启动go程,负责监听退出信号 go watch(&newUser,conn,isQuit)

在read之后,通过cnt判断用户退出,向isQuit写入信号:

测试截图

超时退出 使用定时器来进行超时管理 如果60s没有发送任何数据,那么直接将这个链接关闭 <-time.After(60*time.second)

更新watch函数

// 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理 func watch(user *User,conn net.Conn,isQuit,restTimer <-chan bool){ fmt.Println("启动监听信号退出的go程...") defer fmt.Println("watch go程退出!") for{ select{ case <-isQuit: logoutInfo := fmt.Sprintf("%s exit already!\n",user.name) fmt.Println("删除当前用户:",user.name) delete(allUsers,user.id) message<-logoutInfo conn.Close() return case <-time.After(10*time.Second): logoutInfo := fmt.Sprintf("%s timeout exit elready!\n",user.name) fmt.Println("删除当前用户:",user.name) delete(allUsers,user.id) message<-logoutInfo conn.Close() return case <-restTimer: fmt.Printf("连接%s 重置计数器!\n",user.name) } } }

创建并传入restTimer管道

效果: 最终代码

package main import ( "fmt" "net" "strings" "time" ) type User struct { // 名字 name string // 唯一 的 id id string // 管道 msg chan string } // 创建一个全局的map结构,用于保存所有的用户 var allUsers = make(map[string]User) // 定义一个message全局通道,用于接收任何人发送过来消息 var message = make(chan string,10) func main(){ // 创建服务器 listener,err := net.Listen("tcp",":8087") if err != nil{ fmt.Println("net.Listen err:",err) return } // 启动全局唯一的go程,负责监听message通道,写给所有的用户 go broadcast() fmt.Println("服务器启动成功,监听中...") for { fmt.Println("==>主go程监听中......") // 监听 conn,err := listener.Accept() if err != nil{ fmt.Println("listener.Accept err:",err) return } // 建立连接 fmt.Println("建立连接成功!") // 启动处理业务的go程 go handler(conn) } } // 处理具体业务 func handler(conn net.Conn){ fmt.Println("启动业务...") // 客户端与服务器建立连接的时候,公有ip和port --> 当成user的id clientAddr := conn.RemoteAddr().String() fmt.Println("clientAddr:",clientAddr) // 创建user newUser := User{ id:clientAddr,// id 我们不会修改,这个作为map中的key name:clientAddr,// 可以修改,会提供rename命令修改,建立连接时,初始值与id相同 msg:make(chan string,10), // 注意需要make空间,否则无法写入数据 } // 添加user到map结构 allUsers[newUser.id] = newUser // 定义一个退出信号,用来监听client退出 var isQuit = make(chan bool) // 创建一个用于重置计数器的管道,用于告知watch函数,当前用户正在输入 var restTimer = make(chan bool) // 启动go程,负责监听退出信号 go watch(&newUser,conn,isQuit,restTimer) // 启动go程,负责将msg的信息返回给客户端 go writeBackToClient(&newUser,conn) // 向message写入数据,当我用户上线的消息,用于通知所有人(广播) loginInfo := fmt.Sprintf("[%s][%s] ===> |上线了login!!\n",newUser.id,newUser.name) message <- loginInfo for{ // 具体业务逻辑 buf := make([]byte,1024) // 读取客户端发送来的数据 cnt,err := conn.Read(buf) if cnt == 0 { fmt.Println("客户端主动关闭ctrl+c,准备退出!") // map 删除,用户 conn close掉 // 服务器还可以主动的退出 // 在这里不进行真正的退出动作,而是发出一个退出信号,统一做退出处理,可以使用新的管道来做信号传递 isQuit <- true } if err != nil{ fmt.Println("listener.Read err:",err) return } fmt.Println("客户端接收客户端发来的数据为:",string(buf[:cnt-1]),",cnt:",cnt) // -------------业务逻辑处理 开始------------- // 1.查询当前所有的用户 who // a. 先判断接收的数据是不是who ==》 长度&&字符串 userInput := string(buf[:cnt-1]) // 这是用户输入的数据,最后一个是回车,我们去掉他 if len(userInput) == 3 && userInput == "who"{ // b.遍历allUser这个map(key:userid value:user 本身)。将id和name拼接成一个字符,返回给客户端 fmt.Println("用户即将查询所有用户信息!") // 这个切片包含所有的用户信息 var userInfos []string for _,user := range allUsers{ userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name) userInfos = append(userInfos,userInfo) } // 最终写入到通道中,一定是一个字符串 r := strings.Join(userInfos,"\n") // 将数据返回到客户端 newUser.msg <- r }else if len(userInput) > 8 && userInput[:6] == "rename"{ // 规则:rename|Duke // 获取数据判断长度7,判断字符是rename // 使用|进行分割,获取|后面的部分,作为名字 // 更新用户名字newUser.name = Duke newUser.name = strings.Split(userInput,"|")[1] allUsers[newUser.id] = newUser // 更新map中的user // 通知客户端,更新成功 message <- userInput }else{ // 如果不是用户输入的命令,只是聊天信息,那么只需要写到广播中即可,由其他的go程常规转发 message <- userInput } restTimer <- true // -------------业务逻辑处理 结束------------- } } // 向所有的用户广播消息,启动一个全局唯一的go程 func broadcast(){ fmt.Println("广播go程启动成功...") defer fmt.Println("broadcast 程序退出!") for { // 1. 从message中读取数据 fmt.Println("broadcast监听message中...") info := <-message // 2. 将数据写入到每一个用户的msg管道中 for _,user := range allUsers{ // 如果msg是非缓冲,那么会在这里阻塞 user.msg <- info } } } // 每个用户应该还有一个用来监听msg管道的go程,负责将数据返回给客户端 func writeBackToClient(user *User,conn net.Conn){ fmt.Printf("user:%s 的go程正在监听自己的msg管道:\n",user.name) for data := range user.msg{ fmt.Printf("user:%s 写回给客户端的数据为:%s\n",user.name,data) // Write(b []byte)(n int,err error) _,_ = conn.Write([]byte(data)) } } // 启动一个go程,负责监听退出信号,触发后,进行清零工作:delete map,close conn 都在这里处理 func watch(user *User,conn net.Conn,isQuit,restTimer <-chan bool){ fmt.Println("启动监听信号退出的go程...") defer fmt.Println("watch go程退出!") for{ select{ case <-isQuit: logoutInfo := fmt.Sprintf("%s exit already!\n",user.name) fmt.Println("删除当前用户:",user.name) delete(allUsers,user.id) message<-logoutInfo conn.Close() return case <-time.After(10*time.Second): logoutInfo := fmt.Sprintf("%s timeout exit elready!\n",user.name) fmt.Println("删除当前用户:",user.name) delete(allUsers,user.id) message<-logoutInfo conn.Close() return case <-restTimer: fmt.Printf("连接%s 重置计数器!\n",user.name) } } }

这里还有问题就是,上锁问题。记得在操作map的时候加上读锁和写锁 案例

package main import( "fmt" "sync" "time" ) var idnames = make(map[int]string) var lock sync.RwMutex // map不允许同事读写,如果有不同go程同时操作map,需要对map上锁 func main(){ go func(){ for{ lock.lock() idnames[0] = "duke" lock.Unlock() } }() go func(){ for{ lock.Lock() name := idnames[0] fmt.Println("name:",name) lock.Unlock() } }() for{ fmt.Println("OVER") time.Sleep(1*time.Second) } }

感谢大家观看,我们下次见

标签: [db:标签TAG]

相关文章

shell原理及Linux权限

shell原理及Linux权限...

用 JavaScript 写一个新年倒计时

用 JavaScript 写一个新年倒计时...

接口自动化测试的价值,看完可涨薪3k...

接口自动化测试的价值,看完可涨薪3k......

Spring和Spring Boot的区别

Spring和Spring Boot的区别...

机器学习的几个公式

机器学习的几个公式...

JPA 注解及主键生成策略使用指南

JPA 注解及主键生成策略使用指南...