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同步的写法。响应的内容直接显示在页面上:

[图片上传失败...(image-bb0d81-1623109177730)]

注意,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",显示如下:

[图片上传失败...(image-36fbe0-1623109177730)]

接下来就可以创建房间,在房间聊天了。

整个实现的有几个要点:

其一,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】,共同学习,一起进步~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容

  • Mix Go 是一个基于 Go 进行快速开发的完整系统,类似前端的 Vue CLI,提供: 通过 mix-go/m...
    撸代码的乡下人阅读 615评论 0 4
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,412评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,037评论 0 4