一、目的
運用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 \n") //給自己取一個聊天室的昵稱 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)是多線程環境運行的。
- 連接出錯及正常短開的情況未處理。