本節完整程式碼:GitHub
本文是關於使用 ReactJS 和 Go 構建聊天應用程式的系列文章的第 4 部分。你可以在這裡找到第 3 部分 - 前端實現
這節主要實現處理多個客戶端訊息的功能,並將收到的訊息廣播到每個連線的客戶端。在本系列的這一部分結束時,我們將:
- 實現了一個池機制,可以有效地跟蹤 WebSocket 服務中的連線數。
- 能夠將任何收到的訊息廣播到連線池中的所有連線。
- 當另一個客戶端連線或斷開連線時,能夠通知現有的客戶端。
在本課程的這一部分結束時,我們的應用程式看起來像這樣:
拆分 Websocket 程式碼
現在已經完成了必要的基本工作,我們可以繼續改進程式碼庫。可以將一些應用程式拆分為子包以便於開發。
現在,理想情況下,你的 main.go
檔案應該只是 Go 應用程式的入口,它應該相當小,並且可以呼叫專案中的其他包。
注意 - 我們將參考非官方標準的 Go 專案結構佈局 - golang-standards/project-layout
讓我們在後端專案目錄中建立一個名為 pkg/
的新目錄。在此期間,我們將要建立另一個名為 websocket/
的目錄,該目錄將包含 websocket.go
檔案。
我們將把目前在 main.go
檔案中使用的許多基於 WebSocket 的程式碼移動到這個新的 websocket.go
檔案中。
注意 - 需要注意的一件事是,當複製函式時,需要將每個函式的第一個字母大寫,我們希望這些函式對專案的其餘部分可匯出。
package websocket
import (
"fmt"
"io"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return ws, err
}
return ws, nil
}
func Reader(conn *websocket.Conn) {
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
fmt.Println(string(p))
if err := conn.WriteMessage(messageType, p); err != nil {
log.Println(err)
return
}
}
}
func Writer(conn *websocket.Conn) {
for {
fmt.Println("Sending")
messageType, r, err := conn.NextReader()
if err != nil {
fmt.Println(err)
return
}
w, err := conn.NextWriter(messageType)
if err != nil {
fmt.Println(err)
return
}
if _, err := io.Copy(w, r); err != nil {
fmt.Println(err)
return
}
if err := w.Close(); err != nil {
fmt.Println(err)
return
}
}
}
複製程式碼
現在已經建立了這個新的 websocket
包,然後我們想要更新 main.go
檔案來呼叫這個包。首先必須在檔案頂部的匯入列表中新增一個新的匯入,然後可以通過使用 websocket.
來呼叫該包中的函式。像這樣:
package main
import (
"fmt"
"net/http"
"realtime-chat-go-react/backend/pkg/websocket"
)
func serveWs(pool *websocket.Pool, w http.ResponseWriter, r *http.Request) {
fmt.Println("WebSocket Endpoint Hit")
conn, err := websocket.Upgrade(w, r)
if err != nil {
fmt.Fprintf(w, "%+v\n", err)
}
client := &websocket.Client{
Conn: conn,
Pool: pool,
}
pool.Register <- client
client.Read()
}
func setupRoutes() {
pool := websocket.NewPool()
go pool.Start()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(pool, w, r)
})
}
func main() {
fmt.Println("Distributed Chat App v0.01")
setupRoutes()
http.ListenAndServe(":8080", nil)
}
複製程式碼
經過這些修改,我們應該檢查一下這些是否破壞了現有的功能。嘗試再次執行後端和前端,確保仍然可以傳送和接收訊息:
$ cd backend/
$ go run main.go
複製程式碼
如果成功,我們可以繼續擴充套件程式碼庫來處理多客戶端。
到目前為止,目錄結構應如下所示:
- backend/
- - pkg/
- - - websocket/
- - - - websocket.go
- - main.go
- - go.mod
- - go.sum
- frontend/
- ...
複製程式碼
處理多客戶端
現在已經完成了基本的操作,我們可以繼續改進後端並實現處理多個客戶端的功能。
為此,我們需要考慮如何處理與 WebSocket 服務的連線。每當建立新連線時,我們都必須將它們新增到現有連線池中,並確保每次傳送訊息時,該池中的每個人都會收到該訊息。
使用 Channels
我們需要開發一個具有大量併發連線的系統。在該連線的持續時間內都會啟動新的 goroutine
去處理每一個連線。這意味著我們必須關心這些併發 goroutine
之間的通訊,並確保執行緒安全。
當進一步實現 Pool
結構時,我們必須考慮使用 sync.Mutex
來阻塞其他 goroutine
同時訪問/修改資料,或者我們也可以使用 channels
。
對於這個專案,我認為最好使用 channels
並且以安全的方式在多個併發的 goroutine
中進行通訊。
注意 - 如果想進一步瞭解 Go 中的
channels
,可以在這裡檢視我的其他文章:Go Channels Tutorial
client.go
我們先建立一個名為 client.go
新檔案,它將存在於 pkg/websocket
目錄中,在檔案中將定義一個包含以下內容的 Client
結構體:
- ID:特定連線的唯一可識別字串
- Conn:指向
websocket.Conn
的指標 - Pool:指向
Pool
的指標
還需要定義一個 Read()
方法,該方法將一直監聽此 Client
的 websocket 連線上發出的新訊息。
如果收到新訊息,它將把這些訊息傳遞給池的 Broadcast
channel,該 channel 隨後將接收的訊息廣播到池中的每個客戶端。
package websocket
import (
"fmt"
"log"
"github.com/gorilla/websocket"
)
type Client struct {
ID string
Conn *websocket.Conn
Pool *Pool
}
type Message struct {
Type int `json:"type"`
Body string `json:"body"`
}
func (c *Client) Read() {
defer func() {
c.Pool.Unregister <- c
c.Conn.Close()
}()
for {
messageType, p, err := c.Conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
message := Message{Type: messageType, Body: string(p)}
c.Pool.Broadcast <- message
fmt.Printf("Message Received: %+v\n", message)
}
}
複製程式碼
太棒了,我們已經在程式碼中定義了客戶端,繼續實現池。
Pool 結構體
我們在 pkg/websocket
目錄下建立一個新檔案 pool.go
。
首先定義一個 Pool
結構體,它將包含我們進行併發通訊所需的所有 channels
,以及一個客戶端 map
。
package websocket
import "fmt"
type Pool struct {
Register chan *Client
Unregister chan *Client
Clients map[*Client]bool
Broadcast chan Message
}
func NewPool() *Pool {
return &Pool{
Register: make(chan *Client),
Unregister: make(chan *Client),
Clients: make(map[*Client]bool),
Broadcast: make(chan Message),
}
}
複製程式碼
我們需要確保應用程式中只有一個點能夠寫入 WebSocket 連線,否則將面臨併發寫入問題。所以,定義了 Start()
方法,該方法將一直監聽傳遞給 Pool
channels 的內容,然後,如果它收到傳送給其中一個 channel 的內容,它將採取相應的行動。
- Register - 當新客戶端連線時,
Register channel
將向此池中的所有客戶端傳送New User Joined...
- Unregister - 登出使用者,在客戶端斷開連線時通知池
- Clients - 客戶端的布林值對映。可以使用布林值來判斷客戶端活動/非活動
- Broadcast - 一個 channel,當它傳遞訊息時,將遍歷池中的所有客戶端並通過套接字傳送訊息。
程式碼:
func (pool *Pool) Start() {
for {
select {
case client := <-pool.Register:
pool.Clients[client] = true
fmt.Println("Size of Connection Pool: ", len(pool.Clients))
for client, _ := range pool.Clients {
fmt.Println(client)
client.Conn.WriteJSON(Message{Type: 1, Body: "New User Joined..."})
}
break
case client := <-pool.Unregister:
delete(pool.Clients, client)
fmt.Println("Size of Connection Pool: ", len(pool.Clients))
for client, _ := range pool.Clients {
client.Conn.WriteJSON(Message{Type: 1, Body: "User Disconnected..."})
}
break
case message := <-pool.Broadcast:
fmt.Println("Sending message to all clients in Pool")
for client, _ := range pool.Clients {
if err := client.Conn.WriteJSON(message); err != nil {
fmt.Println(err)
return
}
}
}
}
}
複製程式碼
websocket.go
太棒了,我們再對 websocket.go
檔案進行一些小修改,並刪除一些不再需要的函式和方法:
package websocket
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return nil, err
}
return conn, nil
}
複製程式碼
更新 main.go
最後,我們需要更新 main.go
檔案,在每個連線上建立一個新 Client
,並使用 Pool
註冊該客戶端:
package main
import (
"fmt"
"net/http"
"github.com/TutorialEdge/realtime-chat-go-react/pkg/websocket"
)
func serveWs(pool *websocket.Pool, w http.ResponseWriter, r *http.Request) {
fmt.Println("WebSocket Endpoint Hit")
conn, err := websocket.Upgrade(w, r)
if err != nil {
fmt.Fprintf(w, "%+v\n", err)
}
client := &websocket.Client{
Conn: conn,
Pool: pool,
}
pool.Register <- client
client.Read()
}
func setupRoutes() {
pool := websocket.NewPool()
go pool.Start()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(pool, w, r)
})
}
func main() {
fmt.Println("Distributed Chat App v0.01")
setupRoutes()
http.ListenAndServe(":8080", nil)
}
複製程式碼
測試
現在已經做了所有必要的修改,我們應該測試已經完成的工作並確保一切按預期工作。
啟動你的後端應用程式:
$ go run main.go
Distributed Chat App v0.01
複製程式碼
如果你在幾個瀏覽器中開啟 http://localhost:3000,可以看到到它們會自動連線到後端 WebSocket 服務,現在我們可以傳送和接收來自同一池內的其他客戶端的訊息!
總結
在本節中,我們設法實現了一種處理多個客戶端的方法,並向連線池中連線的每個人廣播訊息。
現在開始變得有趣了。我們可以在下一節中新增新功能,例如自定義訊息。
下一節:Part 5 - 優化前端
原文:tutorialedge.net/projects/ch…
作者:Elliot Forbes 譯者:咔嘰咔嘰 校對:polaris1119