深入Go Module之未说的秘密

正常情况下,我们的go.mod依赖库的版本都是符合语义化版本 2.0.0的版本格式,或者伪版本格式。在前面的文章我没有特别提到一点的事,Go使用服务端提交的日期和commit id生成的伪版本号是符合语义化版本号2.0.0的,因为语义化版本号中规定pre-release以连接号**-**加一连串以逗号分隔的标识符组成,标识符以字母数字和连接号组成,所以你看到-yyyyMMddhhmmss-comitid包含两个连接号,这是正常的。

go要求依赖库要么不包含go.mod,要么依赖库中的go.mod定义的依赖库版本必须以语义化版本 2.0.0格式(或伪版本号)标志(其实更严格,除了+incompatible不能加meta字段),因为这样我们你能够明确标识某个依赖库确切的版本,这样的版本号被称之为canonical version

其实main module还可以定义non-canonical version,通过go get或者go mod tidy更新go.mod的时候,命令会尝试更新go.mod,尝试把non-canonical version转变为canonical version版本。

但是,到底有哪些non-canonical version呢?我还没看到官方文章介绍,本文尝试整理这些non-canonical version。

阅读全文

wio terminal 掌机开发板试用报告

周五收到一款矽递科技的arduino开发板wio terminal,与Arduino和MicroPython兼容,基于ATSAMD51的微控制器,具有Realtek RTL8720DN支持的的无线连接,还带有一个240X320的彩色液晶屏,可以做为树莓派的显示器,很方便了。 话说矽递科技是一家专注物联网硬件解决方案的高新技术企业,全球前三的开源硬件供应商,牛逼了,以至于这款产品的英文资料比中文资料还丰富。

对于硬件相关的领域,我还是一个小白,但是对于物联网或者硬件相关的创客产品,我是非常感兴趣的,这不,拿到这款产品,周末在家就和小朋友一起研究起来。

我给自己的设置的第一个目标就是能写一个hello world的程序,在这款wio terminal显示出来。

阅读全文

实现无限缓存的channel

Go语言的channel有两种类型,一种是无缓存的channel,一种是有缓存的buffer,这两种类型的channel大家都比较熟悉了,但是对于有缓存的channel,它的缓存长度在创建channel的时候就已经确定了,中间不能扩缩容,这导致在一些场景下使用有问题,或者说不太适合特定的场景。

阅读全文

Go sync.Once的三重门

我在极客时间开设的专栏Go 并发编程实战课中,详细介绍了sync.Once并发原语的实现,对于使用这个原语来说,内容已经足够了,但是还是有些同学愿意深入挖掘更深层的设计,并且提出了一些疑问,所以我再专门写一篇文章, 作为这么专栏的补充吧。

阅读全文

轻轻松松打印网页并生成pdf文档

chromedp是一个更快更简单的支持Chrome DevTools Protocol协议的Go库,它是目前最流行的headless浏览器库之一,你可以使用它做很多只能通过浏览器才能执行的任务,比如网页截屏、网页渲染测试、下载视频、模拟登录等,今天我介绍它的一个有用而且很简单的功能:为一个网页生成一个pdf格式的截屏,更多的例子你可以查看官方示例chromedp/examples

阅读全文

rpcx支持websocket协议了!

当前, rpcx支持tcp、kcp、quic、unix domain、http、jsonrpc 2.0等传输协议,并没有考虑websocket的支持,原因在于考虑到微服务更多的是企业内部服务之间的通讯,不太可能暴露给浏览器,企业内部大多采用tcp的方式传输,或者udp族(kcp、quic)方式的传输,但是还是有用户提出希望能支持websocket。

我想websocket可能还是会有一些场景下使用,比如游戏开发,移动端核服务端的通讯等等,所以经过慎重考虑,我决定在 rpcx 1.6.2版本中增加对websocket的支持。

基于rpcx良好的架构设计,实现对websocket的支持并不难。

本来,rpcx对kcp、quic的支持也不复杂,原因在于rpcx的通讯采用的net.Conn的这样一个通用的接口,listent.Accept方法接受客户端的请求后返回一个net.Conn对象,后续请求的解析、处理,写回都是在这个通用接口上操作的,所以非常容易支持新的传输协议。

以前rpcx还支持其它的udp的扩展协议,比如

