Go 每日一庫之 gotalk

簡介

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??

參考

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

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

歡迎關注我的微信公眾號【GoUpUp】癣蟋,共同學習透硝,一起進步~

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市梢薪,隨后出現(xiàn)的幾起案子蹬铺,更是在濱河造成了極大的恐慌,老刑警劉巖秉撇,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甜攀,死亡現(xiàn)場離奇詭異,居然都是意外死亡琐馆,警方通過查閱死者的電腦和手機规阀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘦麸,“玉大人谁撼,你說我怎么就攤上這事∽趟牵” “怎么了厉碟?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屠缭。 經(jīng)常有香客問我箍鼓,道長,這世上最難降的妖魔是什么呵曹? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任款咖,我火速辦了婚禮,結果婚禮上奄喂,老公的妹妹穿的比我還像新娘铐殃。我一直安慰自己,他們只是感情好跨新,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布富腊。 她就那樣靜靜地躺著,像睡著了一般玻蝌。 火紅的嫁衣襯著肌膚如雪蟹肘。 梳的紋絲不亂的頭發(fā)上词疼,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音帘腹,去河邊找鬼贰盗。 笑死,一個胖子當著我的面吹牛阳欲,可吹牛的內(nèi)容都是我干的舵盈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼球化,長吁一口氣:“原來是場噩夢啊……” “哼秽晚!你這毒婦竟也來了?” 一聲冷哼從身側響起筒愚,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤赴蝇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后巢掺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體句伶,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年陆淀,在試婚紗的時候發(fā)現(xiàn)自己被綠了考余。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡轧苫,死狀恐怖楚堤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情含懊,我是刑警寧澤身冬,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站岔乔,受9級特大地震影響吏恭,放射性物質發(fā)生泄漏。R本人自食惡果不足惜重罪,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哀九。 院中可真熱鬧剿配,春花似錦、人聲如沸阅束。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽息裸。三九已至蝇更,卻和暖如春沪编,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背年扩。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工蚁廓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厨幻。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓相嵌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親况脆。 傳聞我的和親對象是個殘疾皇子饭宾,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容

  • Mix Go 是一個基于 Go 進行快速開發(fā)的完整系統(tǒng),類似前端的 Vue CLI格了,提供: 通過 mix-go/m...
    擼代碼的鄉(xiāng)下人閱讀 623評論 0 4
  • 表情是什么看铆,我認為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息盛末。高興了當然就笑了弹惦,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,908評論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者满败,不喜歡去冒險肤频,但是人生放棄了冒險,也就放棄了無數(shù)的可能算墨。 ...
    yichen大刀閱讀 6,046評論 0 4