美文网首页
Websocket在JavaScript中操作字节序 之 客户端

Websocket在JavaScript中操作字节序 之 客户端

作者: 程就人生 | 来源:发表于2020-08-18 15:54 被阅读0次

SpringBoot 2 整合 Netty 实现基于 DTU 的 TCP 服务器 之 服务端
SpringBoot 2 整合 Netty 实现基于 DTU 的 TCP 服务器 之 客户端
在上面的两篇文章中,使用SpringBoot2整合了基于DTU的TCP服务器,还可以进行客户端服务端通讯的模拟。但有一个场景未加考虑,那就是用户通过页面给DTU的服务端发送命令,不是通过后端,而是通过前端页面,这就用到了Websocket。

也曾使用Websocket写过即时通讯的客户端demo,那么使用Websocket如何操作字节序呢?不由得想起以前走过的一段弯路,在这段弯路里遇到了一个很好用的js工具类ByteBuffer,这个类就是为操作字节序而生的。

关于字节序,在一开始还不知道怎么称呼,称它为字节数组,这种叫法显然是错误的。正确的说法应该是字节序。不同数据类型的数据占用的字节大小是不一样的,不同数据类型的字节组成的一个序列。数组里面的数据类型则是同一种,字节序和字节数组根本就不是一个概念,千万不要再搞错了!

下面就看看在页面里如何使用websocket结合ByteBuffer发起及处理字节序的操作。
第一,web端页面代码;
页面上的代码很简单,调用封装好的websocket即可;流程也很简单,建立连接,发起登录操作,然后保持心跳;

<html xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" >
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试</title>
</head>
<body>
    <button onclick="clickWebsocket()" >按钮</button>
    <script th:src="@{/js/websocket.js}"></script>
    <script th:inline="javascript">
    // webSocket连接
    var webSocket;
    function clickWebsocket(){      
        if (webSocket == null) { // 初次连接websocket
            console.log("初次连接websocket...");
            //进行登录操作
            var param={"deviceId":2};
            webSocket = new WsSocket(function () {
                webSocket.login();
            },param);
            webSocket.createWebSocket();
        } else {
            console.log("webSocket连接已建立...");
        }
    }        
    </script>
</body>
</html>

第二,websocket核心代码;
这里对websocket做了一个封装,方便使用。建立连接、发起登录、保持心跳的代码都在这里。其他的业务逻辑根据需要也可以添加在这里。这段代码的核心,就是对字节序操作封包、解包,请求服务器时封包,处理服务器返回的数据时解包,随便加入一个byte、short、int类型的数据都很方便。

document.write("<script type='text/javascript' src='/js/ByteBuffer.js'></script>");

/**
 * 字头,固定
 */
let FIELD_HEAD = 0XCC;

/**
 * 长度(数据长度2位 + 1位校验位)
 */
let FILED_LEN = 3;

//心跳
let HEART_BEAT = 0;

//登录
let LOGIN = 1;

