始终以来对实时通信挺感兴趣,本周就抽空理解了一下websocket。
websocket
WebSocket是一种网络传输协定,可在单个TCP连贯上进行全双工通信,位于OSI模型的应用层。
WebSocket使得客户端和服务器之间的数据交换变得更加简略,容许服务端被动向客户端推送数据。在WebSocket API中,浏览器和服务器只须要实现一次握手,两者之间就能够创立持久性的连贯,并进行双向数据传输。
实际是最好的老师,为了理解websocket
具体实现,打算用websocket
与``java socket进行通信。java socket应用的是传输层协定,而websocket是应用层协定,这就须要咱们手动解决数据。
首先要理解的就是websocket的握手过程和数据帧格局。
websocket握手过程
申请
websocket应用http协定进行握手,首先应用http协定发送申请报文,次要是询问服务器是否反对websocket服务,申请头次要信息如下:
GET ws://localhost:7000/ HTTP/1.1Host: localhost:7000Connection: UpgradeSec-WebSocket-Key: kvMm3tIaxXRCmGHuY01eQw==Sec-WebSocket-Version: 13Upgrade: websocket
这里最重要的就是Sec-WebSocket-Key
,这是客户端生成的随机字符串并base64编码,服务端要对此编码进行响应。
响应
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeWebSocket-Location: ws://127.0.0.1:9527Sec-WebSocket-Accept: Mf7ptCXn+TYF9XtDt8w+j9FCBEg=
最重要的是Sec-WebSocket-Accept
,客户端会对此进行验证,不合乎验证规定都会被视为服务端回绝连贯。生成规定为客户端Sec-WebSocket-Key
去除首尾空白,连贯固定字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)之后,应用sha-1进行hash操作,后果再用base64编码即可。
java代码实现:
public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "WebSocket-Location: ws://127.0.0.1:9527\r\n"; ServerSocket serverSocket = new ServerSocket(7000); while (true) { Socket socket = serverSocket.accept(); // 开启一个新线程 Thread thread = new Thread(() -> { // 响应握手信息 try { // 读取申请头 byte[] bytes = new byte[10000000]; socket.getInputStream().read(bytes); String requestHeaders = new String(bytes, StandardCharsets.UTF_8); // 获取申请头中的 String webSocketKey = ""; for (String header : requestHeaders.split("\r\n")) { if (header.startsWith("Sec-WebSocket-Key")) { webSocketKey = header.split(":")[1].trim(); } } // 将webSocketKey 与 magicKey 拼接用sha1加密之后在进行base64编码 String value = webSocketKey + magicKey; String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8); // 写入返回头 握手完结 胜利建设连贯 String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n"; socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8)); System.out.println("握手胜利,胜利建设连贯"); } } }
首先新建ServerSocket监听7000端口,当tcp连贯建设时,从inputStream中读取客户端发送的http字节数组,将其转换为字符串,此时requestHeaders的值应为http申请头。
从中提取出Sec-WebSocket-Key
的值,再依据规定生成webSocketAccept
,拼接到定义好的RESPONSE_HEADERS
,将此数据写入socket的outputStream
,客户端收到并验证通过后,即胜利建设连贯。
数据帧格局
胜利进行握手后,为了失常通信,还须要理解websocket的数据帧格局:
- FIN:1 bit
示意这是不是音讯的最初一帧。第一帧也有可能是最初一帧。 0: 还有后续帧 。1:最初一帧 - RSV1、RSV2、RSV3:1 bit
扩大字段,除非一个扩大通过协商赋予了非零值的某种含意,否则必须为0 - opcode:4 bit
解释 payload data 的类型,如果收到辨认不了的opcode,间接断开。分类值如下: 0:间断的帧. 1:text帧. 2:binary帧 .3 - 7:为非管制帧而预留的 .8:敞开握手帧 .9:ping帧.A:pong帧 .B - F:为非管制帧而预留的 - MASK:1 bit
标识 Payload data 是否通过掩码解决,如果是 1,Masking-key域的数据即为掩码密钥,用于解码Payload data。协定规定客户端数据须要进行掩码解决,所以此位为1 - Payload len:7 bit | 7+16 bit | 7+64 bit
示意了 “无效负荷数据 Payload data”,以字节为单位: - 如果是 0~125,那么就间接示意了 payload 长度 - 如果是 126(二进制111 1110),那么 先存储 0x7E(=126)接下来的两个字节示意的 16位无符号整型数的值就是 payload 长度 - 如果是 127,那么 先存储 0x7F(=127)接下来的八个字节示意的 64位无符号整型数的值就是 payload 长度. - Masking-key:0 | 4 bytes 掩码密钥,所有从客户端发送到服务端的帧都蕴含一个 32bits 的掩码(如果mask被设置成1),否则为0。一旦掩码被设置,所有接管到的 payload data 都必须与该值以一种算法做异或运算来获取实在值。
- Payload data 利用发送的数据信息
基于websocket的数据帧,咱们须要实现两个办法,一是提取数据帧中的数据,二是将数据转化为数据帧。
解码数据帧
核心思想就是依据管制字段来确定数据字段的读取形式,将其读取并解码。
/** * 将字节数组解码为字符串 * @param bytes websocket帧字节数组 * @return 解析为字符串 */ public static String decodeMessage(byte[] bytes) { int col = 0; boolean isMask = false; int dataStart = 2; // 提取websocket帧中的mask位 if ((bytes[1] & 0x80) == 0x80) { isMask = true; } // 提取playload len int len = bytes[1] & 0x7f; byte[] maskKey = new byte[4]; if (len == 126) { // 如果为126 持续往后两个字节读取作为playload len len = bytes[2]; len = (len << 8) + bytes[3]; // 如mask为1 向后读取4个字节作为maskKey if (isMask) { maskKey[0] = bytes[4]; maskKey[1] = bytes[5]; maskKey[2] = bytes[6]; maskKey[3] = bytes[7]; // payload data 开始的地位在maskKey之后 dataStart = 8; } else { dataStart = 4; } } else if (len == 127) { // 如果为126 持续往后八个字节读取作为playload len // 这里跳过bytes[2]~bytes[5] len = bytes[6]; len = (len << 8) + bytes[7]; len = (len << 8) + bytes[8]; len = (len << 8) + bytes[9]; if (isMask) { maskKey[0] = bytes[10]; maskKey[1] = bytes[11]; maskKey[2] = bytes[12]; maskKey[3] = bytes[13]; dataStart = 14; } else { dataStart = 10; } } else { // 既不是126也不是127 阐明长度仅占七位 不必解决 if (isMask) { maskKey[0] = bytes[2]; maskKey[1] = bytes[3]; maskKey[2] = bytes[4]; maskKey[3] = bytes[5]; dataStart = 6; } else { dataStart = 2; } } // 读取payload data 并依据isMask判读是否进行mask加密 for (int i = 0, count = 0; i < len; i++) { byte t1 = maskKey[count]; byte t2 = bytes[i + dataStart]; // 从datastart 开始读取data char c = isMask ? (char) (((~t1) & t2) | (t1 & (~t2))) : (char) t2; // isMask为真,进行mask加密 bufferRes[col++] = c; count = (count + 1) % 4; } bufferRes[col++] = '\0'; return new String(bufferRes); }
编码信息为数据帧
核心思想就是依据信息格式将要发送的数据转化为websocket数据帧。
/** * 将message编码为websocket帧 * @param message 字符信息 * @param isMask 是否进行mask加密 * @param result 保留帧的字节数组 * @return 字节长度 */ public static int encodeMessage(String message, boolean isMask, byte[] result) { int dataEnd = 0; // 帧的第一个字节为类型, 设置为默认类型为text帧 result[dataEnd++] = (byte) 0x81; byte[] maskKey = new byte[4]; // 获取message 的字节数组 byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); // isMask为真 设置mask位为1 if (isMask) { result[dataEnd] = (byte) 0x80; } // 判断数据的长度 long dataLen = messageBytes.length; if (dataLen < 126) { // 小于126字节 间接赋值 result[dataEnd++] |= dataLen & 0x7f; } else if (dataLen < 65536) { // 小于65536字节,往后赋值两个字节 result[dataEnd++] |= 0x7E; result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF); } else if (dataLen < 0xFFFFFFFF) { // 小于0xFFFFFFFF个字节,往后赋值八个字节 // 防止数据过大 跳过4个字节 result[dataEnd++] |= 0x7F; result[dataEnd++] |= 0; result[dataEnd++] |= 0; result[dataEnd++] |= 0; result[dataEnd++] |= 0; result[dataEnd++] = (byte) ((dataLen >> 24) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 16) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF); result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF); } if (isMask) { // 如果isMask为真 将数据进行mask加密再保留到帧中 new Random().nextBytes(maskKey); result[dataEnd++] = maskKey[0]; result[dataEnd++] = maskKey[1]; result[dataEnd++] = maskKey[2]; result[dataEnd++] = maskKey[3]; for (int i = 0, count = 0; i < dataLen; i++) { byte t1 = maskKey[count]; byte t2 = messageBytes[i]; result[dataEnd++] = (byte) (((~t1) & t2) | (t1 & (~t2))); count = (count + 1) % 4; } } else { // 间接保留到帧中 for (int i = 0; i < dataLen; i++) { result[dataEnd++] = messageBytes[i]; } } return dataEnd; }
试验
java
端提供ServerSocket
用于建设socket连贯,连贯胜利后,对websocket做出握手响应,读取websocket帧
时解码读取信息,发送信息时转化为websocket帧
。
public static int userCount = 0; public static char[] bufferRes = new char[131072]; public static Scanner sc = new Scanner(System.in); public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "WebSocket-Location: ws://127.0.0.1:9527\r\n"; public static String magicKey = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(7000); while (true) { Socket socket = serverSocket.accept(); userCount++; // 开启一个新线程 Thread thread = new Thread(() -> { // 响应握手信息 try { // 读取申请头 byte[] bytes = new byte[10000000]; socket.getInputStream().read(bytes); String requestHeaders = new String(bytes, StandardCharsets.UTF_8); // 获取申请头中的 String webSocketKey = ""; for (String header : requestHeaders.split("\r\n")) { if (header.startsWith("Sec-WebSocket-Key")) { webSocketKey = header.split(":")[1].trim(); } } // 将webSocketKey 与 magicKey 拼接用sha1加密之后在进行base64编码 String value = webSocketKey + magicKey; String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8); // 写入返回头 握手完结 胜利建设连贯 String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n"; socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8)); System.out.println("握手胜利,胜利建设连贯"); // 承受客户端信息 while (true) { System.out.println("读取信息"); socket.getInputStream().read(bytes); String message = decodeMessage(bytes); System.out.println("读取到的信息为:" + message); System.out.println("请回复信息"); String res = sc.next(); byte[] result = new byte[10000000]; int len = encodeMessage(res, false, result); socket.getOutputStream().write(result, 0, len); } } catch (IOException e) { e.printStackTrace(); } System.out.println("finish Read"); }); thread.setName("用户" + userCount); thread.start(); } }
客户端用简略js
代码实现(感激赵凯强同学提供)。
<!DOCTYPE html><html><head> <title>websocket test</title></head><body> <ul id="ul"> </ul> <input id="input" type="input" /> <button onclick="onClick()">发送</button> <script type="text/javascript"> var ws = new WebSocket("ws://localhost:7000"); ws.onopen = function(evt) { console.log("Connection open ..."); }; ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data); addLi('承受: ' + evt.data); }; ws.onclose = function(evt) { console.log("Connection closed."); }; // 点击发送 读取input的值打印到管制台上 function onClick() { var value = '发送: ' + document.getElementById("input").value; ws.send(value); document.getElementById("input").value = ''; addLi(value); } // 一个办法 sring 当调用时, 把字符串插入到列表中 function addLi(value) { document.getElementById("ul").innerHTML += "<li>" + value + "</li>"; } </script></body></html>
成果:
https://zhuanlan.zhihu.com/p/...