Go 每日一庫之 gotalk
簡介
gotalk
專注於進程間的通信,致力於簡化通信協議和流程。同時它:
-
提供簡潔、清晰的 API;
-
支持 TCP,WebSocket 等協議;
-
採用非常簡單而又高效的傳輸協議格式,便於抓包調試;
-
內置了 JavaScript 文件
gotalk.js
,方便開發基於 Web 網頁的客戶端程序; -
內含豐富的示例可供學習參考。
那麼,讓我們來玩一下吧~
快速使用
本文代碼使用 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、操作、消息內容。
-
類型標識:只用一個字節,用來表示消息的類型,是請求消息還是響應消息,流式消息還是非流式的,錯誤、心跳和通知也都有其特定的類型標識。
-
請求 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😄
參考
-
gotalk GitHub:https://github.com/rsms/gotalk
-
Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
歡迎關注我的微信公衆號【GoUpUp】,共同學習,一起進步~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/54HQHXzfVonsEA7gQDLQsA