4.13 Go語言專案實戰:點對點聊天
阿新 • • 發佈:2019-01-13
需求摘要
- 實現一個分散式點對點的聊天系統,所有節點都是對等的,不需要中央伺服器
- 實現註冊節點名稱,節點之間通過節點名稱發起會話
思路分析
- 節點同時具備服務端和客戶端的職能
- 服務端只負責接收其它節點主動傳送過來的訊息
- 客戶端只負責主動向其它節點發送訊息
- 通訊都用短連線,服務端收完訊息/客戶端發完訊息都斷開conn——一方面是節約IO資源,另一方面是為了使邏輯清晰
- 節點名稱註冊到【註冊伺服器】(很像DNS),以便根據節點名稱訪問節點而不是監聽埠
節點程式碼實現
peer.go程式碼實現如下
package main import ( "fmt" "net" "os" "time" ) /* ·用一個可執行程式實現相互聊天 ·實現註冊節點名稱,並通過名稱發起會話 ·實現群發訊息 */ /* 思路概要: ·節點同時具備服務端和客戶端的職能 ·服務端只負責接收其它節點主動傳送過來的訊息 ·客戶端只負責主動向其它節點發送訊息 ·通訊都用短連線,服務端收完訊息/客戶端發完訊息都斷開conn——一方面是節約IO資源,另一方面是為了使邏輯清晰 ·節點名稱註冊到【註冊伺服器】(很像DNS),以便根據節點名稱訪問節點而不是監聽埠 */ /* 節點註冊伺服器地址 提供節點註冊和查詢功能 */ const registerAddress = "127.0.0.1:8888" /* 節點的主業務邏輯 */ func main() { //初始化快取表 cacheMap = make(map[string]string) /*從命令列接收監聽埠和節點名稱*/ //從命令列上接收一個用於監聽的埠:peer pa 1234 peerName = os.Args[1] peerListeningPort = os.Args[2] fmt.Println(peerListeningPort, peerName) /* 向註冊器註冊自己 reg pa 1234 */ peerAddress := RegOrGetPeerListeningAddress("reg " + peerName + " " + peerListeningPort) fmt.Println("節點註冊成功", peerName, peerAddress) /*在獨立併發任務中接收其它節點的訊息*/ go StartServe() /*在獨立併發任務中向其它節點發送訊息*/ go StartRequest() //不能主協程會在此掛掉(如果主協程掛掉,子協程就跟著掛掉了) for { time.Sleep(1 * time.Second) } fmt.Println("GAME OVER") } /*錯誤處理*/ func HandleErr(err error, when string) { if err != nil { fmt.Println("err=", err, when) os.Exit(1) } } //節點監聽埠,節點名稱 var peerListeningPort, peerName string //快取其它通訊節點的監聽地址(如果已經查詢過一回,就沒必要每次都查詢) var cacheMap map[string]string /* 向【註冊器】註冊/獲取【節點的監聽地址】 request 請求命令: reg pa 1234 向註冊機註冊名為pa的節點,監聽在1234埠 get pa 向註冊機獲取名為pa的節點的監聽地址 返回值 pa節點的監聽地址 */ func RegOrGetPeerListeningAddress(request string) string { //撥號【註冊伺服器】 conn, e := net.Dial("tcp", registerAddress) HandleErr(e, "RegOrGetPeerListeningAddress") //傳送註冊/查詢命令 conn.Write([]byte(request)) //得到要註冊/查詢的節點的監聽地址 buffer := make([]byte, 1024) n, e := conn.Read(buffer) HandleErr(e, "RegGetPeerAddressconn.Read(buffer)") peerAddress := string(buffer[:n]) //返回這個監聽地址 return peerAddress } /* 監聽並接收其它節點發送過來的訊息 這是節點【服務端】的一面 */ func StartServe() { //在配置和註冊好的埠建立TCP監聽 listener, e := net.Listen("tcp", ":"+peerListeningPort) HandleErr(e, "net.Listen") /*迴圈接入其它節點*/ for { conn, e := listener.Accept() HandleErr(e, "listener.Accept()") //接收遠端節點的訊息 buffer := make([]byte, 1024) n, err := conn.Read(buffer) HandleErr(err, "conn.Read(buffer)") msg := string(buffer[:n]) fmt.Println(conn.RemoteAddr(), ":", msg) //接收完畢立即斷開 conn.Close() } } /* 主動向其它節點發起會話 這是節點【客戶端】的一面 */ func StartRequest() { //目標節點名稱,要傳送的訊息 var targetName, msg string for { //從控制檯輸入資訊 fmt.Println("請輸入對方名稱:訊息內容") fmt.Scan(&targetName, &msg) //看看快取中是否有節點資訊 var targetAddress string if temp, ok := cacheMap[targetName]; !ok { //向註冊器查詢節點的監聽地址 fmt.Println("從註冊伺服器獲得節點監聽地址") targetAddress = RegOrGetPeerListeningAddress("get " + targetName) //將查詢結果寫入快取 cacheMap[targetName] = targetAddress } else { //使用快取中的監聽地址 fmt.Println("從快取獲得節點監聽地址") targetAddress = temp } //向目標地址傳送訊息 conn, e := net.Dial("tcp", targetAddress) HandleErr(e, "net.Dial") conn.Write([]byte(msg)) //訊息傳送完畢,斷開連線 conn.Close() } }
節點註冊伺服器
- 節點註冊伺服器作為基礎設施,提供節點的註冊和查詢功能
registerer.go 程式碼實現如下
package main import ( "fmt" "net" "os" "strings" ) /* 負責註冊節點名稱:節點執行埠 */ func RHandleErr(err error, when string) { if err != nil{ fmt.Println("註冊器err=",err,when) os.Exit(1) } } //所有節點【名稱-監聽地址】對映表 var peerNameListeningAddressMap map[string]string /*註冊機的主業務*/ func main() { //初始化登錄檔 peerNameListeningAddressMap = make(map[string]string) //開啟註冊服務 listener, e := net.Listen("tcp", ":8888") RHandleErr(e,"net.Listen") buffer := make([]byte, 1024) /*迴圈接受節點的註冊和查詢服務*/ for { conn, e := listener.Accept() RHandleErr(e,"listener.Accept()") /* 接收節點訊息 reg pa 1234 註冊:節點名稱pa,監聽埠1234 get pa 查詢:節點名稱為pa的節點的監聽地址 */ n, e := conn.Read(buffer) RHandleErr(e,"conn.Read(buffer)") msg := string(buffer[:n]) //將訊息炸碎為字串,獲取命令和節點名稱 strs := strings.Split(msg, " ") cmd := strs[0] peerName := strs[1] if cmd == "reg"{ //將節點名稱和【節點-地址】寫入全域性對映表 reg pa 1234 runningAddress := conn.RemoteAddr().String() //拼接節點的IP和監聽埠,得到節點的監聽地址 peerIP := strings.Split(runningAddress, ":")[0] peerListeningPort := strs[2] listenAddress := peerIP +":" + peerListeningPort //節點名稱為鍵,監聽地址為值,寫入map(下次就可以供別人查詢了) peerNameListeningAddressMap[peerName] = listenAddress //將節點的名稱和監聽地址寫入全域性對映表 conn.Write([]byte(listenAddress)) } else if cmd=="get"{ //根據節點名稱查詢節點監聽地址 listeningAddress := peerNameListeningAddressMap[peerName] conn.Write([]byte(listeningAddress)) } //斷開會話,繼續接受其他節點的註冊或查詢請求 conn.Close() } }