《Go Chat Application》系列文章筆記 — 用 Golang 打造一個 WebSocket 聊天室 Server

Emmie Lin
9 min readFeb 19, 2022

--

前言

最近剛好在學習 Golang,而會選擇 Websocket 聊天室這個主題,也是因為對 Websocket 一直都是有點一知半解(因為以前使用的經驗大多都是使用現成的服務或 library,沒有很深入了解這個東西的運作機制),所以想要親手嘗試做做看一個 WebSocket Server。

然而同時要用不熟悉的語言做出沒做過的應用,還是有點太高估自己了,於是便搜尋了很多有關製作 WebSocket 聊天室的資訊,於是最後找到這篇系列文章 — 《Go Chat Application》算是我覺得最詳細且最符合我需求的。

之所以寫這篇文章,是因為先前提到雖然網路上相關資源很多,但大多都是著重於實作面(二話不說直上程式碼?!),很少會把整個架構跟流程闡述清楚(但我本身比較駑鈍又是個非常需要先了解流程的人..)。

所以本文主要在整理我從文章中所理解的設計 WebSocket 聊天室 Server 的方法,順便熟悉 Golang 的語法,程式碼的部分不會放太多(有興趣可以參考原作者的文章)。並且只會整理到第一、二篇 Server 端程式碼的內容,最後成品會是一個可以在同個網頁中開啟多個聊天視窗、同時與多人聊天的一對一簡易聊天室。

WebSocket 簡介

再開始探討方法之前,先簡述一下何謂 Websocket。我們都知道網頁是依照 HTTP 這個協定標準來傳輸的,而 WebSocket 就是另一種傳輸協議。兩者最大的差異在於,HTTP 協議之下,伺服器端永遠是被動等待用戶端傳來 Request ,而在 Websocket 協議之下,伺服器端可以主動推送訊息給用戶端,也就是說兩者之間的溝通可以是雙向且持久連接的,實現了客戶端與伺服器端的即時資料傳輸。

然而,要透過 WebSocket 雙向溝通,第一步還是需要先由客戶端透過 HTTP 的形式來發送一個要求連線的 Request 給伺服器端(這個過程通常被稱為「交握」),接下來才會開啟兩者之間的長連線。

定義資料結構

一個基本的聊天室,一定會有使用者(聊天成員)、訊息、聊天室這些元素存在,使用者一定會有某些資訊需要被紀錄,像是名字、ID 等等,所以第一步就先來整理需要的資料結構:

WsServer

我們運行的 App 會有一個唯一的 WsServer實例,目的在於記錄及管理所有包含所有聊天室、使用者等等的相關資料。

type WsServer struct {
clients map[*Client]bool // 當前所有使用者
register chan *Client // 新加入的使用者佇列
unregister chan *Client // 等待退出的使用者佇列
broadcast chan []byte // 等待被推播的訊息陣列
rooms map[*Room]bool //當前所有房間
}

使用者 Client

當用戶端連線成功,在 Server 端就會為該次連線創建一個 Client實體(可想像成一個瀏覽器分頁就代表著一個 Client)。

type Client struct {
conn *websocket.Conn // 當前 websocket 連線相關資訊
wsServer *WsServer // 當前 App 唯一的 WsServer 實例
send chan []byte // 接收訊息的佇列
rooms map[*Room]bool // 該使用者所有加入的房間
Name string `json:"name"`
ID uuid.UUID `json:"id"`
}

訊息 Message

當用戶端傳送 json 資料進來時,會被解析成 Struct,帶有與訊息相關的資訊。而 Server 主動推送訊息時再將 Struct 轉成 json 格式送出。 訊息主要可分為兩種類型 :

  1. 動作訊息類型:主要是為了某些功能的操作而傳遞的資訊,用戶端及 Server 端都有可能會送出這種類型的訊息 (例如:客戶端「將使用者加入房間的動作」 / Server 端「使用者加入/離開的動作」)。
  2. 文字訊息類型:使用者在聊天室打字後發送訊息的動作,這種類型的訊息也只會由用戶端所發出。