WsSocket = function (onOpen,param) {
    this.socket = null;
    //服务器地址
    this.wsUrl = "ws://127.0.0.1:8502/websocket/";
    //避免重复连接
    this.lockReconnect = false;
    //重连标识
    this.reconnectFlag = null;
    // 最大重连次数
    this.maxReconnectNum = 3;
    // 当前重连次数
    this.currentReconnectNum = 0;
    // 重连间隔,4s
    this.reconnectTime = 4000;
    // 心跳响应
    this.heartCheckTimeoutObj = null;
    // websocket默认是传输字符串的,需要改为arraybuffer二进制传输类型
    this.binaryType = "arraybuffer";
    // 连接建立后处理的方法
    this.onOpen = onOpen;
    //传递过来的参数
    this.param = param;
    // 对消息的处理过程
    this.onMessage = function(evt) {};

    // 建立连接
    this.createWebSocket = function () {
        try {
            if (typeof (WebSocket) == "undefined") {
                console.log("您的浏览器不支持WebSocket");
            } else {
                console.log("您的浏览器支持WebSocket");
            }
            this.socket = new WebSocket(this.wsUrl);
            //websocket默认是传输字符串的,需要改为arraybuffer二进制传输类型
            this.socket.binaryType = this.binaryType;
            //初始化
            this.init();
        } catch (e) {
            this.reconnect();
        }
    };

    //重新连接
    this.reconnect = function () {
        let _self = this;
        if (this.lockReconnect) {
            return;
        }
        this.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        this.reconnectFlag && clearTimeout(this.reconnectFlag);
        this.reconnectFlag = setTimeout(function () {
            if (_self.currentReconnectNum < _self.maxReconnectNum) { // 限制重连次数
                console.log("第" + _self.currentReconnectNum + '次重连');
                _self.currentReconnectNum++;
                _self.createWebSocket();
            } else { // 重连失败,弹出错误信息
                console.log('重连失败!')
                // TODO:页面弹出错误提示框
            }
            _self.lockReconnect = false;
        }, _self.reconnectTime);
    };

    // websocket连接成功后,初始化配置
    this.init = function () {
        let _self = this;
        this.socket.onclose = function () {
            console.log('链接关闭');
            _self.reconnect();
        };
        this.socket.onerror = function () {
            console.log('发生异常了');
            _self.reconnect();
        };
        this.socket.onopen = function () {
            console.log("连接成功!");
            _self.currentReconnectNum = 0; // 重置当前重连次数
            _self.onOpen();           
        };
        this.socket.onmessage = function (event) {  
            //把接收到的数据转换成ByteBuffer
            var bytebuf = new ByteBuffer(event.data);
            //var arr = bytebuf.byte().short().byte().byte().byte().unpack();//结尾调用解包方法;
            var arr = bytebuf.byte().short().byte().unpack();//结尾调用解包方法;            
            console.log(arr); 
            if (arr[2] == LOGIN) { //登录成功
                var arr1 = bytebuf.byte().byte().unpack();
                console.log("已经成功登录~!" + arr1); 
                //登录成功后,开始发送心跳
                _self.heartCheck();
            }else if (arr[2] == HEART_BEAT) { // 消息类型是 [心跳响应]
                //第二次读取
                var arr1 = bytebuf.byte().byte().unpack();
                console.log("HEART_BEAT" + arr1);
                //继续发送心跳
                _self.heartCheck();
            }
        };
    };

    // 登录操作
    this.login = function() {
        //组装消息
        let reqMsg = new ByteBuffer();
        //信息封装成二进制
        reqMsg.byte(FIELD_HEAD).byte(LOGIN).byte(this.param.deviceId);
        let dataLength = reqMsg.blength() + FILED_LEN; //加数据长度本身2位,加上校验位1位
        //数据长度放在第二位
        reqMsg.short(dataLength, 1);
        //计算求和
        let sum = this.sum([FIELD_HEAD, dataLength, LOGIN, this.param.deviceId]);
        //取得低八位
        let verify = this.getLow8(sum);
        //校验位        
        reqMsg.byte(verify);
        console.log("数据长度为:" + dataLength);
        let buf = reqMsg.pack();
        this.socket.send(buf);
    };

    //WebSocket心跳检测
    this.heartCheck = function () {
        let timeout = 50000; //心跳检测时间
        let _self = this;

        this.heartCheckTimeoutObj && clearTimeout(this.heartCheckTimeoutObj);
        this.heartCheckTimeoutObj = setTimeout(function () {
            //这里发送一个心跳,后端收到后,返回一个心跳消息,
            //onmessage拿到返回的心跳就说明连接正常
            console.log(_self.getNowTime() + ' socket心跳检测');
            //发送心跳
            let heartBeat = new ByteBuffer();            
            heartBeat.byte(FIELD_HEAD).byte(HEART_BEAT).byte(_self.param.deviceId);
            let dataLength = heartBeat.blength() + FILED_LEN; //加数据长度本身2位,加上校验位1位
            //数据长度放在第二位
            heartBeat.short(dataLength, 1);
            let sum = _self.sum([FIELD_HEAD, dataLength, HEART_BEAT, _self.param.deviceId]);
            //获取低八位
            let verify = _self.getLow8(sum); 
            //校验位
            heartBeat.byte(verify);
            console.log("数据长度为:" + dataLength);
            let buf = heartBeat.pack();
            _self.socket.send(buf);
        }, timeout)
    }

    this.getNowTime = function () {
        var myDate = new Date();
        //获取当前年
        var year = myDate.getFullYear();
        //获取当前月
        var month = myDate.getMonth() + 1;
        //获取当前日
        var date = myDate.getDate();
        var h = myDate.getHours();       //获取当前小时数(0-23)
        var m = myDate.getMinutes();     //获取当前分钟数(0-59)
        var s = myDate.getSeconds();
        return year + '-' + this.p(month) + "-" + this.p(date) + " " + this.p(h) + ':' + this.p(m) + ":" + this.p(s);
    };
    //处理时间的
    this.p = function (s) {
        return s < 10 ? '0' + s : s;
    };
    // int/byte/short型数据,低八位获取
    this.getLow8 = function (number) {
        return number & 0xff;
    };
    // 求和
    this.sum = function (integers) {
        let result = 0;
        for (let i = 0; i < integers.length; i++) {
            result += integers[i];
        }
        return result;
    };
}

