项目中的IM系统是基于WebSocket做的,所以这里聊一下。
说到WS,不得不提HTTP,HTTP是基于TCP,面向文本的,无状态的,半双工通信协议。由于HTTP1.0的缺陷,后面版本做相应的优化:
通过上面分析HTTP迭代的原因,那么WS的出现是为了解决HTTP什么问题呢?
针对HTTP请求-响应这种半双工通信模式,既要实现全双工通信
为什么要这样呢?主要是因为在没有WS之前,HTTP要实现IM,服务端事件推送等实时场景是很麻烦的,常见的有短轮询,长轮询,SSE(Server Send Event),针对实时通讯的场景方案都不是很优秀,所以有了WS,它最初是在 HTML5 中引入的。经过多年发展后,该协议慢慢被多个浏览器支持,RFC 在 2011 年就把该协议作为一个国际标准,叫 rfc6455。
也许是为了修复HTTP缺陷而诞生的,所以WS的建立是依赖HTTP的,同样规定默认使用80,443端口,当然其错误码是与HTTP不重合的。
WebSocket 是一种基于TCP长连接,面向报文(二进制),支持全双工通信的网络协议。其与HTTP是平级的,相比HTTP1.1,优点如下:
WebSocket主要是解决HTTP无法实时通信的问题,所以没有HTTP2 的多路复用,优先级等复杂功能,所以二进制帧格式](https://www.rfc-editor.org/rfc/rfc6455.html#page-27)简单:
可以看到帧头是16bit,2字节:
可知最大payload data长度 可以用8字节表示。
WS建立复用了 HTTP 的握手请求过程。
客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议。协议完成后,后续的数据交互则遵循 WebSocket 的协议。
GET /websocket HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Sec-WebSocket-Version: 13 #表示WS的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader头,里面包含服务端支持的版本号
Origin: http://www.jsons.cn
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: 9zQPEx8m0sqMkV1vanwJIA== #与服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接(比如特殊的HTTP请求被意外识别成WS)
Connection: keep-alive, Upgrade #告诉服务端要升级协议了
Sec-Fetch-Dest: websocket
Sec-Fetch-Mode: websocket
Sec-Fetch-Site: cross-site
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket #告诉服务端要升级成WS协议
HTTP/1.1 101 Switching Protocols #状态码 101 表示协议切换成功
Upgrade: websocket #表示升级到WS协议
Connection: Upgrade #表示协议升级
Sec-WebSocket-Accept: MzzLu4vmstovah28VVOvEgveq8o= #根据客户端请求首部的 Sec-WebSocket-Key 计算出来
#将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
#通过 SHA1 计算出摘要,并转成 base64 字符串。计算公式如下:
# Base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
协议升级完成后就完全遵循WS协议发送接收数据了。
WebSocket协议也是事件驱动的,客户应用程序不需要轮序服务来得到更新的数据,消息和事件将在服务器发送它们的时候异步到达。
这个其实在1.1小节已经说过了,就是opcode类型,通过ping和pong来实现,不需要自己再写保活接口了,只需要使用ping/pong即可。
以go gorilla/websocket为例:
func WebSocket(c *gin.Context) {
//支持协议升级
conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
http.NotFound(c.Writer, c.Request)
return
}
//添加保活机制
conn.SetPongHandler(func(appData string) error {
library.ZapDebug(appData)
//check something
return nil
})
//deal websocket connect
for {
//接收消息
mt, msg, err := conn.ReadMessage()
if err != nil { //错误,打Trace,因为可能是主动或者网络问题
library.ZapDebug("ReadMessage err:%v", err)
continue
}
//这里可以go func(){}() 包一下解决并发问题
{
//业务处理
library.ZapDebug("mt=%v,msg=%v", mt, string(msg))
//响应消息
err = conn.WriteMessage(mt, []byte(fmt.Sprintf("hello:%s", msg)))
if err != nil {
library.ZapDebug("WriteMessage err:%v", err)
}
}
}
}
经过上述内容,可以知道WS可以看做是对HTTP打的补丁,主要是为了解决HTTP半双工通信弊端,侧重处理数据实时通信场景。
这里再说下WS为什么没有替代HTTP,反而后面出了一样支持长连接的HTTP2 ?
可以这样说,如果不是浏览器是个沙河环境,不允许用户使用TCP,说不定WS压根不会诞生。