websocket 是一个长连接协议,全双工通信,主要应用在及时通信:实时聊天,游戏,在线文档等等。
简单示例
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input id="input" type="text" />
<button onclick="send()">Send</button>
<pre id="output"></pre>
<script>
var input = document.getElementById("input");
var output = document.getElementById("output");
//调用websocket对象建立连接:
//参数:ws/wss(加密)://ip:port (字符串)
var websocket = new WebSocket("ws://xxx.xxx.xxx.xxx:8080/echo");
console.log(websocket.readyState) // 0
// readyState
// 0 链接还没有建立(正在建立链接)
// 1 链接建立成
// 2 链接正在关闭
// 3 链接已经关闭
// 监听链接开启事件
websocket.onopen = function () {
output.innerHTML += "Status: " + websocket.readyState + "\n";
console.log(websocket.readyState)
}
// 监听服务端消息推送事件
websocket.onmessage = function (back) {
output.innerHTML += "Server: " + back.data + "\n";
console.log(back.data)
}
// 监听连接错误信息
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
//监听连接关闭
websocket.onclose = function (evt) {
console.log("Disconnected");
};
// 绑定按钮点击事件
function send() {
websocket.send(input.value)
input.value = "";
}
</script>
</body>
</html>
服务端
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
for {
// messageType is either TextMessage == 1 or BinaryMessage == 2
msgType, msg, err := conn.ReadMessage()
if err != nil {
return
}
fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))
if err = conn.WriteMessage(msgType, msg); err != nil {
return
}
}
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "client.html")
})
fmt.Println("server is running on :8080...")
http.ListenAndServe(":8080", nil)
}
运行 go run server.go
访问 http://xxx.xxx.xxx.xxx:8080/
请求头
GET ws://localhost:8080/echo HTTP/1.1
Host: localhost:8080
Origin: http://localhost:8080
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: h8MnjGsXMtnoAcyCAn+V5Q==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
响应头
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: n+hJTwLJbZTrnaXspOIBJZYIDh8=
websocket 是在 http 的基础上改造而成的,首先客户端改造成上面的请求头,服务端先要构建http服务,然后针对路由/echo
做改造,分析请求头,构建响应头,另外普通的http请求在响应完就会关掉,此时需要改造成不关掉,也就是长连接,至此一个websocket连接就建立了。
因为ws是基于http之上的,所以默认情况下ws的端口是80,安全协议wss端口是443。
WebSocket协议的数据可以是普通的文本数据,也可以是二进制数据,数据量相对较大时,还可以分片多帧进行数据发送与接收。
请求头中的Sec-WebSocket-Key
和响应头中的Sec-WebSocket-Accept
是对应的,提供基本的防护,Sec-WebSocket-Accept
的计算公式为:将Sec-WebSocket-Key
跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接,通过SHA1
计算出摘要,并转成base64字符串
。
websocket RFC标准 https://www.rfc-editor.org/rfc/rfc6455.txt
数据帧格式
WebSocket客户端、服务端通信的最小单位是帧(frame)
,由1个或多个帧组成一条完整的消息(message)。
- 发送端:将消息切割成多个帧,发送给服务端;
- 服务端:接收消息帧,并将关联的帧重新组装成完整的消息数据;
-
FIN (Final)
:是否为最后一个分片(占1比特位)
如果是1,表示这是消息(message)的最后一个分片(fragment);
如果是0,表示不是是消息(message)的最后一个分片(fragment)。 -
RSV1, RSV2, RSV3 (Reserved)
:扩展字段(共占3比特位)
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。
对于 Reserved 字段,这里不做详细说明,如有扩展需求,可自行查阅相关国际标准:
RFC6455: WebSocket协议
https://www.rfc-editor.org/rfc/rfc6455.txt -
Opcode
:操作码(占4比特位)
Opcode的值决定了应该如何解析后续的数据载荷(data payload)。
可选的操作代码如下:Opcode 含义 0x0 表示一个延续帧,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片 0x1 表示这是一个文本帧(frame) 0x2 表示这是一个二进制帧(frame) 0x3-7 保留的操作代码,用于后续定义的非控制帧 0x8 表示连接断开 0x9 表示这是一个ping操作 0xA 表示这是一个pong操作 0xB-F 保留的操作代码,用于后续定义的控制帧 -
Mask
:掩码(占1比特位)
表示是否要对数据载荷进行掩码操作。
从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果Mask是1
,那么在Masking-key
中会定义一个掩码键,并用这个掩码键来对数据载荷进行反掩码。 -
Payload length
:数据载荷的长度,单位是字节(占 7、7+16 或7+64 比特位)
假设 Payload length 中前7 个比特位
的值为x
:
如果x
为 0~125,则x
即为载荷数据的有效长度;
如果x
为 126,则后续16比特位代表一个16位的无符号整数,该无符号整数的值为载荷数据的有效长度;
如果x
为 127,则后续64个比特位代表一个64位的无符号整数(最高位为0),该无符号整数的值为载荷数据的有效长度。 -
Masking-key
:掩码键(占0或32比特位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作(Mask值为1),会携带4字节的Masking-key。
对于 Masking-key 掩码算法,这里不做详细说明,如需了解,可自行查阅相关国际标准:
RFC6455: WebSocket协议
https://www.rfc-editor.org/rfc/rfc6455.txt -
Payload data
:载荷数据(占x+y字节)
载荷数据有两部分组成:扩展数据占 x 字节,应用数据占 y 字节
。
扩展数据:
如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应用数据:
任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。
如何使用wireshark抓取websocket协议
1、wireshark无法抓取本地服务的包,也就是流量必须经过网卡才行。
2、分析 --> 启用的协议 --> websocket
3、必须在建立ws连接之前就开始抓,这一点的确容易忽略。
4、显示过滤器输入websocket
,回车。
正常的效果如下
可以看到,客户端发给服务端的信息被MASK了,而服务端发给客户端的信息是明文的。
如果是在连接之后才开始抓,那么是抓不到websocket
协议记录的,于是我使用IP地址来过滤,只能抓到一些TCP的记录,因为wireshark并没有按照ws协议来解析
客户端发送的是wer
,服务端会返回wer
发出去的载荷是经过Mask转换的,此时wireshark并不知道如何解析它,而服务端返回的载荷确实明文的
关于心跳检测ping/pong
websocket协议中,Opcode为0x9
为ping消息,0xA
为pong消息,因此我们使用的websocket第三方库都会在底层处理掉ping/pong的发送和响应,
比如github.com/gorilla/websocket
,在收到ping
消息时,会自行处理掉,也就是说conn.ReadMessage()
不会有返回,而是直接去读取下一条消息了,使用者无感。而作为客户端,应该要每隔一段时间向服务端发送ping消息。或者使用自定义的方式来实现心跳检测。
另外使用github.com/gorilla/websocket
包需要注意的是,同一个连接上,如果一个消息还没read完新消息就来了,它会重置reader,这会导致正在读的消息丢失,直接开始读取新来的消息。