第三,这是ByteBuffer.js类,里面是对字节序的操作做了封装;
另外由于编码的需要,在里面加了blength()方法,用来返回已ByteBuffer的长度,这是很有必要的。

/*
* 构造方法
* 源码出处:https://github.com/play175/ByteBuffer
* @param org_buf 需要解包的二进制
* @param offset 指定数据在二进制的初始位置 默认是0
*/
ByteBuffer = function (arrayBuf, offset) {

    var Type_Byte = 1;
    var Type_Short = 2;
    var Type_UShort = 3;
    var Type_Int32 = 4;
    var Type_UInt32 = 5;
    var Type_String = 6;//变长字符串,前两个字节表示长度
    var Type_VString = 7;//定长字符串
    var Type_Int64 = 8;
    var Type_Float = 9;
    var Type_Double = 10;
    var Type_ByteArray = 11;

    var _org_buf = arrayBuf ? (arrayBuf.constructor == DataView ? arrayBuf : (arrayBuf.constructor == Uint8Array ? new DataView(arrayBuf.buffer, offset) : new DataView(arrayBuf, offset))) : new DataView(new Uint8Array([]).buffer);
    var _offset = offset || 0;
    var _list = [];
    var _littleEndian = false;

    //指定字节序 为BigEndian
    this.bigEndian = function(){
       _littleEndian = false;
        return this;
    };

    //指定字节序 为LittleEndian
    this.littleEndian = function(){
       _littleEndian = true;
        return this;
    };

    if (!ArrayBuffer.prototype.slice) {
        ArrayBuffer.prototype.slice = function (start, end) {
            var that = new Uint8Array(this);
            if (end == undefined) end = that.length;
            var result = new ArrayBuffer(end - start);
            var resultArray = new Uint8Array(result);
            for (var i = 0; i < resultArray.length; i++)
                resultArray[i] = that[i + start];
            return result;
        }
    }

    function utf8Write(view, offset, str) {
      var c = 0;
      for (var i = 0, l = str.length; i < l; i++) {
        c = str.charCodeAt(i);
        if (c < 0x80) {
          view.setUint8(offset++, c);
        }
        else if (c < 0x800) {
          view.setUint8(offset++, 0xc0 | (c >> 6));
          view.setUint8(offset++, 0x80 | (c & 0x3f));
        }
        else if (c < 0xd800 || c >= 0xe000) {
          view.setUint8(offset++, 0xe0 | (c >> 12));
          view.setUint8(offset++, 0x80 | (c >> 6) & 0x3f);
          view.setUint8(offset++, 0x80 | (c & 0x3f));
        }
        else {
          i++;
          c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
          view.setUint8(offset++, 0xf0 | (c >> 18));
          view.setUint8(offset++, 0x80 | (c >> 12) & 0x3f);
          view.setUint8(offset++, 0x80 | (c >> 6) & 0x3f);
          view.setUint8(offset++, 0x80 | (c & 0x3f));
        }
      }
    }

    function utf8Read(view, offset, length) {
      var string = '', chr = 0;
      for (var i = offset, end = offset + length; i < end; i++) {
        var byte = view.getUint8(i);
        if ((byte & 0x80) === 0x00) {
          string += String.fromCharCode(byte);
          continue;
        }
        if ((byte & 0xe0) === 0xc0) {
          string += String.fromCharCode(
            ((byte & 0x0f) << 6) |
            (view.getUint8(++i) & 0x3f)
          );
          continue;
        }
        if ((byte & 0xf0) === 0xe0) {
          string += String.fromCharCode(
            ((byte & 0x0f) << 12) |
            ((view.getUint8(++i) & 0x3f) << 6) |
            ((view.getUint8(++i) & 0x3f) << 0)
          );
          continue;
        }
        if ((byte & 0xf8) === 0xf0) {
          chr = ((byte & 0x07) << 18) |
            ((view.getUint8(++i) & 0x3f) << 12) |
            ((view.getUint8(++i) & 0x3f) << 6) |
            ((view.getUint8(++i) & 0x3f) << 0);
          if (chr >= 0x010000) { // surrogate pair
            chr -= 0x010000;
            string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);
          } else {
            string += String.fromCharCode(chr);
          }
          continue;
        }
        throw new Error('Invalid byte ' + byte.toString(16));
      }
      return string;
    }

    function utf8Length(str) {
      var c = 0, length = 0;
      for (var i = 0, l = str.length; i < l; i++) {
        c = str.charCodeAt(i);
        if (c < 0x80) {
          length += 1;
        }
        else if (c < 0x800) {
          length += 2;
        }
        else if (c < 0xd800 || c >= 0xe000) {
          length += 3;
        }
        else {
          i++;
          length += 4;
        }
      }
      return length;
    }

    this.byte = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getUint8(_offset,_littleEndian));
            _offset += 1;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_Byte, d: val, l: 1 });
            _offset += 1;
        }
        return this;
    };

    this.short = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getInt16(_offset,_littleEndian));
            _offset += 2;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_Short, d: val, l: 2 });
            _offset += 2;
        }
        return this;
    };

    this.ushort = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getUint16(_offset,_littleEndian));
            _offset += 2;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_UShort, d: val, l: 2 });
            _offset += 2;
        }
        return this;
    };

    this.int32 = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getInt32(_offset,_littleEndian));
            _offset += 4;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_Int32, d: val, l: 4 });
            _offset += 4;
        }
        return this;
    };

    this.uint32 = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getUint32(_offset,_littleEndian));
            _offset += 4;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_UInt32, d: val, l: 4 });
            _offset += 4;
        }
        return this;
    };
    
    /**
     * 新加的方法,获取bytebuffer的长度
     */
    this.blength = function(){
        return _offset;
    };

    /**
    * 变长字符串 前4个字节表示字符串长度
    **/
    this.string = function (val, index) {
        if (arguments.length == 0) {
            var len = _org_buf.getInt32(_offset,_littleEndian);
            _offset += 4;
            _list.push(utf8Read(_org_buf,_offset,len));
            _offset += len;
        } else {
            var len = 0;
            if (val) {
                len = utf8Length(val);
            }
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_String, d: val, l: len });
            _offset += len + 4;
        }
        return this;
    };

    /**
    * 定长字符串 val为null时,读取定长字符串(需指定长度len)
    **/
    this.vstring = function (val, len, index) {
        if (!len) {
            throw new Error('vstring must got len argument');
            return this;
        }
        if (val == undefined || val == null) {
            var vlen = 0;//实际长度
            for (var i = _offset; i < _offset + len; i++) {
                if (_org_buf.getUint8(i) > 0) vlen++;
            }
            _list.push(utf8Read(_org_buf,_offset,vlen));
            _offset += len;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_VString, d: val, l: len });
            _offset += len;
        }
        return this;
    };

    this.int64 = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getFloat64(_offset,_littleEndian));
            _offset += 8;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_Int64, d: val, l: 8 });
            _offset += 8;
        }
        return this;
    };

    this.float = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getFloat32(_offset,_littleEndian));
            _offset += 4;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_Float, d: val, l: 4 });
            _offset += 4;
        }
        return this;
    };

    this.double = function (val, index) {
        if (arguments.length == 0) {
            _list.push(_org_buf.getFloat64(_offset,_littleEndian));
            _offset += 8;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_Double, d: val, l: 8 });
            _offset += 8;
        }
        return this;
    };

    /**
    * 写入或读取一段字节数组
    **/
    this.byteArray = function (val, len, index) {
        if (!len) {
            throw new Error('byteArray must got len argument');
            return this;
        }
        if (val == undefined || val == null) {
            var arr = new Uint8Array(_org_buf.buffer.slice(_offset, _offset + len));
            _list.push(arr);
            _offset += len;
        } else {
            _list.splice(index != undefined ? index : _list.length, 0, { t: Type_ByteArray, d: val, l: len });
            _offset += len;
        }
        return this;
    };

    /**
    * 解包成数据数组
    **/
    this.unpack = function () {
        return _list;
    };

    /**
    * 打包成二进制,在前面加上4个字节表示包长
    **/
    this.packWithHead = function () {
        return this.pack(true);
    };

    /**
    * 打包成二进制
    * @param ifHead 是否在前面加上4个字节表示包长
    **/
    this.pack = function (ifHead) {
        _org_buf = new DataView(new ArrayBuffer((ifHead) ? _offset + 4 : _offset));
        var offset = 0;
        if (ifHead) {
            _org_buf.setUint32(offset, _offset,_littleEndian);
            offset += 4;
        }
        for (var i = 0; i < _list.length; i++) {
            switch (_list[i].t) {
                case Type_Byte:
                    _org_buf.setInt8(offset, _list[i].d);
                    offset += _list[i].l;
                    break;
                case Type_Short:
                    _org_buf.setInt16(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_UShort:
                    _org_buf.setUint16(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_Int32:
                    _org_buf.setInt32(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_UInt32:
                    _org_buf.setUint32(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_String:
                    //前4个字节表示字符串长度
                    _org_buf.setUint32(offset, _list[i].l,_littleEndian);
                    offset += 4;
                    utf8Write(_org_buf,offset,_list[i].d);
                    offset += _list[i].l;
                    break;
                case Type_VString:
                    utf8Write(_org_buf,offset,_list[i].d);
                    var vlen = utf8Length(_list[i].d);//字符串实际长度
                    //补齐\0
                    for (var j = offset + vlen; j < offset + _list[i].l; j++) {
                        _org_buf.setUint8(j, 0);
                    }
                    offset += _list[i].l;
                    break;
                case Type_Int64:
                    _org_buf.setFloat64(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_Float:
                    _org_buf.setFloat32(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_Double:
                    _org_buf.setFloat64(offset, _list[i].d,_littleEndian);
                    offset += _list[i].l;
                    break;
                case Type_ByteArray:
                    var indx = 0;
                    for (var j = offset; j < offset + _list[i].l; j++) {
                        if (indx < _list[i].d.length) {
                            _org_buf.setUint8(j, _list[i].d[indx]);
                        } else {//不够的话,后面补齐0x00
                            _org_buf.setUint8(j, 0);
                        }
                        indx++
                    }
                    offset += _list[i].l;
                    break;
            }
        }
        return _org_buf.buffer;
    };

    /**
   * 未读数据长度
   **/
    this.getAvailable = function () {
        if (!_org_buf) return _offset;
        return _org_buf.buffer.byteLength - _offset;
    };
}

服务器端的代码,敬请期待下一篇文稿。

相关文章

网友评论

      本文标题:Websocket在JavaScript中操作字节序 之 客户端

      本文链接:https://www.haomeiwen.com/subject/tnqkjktx.html