Go 每日一庫之 gotalk

簡介

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

  • 提供簡潔、清晰的 API;
  • 支持 TCP,WebSocket 等協議;
  • 採用很是簡單而又高效的傳輸協議格式,便於抓包調試;
  • 內置了 JavaScript 文件gotalk.js,方便開發基於 Web 網頁的客戶端程序;
  • 內含豐富的示例可供學習參考。

那麼,讓咱們來玩一下吧~html

快速使用

本文代碼使用 Go Modules。java

建立目錄並初始化:git

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

安裝gotalk庫:github

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

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

// 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、操做、消息內容。

  • 類型標識:只用一個字節,用來表示消息的類型,是請求消息仍是響應消息,流式消息仍是非流式的,錯誤、心跳和通知也都有其特定的類型標識。
  • 請求 ID:用 4 個字節表示,方便匹配響應。因爲gotalk能夠同時發送任意個請求並接收以前請求的響應。因此須要有一個 ID 來標識接收到的響應對應以前發送的哪條請求。
  • 操做:即爲咱們上面定義的消息名,例如"echo"。
  • 消息內容:使用長度 + 實際內容格式。

看一個官方請求的示例:

+------------------ SingleRequest
|   +---------------- requestID   "0001"
|   |      +--------- operation   "echo" (text3Size 4, text3Value "echo")
|   |      |       +- payloadSize 25
|   |      |       |
r0001004echo00000019{"message":"Hello World"}
  • r:表示這是一個單條請求。
  • 0001:請求 ID 爲 1,這裏採用十六進制編碼。
  • 004echo:這部分表示操做爲"echo",在實際字符串內容前須要指定長度,不然接收方不知道內容在哪裏結束。004指示"echo"長度爲 4,一樣採用十六進制編碼。
  • 00000019{"message":"Hello World"}:這部分是消息的內容。一樣須要指定長度,十六進制00000019表示長度爲 25。

詳細格式能夠查看官方文檔。

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

聊天室

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

  • 能夠建立房間,默認建立 3 個房間animals/jokes/golang
  • 在房間聊天(基本功能);
  • 一個簡單的 Web 頁面。

運行:

$ 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】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索