一、概述
1、缘起
websocket协议诞生于2008年,在2011年成为国际标准,此时Http 1.1 已诞生了12年(1999年成为国标)。
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。因此,在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
在此之前双向通信(客户端要向服务器传送数据,同时服务器也需要实时的向客户端传送信息,一个聊天系统就是典型的双向通信)时一般会使用这样几种解决方案:
1.轮询(polling):轮询就会造成对网络和通信双方的资源的浪费,且非实时。
2.长轮询:客户端发送一个超时时间很长的Request,服务器hold住这个连接,在有新数据到达时返回Response,相比#1,占用的网络带宽少了,其他类似。
3.长连接:Connection: keep-alive
2、解决了什么问题
1.websocket通过自己的 WS 协议(此处与HTTP协议有所区别)创建一个基于HTTP request请求并创建TCP链接之后,之后的数据交换都不需要再次去创建连接,实现真正的长连接。
2.websocket采用异步回调的方式接受消息,当建立通信连接,可以做到持久性的连接,并进行通信。而不像上面的几种方式一样需要定时进行发起请求到服务器获取最新更新信息,显得相当的被动)
3.实质的推送方式是服务器主动推送,只要有数据就推送到请求方。(变被动为主动)
3、怎么做到的
websocket协议本质上是一个基于TCP(长连接)的协议。建立连接需要握手,客户端首先向服务器发起一条特殊的http请求,带有websocket的升级标识,服务端对请求报文进行握手,生成客户端识别的响应报文,完成websocket连接的建立,直到某一方关闭连接才会结束。
二、连接过程
1.必不可少的TCP三次握手和四次挥手
2、客户端发起请求
可以看出跟http请求的一些不同之处
headers | desc |
---|---|
101 | 首次发送请求进行握手,用的还是http1.1协议,规定服务端返回101 状态码,为握手成功 |
Connection: upgrade | 同意升级 |
Upgrade: websocket | 同意升级 |
Sec-WebSocket-Accept: vPTr9rrkhy8cA5jkXdEpDjFjqVU= | 服务器响应,包含Sec-WebSocket-Key的签名值,证明它支持请求的协议版本 |
用入参Sec-WebSocket-Key+“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”(这个值是固定的)
进行SHA-1算法加密再进行Base64编码返回 |
|Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15| deflate压缩算法,client_max_window_bits=15为算法的最大窗口长度,默认为15,范围必须在8-15之间 |
3.服务端返回报文
headers | desc |
---|---|
101 | 首次发送请求进行握手,用的还是http1.1协议,规定服务端返回101 状态码,为握手成功 |
Connection: upgrade | 同意升级 |
Upgrade: websocket | 同意升级 |
Sec-WebSocket-Accept: vPTr9rrkhy8cA5jkXdEpDjFjqVU= | 服务器响应,包含Sec-WebSocket-Key的签名值,证明它支持请求的协议版本用入参Sec-WebSocket-Key+“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”(这个值是固定的)进行SHA-1算法加密再进行Base64编码返回 |
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15 | deflate压缩算法,client_max_window_bits=15为算法的最大窗口长度,默认为15,范围必须在8-15之间 |
三、数据格式及解析
1.websocket数据格式—rfc6455.pdf(websocket基本协议内容)
以二进制数据进行解析
name | desc |
---|---|
fin | FIN, 长度为 1 bits, 该标志位用于指示当前的 frame 是消息的最后一个分段。WebSocket 支持将长消息切分为若干个 frame 发送, 切分以后, 除了最后一个 frame, 前面的 frame 的 FIN 字段都为 0, 最后一个 frame 的 FIN 字段为 1, 当然, 若消息没有分段, 那么一个 frame 便包含了完成的消息, 此时其 FIN 字段值为 1。 |
RSV | 预留位,但必须是0 |
opcode | 占4bits ,操作描述符 16进制 0x0–0xf 我们只需要关心 1:文本数据 2:二进制数据 8:关闭连接 |
MASK | 长度为 1 bits, 该字段是一个标志位, 用于指示 frame 的数据 (Payload) 是否使用掩码掩盖。 服务端要求客户端请求的MASK必须为 1,反之则不需要。 |
Payload len | 数据长度 7 bits, 7+16 bits, or 7+64 bits 这个是可变的长度7位的长度 只能表达 128(0-127),这个字段有三个可能,0-125 时 数据长度就是此值;126时,往后取16比特 来表示数据长度;127时往后取64比特来表示长度。基本可以表达相当大的数据量,如果还不够就需要考虑数据分片。 |
Masking-key | 长度64 bits,当前面的MASK 为1 时才有此值,用于对真实数据进行掩码处理。掩码操作就是以 掩码数据对后续的业务数据进行遍历异或运算,客户端加掩码时也是这么操作的 |
Payload Data | 业务数据,长度为上文 Payload len所述 |
简易解码逻辑–如何取到业务数据
public static byte[] dataToMsg(IoBuffer in) {
if (in.remaining() < 2)
return null;
//一次get取一个字节,占8个bit位
byte fstByte = in.get();
if ((fstByte & 0x8) != 0) {
//fin位必须为1 后三位为0
return null;
}
int rsv=(fstByte & 0x70) >>> 4;
int rsvNext = rsv;
if ((rsv & RSV_BITMASK) != 0) {
rsvNext = rsv ^ RSV_BITMASK;
}
if(rsvNext != 0){
//验证RSV和操作码组合
return null;
}
int opCode = fstByte & 0x0f;
switch (opCode) {
case 0x0:
return null;
case 0x1:
byte secByte = in.get();
//第一位判断
boolean isMasking = ((secByte & 0x80) != 0);
int dataLength;
//取后7位
byte payload = (byte) (secByte & 0x7F);
logger.info("数据长度:{}",payload);
if (payload == 126)
//如果是126 再取 无符合的short类型 相当于两个字节类型 16位
dataLength = in.getUnsignedShort();
else if (payload == 127)
//如果是127 再取 Long类型 相当于8个字节类型 64位
dataLength = (int) in.getLong();
else
//真实长度
dataLength = payload;
byte[] mask = new byte[4];
byte[] data = new byte[dataLength];
if (isMasking)
in.get(mask);
in.get(data);
// 用掩码处理数据。
for (int i = 0, maskLength = mask.length, looplimit = data.length; i < looplimit; i++)
data[i] = (byte) (data[i] ^ mask[i % maskLength] & 0xFF);
return data;
default:
return null;
}
}
2.deflate压缩算法–2015年引入的rfc7692 协议
一种利用数据的重复结构进行压缩的算法
public static void testInflater(String message){
try {
System.out.println("Original Message: " + message);
System.out.println("Original Message length: " + message.getBytes(StandardCharsets.UTF_8).length);
byte[] input = message.getBytes("UTF-8");
// Compress the bytes
byte[] output = new byte[1024];
Deflater deflater = new Deflater();
deflater.setInput(input);
deflater.finish();
//返回压缩后长度
int compressedDataLength = deflater.deflate(output);
deflater.end();
// System.out.println("Compressed Message: " + output.length);
System.out.println("Compressed Message length: " + compressedDataLength);
// Decompress the bytes
Inflater inflater = new Inflater();
inflater.setInput(output);
byte[] result = new byte[1024*1024];
int resultLength = inflater.inflate(result);
inflater.end();
// Decode the bytes into a String
message = new String(result, 0, resultLength, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (DataFormatException e) {
e.printStackTrace();
}
System.out.println("UnCompressed Message: " + message);
}
如果数据过短可能不会起到好的效果
3.sockJs&StompJs
1.sockJs —维护了请求降级逻辑,即浏览器不支持时,降级为轮询
链接: https://github.com/sockjs/sockjs-client
2.Stomp.Js–更为便捷的简单流文本传输协议
链接: https://github.com/stomp-js/stompjs
简单来说就是定义了websocket-message的内容格式来优雅的实现C/S架构的消息通讯,并维护了服务端->客户端的心跳。
四、开发一个websocket服务器
代码演示–待上传
五、运用场景
代码包:
简易聊天室演示: