Go 每日一庫之 gotalk

簡介

gotalk專注於進程間的通信,致力於簡化通信協議和流程。同時它:

那麼,讓我們來玩一下吧~

快速使用

本文代碼使用 Go Modules。

創建目錄並初始化:

$ mkdir gotalk && cd gotalk
$ go mod init github.com/darjun/go-daily-lib/gotalk

安裝gotalk庫:

$ go get -u github.com/rsms/gotalk

接下來讓我們來編寫一個簡單的 echo 程序,服務端直接返回收到的客戶端信息,不做任何處理。首先是服務端:

// get-started/server/server.go
package main

import (
  "log"

  "github.com/rsms/gotalk"
)

func main() {
  gotalk.Handle("echo", func(in string) (string, error) {
    return in, nil
  })
  if err := gotalk.Serve("tcp"":8080", nil); err != nil {
    log.Fatal(err)
  }
}

通過gotalk.Handle()註冊消息處理,它接受兩個參數。第一個參數爲消息名,字符串類型,保證唯一且可辨識即可。第二個參數爲處理函數,收到對應名稱的消息,調用該函數處理。處理函數接受一個參數,返回兩個值。正常處理完成通過第一個返回值傳遞處理結果,出錯時通過第二個返回值表示錯誤類型。

這裏的處理器函數比較簡單,接受一個字符串參數,直接原樣返回。

然後,調用gotalk.Serve()啓動服務器,監聽端口。它接受 3 個參數,協議類型、監聽地址、處理器對象。此處我們使用 TCP 協議,監聽本地8080端口,使用默認處理器對象,傳入nil即可。

服務器內部一直循環處理請求。

然後是客戶端:

func main() {
  s, err := gotalk.Connect("tcp"":8080")
  if err != nil {
    log.Fatal(err)
  }

  for i := 0; i < 5; i++ {
    var echo string
    if err := s.Request("echo""hello"&echo); err != nil {
      log.Fatal(err)
    }

    fmt.Println(echo)
  }

  s.Close()
}

客戶端首先調用gotalk.Connect()連接服務器,它接受兩個參數:協議和地址(IP + 端口)。我們使用與服務器一致的協議和地址即可。連接成功會返回一個連接對象。調用連接對象的Request()方法,即可向服務器發送消息。Request()方法接受 3 個參數。第一個參數爲消息名,這對應於服務器註冊的消息名,請求一個不存在的消息名會返回錯誤。第二個參數是傳給服務器的參數,有且只能有一個參數,對應處理器函數的入參。第三個參數爲返回值的指針,用於接受服務器返回的結果。

如果請求失敗,返回錯誤err。使用完成之後不要忘記關閉連接對象。

先運行服務器:

$ go run server.go

在開啓一個命令行,運行客戶端:

$ go run client.go
hello
hello
hello
hello
hello

實際上如果瞭解標準庫net/http,你應該就會發現,使用gotalk的服務端代碼與使用net/http編寫 Web 服務器非常相似。都非常簡單,清晰:

// get-started/http/main.go
package main

import (
  "fmt"
  "log"
  "net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "hello world")
}

func main() {
  http.HandleFunc("/", index)

  if err := http.ListenAndServe(":8888", nil); err != nil {
    log.Fatal(err)
  }
}

運行:

$ go run main.go

使用 curl 驗證:

$ curl localhost:8888
hello world

WebSocket

除了 TCP,gotalk還支持基於 WebSocket 協議的通信。下面我們使用  WebSocket 重寫上面的服務端程序,然後編寫一個簡單 Web 頁面與之通信。

服務端:

func main() {
  gotalk.Handle("echo", func(in string) (string, error) {
    return in, nil
  })

  http.Handle("/gotalk/", gotalk.WebSocketHandler())
  http.Handle("/", http.FileServer(http.Dir(".")))
  if err := http.ListenAndServe(":8080", nil); err != nil {
    log.Fatal(err)
  }
}

gotalk消息處理函數的註冊還是與前面的一樣。不同的是這裏將 HTTP 路徑/gotalk/的請求交由gotalk.WebSocketHandler()處理,這個處理器負責 WebSocket 請求。同時,在當前工作目錄開啓一個文件服務器,掛載到 HTTP 路徑/上。文件服務器是爲了客戶端方便地請求index.html頁面。最後調用http.ListenAndServe()開啓 Web 服務器,監聽端口 8080。