因为这两个协议用的人很少,所以在rpcx 1.6中把这两个协议的支持去掉了。

所以,对于websocket来说,找到一个可信赖的、合适的websocket库,可以很容易的实现基于websocket传输的微服务框架。

目前Go生态圈常用的websocket库如下:

虽然像gobwas/ws性能优良、gorilla/websocket功能齐全,但是考虑到API的便利性,我还是决定使用Go官方的x/net/websocket库,一来有官方背书,值得信赖,二来rpcx使用websocket功能比较简单,就是期望能获得一个net.Conn对象,x/net/websocket库正好能满足需求。

服务端

如果服务端采用websocket协议,那么network应该写成ws或者wss,而不是tcp或者quic

服务端会启动一个http server处理websocket请求。 默认的websocket path为 share.DefaultRPCPath,只要服务端核客户端保持一致即可。

ServeWS会传入一个*websocket.Conn参数,它实现了net.Conn接口,所以后续的处理使用通用的s.serveConn(conn)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

func (s *Server) Serve(network, address string) (err error) {
......

if network == "ws" || network == "wss" {
s.serveByWS(ln, "")
return nil
}

......
}


func (s *Server) serveByWS(ln net.Listener, rpcPath string) {
s.ln = ln

if rpcPath == "" {
rpcPath = share.DefaultRPCPath
}
mux := http.NewServeMux()
mux.Handle(rpcPath, websocket.Handler(s.ServeWS))
srv := &http.Server{Handler: mux}

srv.Serve(ln)
}

func (s *Server) ServeWS(conn *websocket.Conn) {
s.mu.Lock()
s.activeConn[conn] = struct{}{}
s.mu.Unlock()

s.serveConn(conn)
}

服务端的改造就如此简单,接下来看看如何部署一个服务端的websocket服务:

server.go
1
2
3
4
5
6
7
8
9
10
func main() {
flag.Parse()

s := server.NewServer()
s.RegisterName("Arith", new(Arith), "")
err := s.Serve("ws", *addr)
if err != nil {
panic(err)
}
}

客户端

客户端最重要的是要实现一个websocket协议的建连,如果实现了这个方法,客户端的改造基本就完成了。这个方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func newDirectWSConn(c *Client, network, address string) (net.Conn, error) {
if c == nil {
return nil, errors.New("empty client")
}
path := c.option.RPCPath
if path == "" {
path = share.DefaultRPCPath
}

var conn net.Conn
var err error

// url := "ws://localhost:12345/ws"

var url, origin string
if network == "ws" {
url = fmt.Sprintf("ws://%s%s", address, path)
origin = fmt.Sprintf("http://%s", address)
} else {
url = fmt.Sprintf("wss://%s%s", address, path)
origin = fmt.Sprintf("https://%s", address)
}

if c.option.TLSConfig != nil {
config, err := websocket.NewConfig(url, origin)
if err != nil {
return nil, err
}
config.TlsConfig = c.option.TLSConfig
conn, err = websocket.DialConfig(config)
} else {
conn, err = websocket.Dial(url, "", origin)
}

return conn, err
}

在建立连接的时候为wswss协议调用这个方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
func (c *Client) Connect(network, address string) error {
var conn net.Conn
var err error

switch network {
case "http":
conn, err = newDirectHTTPConn(c, network, address)
case "ws", "wss":
conn, err = newDirectWSConn(c, network, address)
case "kcp":
conn, err = newDirectKCPConn(c, network, address)
.......

客户端改造完成后,我们写一个客户端调用改车把刚才启动的websocket服务:

client.fo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
flag.Parse()

d, _ := client.NewPeer2PeerDiscovery("ws@"+*addr, "")
opt := client.DefaultOption
opt.SerializeType = protocol.JSON

xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, opt)
defer xclient.Close()

args := example.Args{
A: 10,
B: 20,
}

reply := &example.Reply{}
err := xclient.Call(context.Background(), "Mul", args, reply)
if err != nil {
log.Fatalf("failed to call: %v", err)
}

log.Printf("%d * %d = %d", args.A, args.B, reply.C)

}

所以你看到,对于一个新的传输协议来说,如果它的连接能够遵循net.Conn接口,rpcx支持它还是很容易的。

完整的使用websocket传输协议的例子: websocket example