正文
一、那年毕业设计
学习《Go 语言编程之旅》接着 gRPC 之后,现在开始学 websocket 在 go 语言里的使用了。这里是要跟着做一个聊天室。不禁让我想起那年毕业设计:基于 Java 的即时通讯系统。
大学毕业那会,我一个人搞定了三个毕业设计,一个是邮件收发系统,一个是学生管理系统,我自己的是即时通讯系统。很遗憾,别人的毕设一次性就过了,我自己的反倒去了二辩。不过那时我也不慌,因为都是自己做的,不可能不过。
当时我正在学 Spring 框架,就画了下面的系统架构图,技术栈:HTML 5 + CSS 3 + Jquery + Spring MVC + Spring + Hibernate + MySQL。很遗憾,当时学的是 SpringMVC,其实当时已经有很多人用 SpringBoot 了,因此我选技术的时候就落伍了(当时太年轻,并不是自己选的),这也导致我后来在 Java web 上不愿意再做提升了,就是觉得很遗憾。
系统架构
看看下面界面,这么一个小即时通讯系统,当时觉得还是挺酷的。当时觉得自己懂的好多,HTML 5、 CSS 3 一边学着,学完立马就往即时通讯系统里硬灌,虽然现在看来不成样子,但当时真的特别开心。
记得答辩那天,有个哥们掏出来一个 cmd 命令行程序,界面就是黑窗口,被老师狠狠批了一顿,我当时看着自己的五颜六色的界面,还是挺开心的(虽然四不像,哈哈)。即使一辩挂了回忆还是美美的。
二、WebSocket 简介
WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。Web IDL中的WebSocket API由W3C标准化。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
三、WebSocket 的优点
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
- 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
四、nhooyr.io/websocket 的使用
毕业设计自然是玩笑,其实大半年前,我也做过另一个即时通讯系统,最开始使用的是腾讯云现成的即时通讯SDK,每个月都是收费的,而且聊天记录不能永久保存。后来我基于 Elasticsearch 和 gorilla/websocket 做了系统重构,替代了腾讯云的收费 SDK。
这次使用 nhooyr.io/websocket 是因为教程里用的是这个库,而且写的优点也挺吸引人的,因此我还是愿意再重试一下这个库。
核心特色如下:
- 有很小且符合 Go 习惯的 API。
- 核心代码 2200 行。
- 一流的 context.Context 支持。
- 有全面的测试。
- 不依赖任何第三方库。
- 支持 JSON 和 ProtoBuf。
- 默认就有高度的性能优化。
- 开箱即用的并发支持。
- 全面的 Wasm 支持。
- CLose handshake。
服务端代码
package main import ( "context" "fmt" "log" "net/http" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" "time" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, "HTTP, Hello") }) http.HandleFunc("/ws", func(w http.ResponseWriter, req *http.Request) { conn, err := websocket.Accept(w, req, nil) if err != nil { log.Println(err) return } defer conn.Close(websocket.StatusInternalError, "内部出错了") ctx, cancel := context.WithTimeout(req.Context(), time.Second*10) defer cancel() var v interface{} err = wsjson.Read(ctx, conn, &v) if err != nil { log.Println(err) return } log.Printf("接收到客户端: %v\n", v) err = wsjson.Write(ctx, conn, "Hello WebSocket Client") if err != nil { log.Println(err) return } conn.Close(websocket.StatusNormalClosure, "") }) log.Fatal(http.ListenAndServe(":2022", nil)) }
客户端代码
package main import ( "context" "fmt" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() c, _, err := websocket.Dial(ctx, "ws://localhost:2022/ws", nil) if err != nil { panic(err) } defer c.Close(websocket.StatusInternalError, "内部错误!") err = wsjson.Write(ctx, c, "Hello WebSocket Server") if err != nil { panic(err) } var v interface{} err = wsjson.Read(ctx, c, &v) if err != nil { panic(err) } fmt.Printf("接收到服务端响应: %v\n", v) c.Close(websocket.StatusNormalClosure, "") }
运行结果
服务端
客户端