2小时快速搭建一个高可用的IM系统( 三 )


数据是否真的像 Frame 中展示的那样客户端直接将一大篇文本数据发送到服务端 , 服务端接收到数据之后 , 再将一大篇文本数据返回给客户端呢?
这当然是不可能的 , 我们都知道 HTTP 协议是基于 TCP 实现的 , HTTP 发送数据也是分包转发的 , 就是将大数据根据报文形式分割成一小块一小块发送到服务端,服务端接收到客户端发送的报文后 , 再将小块的数据拼接组装 。
关于 HTTP 的分包策略 , 大家可以查看相关资料进行研究 , WebSocket 协议也是通过分片打包数据进行转发的 , 不过策略上和 HTTP 的分包不一样 。
Frame(帧)是 WebSocket 发送数据的基本单位 , 下边是它的报文格式:

2小时快速搭建一个高可用的IM系统

文章插图
 
报文内容中规定了数据标示,操作代码、掩码、数据、数据长度等格式 。不太理解没关系 , 下面我通过讲解大家只要理解报文中重要标志的作用就可以了 。
首先我们明白了客户端和服务端进行 WebSocket 消息传递是这样的:
  • 客户端:将消息切割成多个帧 , 并发送给服务端 。
  • 服务端:接收消息帧 , 并将关联的帧重新组装成完整的消息 。
服务端在接收到客户端发送的帧消息的时候 , 将这些帧进行组装 , 它怎么知道何时数据组装完成的呢?
这就是报文中左上角 FIN(占一个比特)存储的信息 , 1 表示这是消息的最后一个分片(fragment)如果是 0 , 表示不是消息的最后一个分片 。
WebSocket 通信中 , 客户端发送数据分片是有序的 , 这一点和 HTTP 不一样 。
HTTP 将消息分包之后 , 是并发无序的发送给服务端的 , 包信息在数据中的位置则在 HTTP 报文中存储 , 而 WebSocket 仅仅需要一个 FIN 比特位就能保证将数据完整的发送到服务端 。
接下来的 RSV1 , RSV2 , RSV3 三个比特位的作用又是什么呢?这三个标志位是留给客户端开发者和服务端开发者开发过程中协商进行拓展的 , 默认是 0 。
拓展如何使用必须在握手的阶段就协商好 , 其实握手本身也是客户端和服务端的协商 。
④WebSocket 连接保持和心跳检测
WebSocket 是长连接 , 为了保持客户端和服务端的实时双向通信 , 需要确保客户端和服务端之间的 TCP 通道保持连接没有断开 。
但是对于长时间没有数据往来的连接 , 如果依旧保持着 , 可能会浪费服务端资源 。
不排除有些场景 , 客户端和服务端虽然长时间没有数据往来 , 仍然需要保持连接 , 就比如说你几个月没有和一个 QQ 好友聊天了 , 突然有一天他发 QQ 消息告诉你他要结婚了 , 你还是能在第一时间收到 。
那是因为 , 客户端和服务端一直再采用心跳来检查连接 。客户端和服务端的心跳连接检测就像打乒乓球一样:
  • 发送方→接收方:ping
  • 接收方→发送方:pong
等什么时候没有 ping、pong 了 , 那么连接一定是存在问题了 。
说了这么多 , 接下来我使用 Go 语言来实现一个心跳检测 , WebSocket 通信实现细节是一件繁琐的事情 , 直接使用开源的类库是比较不错的选择 , 我使用的是:gorilla/websocket 。
这个类库已经将 WebSocket 的实现细节(握手 , 数据解码)封装的很好啦 。下面我就直接贴代码了:
package mainimport ("net/http""time""github.com/gorilla/websocket" )var (//完成握手操作upgrade = websocket.Upgrader{//允许跨域(一般来讲,websocket都是独立部署的)CheckOrigin:func(r *http.Request) bool {return true},} )func wsHandler(w http.ResponseWriter, r *http.Request) {var (conn *websocket.Connerr errordata []byte)//服务端对客户端的http请求(升级为websocket协议)进行应答 , 应答之后 , 协议升级为websocket , http建立连接时的tcp三次握手将保持 。if conn, err = upgrade.Upgrade(w, r, nil); err != nil {return}//启动一个协程 , 每隔1s向客户端发送一次心跳消息go func() {var (err error)for {if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {return}time.Sleep(1 * time.Second)}}()//得到websocket的长链接之后,就可以对客户端传递的数据进行操作了for {//通过websocket长链接读到的数据可以是text文本数据 , 也可以是二进制Binaryif _, data, err = conn.ReadMessage(); err != nil {goto ERR}if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {goto ERR}} ERR://出错之后 , 关闭socket连接conn.Close() }func main() {http.HandleFunc("/ws", wsHandler)http.ListenAndServe("0.0.0.0:7777", nil) }


推荐阅读