一、目的
运用Go语言中的goroutine和通道实现一个简单的一个服务器端对多个客户端的在线聊天
软件环境:Goland,Go1.9
二、设计思路
与一对一的设计思路类似,就是加了个线程的操作。
1,服务器端声明一个map,并打开监听端口;
2,客户端打开监听端口,同时连入服务器端;
3,在客户端上给自己起一个昵称,并输出,同时启动一个线程;
4,服务器端接收一个昵称,并存入map;
5,声明一个空的字符串,并写入要群发的消息;
6,服务器端解析发送的消息(msg_str[0]的值):
- nick:使该客户端加入聊天室并广播连上服务器端的所有其他客户端;
- say:广播客户端发出的消息;
- quit:使该客户端退出,断开与服务器端的连接,并将退出消息广播给其他连上服务器端的所有其他客户端;
三、Go代码
Server端
// one sever to more client chat room //This is chat sever package main import ( "fmt" "net" "strings" ) var ConnMap map[string]net.Conn = make(map[string]net.Conn) //声明一个集合 //ConnMap := make(map[string]net.Conn) func main() { listen_socket, err := net.Listen("tcp", "127.0.0.1:8000") //打开监听接口 if err != nil { fmt.Println("server start error") } defer listen_socket.Close() fmt.Println("server is wating ....") for { conn, err := listen_socket.Accept() //收到来自客户端发来的消息 if err != nil { fmt.Println("conn fail ...") } fmt.Println(conn.RemoteAddr(), "connect successed") go handle(conn) //创建线程 } } func handle(conn net.Conn) { for { data := make([]byte, 255) //创建字节流 (此处同 一对一 通信) msg_read, err := conn.Read(data) //声明并将从客户端读取的消息赋给msg_read 和err if msg_read == 0 || err != nil { continue } //解析协议 msg_str := strings.Split(string(data[0:msg_read]), "|") //将从客户端收到的字节流分段保存到msg_str这个数组中 switch msg_str[0] { case "nick": //加入聊天室 fmt.Println(conn.RemoteAddr(), "-->", msg_str[1]) //nick占在数组下标0上,客户端上写的昵称占在数组下标1上 for k, v := range ConnMap { //遍历集合中存储的客户端消息 if k != msg_str[1] { v.Write([]byte("[" + msg_str[1] + "]: join...")) } } ConnMap[msg_str[1]] = conn case "say": //转发消息 for k, v := range ConnMap { //k指客户端昵称 v指客户端连接服务器端后的地址 if k != msg_str[1] { //判断是不是给自己发,如果不是 fmt.Println("Send "+msg_str[2]+" to ", k) //服务器端将消息转发给集合中的每一个客户端 v.Write([]byte("[" + msg_str[1] + "]: " + msg_str[2])) //给除了自己的每一个客户端发送自己之前要发送的消息 } } case "quit": //退出 for k, v := range ConnMap { //遍历集合中的客户端昵称 if k != msg_str[1] { //如果昵称不是自己 v.Write([]byte("[" + msg_str[1] + "]: quit")) //给除了自己的其他客户端昵称发送退出的消息,并使Write方法阻塞 } } delete(ConnMap, msg_str[1]) //退出聊天室 } } }
Client端
// one sever to more client chat room //This is chat client package main import ( "fmt" "net" ) var nick string = "" //声明聊天室的昵称 func main() { conn, err := net.Dial("tcp", "127.0.0.1:8000") //打开监听端口 if err != nil { fmt.Println("conn fail...") } defer conn.Close() fmt.Println("client connect server successed ") //给自己取一个聊天室的昵称 fmt.Printf("Make a nickname:") fmt.Scanf("%s", &nick) //输入昵称 fmt.Println("hello : ", nick) //客户端输出 conn.Write([]byte("nick|" + nick)) //将信息发送给服务器端 go Handle(conn) //创建线程 var msg string for { msg = "" //声明一个空的消息 fmt.Scan(&msg) //输入消息 conn.Write([]byte("say|" + nick + "|" + msg)) //三段字节流 say | 昵称 | 发送的消息 if msg == "quit" { //如果消息为quit conn.Write([]byte("quit|" + nick)) //将quit字节流发送给服务器端 break //程序结束运行 } } } func Handle(conn net.Conn) { for { data := make([]byte, 255) //创建一个字节流 msg_read, err := conn.Read(data) //将读取的字节流赋值给msg_read和err if msg_read == 0 || err != nil { //如果字节流为0或者有错误 break } fmt.Println(string(data[0:msg_read])) //把字节流转换成字符串 } }
四、参考资料
五、总结与感受
着重关注收发消息的判定,收消息后的解包过程和开多线程;注意发消息与收消息时字节流与字符串的转换。
从初学Go到一对一再到一对多,我已经逐渐体会到使用Go语言做服务器端的方便与强大。
六、补充:还存在的问题
昨天把代码发给服务器主程大佬看,他看过后提出以下需要考虑和完善的问题,先忽略程序设计上的问题:
程序正确性无法保证
- Read可能一次性收到两个包,也可能收到半包。出现以上两种情况的时候协议解析都会出现问题。
- Write不保证一次调用时全部写完,存在短写的情况。
- ConnMap非线程安全。func handle(conn net.Conn)是多线程环境运行的。
- 连接出错及正常短开的情况未处理。