簡介
gotalk
專注于進程間的通信浊服,致力于簡化通信協(xié)議和流程统屈。同時它:
- 提供簡潔、清晰的 API牙躺;
- 支持 TCP愁憔,WebSocket 等協(xié)議;
- 采用非常簡單而又高效的傳輸協(xié)議格式述呐,便于抓包調試惩淳;
- 內(nèi)置了 JavaScript 文件
gotalk.js
,方便開發(fā)基于 Web 網(wǎng)頁的客戶端程序; - 內(nèi)含豐富的示例可供學習參考思犁。
那么代虾,讓我們來玩一下吧~
快速使用
本文代碼使用 Go Modules。
創(chuàng)建目錄并初始化:
$ 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()
注冊消息處理学辱,它接受兩個參數(shù)乘瓤。第一個參數(shù)為消息名,字符串類型策泣,保證唯一且可辨識即可衙傀。第二個參數(shù)為處理函數(shù),收到對應名稱的消息萨咕,調用該函數(shù)處理统抬。處理函數(shù)接受一個參數(shù),返回兩個值危队。正常處理完成通過第一個返回值傳遞處理結果聪建,出錯時通過第二個返回值表示錯誤類型。
這里的處理器函數(shù)比較簡單茫陆,接受一個字符串參數(shù)金麸,直接原樣返回。
然后簿盅,調用gotalk.Serve()
啟動服務器挥下,監(jiān)聽端口。它接受 3 個參數(shù)挪鹏,協(xié)議類型见秽、監(jiān)聽地址、處理器對象讨盒。此處我們使用 TCP 協(xié)議解取,監(jiān)聽本地8080
端口,使用默認處理器對象返顺,傳入nil
即可禀苦。
服務器內(nèi)部一直循環(huán)處理請求。
然后是客戶端:
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()
連接服務器遂鹊,它接受兩個參數(shù):協(xié)議和地址(IP + 端口)振乏。我們使用與服務器一致的協(xié)議和地址即可。連接成功會返回一個連接對象秉扑。調用連接對象的Request()
方法慧邮,即可向服務器發(fā)送消息调限。Request()
方法接受 3 個參數(shù)。第一個參數(shù)為消息名误澳,這對應于服務器注冊的消息名耻矮,請求一個不存在的消息名會返回錯誤。第二個參數(shù)是傳給服務器的參數(shù)忆谓,有且只能有一個參數(shù)裆装,對應處理器函數(shù)的入?yún)ⅰ5谌齻€參數(shù)為返回值的指針倡缠,用于接受服務器返回的結果哨免。
如果請求失敗,返回錯誤err
昙沦。使用完成之后不要忘記關閉連接對象琢唾。
先運行服務器:
$ go run server.go
在開啟一個命令行,運行客戶端:
$ go run client.go
hello
hello
hello
hello
hello
實際上如果了解標準庫net/http
桅滋,你應該就會發(fā)現(xiàn)慧耍,使用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 協(xié)議的通信煌珊。下面我們使用 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
消息處理函數(shù)的注冊還是與前面的一樣定庵。不同的是這里將 HTTP 路徑/gotalk/
的請求交由gotalk.WebSocketHandler()
處理吏饿,這個處理器負責 WebSocket 請求。同時蔬浙,在當前工作目錄開啟一個文件服務器猪落,掛載到 HTTP 路徑/
上。文件服務器是為了客戶端方便地請求index.html
頁面畴博。最后調用http.ListenAndServe()
開啟 Web 服務器笨忌,監(jiān)聽端口 8080。
然后是客戶端俱病,gotalk
為了方便 Web 程序的編寫官疲,將 WebSocket 通信細節(jié)封裝在一個 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()
方法,分別注冊連接建立和斷開的回調。然后給按鈕添加回調犀盟,每次點擊將輸入框中的內(nèi)容發(fā)送給服務端而晒。調用連接對象的requestp()
方法發(fā)送請求,第一個參數(shù)為消息名且蓬,對應在服務端使用gotalk.Handle()
注冊的名字欣硼。第二個即為處理參數(shù),會一并發(fā)送給服務端恶阴。這里使用 Promise 處理異步請求和響應诈胜,為了編寫方便和易于理解使用async-await
同步的寫法。響應的內(nèi)容直接顯示在頁面上:
[圖片上傳失敗...(image-bb0d81-1623109177730)]
注意冯事,gotalk.js
文件需要放在服務器運行目錄的gotalk
目錄下焦匈。
協(xié)議格式
gotalk
采用基于 ASCII 的協(xié)議格式,設計為方便人類閱讀且靈活的昵仅。每條傳輸?shù)南⒍挤譃閹讉€部分:類型標識缓熟、請求ID、操作摔笤、消息內(nèi)容够滑。
- 類型標識:只用一個字節(jié),用來表示消息的類型吕世,是請求消息還是響應消息彰触,流式消息還是非流式的,錯誤命辖、心跳和通知也都有其特定的類型標識况毅。
- 請求 ID:用 4 個字節(jié)表示,方便匹配響應尔艇。由于
gotalk
可以同時發(fā)送任意個請求并接收之前請求的響應尔许。所以需要有一個 ID 來標識接收到的響應對應之前發(fā)送的哪條請求。 - 操作:即為我們上面定義的消息名终娃,例如"echo"味廊。
- 消息內(nèi)容:使用長度 + 實際內(nèi)容格式。
看一個官方請求的示例:
+------------------ SingleRequest
| +---------------- requestID "0001"
| | +--------- operation "echo" (text3Size 4, text3Value "echo")
| | | +- payloadSize 25
| | | |
r0001004echo00000019{"message":"Hello World"}
-
r
:表示這是一個單條請求尝抖。 -
0001
:請求 ID 為 1毡们,這里采用十六進制編碼。 -
004echo
:這部分表示操作為"echo"昧辽,在實際字符串內(nèi)容前需要指定長度衙熔,否則接收方不知道內(nèi)容在哪里結束。004
指示"echo"長度為 4搅荞,同樣采用十六進制編碼红氯。 -
00000019{"message":"Hello World"}
:這部分是消息的內(nèi)容框咙。同樣需要指定長度,十六進制00000019
表示長度為 25痢甘。
詳細格式可以查看官方文檔喇嘱。
使用這種可閱讀的格式給問題排查帶來了極大的便利。但是在實際使用中塞栅,可能需要考慮安全和隱私的問題者铜。
聊天室
examples
內(nèi)置一個基于 WebSocket 的聊天室示例程序。特性如下:
- 可以創(chuàng)建房間放椰,默認創(chuàng)建 3 個房間
animals/jokes/golang
作烟; - 在房間聊天(基本功能);
- 一個簡單的 Web 頁面砾医。
運行:
$ go run server.go
打開瀏覽器拿撩,輸入"localhost:1235",顯示如下:
[圖片上傳失敗...(image-36fbe0-1623109177730)]
接下來就可以創(chuàng)建房間如蚜,在房間聊天了压恒。
整個實現(xiàn)的有幾個要點:
其一,gotalk.WebSocketHandler()
創(chuàng)建的 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
設置處理器函數(shù)可以有兩個參數(shù)撬呢,第一個表示當前連接期吓,第二個才是實際接收到的消息參數(shù)。
其三倾芝,enableGracefulShutdown()
函數(shù)實現(xiàn)了 Web 服務器的優(yōu)雅關閉,非常值得學習箭跳。接收到SIGINT
信號晨另,先關閉所有的連接,再退出程序谱姓。注意監(jiān)聽信號和運行 HTTP 服務器并不是同一個 goroutine借尿,看它們是如何協(xié)作的:
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
錯誤屉来,退出循環(huán):
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
實現(xiàn)了一個簡單、易用的通信庫慨绳。并且提供了 JavaScript 文件gotalk.js
掉冶,方便 Web 程序的開發(fā)真竖。協(xié)議格式清晰,易調試厌小。內(nèi)置豐富的示例恢共。整個庫的代碼也不長,建議深入了解璧亚。
大家如果發(fā)現(xiàn)好玩讨韭、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??
參考
- gotalk GitHub:https://github.com/rsms/gotalk
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
歡迎關注我的微信公眾號【GoUpUp】癣蟋,共同學習透硝,一起進步~