[譯] 使用 Go 和 ReactJS 構建聊天系統 (四)

咔嘰咔嘰發表於2019-08-02

本節完整程式碼:GitHub

本文是關於使用 ReactJS 和 Go 構建聊天應用程式的系列文章的第 4 部分。你可以在這裡找到第 3 部分 - 前端實現

這節主要實現處理多個客戶端訊息的功能,並將收到的訊息廣播到每個連線的客戶端。在本系列的這一部分結束時,我們將:

  • 實現了一個池機制,可以有效地跟蹤 WebSocket 服務中的連線數。
  • 能夠將任何收到的訊息廣播到連線池中的所有連線。
  • 當另一個客戶端連線或斷開連線時,能夠通知現有的客戶端。

在本課程的這一部分結束時,我們的應用程式看起來像這樣:

[譯] 使用 Go 和 ReactJS 構建聊天系統 (四)

拆分 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 服務,現在我們可以傳送和接收來自同一池內的其他客戶端的訊息!

[譯] 使用 Go 和 ReactJS 構建聊天系統 (四)

總結

在本節中,我們設法實現了一種處理多個客戶端的方法,並向連線池中連線的每個人廣播訊息。

現在開始變得有趣了。我們可以在下一節中新增新功能,例如自定義訊息。

下一節:Part 5 - 優化前端


原文:tutorialedge.net/projects/ch…

作者:Elliot Forbes 譯者:咔嘰咔嘰 校對:polaris1119

本文由 GCTT 原創編譯,Go 中文網 榮譽推出

[譯] 使用 Go 和 ReactJS 構建聊天系統 (四)

相關文章