Golang網路程式設計實現併發網路聊天室伺服器
阿新 • • 發佈:2021-02-05
技術標籤:Golang語言專案案例golang併發程式設計網路go語言伺服器
文章目錄
聊天室模組劃分:
主go程:
建立監聽socket。 for 迴圈 Accept() 客戶端連線 —— conn。 啟動 go 程 HandlerConnect:
HandlerConnect:
建立使用者結構體物件。 存入 onlineMap。傳送使用者登入廣播、聊天訊息。處理查詢線上使用者、改名、下線、超時提出。
Manager:
監聽 全域性 channel message, 將讀到的訊息 廣播給 onlineMap 中的所有使用者。
WriteMsgToClient:
讀取 每個使用者自帶 channel C 上訊息(由Manager傳送該訊息)。回寫給使用者。
全域性資料模組:
使用者結構體: Client { C、Name、Addr string }
在現使用者列表: onlineMap[string]Client key: 客戶端IP+port value: Client
訊息通道: message
廣播使用者上線:
1. 主go程中,建立監聽套接字。 記得defer
2. for 迴圈監聽客戶端連線請求。Accept()
3. 有一個客戶端連線,建立新 go 程 處理客戶端資料 HandlerConnet(conn) defer
4. 定義全域性結構體型別 C 、Name、Addr
5. 建立全域性map、channel
6. 實現HandlerConnet, 獲取客戶端IP+port —— RemoteAddr()。 初始化新使用者結構體資訊。 name == Addr
7. 建立 Manager 實現管理go程。 —— Accept() 之前。
8. 實現 Manager 。 初始化 線上使用者 map。 迴圈 讀取全域性 channel,如果無資料,阻塞。 如果有資料, 遍歷線上使用者 map ,將資料寫到 使用者的 C 裡
9. 將新使用者新增到 線上使用者 map 中 。 Key == IP+ port value= 新使用者結構體
10. 建立 WriteMsgToClient go程,專門給當前使用者寫資料。 —— 來源於 使用者自帶的 C 中
11. 實現 WriteMsgToClient(clnt,conn) 。遍歷自帶的 C ,讀資料,conn.Write 到 客戶端。
12. HandlerConnet中,結束位置,組織使用者上線資訊, 將 使用者上線資訊 寫 到全域性 channel —— Manager 的讀就被啟用(原來一直阻塞)
13. HandlerConnet中,結尾 加 for { ;}
廣播使用者訊息:
1. 封裝 函式 MakeMsg() 來處理廣播、使用者訊息
2. HandlerConnet中, 建立匿名go程, 讀取使用者socket上傳送來的 聊天內容。寫到 全域性 channel
3. for 迴圈 conn.Read n == 0 err != nil
4. 寫給全域性 message —— 後續的事,原來廣播使用者上線模組 完成。(Manager、WriteMsgToClient)
查詢線上使用者:
1. 將讀取到的使用者訊息 msg 結尾的 “\n”去掉。
2. 判斷是否是“who”命令
3. 如果是,遍歷線上使用者列表,組織顯示資訊。寫到 socket 中。
4. 如果不是。 寫給全域性 message
修改使用者名稱:
1. 將讀取到的使用者訊息 msg 判斷是否包含 “rename|”
2. 提取“|”後面的字串。存入到Client的Name成員中
3. 更新線上使用者列表。onlineMap。 key —— IP + prot
4. 提示使用者更新完成。conn.Write
使用者退出:
1. 在 使用者成功登陸之後, 建立監聽 使用者退出的 channel —— isQuit
2. 當 conn.Read == 0 , isQuit <- true
3. 在 HandlerConnet 結尾 for 中, 新增 select 監聽 <-isQuit
4. 條件滿足。 將使用者從線上列表移除。 組織使用者下線訊息,寫入 message (廣播)
超時強踢:
1. 在 select 中 監聽定時器。(time.After())計時到達。將使用者從線上列表移除。 組織使用者下線訊息,寫入 message (廣播)
2. 建立監聽 使用者活躍的 channel —— hasData
3. 只用戶執行:聊天、改名、who 任意一個操作,hasData<- true
4. 在 select 中 新增監聽 <-hasData。 條件滿足,不做任何事情。 目的是重置計時器。
程式碼實現:
package main
import (
"net"
"fmt"
"strings"
"time"
)
// 建立使用者結構體型別!
type Client struct {
C chan string
Name string
Addr string
}
// 建立全域性map,儲存線上使用者
var onlineMap map[string]Client
// 建立全域性 channel 傳遞使用者訊息。
var message = make(chan string)
func WriteMsgToClient(clnt Client, conn net.Conn) {
// 監聽 使用者自帶Channel 上是否有訊息。
for msg := range clnt.C {
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(clnt Client, msg string) (buf string) {
buf = "[" + clnt.Addr + "]" + clnt.Name + ": " + msg
return
}
func HandlerConnect(conn net.Conn) {
defer conn.Close()
// 建立channel 判斷,使用者是否活躍。
hasData := make(chan bool)
// 獲取使用者 網路地址 IP+port
netAddr := conn.RemoteAddr().String()
// 建立新連線使用者的 結構體. 預設使用者是 IP+port
clnt := Client{make(chan string), netAddr, netAddr}
// 將新連線使用者,新增到線上使用者map中. key: IP+port value:client
onlineMap[netAddr] = clnt
// 建立專門用來給當前 使用者傳送訊息的 go 程
go WriteMsgToClient(clnt, conn)
// 傳送 使用者上線訊息到 全域性channel 中
//message <- "[" + netAddr + "]" + clnt.Name + "login"
message <- MakeMsg(clnt, "login")
// 建立一個 channel , 用來判斷用退出狀態
isQuit := make(chan bool)
// 建立一個匿名 go 程, 專門處理使用者傳送的訊息。
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
isQuit <- true
fmt.Printf("檢測到客戶端:%s退出\n", clnt.Name)
return
}
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
// 將讀到的使用者訊息,儲存到msg中,string 型別
msg := string(buf[:n-1])
// 提取線上使用者列表
if msg == "who" && len(msg) == 3 {
conn.Write([]byte("online user list:\n"))
// 遍歷當前 map ,獲取線上使用者
for _, user := range onlineMap {
userInfo := user.Addr + ":" + user.Name + "\n"
conn.Write([]byte(userInfo))
}
// 判斷使用者傳送了 改名 命令
} else if len(msg) >=8 && msg[:6] == "rename" { // rename|
newName := strings.Split(msg, "|")[1] // msg[8:]
clnt.Name = newName // 修改結構體成員name
onlineMap[netAddr] = clnt // 更新 onlineMap
conn.Write([]byte("rename successful\n"))
}else {
// 將讀到的使用者訊息,寫入到message中。
message <- MakeMsg(clnt, msg)
}
hasData <- true
}
}()
// 保證 不退出
for {
// 監聽 channel 上的資料流動
select {
case <-isQuit:
delete(onlineMap, clnt.Addr) // 將使用者從 online移除
message <- MakeMsg(clnt, "logout") // 寫入使用者退出訊息到全域性channel
return
case <-hasData:
// 什麼都不做。 目的是重置 下面 case 的計時器。
case <-time.After(time.Second * 60):
delete(onlineMap, clnt.Addr) // 將使用者從 online移除
message <- MakeMsg(clnt, "time out leaved") // 寫入使用者退出訊息到全域性channel
return
}
}
}
func Manager() {
// 初始化 onlineMap
onlineMap = make(map[string]Client)
// 監聽全域性channel 中是否有資料, 有資料儲存至 msg, 無資料阻塞。
for {
msg := <-message
// 迴圈傳送訊息給 所有線上使用者。要想執行,必須 msg := <-message 執行完, 解除阻塞。
for _, clnt := range onlineMap {
clnt.C <- msg
}
}
}
func main() {
// 建立監聽套接字
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("Listen err", err)
return
}
defer listener.Close()
// 建立管理者go程,管理map 和全域性channel
go Manager()
// 迴圈監聽客戶端連線請求
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err", err)
return
}
// 啟動go程處理客戶端資料請求
go HandlerConnect(conn)
}
}