关于websocket:Spring-Boot-websocket-自定义协议开发

51次阅读

共计 8031 个字符,预计需要花费 21 分钟才能阅读完成。

大家都晓得应用 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 端

新建文件 websocketdemo.html

<!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…

可能存在的纳闷点 arr.push(dataView.getInt8(i));
java 服务器 发送给客户端的是 byte[] , java 中 byte 数据类型是 8 位、有符号的,以二进制补码示意的整数 java 的根本数据类型

所以 代码是 dataView.getInt8(i) 而不是 dataView.getUint8(i)

这一点和 golang 有些不一样 依据 golang 的文档形容

The Go Programming Language Specification
Numeric types
uint8 the set of all unsigned 8-bit integers (0 to 255)
byte alias for uint8
JAVA byte int8 -128 ~ 127
GOLANG byte uint8 0 ~ 255

测试

正文完
 0