1. 程式人生 > 其它 >Golang網路程式設計實現併發網路聊天室伺服器

Golang網路程式設計實現併發網路聊天室伺服器

技術標籤: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)
	}
}