Spring Boot + websocket 自定义协议开发

SegmentFault

共 27339字,需浏览 55分钟

 ·

2021-04-24 20:51

作者: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端

新建文件 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

测试



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -


浏览 98
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报