基于Go語言實現(xiàn)簡單網(wǎng)絡(luò)聊天室(命令行模式)
實戰(zhàn)簡介
網(wǎng)絡(luò)聊天室(命令行模式)
要求:
- 輸入網(wǎng)名,可以進入聊天室
- 聊天內(nèi)信息實時更新
- 利用協(xié)程處理多任務(wù)并發(fā)
基于tcp協(xié)議實現(xiàn)功能
服務(wù)器端
接受用戶消息和循環(huán)轉(zhuǎn)發(fā)
對功能命令進行處理
- ./cd1 或 ./menu 功能菜單
- ./cd2 或 ./changeName 更改昵稱
- ./cd3 或 ./online 在線用戶數(shù)量查詢
- ./cd4 或 ./quit 退出聊天室
客戶端
接受服務(wù)器發(fā)送的信息并處理
接受用戶的輸入處理后發(fā)往服務(wù)器
結(jié)構(gòu)和示例
用戶登錄示例
功能行命令測試
發(fā)送消息廣播測試
用戶退出示例
服務(wù)器端
基本流程
1. 初始化
init() 函數(shù)被調(diào)用,用于初始化全局變量:
- 創(chuàng)建一個映射 onlineList 用來保存在線用戶的信息。
- 創(chuàng)建一個帶緩沖的消息通道 message 用于廣播消息給所有在線用戶。
2. 主函數(shù) main()
- 使用 net.Listen 監(jiān)聽 TCP 地址 "127.0.0.1:8080"。
- 啟動 manger() 協(xié)程來監(jiān)聽消息通道 message,并將消息廣播給所有在線用戶。
- 主循環(huán)中,使用 Accept 接受新的客戶端連接,并為每個客戶端啟動一個新的協(xié)程 handleConnection(conn)。
3. 處理客戶端連接 handleConnection(conn)
對于每個客戶端連接,首先增加在線用戶計數(shù) count。
調(diào)用 addUser(conn) 添加新用戶到在線用戶列表,并返回一個 client 結(jié)構(gòu)體實例。
創(chuàng)建一個 quit 通道,用于在客戶端斷開連接時發(fā)送信號。
啟動兩個協(xié)程:
- writeMsgToClient(conn, quit):從用戶的 userChannel 中讀取消息并發(fā)送給客戶端。
- readClient(conn, quit):從客戶端讀取消息并處理。
監(jiān)聽 quit 通道以檢測客戶端是否斷開連接。
4. 添加新用戶 addUser(conn)
創(chuàng)建一個新的 client 實例,其中包含一個用于消息的通道 userChannel、客戶端連接 conn 和默認(rèn)名稱(客戶端地址)。
將新用戶添加到 onlineList 映射中。
5. 管理消息廣播 manger()
從消息通道 message 中讀取消息,并將消息廣播給所有在線用戶。
6. 寫消息到客戶端 writeMsgToClient(conn, quit)
從客戶端的 userChannel 讀取消息,使用 module.Encode 對消息進行編碼,并通過客戶端連接 conn 發(fā)送給客戶端。
如果發(fā)生錯誤或客戶端斷開連接,關(guān)閉客戶端連接并發(fā)送信號到 quit 通道。
7. 讀取客戶端消息 readClient(conn, quit)
從客戶端讀取消息,根據(jù)消息的內(nèi)容執(zhí)行不同的操作:
- 如果消息是以 !@#$@!cd1changeName 開頭,則處理昵稱更改請求。
- 如果消息是以 !@#$@!cd4exit 開頭,則處理客戶端退出請求。
- 如果消息是以 !@#$@!menu 開頭,則向客戶端發(fā)送菜單命令。
- 如果消息是以 !@#$@!cd3online 開頭,則向客戶端發(fā)送在線用戶數(shù)量。
- 其他情況下,將消息廣播給所有在線用戶。
如果客戶端斷開連接,則發(fā)送信號到 quit 通道。
8. 處理客戶端退出
當(dāng) quit 通道接收到信號時,從在線用戶列表中刪除該客戶端,并減少在線用戶計數(shù)。
如果所有客戶端都已斷開連接,則輸出“等待用戶連接中…”。
代碼
package main import ( "bufio" "chatRoom/chatRoom/module" // 消息的編碼和解碼模塊 "fmt" "io" "log" "net" "strconv" "strings" "sync" "time" ) // 定義客戶端結(jié)構(gòu)體 type client struct { userChannel chan string // 用戶的消息通道 conn net.Conn // 網(wǎng)絡(luò)連接 name string // 用戶名 addr string // 客戶端地址 } // 定義在線用戶計數(shù)器 var count int // 定義互斥鎖 var mu sync.Mutex // 定義在線用戶列表 var onlineList map[string]*client // 定義消息廣播通道 var message chan string // 初始化函數(shù) func init() { onlineList = make(map[string]*client) // 初始化在線用戶列表 message = make(chan string, 1024) // 初始化消息廣播通道 } // 主函數(shù) func main() { fmt.Println("端口監(jiān)聽中...") listener, err := net.Listen("tcp", "127.0.0.1:8080") if err != nil { log.Fatal(err) // 如果監(jiān)聽失敗,記錄錯誤并退出程序 } defer listener.Close() time.Sleep(time.Second) fmt.Println("端口監(jiān)聽成功") // 啟動管理消息廣播的協(xié)程 go manger() // 主循環(huán),接受客戶端連接 for { conn, err := listener.Accept() if err != nil { continue } go handleConnection(conn) } } // 處理客戶端連接的函數(shù) func handleConnection(conn net.Conn) { defer conn.Close() count++ // 增加在線用戶計數(shù) fmt.Println("有新用戶連接服務(wù),當(dāng)前連接數(shù):", count) // 添加新用戶 addUser(conn) // 創(chuàng)建退出信號通道 var quit = make(chan bool) // 啟動寫消息到客戶端的協(xié)程 go writeMsgToClient(conn, quit) // 啟動讀取客戶端消息的協(xié)程 go readClient(conn, quit) // 監(jiān)聽退出信號 select { case <-quit: // 用戶下線處理 connName := onlineList[conn.RemoteAddr().String()].name mu.Lock() close(onlineList[conn.RemoteAddr().String()].userChannel) mu.Unlock() mu.Lock() delete(onlineList, conn.RemoteAddr().String()) mu.Unlock() count-- message <- "< 系統(tǒng)消息 > [ " + connName + " ]" + "下線了 當(dāng)前在線人數(shù) " + strconv.Itoa(len(onlineList)) + " 人" fmt.Println("有用戶下線了,當(dāng)前連接數(shù):", count) if count == 0 { fmt.Println("等待用戶連接中...") } return } } // 修改用戶名的方法 func (c *client) changeName(newUserName string) bool { mu.Lock() defer mu.Unlock() // 更新用戶名 c.name = newUserName return true } // 管理消息廣播的函數(shù) func manger() { fmt.Println("開始監(jiān)聽 message通道") defer fmt.Println("結(jié)束監(jiān)聽 message通道") for msg := range message { mu.Lock() for _, v := range onlineList { v.userChannel <- msg } mu.Unlock() } } // 寫消息到客戶端的協(xié)程 func writeMsgToClient(conn net.Conn, quit chan bool) { fmt.Println(onlineList[conn.RemoteAddr().String()].name, "的信息通道監(jiān)聽成功") defer fmt.Println(onlineList[conn.RemoteAddr().String()].name, "的信息通道監(jiān)聽結(jié)束") for msg := range onlineList[conn.RemoteAddr().String()].userChannel { king, err := module.Encode(msg + "\n") if err != nil { fmt.Println("發(fā)送消息失敗") continue } _, err = conn.Write(king) if err != nil { fmt.Println("發(fā)送消息失敗") quit <- true } } fmt.Println("函數(shù)writeMsgToClient函數(shù)結(jié)束") } // 添加新用戶 func addUser(conn net.Conn) client { fmt.Println("開始使用添加新用戶" + conn.RemoteAddr().String()) newUser := client{ make(chan string), // 創(chuàng)建用戶消息通道 conn, // 網(wǎng)絡(luò)連接 conn.RemoteAddr().String(), // 用戶名,初始化為客戶端地址 conn.RemoteAddr().String(), // 客戶端地址 } onlineList[conn.RemoteAddr().String()] = &newUser // 添加到在線用戶列表 fmt.Println("addUser函數(shù)結(jié)束,用戶" + conn.RemoteAddr().String() + "添加成功") return newUser } // 讀取客戶端消息的協(xié)程 func readClient(conn net.Conn, quit chan bool) { fmt.Println("開始讀取客戶端發(fā)送的信息") defer fmt.Println("客戶端發(fā)送信息讀取結(jié)束") userChannel := onlineList[conn.RemoteAddr().String()].userChannel reader := bufio.NewReader(conn) for { msg, err := module.Decode(reader) if err == io.EOF { quit <- true } if err != nil { fmt.Println("decode msg failed, err:", err) quit <- true } if len(msg) == 0 { continue } fmt.Println("收到client發(fā)來的數(shù)據(jù):", msg) // 處理客戶端發(fā)送的不同類型的消息 switch { case strings.HasPrefix(msg, "!@#$@!cd1changeName"): king := true oldName := onlineList[conn.RemoteAddr().String()].name newName := strings.TrimPrefix(msg, "!@#$@!cd1changeName") if strings.HasPrefix(msg, "!@#$@!cd1changeNameFirst") { newName = strings.TrimPrefix(msg, "!@#$@!cd1changeNameFirst") } if newName == "" { newName = conn.RemoteAddr().String() } for _, v := range onlineList { mapName := v.name if mapName == newName { king = false break } } if strings.HasPrefix(msg, "!@#$@!cd1changeNameFirst") && king == false { message <- "< 系統(tǒng)消息 > [ " + conn.RemoteAddr().String() + " ] [ " + oldName + " ] 上線了!" userChannel <- "< 系統(tǒng)消息 > [ " + onlineList[conn.RemoteAddr().String()].name + " ]" + "名字: " + newName + " 已存在,請更換一個名字嘗試" userChannel <- "< 系統(tǒng)消息 > 你當(dāng)前昵稱為: " + oldName + " ( 輸入cd2可進行名字修改 )" continue } if king == false { userChannel <- "< 系統(tǒng)消息 > 昵稱修改失敗?。?!" userChannel <- "< 系統(tǒng)消息 > [ " + onlineList[conn.RemoteAddr().String()].name + " ]" + "名字: " + newName + " 已存在,請更換一個名字嘗試" userChannel <- "< 系統(tǒng)消息 > 你當(dāng)前昵稱為: " + oldName continue } isSuccess := onlineList[conn.RemoteAddr().String()].changeName(newName) if isSuccess { userChannel <- "!@#$@!cd1changeName" + newName if strings.HasPrefix(msg, "!@#$@!cd1changeNameFirst") { message <- "< 系統(tǒng)消息 > [ " + conn.RemoteAddr().String() + " ] [ " + newName + " ] 上線了!" time.Sleep(time.Millisecond * 50) userChannel <- "< 系統(tǒng)消息 > 你當(dāng)前的昵稱為:" + newName continue } userChannel <- "< 系統(tǒng)消息 > 昵稱修改成功 你當(dāng)前昵稱為: " + newName } else { userChannel <- "< 系統(tǒng)消息 > 昵稱修改失?。。?!" } message <- "< 系統(tǒng)消息 > [ " + conn.RemoteAddr().String() + " ]" + " 舊昵稱為: " + oldName + " 新昵稱為: " + newName case strings.HasPrefix(msg, "!@#$@!cd4exit"): fmt.Println("[ " + onlineList[conn.RemoteAddr().String()].name + " ] " + "下線了") quit <- true return case strings.HasPrefix(msg, "!@#$@!menu"): userChannel <- "< 系統(tǒng)消息 > \n * ./cd1 或 ./menu 功能菜單\n * ./cd2 或 ./changeName 更改昵稱\n * ./cd3 或 ./online 在線用戶數(shù)量查詢\n * ../cd4 或 ./quit 退出聊天室" case strings.HasPrefix(msg, "!@#$@!cd3online"): fmt.Println("在線人數(shù):", count) userChannel <- "< 系統(tǒng)消息 > 當(dāng)前在線人數(shù):" + strconv.Itoa(count) default: message <- "[ " + onlineList[conn.RemoteAddr().String()].name + " ]" + ": " + msg fmt.Println("信息廣播成功") } } }
客戶端
基本流程
1. 主函數(shù) main()
- 嘗試連接到服務(wù)器 "127.0.0.1:8080"。
- 如果連接失敗,打印錯誤信息并退出程序。
- 如果連接成功,獲取用戶的昵稱并發(fā)送給服務(wù)器。
- 創(chuàng)建一個 exit 通道,用于接收退出信號。
- 打印歡迎信息和命令提示。
- 啟動一個協(xié)程 readMsg(conn) 用于讀取消息。
- 啟動另一個協(xié)程用于處理用戶輸入。
- 主循環(huán)中監(jiān)聽 exit 通道,如果接收到信號則退出程序。
2. 獲取用戶輸入 getUserInput(prompt string)
- 根據(jù)傳入的提示信息顯示相應(yīng)的提示。
- 讀取用戶從標(biāo)準(zhǔn)輸入的輸入。
- 返回去除空白字符的輸入字符串。
3. 讀取消息 readMsg(conn)
- 從服務(wù)器讀取消息。
- 如果讀取到 EOF (文件結(jié)束),則表示服務(wù)器連接已斷開,終止程序。
- 如果讀取過程中出現(xiàn)其他錯誤,則打印錯誤信息并退出函數(shù)。
- 如果接收到的消息為空,則跳過本次循環(huán)。
- 如果接收到的消息是以 !@#$@!cd1changeName 開頭,則更新用戶的昵稱。
- 打印接收到的消息的時間戳和內(nèi)容。
4. 處理用戶輸入
- 循環(huán)讀取用戶輸入,并根據(jù)不同的命令構(gòu)建消息。
- 如果命令是 ./cd1 或 ./menu,則發(fā)送 !@#$@!menu 消息。
- 如果命令是 ./cd2 或 ./changeName,則請求用戶輸入新的昵稱,并發(fā)送 !@#$@!cd1changeName 加上新的昵稱。
- 如果命令是 ./cd3 或 ./online,則發(fā)送 !@#$@!cd3online 消息。
- 如果命令是 cd4 或 ./quit,則發(fā)送 !@#$@!cd4exit 消息,并發(fā)送退出信號到 exit 通道。
- 對于其他消息,編碼并發(fā)送到服務(wù)器。
代碼
package main import ( "bufio" "chatRoom/chatRoom/module" // 消息的編碼和解碼模塊 "fmt" "io" "net" "os" "strings" "time" ) // 定義一個全局變量用于存儲用戶的昵稱 var name string // 主函數(shù) func main() { // 嘗試連接到服務(wù)器 conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("服務(wù)器連接失敗 err =", err) return } defer conn.Close() // 打印連接成功的消息 fmt.Println("服務(wù)器連接成功") // 獲取用戶的昵稱 name = getUserInput("請輸入你的昵稱:") // 構(gòu)建一條特殊的消息,用于通知服務(wù)器用戶昵稱 data, err := module.Encode("!@#$@!cd1changeNameFirst" + name) if err != nil { fmt.Println("encode msg failed, err:", err) return } // 發(fā)送消息到服務(wù)器 _, err = conn.Write(data) if err != nil { fmt.Println("發(fā)送數(shù)據(jù)失敗1 err =", err) } // 創(chuàng)建一個通道,用于接收退出信號 var exit = make(chan bool) // 確保在函數(shù)退出時關(guān)閉通道 defer close(exit) // 顯示歡迎信息和命令提示 fmt.Println("--------------歡迎進入多人聊天室系統(tǒng)----------------") fmt.Println(" * ./cd1 或 ./menu 功能菜單") fmt.Println(" * ./cd2 或 ./changeName 更改昵稱") fmt.Println(" * ./cd3 或 ./online 在線用戶數(shù)量查詢") fmt.Println(" * ./cd4 或 ./quit 退出聊天室") fmt.Println("---------------指令字母不區(qū)分大小寫-----------------") // 啟動一個協(xié)程用于讀取消息 go readMsg(conn) // 啟動一個協(xié)程用于處理用戶輸入 go func() { for { // 獲取用戶輸入 msg := getUserInput("") // 根據(jù)用戶輸入特殊消息處理命令 if strings.EqualFold(msg, "./cd1") || strings.EqualFold(msg, "./menu") { msg = "!@#$@!menu" } if strings.EqualFold(msg, "./cd2") || strings.EqualFold(msg, "./changeName") { newMsg := getUserInput("請輸入新的昵稱:") msg = "!@#$@!cd1changeName" + newMsg } if strings.EqualFold(msg, "./cd3") || strings.EqualFold(msg, "./online") { msg = "!@#$@!cd3online" } if strings.EqualFold(msg, "./cd4") || strings.EqualFold(msg, "./quit") { msg = "!@#$@!cd4exit" // 編碼并發(fā)送退出消息 data, err := module.Encode(msg) if err != nil { fmt.Println("消息數(shù)據(jù)失敗1, err:", err) return } _, err = conn.Write(data) if err != nil { // 如果發(fā)送失敗,打印錯誤信息 fmt.Println("發(fā)送數(shù)據(jù)失敗2 err =", err) } // 打印退出信息 fmt.Println("正在退出...") // 發(fā)送退出信號 exit <- true return } // 編碼并發(fā)送普通消息 data, err := module.Encode(msg) if err != nil { // 如果消息編碼失敗,打印錯誤信息并退出協(xié)程 fmt.Println("發(fā)送數(shù)據(jù)失敗3, err:", err) return } _, err = conn.Write(data) if err != nil { // 如果發(fā)送失敗,打印錯誤信息 fmt.Println("發(fā)送數(shù)據(jù)失敗4 err =", err) } } }() // 主循環(huán),監(jiān)聽退出信號 for { select { case <-exit: // 當(dāng)收到退出信號時,打印退出成功并退出程序 fmt.Println("退出成功") return } } } // getUserInput 函數(shù)用于獲取用戶輸入 func getUserInput(prompt string) string { time.Sleep(time.Millisecond * 100) // 根據(jù)不同的提示信息顯示相應(yīng)的提示 switch prompt { case "請輸入你的昵稱:": fmt.Print("請輸入你的昵稱:") case "請輸入新的昵稱:": fmt.Println("請輸入新的昵稱:") } // 創(chuàng)建一個標(biāo)準(zhǔn)輸入的緩沖讀取器 reader := bufio.NewReader(os.Stdin) // 讀取一行輸入 input, err := reader.ReadString('\n') if err != nil { // 如果讀取失敗,打印錯誤信息并返回錯誤信息 fmt.Println("用戶輸入獲取失?。篹rr =", err) return "客戶端信息讀取錯誤" } // 返回去掉空格的輸入字符串 return strings.TrimSpace(input) } // readMsg 函數(shù)用于讀取消息 func readMsg(conn net.Conn) { defer conn.Close() // 創(chuàng)建一個緩沖讀取器來讀取連接中的數(shù)據(jù) reader := bufio.NewReader(conn) for { // 解碼消息 msg, err := module.Decode(reader) if err == io.EOF { // 如果遇到EOF(文件結(jié)束),表示連接已斷開 fmt.Println("服務(wù)器連接已斷開 ") // 終止程序 os.Exit(1) } if err != nil { fmt.Println("服務(wù)器斷開連接 2 err =", err) return } if msg == "" { // 如果消息為空,則跳過本次循環(huán) continue } if strings.HasPrefix(msg, "!@#$@!cd1changeName") { // 如果消息是更改昵稱的通知 msg1 := strings.TrimPrefix(msg, "!@#$@!cd1changeName") name = strings.TrimRight(msg1, "\n") // 更新昵稱 continue } // 打印消息的時間戳和內(nèi)容 fmt.Print("【 ", time.Now().Format("15:04"), " 】", msg) } }
消息封包和解包的函數(shù)
作用:防止tcp粘包的情況影響消息的讀取
1.為什么會出現(xiàn)粘包
主要原因就是tcp數(shù)據(jù)傳遞模式是流模式,在保持長連接的時候可以進行多次的收和發(fā)。
“粘包"可發(fā)生在發(fā)送端也可發(fā)生在接收端:
- 由Nagle算法造成的發(fā)送端的粘包:Nagle算法是一種改善網(wǎng)絡(luò)傳輸效率的算法。簡單來說就是當(dāng)我們提交一段數(shù)據(jù)給TCP發(fā)送時,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去。
- 接收端接收不及時造成的接收端粘包:TCP會把接收到的數(shù)據(jù)存在自己的緩沖區(qū)中,然后通知應(yīng)用層取數(shù)據(jù)。當(dāng)應(yīng)用層由于某些原因不能及時的把TCP的數(shù)據(jù)取出來,就會造成TCP緩沖區(qū)中存放了幾段數(shù)據(jù)。
2.解決辦法
出現(xiàn)"粘包"的關(guān)鍵在于接收方不確定將要傳輸?shù)臄?shù)據(jù)包的大小,因此我們可以對數(shù)據(jù)包進行封包和拆包的操作。
封包:封包就是給一段數(shù)據(jù)加上包頭,這樣一來數(shù)據(jù)包就分為包頭和包體兩部分內(nèi)容了(過濾非法包時封包會加入"包尾"內(nèi)容)。包頭部分的長度是固定的,并且它存儲了包體的長度,根據(jù)包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數(shù)據(jù)包。
我們可以自己定義一個協(xié)議,比如數(shù)據(jù)包的前4個字節(jié)為包頭,里面存儲的是發(fā)送的數(shù)據(jù)的長度。
代碼
package module import ( "bufio" "bytes" "encoding/binary" ) func Encode(message string) ([]byte, error) { // 讀取消息的長度,轉(zhuǎn)換成int32類型(占4個字節(jié)) var length = int32(len(message)) var pkg = new(bytes.Buffer) // 寫入消息頭 err := binary.Write(pkg, binary.LittleEndian, length) if err != nil { return nil, err } // 寫入消息實體 err = binary.Write(pkg, binary.LittleEndian, []byte(message)) if err != nil { return nil, err } return pkg.Bytes(), nil } // Decode 解碼消息 func Decode(reader *bufio.Reader) (string, error) { // 讀取消息的長度 lengthByte, _ := reader.Peek(4) // 讀取前4個字節(jié)的數(shù)據(jù) lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "", err } // Buffered返回緩沖中現(xiàn)有的可讀取的字節(jié)數(shù)。 if int32(reader.Buffered()) < length+4 { return "", err } // 讀取真正的消息數(shù)據(jù) pack := make([]byte, int(4+length)) _, err = reader.Read(pack) if err != nil { return "", err } return string(pack[4:]), nil }
以上就是基于Go語言實現(xiàn)簡單網(wǎng)絡(luò)聊天室(命令行模式)的詳細內(nèi)容,更多關(guān)于Go聊天室的資料請關(guān)注腳本之家其它相關(guān)文章!