Spring Boot + websocket 自定义协议开发
作者:myworld
来源:SegmentFault 思否社区
大家都知道使用socket通信都是二进制,通信框架多是使用二进制通信,高效且快速,但在前端如何编辑发送二进制,二进制数据在日常的JavaScript中很少遇到,但是当你使用WebSocket与后端进行数据交互时,就有可能会用到二进制的数据格式。
这里我们自定义一个简单协议 写一个前后端websocket交互的示例
定义协议
前2个字节 定义消息类型(如心跳包/权限检查包等)
剩余字节 定义消息体
服务端代码
我们在上篇 websocket入门笔记 的基础上再次开发
修改 MyWebsocketHandler 继承 BinaryWebSocketHandler
package com.ben.websocketdemo;
import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import org.springframework.web.socket.handler.BinaryWebSocketHandler;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
@Component
public class MyWebsocketHandler extends BinaryWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("afterConnectionEstablished");
}
// // 发送
// @Override
// protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// String msg = message.getPayload();
//
// // 向客户端发送数据
// session.sendMessage(new TextMessage("你好哦: " + msg));
// }
// 发送二进制消息
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
ByteBuffer byteBuffer = message.getPayload();
short mytype = byteBuffer.getShort();
byte[] bytePrefix = ByteBuffer.allocate(2).putShort(mytype).array();
Calendar calendar= Calendar.getInstance();
int m = calendar.get(Calendar.MINUTE);
int s = calendar.get(Calendar.SECOND);
String time = String.format("%02d", m) + ":" + String.format("%02d", s);
switch (mytype){
case 1: // 心跳包
byte[] content = time.getBytes(StandardCharsets.UTF_8);
byte[] bytes = byteMergerAll(bytePrefix,content);
session.sendMessage(new BinaryMessage(bytes));
break;
default:
byte[] contentRecevid = new byte[byteBuffer.remaining()];
byteBuffer.get(contentRecevid);
String recevidMsg = new String(contentRecevid, StandardCharsets.UTF_8);
System.out.println("收到客户端消息: " + recevidMsg);
String respStr = time + " 服务端已处理: " + recevidMsg;
byte[] respcontent1 = respStr.getBytes(StandardCharsets.UTF_8);
byte[] bytes1 =byteMergerAll(bytePrefix,respcontent1);
session.sendMessage(new BinaryMessage(bytes1));
break;
}
}
private static byte[] byteMergerAll(byte[]... values) {
int length_byte = 0;
for (int i = 0; i < values.length; i++) {
length_byte += values[i].length;
}
byte[] all_byte = new byte[length_byte];
int countLength = 0;
for (int i = 0; i < values.length; i++) {
byte[] b = values[i];
System.arraycopy(b, 0, all_byte, countLength, b.length);
countLength += b.length;
}
return all_byte;
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
System.out.println("afterConnectionClosed");
}
}
上面 handleBinaryMessage 方法注意几点
根据我们自定义协议, byte[] 前2个字节 用于定义消息类型 我们通过 byteBuffer.getShort() 获取 , 之后byteBuffer的position增加2 表明前2个字节已经读取过了 default分支中通过 byteBuffer.get() 获取的是byteBuffer[position,limit]的内容 关于 ByteBuffer 的相关介绍 可以自行搜索下 java ByteBuffer
js端
<!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>
<form name="publish">
<input type="text" name="message">
<input type="submit" value="send">
</form>
<div id="messages"></div>
</body>
<script>
let websocket = new WebSocket("ws://127.0.0.1:8080/bensocket");
websocket.binaryType = "arraybuffer";
const protoLen = 2; //定义协议前部固定长度
document.forms.publish.onsubmit = function () {
let msg = this.message.value;
let content = stringToBytes(msg);
var buffer = new ArrayBuffer(content.length + protoLen);
// let buffer = new ArrayBuffer(protoLen + content.byteLength);
let dataView = new DataView(buffer);
dataView.setInt16(0, 88); // 从第0个Byte位置开始,放置一个数字为1的Short类型数据(占2 Byte) 数字代表消息类型 1是心跳包 其他根据业务自定义
for (var i = 0; i < content.length; i++) {
dataView.setUint8(protoLen + i, content[i]);
}
websocket.send(buffer);
return false;
}
setInterval(() => {
let buffer = new ArrayBuffer(protoLen);
let dataView = new DataView(buffer);
dataView.setInt16(0, 1); // 1表示心跳包
websocket.send(buffer);
}, 1000);
websocket.onopen = function (evt) {
let el = document.createElement('div');
el.textContent = "onopend";
document.getElementById("messages").prepend(el);
};
websocket.onclose = function (evt) {
let el = document.createElement('div');
el.textContent = "onclose";
document.getElementById("messages").prepend(el);
};
websocket.onmessage = function (evt) {
let { data } = evt;
let len = data.byteLength;
let buffer = new ArrayBuffer(len);
let dataView = new DataView(data);
let type = dataView.getInt16(0)
var arr = [];
for (var i = protoLen; i < len; i++) {
arr.push(dataView.getInt8(i));
}
let content = utf8ByteToUnicodeStr(arr);
let el = document.createElement('div');
el.textContent = (type == 1 ? "heart: " : "normal: ") + content;
document.getElementById("messages").prepend(el);
};
websocket.onerror = function (evt) {
document.getElementById("messages").prepend("onerror");
};
/**
*@description:将string转为UTF-8格式signed char字节数组
*
*/
function stringToBytes(str) {
var bytes = new Array();
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
var s = parseInt(c).toString(2);
if (c >= parseInt('000080', 16) && c <= parseInt('0007FF', 16)) {
var af = '';
for (var j = 0; j < (11 - s.length); j++) {
af += '0';
}
af += s;
var n1 = parseInt('110' + af.substring(0, 5), 2);
var n2 = parseInt('110' + af.substring(5), 2);
if (n1 > 127) n1 -= 256;
if (n2 > 127) n2 -= 256;
bytes.push(n1);
bytes.push(n2);
} else if (c >= parseInt('000800', 16) && c <= parseInt('00FFFF', 16)) {
var af = '';
for (var j = 0; j < (16 - s.length); j++) {
af += '0';
}
af += s;
var n1 = parseInt('1110' + af.substring(0, 4), 2);
var n2 = parseInt('10' + af.substring(4, 10), 2);
var n3 = parseInt('10' + af.substring(10), 2);
if (n1 > 127) n1 -= 256;
if (n2 > 127) n2 -= 256;
if (n3 > 127) n3 -= 256;
bytes.push(n1);
bytes.push(n2);
bytes.push(n3);
} else if (c >= parseInt('010000', 16) && c <= parseInt('10FFFF', 16)) {
var af = '';
for (var j = 0; j < (21 - s.length); j++) {
af += '0';
}
af += s;
var n1 = parseInt('11110' + af.substring(0, 3), 2);
var n2 = parseInt('10' + af.substring(3, 9), 2);
var n3 = parseInt('10' + af.substring(9, 15), 2);
var n4 = parseInt('10' + af.substring(15), 2);
if (n1 > 127) n1 -= 256;
if (n2 > 127) n2 -= 256;
if (n3 > 127) n3 -= 256;
if (n4 > 127) n4 -= 256;
bytes.push(n1);
bytes.push(n2);
bytes.push(n3);
bytes.push(n4);
} else {
bytes.push(c & 0xff);
}
}
return bytes;
}
function byteToString(array) {
var result = "";
for (var i = 0; i < array.length; i++) {
result += String.fromCharCode(parseInt(array[i], 2));
}
return result;
}
function utf8ByteToUnicodeStr(utf8Bytes){
var unicodeStr ="";
for (var pos = 0; pos < utf8Bytes.length;){
var flag= utf8Bytes[pos];
var unicode = 0 ;
if ((flag >>>7) === 0 ) {
unicodeStr+= String.fromCharCode(utf8Bytes[pos]);
pos += 1;
} else if ((flag &0xFC) === 0xFC ){
unicode = (utf8Bytes[pos] & 0x3) << 30;
unicode |= (utf8Bytes[pos+1] & 0x3F) << 24;
unicode |= (utf8Bytes[pos+2] & 0x3F) << 18;
unicode |= (utf8Bytes[pos+3] & 0x3F) << 12;
unicode |= (utf8Bytes[pos+4] & 0x3F) << 6;
unicode |= (utf8Bytes[pos+5] & 0x3F);
unicodeStr+= String.fromCharCode(unicode) ;
pos += 6;
}else if ((flag &0xF8) === 0xF8 ){
unicode = (utf8Bytes[pos] & 0x7) << 24;
unicode |= (utf8Bytes[pos+1] & 0x3F) << 18;
unicode |= (utf8Bytes[pos+2] & 0x3F) << 12;
unicode |= (utf8Bytes[pos+3] & 0x3F) << 6;
unicode |= (utf8Bytes[pos+4] & 0x3F);
unicodeStr+= String.fromCharCode(unicode) ;
pos += 5;
} else if ((flag &0xF0) === 0xF0 ){
unicode = (utf8Bytes[pos] & 0xF) << 18;
unicode |= (utf8Bytes[pos+1] & 0x3F) << 12;
unicode |= (utf8Bytes[pos+2] & 0x3F) << 6;
unicode |= (utf8Bytes[pos+3] & 0x3F);
unicodeStr+= String.fromCharCode(unicode) ;
pos += 4;
} else if ((flag &0xE0) === 0xE0 ){
unicode = (utf8Bytes[pos] & 0x1F) << 12;;
unicode |= (utf8Bytes[pos+1] & 0x3F) << 6;
unicode |= (utf8Bytes[pos+2] & 0x3F);
unicodeStr+= String.fromCharCode(unicode) ;
pos += 3;
} else if ((flag &0xC0) === 0xC0 ){ //110
unicode = (utf8Bytes[pos] & 0x3F) << 6;
unicode |= (utf8Bytes[pos+1] & 0x3F);
unicodeStr+= String.fromCharCode(unicode) ;
pos += 2;
} else{
unicodeStr+= String.fromCharCode(utf8Bytes[pos]);
pos += 1;
}
}
return unicodeStr;
}
</script>
</html>
js代码中使用到 ArrayBuffer 和 DataView 来操作字节 具体介绍和用法 参考文章 https://www.jianshu.com/p/468...
java服务器 发送给客户端的是 byte[] , java中byte数据类型是8位、有符号的,以二进制补码表示的整数 java的基本数据类型
The Go Programming Language Specification
Numeric types
uint8 the set of all unsigned 8-bit integers (0 to 255)
byte alias for uint8
测试
评论