type Message struct {
Action string `json:"action"` // 訊息類型
Message string `json:"message"` // 訊息內容
Target *Room `json:"target"` // 欲傳送到的目標房間
Sender *Client `json:"sender"` // 傳送者
}

聊天室 Room

Room 內部的架構和 WsServer 有點類似,有自己專屬的 register/unregister/broadcast channel 以及 clients。

type Room struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Private bool `json:"private"` // 是否為私人(一對一)
clients map[*Client]bool
register chan *Client
unregister chan *Client
broadcast chan *Message
}

以關聯式的資料型態來想像的話大概就是:

  • 一個 WsServer 底下會有許多個 Room,也會有許多個 Client(WsServer 與 Client 和 Room 皆為一對多)
  • Room 則會有許多個 Client,同時 Client 也能加入多個 Room(Client 和 Room 為多對多)
  • 而一個 Client 會發送多個 Message(Client 和 Message 為一對多)
資料之間的關聯性

運作流程與架構

定義完資料結構,就可以接著看如何實現聊天室的功能,我把主要功能(傳送一對一聊天訊息)的流程分成兩個部分來說明,1. Server 跑起來之後 2. 使用者傳遞訊息到 Server 之後。

Server 跑起來之後

初始化的作業, 開啟多個線程監聽使用者的行為

  • 主線程監聽 HTTP Request:
    當用戶發送 Request 要求連線時,將連線升級成 WS 協議,建立新的 Client 實例,將使用者丟入 WsServer.register channel。接著開啟兩個 gorutine 分別負責:
    (1) 訊息推送:監聽 client.send channel,當該 channel 有新訊息,就將訊息透過 WebSocket 連線推送給用戶。
    (2) 訊息接收:監聽來自用戶端傳來的新訊息,一有訊息會根據不同訊息類型(Message.Action)進行不同的行為,稍等會提到。
  • 同步開啟監聽 broadcast、register、unregister 三個 channel 的 gorutine:
    - broadcast:一有訊息就推到 Server 所有用戶收到的訊息佇列(client.send)
    - register:一有用戶被丟進佇列就將用戶加入 Server.clients
    - unregister:一有用戶被丟進佇列就將用戶從 Server.clients 移除

使用者傳遞訊息到 Server 之後

由使用者推送訊息給 Server 後才會觸發的功能(建立聊天室、訊息推播)

依照傳入的訊息類型(Message.Action)不同,分為兩種行為:

動作類型——將使用者將入房間

  • 依傳入資訊找到用戶要傳送一對一訊息的對象用戶。
  • 創建一個專屬的新房間,將兩位用戶都加入。
  • 若此時沒有該房間存在,就會建立一個新的房間。而建立房間時會同時開啟監聽 broadcast、register、unregister 三個 channel 的 gorutine:
    - broadcast:一有訊息就推到 Room 所有用戶收到的訊息佇列(client.send)
    - register:一有用戶被丟進佇列就將用戶加入 Room.clients
    - unregister:一有用戶被丟進佇列就將用戶從 Room.clients 移除

訊息類型——使用者傳送文字訊息到房間

  • 依傳入資訊找到對應房間。
  • 將訊息送入房間的推播佇列(Room.broacast)。此時監聽該 channel 的線程就會將訊息推送到所有 Room.clients 的 send channel,而每個監聽 send channel 的線程就會將訊息透過 WebSocket 連線推送給用戶。結果就是所有在該聊天室的成員包含傳送者皆會收到該則訊息。

至此,已經將聊天室的運作流程大致分析完畢,當然還有許多細節的部分沒有提到,但這些就是在程式碼裡面了。經過這次逐步跟著文章手把手教學,把成品實作了一遍(雖然大部分是在複製貼上XD),但理解往往比實際動手打程式碼花費上更多倍的時間。

第一次寫這種分析別人如何設計應用程式的文章,沒寫什麼程式卻花了我比以往還多的時間才完成,也不曉得是否能夠有人會看甚至有幫助,但就當作是給自己這段學習歷程的紀錄吧(太久沒寫文章廢話好像也特多)。

參考資料

--

--