然後是客戶端,gotalk爲了方便 Web 程序的編寫,將 WebSocket 通信細節封裝在一個 JavaScript 文件gotalk.js中。可以直接從倉庫中的 js 目錄下獲取使用。接着我們編寫頁面index.html,引入gotalk.js

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <script type="text/javascript" src="gotalk/gotalk.js"></script>
  </head>
  <body>
    <input id="txt">
    <button id="snd">send</button><br>
    <script>
    let c = gotalk.connection()
      .on('open'() => log(`connection opened`))
      .on('close'reason => log(`connection closed (reason: ${reason})`))
    let btn = document.querySelector("#snd")
    let txt = document.querySelector("#txt")
    btn.onclick = async () ={
      let content = txt.value
      if (content.length === 0) {
        alert("no message")
        return
      }
      let res = await c.requestp('echo', content)
      log(`reply: ${JSON.stringify(res, null, 2)}`)
      return false
    }
    function log(message) {
      document.body.appendChild(document.createTextNode(message))
      document.body.appendChild(document.createElement("br"))
    }
    </script>
  </body>
</html>

首先調用gotalk.connection()連接服務端,返回一個連接對象。調用此對象的on()方法,分別註冊連接建立和斷開的回調。然後給按鈕添加回調,每次點擊將輸入框中的內容發送給服務端。調用連接對象的requestp()方法發送請求,第一個參數爲消息名,對應在服務端使用gotalk.Handle()註冊的名字。第二個即爲處理參數,會一併發送給服務端。這裏使用 Promise 處理異步請求和響應,爲了編寫方便和易於理解使用async-await同步的寫法。響應的內容直接顯示在頁面上:

注意,gotalk.js文件需要放在服務器運行目錄的gotalk目錄下。

協議格式

gotalk採用基於 ASCII 的協議格式,設計爲方便人類閱讀且靈活的。每條傳輸的消息都分爲幾個部分:類型標識、請求 ID、操作、消息內容。

看一個官方請求的示例:

+------------------ SingleRequest
|   +---------------- requestID   "0001"
|   |      +--------- operation   "echo" (text3Size 4, text3Value "echo")
|   |      |       +- payloadSize 25
|   |      |       |
r0001004echo00000019{"message":"Hello World"}

詳細格式可以查看官方文檔。

使用這種可閱讀的格式給問題排查帶來了極大的便利。但是在實際使用中,可能需要考慮安全和隱私的問題。

聊天室

examples內置一個基於 WebSocket 的聊天室示例程序。特性如下:

運行:

$ go run server.go

打開瀏覽器,輸入 "localhost:1235",顯示如下:

接下來就可以創建房間,在房間聊天了。

整個實現的有幾個要點:

其一,gotalk.WebSocketHandler()創建的 WebSocket 處理器可以設置連接回調:

gh := gotalk.WebSocketHandler()
gh.OnConnect = onConnect

在回調中設置隨機用戶名,並將當前連接的gotalk.Sock存儲下來,方便消息廣播:

func onConnect(s *gotalk.WebSocket) {
  socksmu.Lock()
  defer socksmu.Unlock()
  socks[s] = 1

  username := randomName()
  s.UserData = username
}

其二,gotalk設置處理器函數可以有兩個參數,第一個表示當前連接,第二個纔是實際接收到的消息參數。

其三,enableGracefulShutdown()函數實現了 Web 服務器的優雅關閉,非常值得學習。接收到SIGINT信號,先關閉所有的連接,再退出程序。注意監聽信號和運行 HTTP 服務器並不是同一個 goroutine,看它們是如何協作的:

func enableGracefulShutdown(server *http.Server, timeout time.Duration) chan struct{} {
  server.RegisterOnShutdown(func() {
    // close all connected sockets
    fmt.Printf("graceful shutdown: closing sockets\n")
    socksmu.RLock()
    defer socksmu.RUnlock()
    for s := range socks {
      s.CloseHandler = nil // avoid deadlock on socksmu (also not needed)
      s.Close()
    }
  })
  done := make(chan struct{})
  quit := make(chan os.Signal, 1)
  signal.Notify(quit, syscall.SIGINT)
  go func() {
    <-quit // wait for signal

    fmt.Printf("graceful shutdown initiated\n")
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    server.SetKeepAlivesEnabled(false)
    if err := server.Shutdown(ctx); err != nil {
      fmt.Printf("server.Shutdown error: %s\n", err)
    }

    fmt.Printf("graceful shutdown complete\n")
    close(done)
  }()
  return done
}

接收到SIGINT信號後done通道關閉,server.ListenAndServe()返回http.ErrServerClosed錯誤,退出循環:

done := enableGracefulShutdown(server, 5*time.Second)

// Start server
fmt.Printf("Listening on http://%s/\n", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  panic(err)
}

<- done

整個聊天室功能比較簡單,代碼也比較短,建議深入理解。在此基礎之上做擴展也比較簡單。

總結

gotalk實現了一個簡單、易用的通信庫。並且提供了 JavaScript 文件gotalk.js,方便 Web 程序的開發。協議格式清晰,易調試。內置豐富的示例。整個庫的代碼也不長,建議深入瞭解。

大家如果發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. gotalk GitHub:https://github.com/rsms/gotalk

  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

歡迎關注我的微信公衆號【GoUpUp】,共同學習,一起進步~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/54HQHXzfVonsEA7gQDLQsA