Go語言專案實戰:多人聊天室

尹成發表於2018-11-16

功能需求

  • 實現單撩
  • 實現群撩
  • 實現使用者上線的全網通知
  • 實現使用者暱稱
  • 實現聊天日誌的儲存和檢視

服務端實現

type Client struct {
	conn net.Conn
	name string
	addr string
}

var (
	//客戶端資訊,用暱稱為鍵
	//clientsMap = make(map[string]net.Conn)
	clientsMap = make(map[string]Client)
)

func SHandleError(err error, why string) {
	if err != nil {
		fmt.Println(why, err)
		os.Exit(1)
	}
}

func main() {

	//建立服務端監聽
	listener, e := net.Listen("tcp", "127.0.0.1:8888")
	SHandleError(e, "net.Listen")
	defer func() {
		for _, client := range clientsMap {
			client.conn.Write([]byte("all:伺服器進入維護狀態,大家都洗洗睡吧!"))
		}
		listener.Close()
	}()

	for {
		//迴圈接入所有女朋友
		conn, e := listener.Accept()
		SHandleError(e, "listener.Accept")
		clientAddr := conn.RemoteAddr()

		//TODO:接收並儲存暱稱
		buffer := make([]byte, 1024)
		var clientName string
		for {
			n, err := conn.Read(buffer)
			SHandleError(err, "conn.Read(buffer)")
			if n > 0 {
				clientName = string(buffer[:n])
				break
			}
		}
		fmt.Println(clientName + "上線了")

		//TODO:將每一個女朋友丟入map
		client := Client{conn, clientName, clientAddr.String()}
		clientsMap[clientName] = client

		//TODO:給已經線上的使用者傳送上線通知——使用暱稱
		for _, client := range clientsMap {
			client.conn.Write([]byte(clientName + "上線了"))
		}

		//在單獨的協程中與每一個具體的女朋友聊天
		go ioWithClient(client)
	}

	//設定優雅退出邏輯

}

//與一個Client做IO
func ioWithClient(client Client) {
	//clientAddr := conn.RemoteAddr().String()
	buffer := make([]byte, 1024)

	for {
		n, err := client.conn.Read(buffer)
		if err != io.EOF {
			SHandleError(err, "conn.Read")
		}

		if n > 0 {
			msg := string(buffer[:n])
			fmt.Printf("%s:%s\n", client.name, msg)

			//將客戶端說的每一句話記錄在【以他的名字命名的檔案裡】
			writeMsgToLog(msg, client)

			strs := strings.Split(msg, "#")
			if len(strs) > 1 {
				//all#hello
				//zqd#hello

				//要傳送的目標暱稱
				targetName := strs[0]
				targetMsg := strs[1]

				//TODO:使用暱稱定位目標客戶端的Conn
				if targetName == "all" {
					//群發訊息
					for _, c := range clientsMap {
						c.conn.Write([]byte(client.name + ":" + targetMsg))
					}
				} else {
					//點對點訊息
					for key, c := range clientsMap {
						if key == targetName {
							c.conn.Write([]byte(client.name + ":" + targetMsg))

							//在點對點訊息的目標端也記錄日誌
							go writeMsgToLog(client.name + ":" + targetMsg,c)
							break
						}
					}
				}

			} else {

				//客戶端主動下線
				if msg == "exit" {
					//將當前客戶端從線上使用者中除名
					//向其他使用者傳送下線通知
					for name, c := range clientsMap {
						if c == client {
							delete(clientsMap, name)
						} else {
							c.conn.Write([]byte(name + "下線了"))
						}
					}
				}else if strings.Index(msg,"log@")==0 {
					//log@all
					//log@張全蛋
					filterName := strings.Split(msg, "@")[1]
					//向客戶端傳送它的聊天日誌
					go sendLog2Client(client,filterName)
				} else {
					client.conn.Write([]byte("已閱:" + msg))
				}

			}

		}
	}

}

//向客戶端傳送它的聊天日誌
func sendLog2Client(client Client,filterName string) {
	//讀取聊天日誌
	logBytes, e := ioutil.ReadFile("D:/BJBlockChain1801/demos/W4/day1/01ChatRoomII/logs/" + client.name + ".log")
	SHandleError(e,"ioutil.ReadFile")

	if filterName != "all"{
		//查詢與某個人的聊天記錄
		//從內容中篩選出帶有【filterName#或filterName:】的行,拼接起來
		logStr := string(logBytes)
		targetStr := ""
		lineSlice := strings.Split(logStr, "\n")
		for _,lineStr := range lineSlice{
			if len(lineStr)>20{
				contentStr := lineStr[20:]
				if strings.Index(contentStr,filterName+"#")==0 || strings.Index(contentStr,filterName+":")==0{
					targetStr += lineStr+"\n"
				}
			}
		}
		client.conn.Write([]byte(targetStr))
	}else{
		//查詢所有的聊天記錄
		//向客戶端傳送
		client.conn.Write(logBytes)
	}

}

//將客戶端說的一句話記錄在【以他的名字命名的檔案裡】
func writeMsgToLog(msg string, client Client) {
	//開啟檔案
	file, e := os.OpenFile(
		"D:/BJBlockChain1801/demos/W4/day1/01ChatRoomII/logs/"+client.name+".log",
		os.O_CREATE|os.O_WRONLY|os.O_APPEND,
		0644)
	SHandleError(e, "os.OpenFile")
	defer file.Close()

	//追加這句話
	logMsg := fmt.Sprintln(time.Now().Format("2006-01-02 15:04:05"), msg)
	file.Write([]byte(logMsg))
}

客戶端實現

import (
	"net"
	"fmt"
	"os"
	"bufio"
	"io"
	"flag"
)

var (
	chanQuit = make(chan bool, 0)
	conn     net.Conn
)

