1. 程式人生 > >4.13 Go語言專案實戰:點對點聊天

4.13 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()
	}

}