golang 实现 tcp-聊天室
golang 實(shí)現(xiàn) tcp-聊天室
以下代碼都已傳到github平臺(tái):https://github.com/ElzatAhmed/go-tcp-chatroom
想必做golang網(wǎng)絡(luò)開發(fā)的同學(xué)對(duì)gosdk中net/http包十分熟悉,使用net/http可以快速實(shí)現(xiàn)http服務(wù)器的搭建。但是大家對(duì)tcp協(xié)議的直接使用可能就沒有那么熟悉了。在接下來的文檔里我將講解如何使用gosdk中的net包實(shí)現(xiàn)一個(gè)以tcp協(xié)議為基礎(chǔ)的簡(jiǎn)易的終端群聊聊天室應(yīng)用。
在dive in之前,需要回顧/介紹以下golang并發(fā)編程中的核心概念:
goroutine
在go.dev中是這么介紹goroutine的 A goroutine is a lightweight thread managed by the Go runtime,goroutine是一種被go runtime管理的輕量級(jí)線程,其并不會(huì)直接對(duì)應(yīng)到重量級(jí)的系統(tǒng)線程上,而是被go scheduler調(diào)度到少量的系統(tǒng)線程上進(jìn)行維護(hù)。所以在一個(gè)go程序中,我們可以同時(shí)創(chuàng)造成千上萬個(gè)goroutine,將其交給go runtime去進(jìn)行調(diào)度和垃圾回收。
channel
不知道你是否聽說過一種理論叫做CSP,Communicating Sequential Processes,go將其核心思想總結(jié)成以下 Do not communicate by sharing memory, share memory by communicating,也就是說不要利用共享內(nèi)存的方式去交互,利用交互的方式去共享內(nèi)存(這里的交互這一名詞可能用得不太對(duì),大家可以以communication去理解)。channel正是利用這種思想而定義的。channel可以被理解為支持寫入和讀取的消息管道,其是完全并發(fā)安全,channel的每一個(gè)元操作都是以單線程的方式進(jìn)行的,這就代表我們不僅能利用channel進(jìn)行線程間通信,我們還可以利用它實(shí)現(xiàn)鎖。以下是一種利用buffered channel (size大于0的channel) 實(shí)現(xiàn)的結(jié)構(gòu)非常簡(jiǎn)單的鎖:
package chlock// Chlock is a locker implemented using channel type Chlock struct {ch chan interface{} }// New returns the pointer to a new Chlock struct func New() *Chlock {return &Chlock{ch: make(chan interface{}, 1),} }// Lock writes once to the channel and // blocks all the other goroutines which tries to write func (lock *Chlock) Lock() {lock.ch <- struct{}{} }// Unlock reads from the channel and unblock one goroutine func (lock *Chlock) Unlock() {<-lock.ch }創(chuàng)建1000個(gè)goroutine對(duì)同一個(gè)全局變量count進(jìn)行加一操作去測(cè)試我們實(shí)現(xiàn)的ChLock是否有效:
package mainimport ("fmt""sync""github.com/elzatahmed/channel-lock/chlock" )var count intfunc main() {var wg sync.WaitGroupwg.Add(1000)lock := chlock.New()for i := 0; i < 1000; i++ {go add(lock, &wg)}wg.Wait()fmt.Printf("count = %d\n", count) }func add(lock *chlock.Chlock, wg *sync.WaitGroup) {lock.Lock()defer wg.Done()defer lock.Unlock()count++ }輸出:
? channel-lock go run main.go count = 1000介紹完goroutine和channel之后我們就dive in到實(shí)現(xiàn)tcp聊天室的過程當(dāng)中:
tcp聊天室的實(shí)現(xiàn)
首先我們開始打造聊天室的模型基礎(chǔ):
模型
第一個(gè)模型即用戶模型,我們簡(jiǎn)單的以用戶的名字作為用戶的主鍵,并為其創(chuàng)建兩個(gè)channel:
message 是我定義的聊天室消息模型:
type msgType uint8// message is the model for every message flows through the chatroom type message struct {typ msgType // 利用msgType來區(qū)分其應(yīng)該是系統(tǒng)消息還是用戶消息from stringcontent stringwhen time.Time }const (msgTypeUser msgType = iotamsgTypeSystem )接下來建立聊天室模型,可想而知聊天室由多個(gè)用戶組成,所以我們需要一個(gè)存儲(chǔ)用戶指針的slice,同時(shí)利用聊天室名去作為主鍵區(qū)分不同的聊天室,為了聊天室強(qiáng)化聊天室的功能在添加一個(gè)歷史消息組件,存儲(chǔ)一定數(shù)量的歷史消息,在新的用戶進(jìn)入聊天室后將歷史消息一并發(fā)送給用戶:
// chatroom is the collection of users which they can receive every message from each other type chatroom struct {name stringusers []*usermu sync.Mutex // 在對(duì)用戶slice進(jìn)行操作進(jìn)行加鎖時(shí)使用his *history }最后時(shí)server模型的創(chuàng)建,一個(gè)tcp聊天室中應(yīng)包含多個(gè)群聊聊天室和多個(gè)群聊用戶,我們利用map結(jié)構(gòu)的方式去存儲(chǔ)這些數(shù)據(jù),同時(shí)聊天室需要有一個(gè)網(wǎng)絡(luò)地址:
// chatServer is the listening and dispatching server for tcp chatroom, // it stores information about all the rooms and all the users ever created type chatServer struct {addr stringrooms map[string]*chatroomusers map[string]*usermuRoom sync.MutexmuUser sync.Mutex }交互過程
在開始講解交互過程之前,我們首先需要了解net包中的一個(gè)interface net.Conn:
type Conn interface {Read(b []byte) (n int, err error)Write(b []byte) (n int, err error)Close() errorLocalAddr() AddrRemoteAddr() AddrSetDeadline(t time.Time) errorSetReadDeadline(t time.Time) errorSetWriteDeadline(t time.Time) error }net.Conn實(shí)現(xiàn)了io.Reader和io.Writer接口,也就是說我們可以從Conn中讀取字節(jié),也可以往Conn中寫入字節(jié),更好的是我們可以利用bufio包中的工具去對(duì)他進(jìn)行操作。
user goroutine
交互過程的第一步,我們要在user結(jié)構(gòu)體下編寫編寫一個(gè)listen的函數(shù),其主要作用就是讀取receive channel中的內(nèi)容并編寫到net.Conn中,在這我利用到了go經(jīng)典的select case語句模型,也就是說當(dāng)我從done channel中讀取到任何一種內(nèi)容時(shí)我就要停止讀取:
// listen starts a loop to receive from the receive channel and writes to the net.Conn func (u *user) listen(conn net.Conn) {for {select {case msg := <-u.receive:_, _ = conn.Write(msg.bytes()) // msg.bytes()是我在message結(jié)構(gòu)體下定義的將message打包成字符串// 并轉(zhuǎn)化成字節(jié)數(shù)組返回的函數(shù)case <-u.done:break}} }這里我為了簡(jiǎn)便忽視了conn.Write返回的錯(cuò)誤(這是一個(gè)不好的習(xí)慣,建議大家在編寫時(shí)處理該錯(cuò)誤)。我們將在后續(xù)的server啟動(dòng)函數(shù)中將該函數(shù)對(duì)待成goroutine來啟動(dòng)它。
chatroom 廣播
我們?cè)诙xuser結(jié)構(gòu)體下的listen函數(shù)時(shí)從receive channel中讀取了message,那么我們需要一個(gè)goroutine往該channel中寫入內(nèi)容。chatroom下的broadcast(廣播)函數(shù),接受一個(gè)message并將其廣播發(fā)送給聊天室中的每一個(gè)用戶:
// broadcast sends the message to every user in the chatroom except the sender func (room *chatroom) broadcast(msg message) {for _, u := range room.users {if u.name == msg.from {continue}// 這里啟動(dòng)的goroutine是為了定義寫入的超時(shí)時(shí)間(因?yàn)閷懭肟赡軙?huì)block)// 如果不需要也可以拋棄這里的goroutine,直接進(jìn)行寫入go func(u *user) {select {case u.receive <- msg:breakcase <-time.After(3 * time.Second):break}}(u)} }該函數(shù)將在下一部分chatroom goroutine內(nèi)容中定義的函數(shù)中利用
chatroom goroutine
每一個(gè)聊天室對(duì)每一個(gè)用戶連接都需要保持一個(gè)tcp連接,即tcp連接的數(shù)量 = 聊天室1 * 聊天室1用戶數(shù)量 + 聊天室2 * 聊天室2用戶數(shù)量 + ···
每一個(gè)tcp連接利用下面定義的chatroom結(jié)構(gòu)體下的newUser函數(shù)來維持:
上述函數(shù)的主體內(nèi)容和步驟可以總結(jié)為以下內(nèi)容:
server的建立
有c++、java或python tcp編程的經(jīng)驗(yàn)的同學(xué)都知道,tcp連接是由socket實(shí)現(xiàn)的,socket分為兩種(監(jiān)聽socket和通信socket),每個(gè)socket又是四元組(兩組ip和端口號(hào))。golang中并沒有socket的概念,golang中的兩種socket就是net包中的net.Listener和net.Conn接口,net.Listner對(duì)應(yīng)監(jiān)聽socket,net.Conn對(duì)應(yīng)通信socket。net.Listener的建立我們只需要利用net包中的net.Listen函數(shù)并傳入?yún)f(xié)議名稱(這里是tcp)和服務(wù)器地址即可。在利用net.Listenet的Listen方法連接新的用戶創(chuàng)建通信socket(net.Conn),而這里的通信socket正式貫通了整個(gè)tcp聊天室的tcp連接體。
// Spin starts the chatServer at given address func (server *chatServer) Spin() {listener, err := net.Listen("tcp", server.addr)if err != nil {log.Fatalf("failed to start the server at %s, err: %s\n", server.addr, err.Error())}log.Printf("server started at address %s...\n", server.addr)for {// 開啟循環(huán)接受新的連接conn, err := listener.Accept()log.Printf("server accepted a new connection from %s\n", conn.RemoteAddr())if err != nil {continue}// 將獲取的conn對(duì)象傳給spin方法(注意不是Spin方法),開啟新的goroutinego server.spin(conn)} }連接建立以后,我們需要提前定一個(gè)簡(jiǎn)單的協(xié)議讓用戶決定自己的用戶名和想要加入的聊天室名稱。這里我們就定一行字符串username;chatroomName利用分號(hào)分割用戶名和聊天室名稱。在服務(wù)器接收到連接以后的第一步就是要讀取用戶發(fā)來的協(xié)議請(qǐng)求,解析創(chuàng)建并將用戶分配到對(duì)應(yīng)的聊天室中,其次在啟動(dòng)新的chatroom goroutine和user goroutine即可:
// spin do the protocol procedure and starts the connection goroutines func (server *chatServer) spin(conn net.Conn) {reader := bufio.NewReader(conn)bytes, err := reader.ReadBytes('\n')if err != nil {log.Printf("connection failed with client %s with err: %s\n",conn.RemoteAddr(), err.Error())return}username, roomname, err := parseProtocol(bytes)if err != nil {_, _ = conn.Write(comm.BytesProtocolErr)return}if _, ok := server.users[username]; ok {_, _ = conn.Write(comm.BytesUsernameExists)_ = conn.Close()log.Printf("connection from %s closed by server\n", conn.RemoteAddr())return}log.Printf("connecting user %s to chatroom %s...\n", username, roomname)u := server.newUser(username)room, ok := server.rooms[roomname]if !ok {room = server.newRoom(roomname)}go room.newUser(u, conn)go u.listen(conn)log.Printf("user %s is connected to chatroom %s\n", username, roomname) }以上函數(shù)的內(nèi)容和步驟可以總結(jié)為以下:
到此服務(wù)器內(nèi)容全部結(jié)束,現(xiàn)在我們就開始測(cè)試我們的服務(wù)器,首先建立main函數(shù):
func main() {// 利用flag獲取命令行輸入?yún)?shù)host := flag.String("h", "127.0.0.1", "the host name of the server")port := flag.Int("p", 8888, "the port number of the server")flag.Parse()chatServer := server.New(*host, *port)chatServer.Spin() }在終端中啟動(dòng)服務(wù)器:
? go-tcp-chatroom go run server.go -h 127.0.0.1 -p 8888 2022/07/03 22:47:53 server started at address 127.0.0.1:8888...我們利用linux工具nc(netcat)進(jìn)行對(duì)服務(wù)器的tcp連接發(fā)送內(nèi)容:
? go-tcp-chatroom nc 127.0.0.1 8888 xiaoming;room1 2022-07-03 22:49:02 server: user xiaoming entered the chatroom and says hello! hello everyone! my name is xiaoming! 2022-07-03 22:49:31 server: user xiaohong entered the chatroom and says hello! 2022-07-03 22:49:37 xiaohong: hello xiaoming! 2022-07-03 22:49:42 xiaohong: my name is xiaohong! ? go-tcp-chatroom nc 127.0.0.1 8888 xiaohong;room1 2022-07-03 22:49:10 xiaoming: hello everyone! 2022-07-03 22:49:18 xiaoming: my name is xiaoming! 2022-07-03 22:49:31 server: user xiaohong entered the chatroom and says hello! hello xiaoming! my name is xiaohong! ? go-tcp-chatroom go run server.go -h 127.0.0.1 -p 8888 2022/07/03 22:47:53 server started at address 127.0.0.1:8888... 2022/07/03 22:48:52 server accepted a new connection from 127.0.0.1:65236 2022/07/03 22:49:02 connecting user xiaoming to chatroom room1... 2022/07/03 22:49:02 user xiaoming is connected to chatroom room1 2022/07/03 22:49:10 xiaoming -> room1: hello everyone! 2022/07/03 22:49:18 xiaoming -> room1: my name is xiaoming! 2022/07/03 22:49:26 server accepted a new connection from 127.0.0.1:65330 2022/07/03 22:49:31 connecting user xiaohong to chatroom room1... 2022/07/03 22:49:31 user xiaohong is connected to chatroom room1 2022/07/03 22:49:37 xiaohong -> room1: hello xiaoming! 2022/07/03 22:49:42 xiaohong -> room1: my name is xiaohong!PS: 源代碼中實(shí)現(xiàn)client端,大家可以在源代碼中查看client端實(shí)現(xiàn)邏輯,歡迎大家在github上關(guān)注我,如果上述中有表達(dá)錯(cuò)誤也希望大家能提出來!
總結(jié)
以上是生活随笔為你收集整理的golang 实现 tcp-聊天室的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 探索入门云计算风向标Amazon的ECS
- 下一篇: 住逻辑APP全面升级,只为让好设计完美落