func CHandleError(err error, why string) {
	if err != nil {
		fmt.Println(why, err)

		os.Exit(1)
	}
}

func main() {

	//TODO:在命令列引數中攜帶暱稱
	nameInfo := [3]interface{}{"name", "無名氏", "暱稱"}
	retValuesMap := GetCmdlineArgs(nameInfo)
	name := retValuesMap["name"].(string)

	//撥號連線,獲得connection
	var e error
	conn, e = net.Dial("tcp", "127.0.0.1:8888")
	CHandleError(e, "net.Dial")
	defer func() {
		conn.Close()
	}()

	//在一條獨立的協程中輸入,併傳送訊息
	go handleSend(conn,name)

	//在一條獨立的協程中接收服務端訊息
	go handleReceive(conn)

	//設定優雅退出邏輯
	<-chanQuit

}

func handleReceive(conn net.Conn) {
	buffer := make([]byte, 1024)
	for {
		n, err := conn.Read(buffer)
		if err != io.EOF {
			CHandleError(err, "conn.Read")
		}

		if n > 0 {
			msg := string(buffer[:n])
			fmt.Println(msg)
		}
	}

}

func handleSend(conn net.Conn,name string) {
	//TODO:傳送暱稱到服務端
	_, err := conn.Write([]byte(name))
	CHandleError(err,"conn.Write([]byte(name))")

	reader := bufio.NewReader(os.Stdin)
	for {
		//讀取標準輸入
		lineBytes, _, _ := reader.ReadLine()

		//傳送到服務端
		_, err := conn.Write(lineBytes)
		CHandleError(err, "conn.Write")

		//正常退出
		if string(lineBytes) == "exit" {
			os.Exit(0)
		}

	}
}

func GetCmdlineArgs(argInfos ...[3]interface{}) (retValuesMap map[string]interface{}) {

	fmt.Printf("type=%T,value=%v\n", argInfos, argInfos)

	//初始化返回結果
	retValuesMap = map[string]interface{}{}

	//預定義【使用者可能輸入的各種型別的指標】
	var strValuePtr *string
	var intValuePtr *int

	//預定義【使用者可能輸入的各種型別的指標】的容器
	//使用者可能輸入好幾個string型的引數值,存放在好幾個string型的指標中,將這些同種型別的指標放在同種型別的map中
	//例如:flag.Parse()了以後,可以根據【strValuePtrsMap["cmd"]】拿到【存放"cmd"值的指標】
	var strValuePtrsMap = map[string]*string{}
	var intValuePtrsMap = map[string]*int{}

	/*	var floatValuePtr *float32
		var floatValuePtrsMap []*float32
		var boolValuePtr *bool
		var boolValuePtrsMap []*bool*/

	//遍歷使用者需要接受的所有命令定義
	for _, argArray := range argInfos {

		/*
		先把每個命令的名稱和用法拿出來,
		這倆貨都是string型別的,所有都可以通過argArray[i].(string)輕鬆愉快地獲得其字串
		一個叫“cmd”,一個叫“你想幹嘛”
		"cmd"一會會用作map的key
		*/
		//[3]interface{}
		//["cmd" "未知型別" "你想幹嘛"]
		//["gid"     0     "要查詢的商品ID"]
		//上面的破玩意型別[string 可能是任意型別 string]
		nameValue := argArray[0].(string)  //拿到第一個元素的string值,是命令的name
		usageValue := argArray[2].(string) //拿到最後一個元素的string值,是命令的usage

		//判斷argArray[1]的具體型別
		switch argArray[1].(type) {
		case string:
			//得到【存放cmd的指標】,cmd的值將在flag.Parse()以後才會有
			//cmdValuePtr = flag.String("cmd", argArray[1].(string), "你想幹嘛")
			strValuePtr = flag.String(nameValue, argArray[1].(string), usageValue)

			//將這個破指標以"cmd"為鍵,存在【專門放置string型指標的map,即strValuePtrsMap】中
			strValuePtrsMap[nameValue] = strValuePtr

		case int:
			//得到【存放gid的指標】,gid的值將在flag.Parse()以後才會有
			//gidValuePtr = flag.String("gid", argArray[1].(int), "商品ID")
			intValuePtr = flag.Int(nameValue, argArray[1].(int), usageValue)

			//將這個破指標以"gid"為鍵,存在【專門放置int型指標的map,即intValuePtrsMap】中
			intValuePtrsMap[nameValue] = intValuePtr
		}

	}

	/*
	程式執行到這裡,所有不同型別的【存值指標】都放在對相應型別的map中了
	flag.Parse()了以後,可以從map中以引數名字獲取出【存值指標】,進而獲得【使用者輸入的值】
	*/

	//使用者輸入完了,解析,【使用者輸入的值】全都放在對應的【存值指標】中
	flag.Parse()

	/*
	遍歷各種可能型別的【存值指標的map】
	*/
	if len(strValuePtrsMap) > 0 {
		//從【cmd存值指標的map】中拿取cmd的值,還以cmd為鍵存入結果map中
		for k, vPtr := range strValuePtrsMap {
			retValuesMap[k] = *vPtr
		}
	}
	if len(intValuePtrsMap) > 0 {
		//從【gid存值指標的map】中拿取gid的值,還以gid為鍵存入結果map中
		for k, vPtr := range intValuePtrsMap {
			retValuesMap[k] = *vPtr
		}
	}

	//返回結果map
	return
}

學院Go語言視訊主頁
https://edu.csdn.net/lecturer/1928

[清華團隊帶你實戰區塊鏈開發]

(https://ke.qq.com/course/344443?tuin=3d17195d)
掃碼獲取海量視訊及原始碼 QQ群:721929980
在這裡插入圖片描述

相關文章