5.8 Go語言項目實戰:點對點聊天

需求摘要

  • 實現一個分佈式點對點的聊天系統,所有節點都是對等的,不需要中央服務器
  • 實現註冊節點名稱,節點之間通過節點名稱發起會話

思路分析

  • 節點同時具備服務端和客戶端的職能
  • 服務端只負責接收其它節點主動發送過來的消息
  • 客戶端只負責主動向其它節點發送消息
  • 通信都用短連接,服務端收完消息/客戶端發完消息都斷開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()
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章