websocket出来好久了,一直没有动手去玩玩,今天抽了点时间写了一个golang的例子,下面简单记录一下。
协议
websocket是个二进制协议,需要先通过Http协议进行握手,从而协商完成从Http协议向websocket协议的转换。一旦握手结束,当前的TCP连接后续将采用二进制websocket协议进行双向双工交互,自此与Http协议无关。
可以通过这篇知乎了解一下websocket协议的基本原理:《WebSocket 是什么原理?为什么可以实现持久连接?》。
粘包
我们开发过TCP服务的都知道,需要通过协议decode从TCP字节流中解析出一个一个请求,那么websocket又怎么样呢?
websocket以message为单位进行通讯,本身就是一个在TCP层上的一个分包协议,其实并不需要我们再进行粘包处理。但是因为单个message可能很大很大(比如一个视频文件),那么websocket显然不适合把一个视频作为一个message传输(中途断了前功尽弃),所以websocket协议其实是支持1个message分多个frame帧传输的。
我们的浏览器提供的编程API都是message粒度的,把frame拆帧的细节对开发者隐蔽了,而服务端websocket框架一般也做了同样的隐藏,会自动帮我们收集所有的frame后拼成messasge再回调,所以结论就是:
websocket以message为单位通讯,不需要开发者自己处理粘包问题。
golang实现
golang官方标准库里有一个websocket的包,但是它提供的就是frame粒度的API,压根不能用。
不过官方其实已经认可了一个准标准库实现,它实现了message粒度的API,让开发者不需要关心websocket协议细节,开发起来非常方便,其文档地址:https://godoc.org/github.com/gorilla/websocket。
开发websocket服务时,首先要基于http库对外暴露接口,然后由websocket库接管TCP连接进行协议升级,然后进行websocket协议的数据交换,所以开发时总是要用到http库和websocket库。
上述websocket文档中对开发websocket服务有明确的注意事项要求,主要是指:
- 读和写API不是并发安全的,需要启动单个goroutine串行处理。
- 关闭API是线程安全的,一旦调用则阻塞的读和写API会出错返回,从而终止处理。
在我的实现中,我对websocket进行了封装,简化应用层开发的复杂度,主要思路是:
- 请求和应答都放入管道中排队。
- 读协程阻塞读websocket,将message放入请求队列。
- 写协程阻塞读应答channel,将message写给websocket。
如何处理websocket错误和主动关闭websocket呢?
- 读/写协程调用websocket若返回错误,那么直接调用websocket的Close关闭连接,协程退出。(此时用户可能仍旧持有连接对象,继续向下阅读!)
- websocket连接关闭后,用户通常正阻塞在读/写channel上而不知情,所以每个连接配套一个closeChan专门用于唤醒用户代码,关闭websocket连接同时关闭closeChan,这会令<-closeChan总是立即返回。
- 因为上一条设计,所以用户读/写channel时总是select同时监听channel和closeChan,以便实时感知到websocket连接的关闭。
- 用户可以主动关闭连接,websocket连接重复Close没有影响,而closeChan重复关闭会报错,所以通过一个上锁的状态位判重处理。
描述比较繁琐,实际并不复杂,看看我的代码吧:https://github.com/owenliang/go-websocket。
体验项目
首先运行server.go,然后打开client.html页面,即可体验所有流程:
nginx反向代理
网上有很多nginx如何反向代理websocket服务的配置,通过Proxy即可实现,不再演示。