需求摘要
- 實現一個分佈式點對點的聊天系統,所有節點都是對等的,不需要中央服務器
- 實現註冊節點名稱,節點之間通過節點名稱發起會話
思路分析
- 節點同時具備服務端和客戶端的職能
- 服務端只負責接收其它節點主動發送過來的消息
- 客戶端只負責主動向其它節點發送消息
- 通信都用短連接,服務端收完消息/客戶端發完消息都斷開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()
}
}