<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 每日一庫(kù)之 gotalk

          共 10370字,需瀏覽 21分鐘

           ·

          2021-06-03 00:16

          簡(jiǎn)介

          gotalk專注于進(jìn)程間的通信,致力于簡(jiǎn)化通信協(xié)議和流程。同時(shí)它:

          • 提供簡(jiǎn)潔、清晰的 API;
          • 支持 TCP,WebSocket 等協(xié)議;
          • 采用非常簡(jiǎn)單而又高效的傳輸協(xié)議格式,便于抓包調(diào)試;
          • 內(nèi)置了 JavaScript 文件gotalk.js,方便開(kāi)發(fā)基于 Web 網(wǎng)頁(yè)的客戶端程序;
          • 內(nèi)含豐富的示例可供學(xué)習(xí)參考。

          那么,讓我們來(lái)玩一下吧~

          快速使用

          本文代碼使用 Go Modules。

          創(chuàng)建目錄并初始化:

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

          安裝gotalk庫(kù):

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

          接下來(lái)讓我們來(lái)編寫一個(gè)簡(jiǎn)單的 echo 程序,服務(wù)端直接返回收到的客戶端信息,不做任何處理。首先是服務(wù)端:

          // 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)
            }
          }

          通過(guò)gotalk.Handle()注冊(cè)消息處理,它接受兩個(gè)參數(shù)。第一個(gè)參數(shù)為消息名,字符串類型,保證唯一且可辨識(shí)即可。第二個(gè)參數(shù)為處理函數(shù),收到對(duì)應(yīng)名稱的消息,調(diào)用該函數(shù)處理。處理函數(shù)接受一個(gè)參數(shù),返回兩個(gè)值。正常處理完成通過(guò)第一個(gè)返回值傳遞處理結(jié)果,出錯(cuò)時(shí)通過(guò)第二個(gè)返回值表示錯(cuò)誤類型。

          這里的處理器函數(shù)比較簡(jiǎn)單,接受一個(gè)字符串參數(shù),直接原樣返回。

          然后,調(diào)用gotalk.Serve()啟動(dòng)服務(wù)器,監(jiān)聽(tīng)端口。它接受 3 個(gè)參數(shù),協(xié)議類型、監(jiān)聽(tīng)地址、處理器對(duì)象。此處我們使用 TCP 協(xié)議,監(jiān)聽(tīng)本地8080端口,使用默認(rèn)處理器對(duì)象,傳入nil即可。

          服務(wù)器內(nèi)部一直循環(huán)處理請(qǐng)求。

          然后是客戶端:

          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()
          }

          客戶端首先調(diào)用gotalk.Connect()連接服務(wù)器,它接受兩個(gè)參數(shù):協(xié)議和地址(IP + 端口)。我們使用與服務(wù)器一致的協(xié)議和地址即可。連接成功會(huì)返回一個(gè)連接對(duì)象。調(diào)用連接對(duì)象的Request()方法,即可向服務(wù)器發(fā)送消息。Request()方法接受 3 個(gè)參數(shù)。第一個(gè)參數(shù)為消息名,這對(duì)應(yīng)于服務(wù)器注冊(cè)的消息名,請(qǐng)求一個(gè)不存在的消息名會(huì)返回錯(cuò)誤。第二個(gè)參數(shù)是傳給服務(wù)器的參數(shù),有且只能有一個(gè)參數(shù),對(duì)應(yīng)處理器函數(shù)的入?yún)ⅰ5谌齻€(gè)參數(shù)為返回值的指針,用于接受服務(wù)器返回的結(jié)果。

          如果請(qǐng)求失敗,返回錯(cuò)誤err。使用完成之后不要忘記關(guān)閉連接對(duì)象。

          先運(yùn)行服務(wù)器:

          $ go run server.go

          在開(kāi)啟一個(gè)命令行,運(yùn)行客戶端:

          go run client.go
          hello
          hello
          hello
          hello
          hello

          實(shí)際上如果了解標(biāo)準(zhǔn)庫(kù)net/http,你應(yīng)該就會(huì)發(fā)現(xiàn),使用gotalk的服務(wù)端代碼與使用net/http編寫 Web 服務(wù)器非常相似。都非常簡(jiǎn)單,清晰:

          // 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)
            }
          }

          運(yùn)行:

          $ go run main.go

          使用 curl 驗(yàn)證:

          $ curl localhost:8888
          hello world

          WebSocket

          除了 TCP,gotalk還支持基于 WebSocket 協(xié)議的通信。下面我們使用  WebSocket 重寫上面的服務(wù)端程序,然后編寫一個(gè)簡(jiǎn)單 Web 頁(yè)面與之通信。

          服務(wù)端:

          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ù)的注冊(cè)還是與前面的一樣。不同的是這里將 HTTP 路徑/gotalk/的請(qǐng)求交由gotalk.WebSocketHandler()處理,這個(gè)處理器負(fù)責(zé) WebSocket 請(qǐng)求。同時(shí),在當(dāng)前工作目錄開(kāi)啟一個(gè)文件服務(wù)器,掛載到 HTTP 路徑/上。文件服務(wù)器是為了客戶端方便地請(qǐng)求index.html頁(yè)面。最后調(diào)用http.ListenAndServe()開(kāi)啟 Web 服務(wù)器,監(jiān)聽(tīng)端口 8080。

          然后是客戶端,gotalk為了方便 Web 程序的編寫,將 WebSocket 通信細(xì)節(jié)封裝在一個(gè) JavaScript 文件gotalk.js中。可以直接從倉(cāng)庫(kù)中的 js 目錄下獲取使用。接著我們編寫頁(yè)面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, null2)}`)
                return false
              }
              function log(message{
                document.body.appendChild(document.createTextNode(message))
                document.body.appendChild(document.createElement("br"))
              }
              
          </script>
            </body>
          </html>

          首先調(diào)用gotalk.connection()連接服務(wù)端,返回一個(gè)連接對(duì)象。調(diào)用此對(duì)象的on()方法,分別注冊(cè)連接建立和斷開(kāi)的回調(diào)。然后給按鈕添加回調(diào),每次點(diǎn)擊將輸入框中的內(nèi)容發(fā)送給服務(wù)端。調(diào)用連接對(duì)象的requestp()方法發(fā)送請(qǐng)求,第一個(gè)參數(shù)為消息名,對(duì)應(yīng)在服務(wù)端使用gotalk.Handle()注冊(cè)的名字。第二個(gè)即為處理參數(shù),會(huì)一并發(fā)送給服務(wù)端。這里使用 Promise 處理異步請(qǐng)求和響應(yīng),為了編寫方便和易于理解使用async-await同步的寫法。響應(yīng)的內(nèi)容直接顯示在頁(yè)面上:

          注意,gotalk.js文件需要放在服務(wù)器運(yùn)行目錄的gotalk目錄下。

          協(xié)議格式

          gotalk采用基于 ASCII 的協(xié)議格式,設(shè)計(jì)為方便人類閱讀且靈活的。每條傳輸?shù)南⒍挤譃閹讉€(gè)部分:類型標(biāo)識(shí)、請(qǐng)求ID、操作、消息內(nèi)容。

          • 類型標(biāo)識(shí):只用一個(gè)字節(jié),用來(lái)表示消息的類型,是請(qǐng)求消息還是響應(yīng)消息,流式消息還是非流式的,錯(cuò)誤、心跳和通知也都有其特定的類型標(biāo)識(shí)。
          • 請(qǐng)求 ID:用 4 個(gè)字節(jié)表示,方便匹配響應(yīng)。由于gotalk可以同時(shí)發(fā)送任意個(gè)請(qǐng)求并接收之前請(qǐng)求的響應(yīng)。所以需要有一個(gè) ID 來(lái)標(biāo)識(shí)接收到的響應(yīng)對(duì)應(yīng)之前發(fā)送的哪條請(qǐng)求。
          • 操作:即為我們上面定義的消息名,例如"echo"。
          • 消息內(nèi)容:使用長(zhǎng)度 + 實(shí)際內(nèi)容格式。

          看一個(gè)官方請(qǐng)求的示例:

          +------------------ SingleRequest
          |   +---------------- requestID   "0001"
          |   |      +--------- operation   "echo" (text3Size 4, text3Value "echo")
          |   |      |       +- payloadSize 25
          |   |      |       |
          r0001004echo00000019{"message":"Hello World"}
          • r:表示這是一個(gè)單條請(qǐng)求。
          • 0001:請(qǐng)求 ID 為 1,這里采用十六進(jìn)制編碼。
          • 004echo:這部分表示操作為"echo",在實(shí)際字符串內(nèi)容前需要指定長(zhǎng)度,否則接收方不知道內(nèi)容在哪里結(jié)束。004指示"echo"長(zhǎng)度為 4,同樣采用十六進(jìn)制編碼。
          • 00000019{"message":"Hello World"}:這部分是消息的內(nèi)容。同樣需要指定長(zhǎng)度,十六進(jìn)制00000019表示長(zhǎng)度為 25。

          詳細(xì)格式可以查看官方文檔。

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

          聊天室

          examples內(nèi)置一個(gè)基于 WebSocket 的聊天室示例程序。特性如下:

          • 可以創(chuàng)建房間,默認(rèn)創(chuàng)建 3 個(gè)房間animals/jokes/golang
          • 在房間聊天(基本功能);
          • 一個(gè)簡(jiǎn)單的 Web 頁(yè)面。

          運(yùn)行:

          go run server.go

          打開(kāi)瀏覽器,輸入"localhost:1235",顯示如下:

          接下來(lái)就可以創(chuàng)建房間,在房間聊天了。

          整個(gè)實(shí)現(xiàn)的有幾個(gè)要點(diǎn):

          其一,gotalk.WebSocketHandler()創(chuàng)建的 WebSocket 處理器可以設(shè)置連接回調(diào):

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

          在回調(diào)中設(shè)置隨機(jī)用戶名,并將當(dāng)前連接的gotalk.Sock存儲(chǔ)下來(lái),方便消息廣播:

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

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

          其二,gotalk設(shè)置處理器函數(shù)可以有兩個(gè)參數(shù),第一個(gè)表示當(dāng)前連接,第二個(gè)才是實(shí)際接收到的消息參數(shù)。

          其三,enableGracefulShutdown()函數(shù)實(shí)現(xiàn)了 Web 服務(wù)器的優(yōu)雅關(guān)閉,非常值得學(xué)習(xí)。接收到SIGINT信號(hào),先關(guān)閉所有的連接,再退出程序。注意監(jiān)聽(tīng)信號(hào)和運(yùn)行 HTTP 服務(wù)器并不是同一個(gè) 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信號(hào)后done通道關(guān)閉,server.ListenAndServe()返回http.ErrServerClosed錯(cuò)誤,退出循環(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

          整個(gè)聊天室功能比較簡(jiǎn)單,代碼也比較短,建議深入理解。在此基礎(chǔ)之上做擴(kuò)展也比較簡(jiǎn)單。

          總結(jié)

          gotalk實(shí)現(xiàn)了一個(gè)簡(jiǎn)單、易用的通信庫(kù)。并且提供了 JavaScript 文件gotalk.js,方便 Web 程序的開(kāi)發(fā)。協(xié)議格式清晰,易調(diào)試。內(nèi)置豐富的示例。整個(gè)庫(kù)的代碼也不長(zhǎng),建議深入了解。

          大家如果發(fā)現(xiàn)好玩、好用的 Go 語(yǔ)言庫(kù),歡迎到 Go 每日一庫(kù) GitHub 上提交 issue??

          參考

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


          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

          瀏覽 52
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本精品一区二区三区视频 | 中文字幕有码无码人妻aV蜜桃 | 91成h| 老妇裸体乱婬A片 | 欧美一区二区三区成人片下载 |