Shiv ENOW large front end

Company official website: CVTE(Guangzhou Shiyuan Stock)

Team: ENOW team of CVTE software Platform Center for Future Education

In this paper, the author

preface

At work, I was in charge of maintaining the company’s Websocket-SDK library. Websocket sends and receives data through ArrayBuffer, but front-end engineers rarely have the opportunity to deal with binary codec, so I had no idea about ArrayBuffer and binary codec scheme at the beginning. I’m going to share my practice with you today.

Through this article, you can learn:

  1. What are TypedArray/ArrayBuffer classes related to binary codec?
  2. Why should you use binary encoding?
  3. Introduction to JavaScript binary codec scheme
  4. How to make Websocket-SDK compatible with wechat applets

ArrayBuffer

An ArrayBuffer represents a chunk of memory that stores binary data. It cannot be read or written directly, but can only be read or written through views (TypedArray and DataView views) that interpret binary data in a specified format.

TypedArray

The TypedArray view describes an underlying binary data buffer, which can be understood as an interface for JavaScript to manipulate binary data. In fact, there is no global property named TypedArray, and there is no constructor named TypedArray. Instead, there are many different global properties whose values are typed array constructors for a particular element type. The TypedArray view supports nine data types.

The data type bytes meaning The corresponding C language type
Int8 1 An 8-bit signed integer signed char
Uint8 1 An 8-bit unsigned integer unsigned char
Uint8C 1 8-bit unsigned integer (automatic filter overflow) unsigned char
Int16 2 16 – bit signed integer short
Uint16 2 16 – bit unsigned integer unsigned short
Int32 4 32 – bit signed integer int
Uint32 4 A 32-bit unsigned integer unsigned int
Float32 4 32-bit floating point number float
Float64 8 64-bit floating point number double

Why should you use binary encoding

To make the data smaller. JSON is characterized by key-value pairs that store data, but if we agree on which fields the data object contains, then the Key Value is not needed. The resulting packets will no longer contain Key values, reducing their size.

For example, we have a data class called Person that we need to encode. Assume that under ideal conditions, we already have two utility classes: WriteBlock and ReadBlock, which write and read binary data, respectively. (Detailed code implementation is attached at the end of the paper)

// Help us write binary data
class WriteBlock {
    // omit details
}

// Implement binary data reading
class ReadBlock {
    // omit details
}

class Person {
  name: string; / / name
  age: number; / / age
  gender: string; / / gender

  constructor(info) {
    this.name = info.name;
    this.age = info.age;
    this.gender = info.gender;
  }

  Encode: () = > ArrayBuffer = () = > {
    const wb = new WriteBlock();
    // Write data in sequence
    wb.WriteStringField(1.this.name);
    wb.WriteIntField(2.this.age);
    wb.WriteStringField(3.this.gender);
    return wb.getArrayBuffer();
  };

  Decode: (
    buffer: ArrayBuffer
  ) = > {
    name: string;
    age: number;
    gender: string;
  } = (buffer) = > {
    const rb = new ReadBlock(buffer);
    // Parse data in sequence according to the agreed protocol
    return {
        name: rb.ReadStringField(1),
        age: rb.ReadIntField(2),
        gender: rb.ReadStringField(3),}}; }Copy the code

Introduction to JavaScript binary codec scheme

JavaScripts use UCS-2 encoding, which is similar to UTF-16, and each character is two bytes. So you can encode JavaScript characters as an ArrayBuffer as follows:

function str2ab(str) {
  var buf = new ArrayBuffer(str.length*2); // Each character takes two bytes
  var bufView = new Uint16Array(buf);
  for (var i=0, strLen=str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}
Copy the code

Decoding method:

1 / / method
function ab2str(buf) {
  return String.fromCharCode.apply(null.new Uint16Array(buf));
}

/ / method 2:
function ab2str(buf) {
  var uint8 = new Uint8Array(buffer)
  var decoder = new TextDecoder('utf8')
  var str = decoder.decode(uint8)
  return str
}

Copy the code

How to make Websocket-SDK compatible with wechat applets

In the wechat applet environment, the websocket interface is different from that in the browser environment. The difference is reflected in the different WEBSocket-related API.

Initialize the websocket API Style
The browser new WebSocket(url) websocket.onopen = () => {}
Wechat applets wx.connectSocket(object) websocket.onOpen(function callback)

With the adapter pattern, we can smooth out differences at the adapter layer and provide a consistent access interface to the upper layer.

First we respectively implement the browser environment and small program environment Websocket control class

// Omit the interface declaration here
export class BrowserWebSocket implements GeneralWebSocket {
  private webSocket: WebSocket;

  constructor(url: string) {
    this.webSocket = new WebSocket(url)
    this.webSocket.binaryType = 'arraybuffer'
  }

  onOpen (cb) {
    this.webSocket.onopen = cb
  }

  onClose (cb) {
    this.webSocket.onclose = cb
  }

  onMessage (cb) {
    this.webSocket.onmessage = cb
  }

  onError (cb) {
    this.webSocket.onerror = cb
  }

  send (data) {
    this.webSocket.send(data)
  }
  getReadyState () {
    return this.webSocket.readyState
  }

  close (data) {
    this.webSocket.close()
  }
}

export class WeChatWebSocket implements MiniProgramWebSocket {
  private webSocket: WechatMiniprogram.SocketTask;

  constructor(url) {
    this.webSocket = wx.connectSocket({
      url,
      success: () = > {
        console.log('Websocket connection successful')},fail: () = > {
        console.log('Websocket connection failed')
      },
    })
  }

  close (data?) {
    this.webSocket.close(data)
  }
  onOpen (cb) {
    this.webSocket.onOpen(cb)
  }

  onClose (cb) {
    this.webSocket.onClose(cb)
  }

  onError (cb) {
    this.webSocket.onError(cb)
  }

  onMessage (cb: (evt) = > any) {
    this.webSocket.onMessage(cb)
  }
  send (data: any) {
    this.webSocket.send({
      data,
      success: (info) = > {
        console.log('send msg success', info)
      },
      fail: (info) = > {
        console.log('send msg fail:', info)
      },
      complete: () = > {
        console.log('send complete')
      },
    })
  }
  getReadyState () {
    return (this.webSocket as any).readyState
  }
}

Copy the code

Smooth out webSocket differences in the WebsocketAdaptor class:

class WebSocketAdaptor {
  private webSocket: WeChatWebSocket | BrowserWebSocket
  constructor(env, url) {
    if (env === 'wechat') {
      this.webSocket = new WeChatWebSocket(url)
    } else if (env === 'browser') {
      this.webSocket = new BrowserWebSocket(url)
    }
  }

  onOpen (cb) {
    this.webSocket.onOpen(cb)
  }

  onMessage (cb) {
    this.webSocket.onMessage(cb)
  }

  onError (cb) {
    this.webSocket.onError(cb)
  }

  onClose (cb) {
    this.webSocket.onClose(cb)
  }

  send (data) {
    this.webSocket.send(data)
  }

  getReadyState () {
    return this.webSocket.getReadyState()
  }

  close (data?) {
    this.webSocket.close(data)
  }
}
Copy the code

conclusion

  • JavaScript can manipulate binary data through TypedArray views
  • Using binary encoding can significantly reduce the size of the data being transmitted
  • Through the adapter mode, it is convenient to realize the COMPATIBILITY of SDK to websocket in different environments

Attached is a complete implementation of the binary read and write helper classes


class ReadBlock {
  dv: DataView;
  pos: number;
  size: number;

  constructor(buffer: ArrayBuffer) {
    this.dv = new DataView(buffer)
    this.pos = 0
    this.size = buffer.byteLength
  }

  GetHead: (index: number, type: number) = > boolean = (index, type) = > {
    if (this.pos >= this.size) {
      return false
    }
    const head = this.dv.getUint8(this.pos)
    if ((head >> 3) != index || (head & 0x07) != type) {
      return false
    }
    this.pos++
    return true
  }

  GetIntValue: () = > number = () = > {
    let ret = 0
    let val = this.dv.getUint8(this.pos++)
    let step = 1
    while (val > 127) {
      val &= 0x7F
      ret += (val * step)
      step *= 128
      val = this.dv.getUint8(this.pos++)
    }
    ret += (val * step)
    return ret
  }

  ReadIntField: (index: number) = > number = (index) = > {
    if (!this.GetHead(index, 0)) {
      return 0
    }
    const val = this.GetIntValue()
    return val
  }

  ReadStringField: (index: number) = > string = (index) = > {
    let str = ' '
    if (!this.GetHead(index, 2)) {
      return str
    }

    const len = this.GetIntValue()
    for (let i = 0; i < len; i++) {
      str += String.fromCharCode(this.dv.getUint8(this.pos++))
    }

    return str
  }

  ReadArrayField: (index: number) = > ArrayBuffer = (index) = > {
    const arr = []
    if (!this.GetHead(index, 2)) {
      return new Uint8Array(arr).buffer
    }

    const len = this.GetIntValue()
    for (let i = 0; i < len; i++) {
      arr.push(this.dv.getUint8(this.pos++))
    }
    return new Uint8Array(arr).buffer
  }
}

class WriteBlock {
  arr: Array<number>;

  constructor() {
    this.arr = []
  }

  SetHead: (index: number, type: number) = > void = (index, type) = > {
    const val = (index << 3) | type
    this.arr.push(val & 0xFF)}SetIntValue: (val: number) = > void = (val) = > {
    while (val > 127) {
      this.arr.push(val % 128 + 128)
      val = Math.floor(val / 128)}this.arr.push(val)
  }

  WriteIntField: (index: number, val: number) = > void = (index, val) = > {
    this.SetHead(index, 0)
    this.SetIntValue(val)
  }


  WriteBooleanField: (index: number, val: boolean) = > void = (index, val) = > {
    this.SetHead(index, 0)
    this.SetIntValue(+val)
  }

  WriteStringField: (index: number, str: string) = > void = (index, str) = > {
    this.SetHead(index, 2)
    this.SetIntValue(str.length)

    for (let i = 0; i < str.length; ++i) {
      this.arr.push(str.charCodeAt(i))
    }
  }

  WriteArrayBufferField: (index: number, buf: ArrayBuffer) = > void = (index, buf) = > {
    this.SetHead(index, 2)
    this.SetIntValue(buf.byteLength)
    const ui8 = new Uint8Array(buf)
    for (let i = 0; i < ui8.length; i++) {
      this.arr.push(ui8[i])
    }
  }

  getArrayBuffer: () = > ArrayBuffer = () = > {
    return new Uint8Array(this.arr).buffer
  }
}
Copy the code