乐趣区

如何从零开始定义一个类似websocket的即时通讯协议

深南大道镇楼

定义一个自己的通讯协议并不难,关键在于这个协议的可用性,可拓展性,复杂业务场景的实用性

即时通讯应用中,客户端和服务器端都可以看成一个服务器

一起复习一下websocket

  • WebSocket是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 通信协议于 2011 年被 IETF 定为标准 RFC 6455,并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。
  • WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据 , 在WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

说说 ws 协议的优点:

  • 说到优点,这里的对比参照物是 HTTP 协议,概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。
  • 支持双向通信,实时性更强。
  • 更好的二进制支持。
  • 较少的控制开销。连接创建后,ws 客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不 * 包含头部的情况下,服务端到客户端的包头只有 2~10 字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的 4 字节的掩码。而 HTTP 协议每次通信都需要携带完整的头部。
  • 支持扩展。ws 协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

我们先看看 web socket 协议的实现具体过程,再用代码抽象,定义自己的即时通讯协议:

  • 连接握手过程

    • 关于 WebSocket 有一句很常见的话: Websocket 复用了 HTTP 的握手通道, 它具体指的是:
    • 客户端通过 HTTP 请求与 WebSocket 服务器协商升级协议, 协议升级完成后, 后续的数据交换则遵照 WebSocket 协议
    • 客户端: 申请协议升级
    • 首先由客户端换发起协议升级请求, 根据 WebSocket 协议规范, 请求头必须包含如下的内容
    GET / HTTP/1.1
    Host: localhost:8080
    Origin: http://127.0.0.1:3000
    Connection: Upgrade
    Upgrade: websocket
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw
  • 请求头详解

    • 请求行: 请求方法必须是 GET, HTTP 版本至少是 1.1
    • 请求必须含有 Host
    • 如果请求来自浏览器客户端, 必须包含 Origin
    • 请求必须含有 Connection, 其值必须含有 ”Upgrade” 记号
    • 请求必须含有 Upgrade, 其值必须含有 ”websocket” 关键字
    • 请求必须含有 Sec-Websocket-Version, 其值必须是 13
    • 请求必须含有 Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接
  • 1.2 服务器: 响应协议升级
  • 服务器返回的响应头必须包含如下的内容

    • HTTP/1.1 101 Switching Protocols
    • Connection:Upgrade
    • Upgrade: websocket
    • Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
    • 响应行: HTTP/1.1 101 Switching Protocols
    • 响应必须含有Upgrade, 其值为"weboscket"
    • 响应必须含有Connection, 其值为"Upgrade"
    • 响应必须含有 Sec-Websocket-Accept, 根据请求首部的 Sec-Websocket-key 计算出来

Sec-WebSocket-Key/Accept 的计算

  • 规范提到:
  • Sec-WebSocket-Key 值由一个随机生成的 16 字节的随机数通过 base64 编码得到的
  • Key 可以避免服务器收到非法的 WebSocket 连接, 比如 http 请求连接到 websocket, 此时服务端可以直接拒绝
  • Key 可以用来初步确保服务器认识 ws 协议, 但也不能排除有的 http 服务器只处理 Sec-WebSocket-Key, 并不实现 ws 协议
  • Key 可以避免反向代理缓存
  • 在浏览器中发起 ajax 请求, Sec-Websocket-Key 以及相关 header 是被禁止的, 这样可以避免客户端发送 ajax 请求时, 意外请求协议升级
  • 最终需要强调的是: Sec-WebSocket-Key/Accept 并不是用来保证数据的安全性, 因为其计算 / 转换公式都是公开的, 而且非常简单, 最主要的作用是预防一些意外的情况

WebSocket通信的最小单位是帧, 由一个或多个帧组成一条完整的消息, 交换数据的过程中, 发送端和接收端需要做的事情如下:

  • 发送端: 将消息切割成多个帧, 并发送给服务端
  • 接收端: 接受消息帧, 并将关联的帧重新组装成完整的消息

数据帧格式详解

  • FIN: 占 1bit
  • 0 表示不是消息的最后一个分片
  • 1 表示是消息的最后一个分片
  • RSV1, RSV2, RSV3: 各占 1bit, 一般情况下全为 0, 与 Websocket 拓展有关, 如果出现非零的值且没有采用 WebSocket 拓展, 连接出错
Opcode: 占 4bit

%x0: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片
%x1: 表示这是一个文本帧
%x2: 表示这是一个二进制帧
%x3-7: 保留的操作代码, 用于后续定义的非控制帧
%x8: 表示连接断开
%x9: 表示这是一个心跳请求(ping)
%xA: 表示这是一个心跳响应(pong)
%xB-F: 保留的操作代码, 用于后续定义的非控制帧
  • Mask: 占 1bit

    • 0 表示不对数据载荷进行掩码异或操作
    • 1 表示对数据载荷进行掩码异或操作
  • Payload length: 占 7 或 7 +16 或 7 +64bit

    • 0~125: 数据长度等于该值
    • 126: 后续的 2 个字节代表一个 16 位的无符号整数, 值为数据的长度
    • 127: 后续的 8 个字节代表一个 64 位的无符号整数, 值为数据的长度
  • Masking-key: 占 0 或 4bytes

    • 1: 携带了 4 字节的 Masking-key
    • 0: 没有 Masking-key
    • 掩码的作用并不是防止数据泄密, 而是为了防止早期版本协议中存在的代理缓存污染攻击等问题
  • payload data: 载荷数据
  • 数据传递

    • WebSocket 的每条消息可能被切分成多个数据帧, 当接收到一个数据帧时, 会根据 FIN 值来判断, 是否为最后一个数据帧
    • 数据帧传递示例:
    • FIN=0, Opcode=0x1: 发送文本类型, 消息还没有发送完成, 还有后续帧
    • FIN=0, Opcode=0x0: 消息没有发送完成, 还有后续帧, 接在上一条后面
    • FIN=1, Opcode=0x0: 消息发送完成, 没有后续帧, 接在上一条后面组成完整消息

正式开始定义属于我们自己的通讯协议:

  • 我们为什么要自定义 TCP 应用层传输协议?
  • 针对特定的用户群体,实现通讯信息的真正加密,复杂场景下更灵活的通信
  • 因为在 TCP 流传输的过程中,可能会出现分包与黏包的现象。我们为了解决这些问题,需要我们自定义通信协议进行封包与解包。
  • 什么是分包与黏包?
  • 分包:指接受方没有接受到一个完整的包,只接受了部分。
  • 黏包:指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
  • PS: 因为 TCP 是面向字节流的,是没有边界的概念的,严格意义上来说,是没有分包和黏包的概念的,但是为了更好理解,也更好来描述现象,我在这里就接着采用这两个名词来解释现象了。我觉得大家知道这个概念就行了,不必细扣,能解决问题就行。
  • 产生分包与黏包现象的原因是什么?
  • 产生分包原因:
  • 可能是 IP 分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。
  • 产生黏包的原因:
  • 由于 TCP 协议本身的机制(面向连接的可靠地协议 - 三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用 Nagle 算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP 的网络延迟要 UDP 的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象
  • 什么是封包与解包?
  • TCP/IP 网络数据以流的方式传输,数据流是由包组成,如何判定接收方收到的包是否是一个完整的包就要在发送时对包进行处理,这就是封包技术,将包处理成包头,包体。

包头是包的开始标记,整个包的大小就是包的结束标。

  • 如何自定义协议?
  • 发送时数据包是由包头 + 数据 组成的:其中包头内容分为包类型 + 包长度。
  • 接收时,只需要先保证将数据包的包头读完整,通过收到的数据包包头里的数据长度和数据包类型,判断出我们将要收到一个带有什么样类型的多少长度的数据。然后循环接收直到接收的数据大小等于数据长度停止,此时我们完成接收一个完整数据包。

用代码书写一个常见的解密后的包:

{
header:{
cmdid:oxa212,
msgid:xxxxxx,
sessionid:xxxx
....
},
body:{
sessiontype:1,
datalength:100,
formid:xxx,
told:xxxx,
msgid:xxxxxxx,content:'dear'
}
}
  • 今天为了降低难度,没有使用 prob 格式传输哦。
  • 当然还有心跳的发包和回包,与上面类似,只是内容不一致。

今天只书写客户端 node.js 的部分代码,服务端的代码,打算后期使用 golang 书写。

  • 上面说到了,WebSocket通信的最小单位是帧, 由一个或多个帧组成一条完整的消息, 交换数据的过程中, 发送端和接收端需要做的事情如下:
  • 发送端: 将消息切割成多个帧, 并发送给服务端
  • 接收端: 接受消息帧, 并将关联的帧重新组装成完整的消息
  • 出现黏包和分包的问题,通俗易懂的说就是,创建 buffer 缓冲区,把二进制的数据一点一点点切出来,然后变成特定的 js 对象使用。

第一步 先与服务端建立 tcp 链接

const {Socket} = require('net') 
const tcp = new Socket()
tcp.setKeepAlive(true);
tcp.setNoDelay(true);
// 保持底层 tcp 链接不断,长连接

第二步,指定对应域名端口号链接


tcp.connect(80,142.122.0.0)

第三步 建立成功链接后发送心跳包,并且服务端回复心跳包

  • 每个人定制的心跳发包回包都不一样,具体格式可以参考上面,自行定制心跳包的内容和检测时间,多长时间检测不到心跳的处理机制。

第四步,收到服务端数据,拆包,根据不同的数据类型给予不同的处理机制,决定哪些渲染到页面上,哪些放入数据库,做持久性存储等。

  • 这里写一点拆包代码
根据后端传送的数据类型 使用对应不同的解析
readUInt8 readUInt16LE readUInt32LE readIntLE 等处理后得到 myBuf 

const myBuf = buffer.slice(start);// 从对应的指针开始的位置截取 buffer
const header = myBuf.slice(headstart,headend)// 截取对应的头部 buffer
const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring()) 
// 精确截取 body 的 buffer, 并且转化成 js 对象

怎么拆包,长度是多少,要看大家各自的定义方式,参考 websocket 的定义格式:

Node.js目前支持的字符编码包括:

  • ascii – 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
  • utf8 – 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8。
  • utf16le – 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  • ucs2 – utf16le 的别名。
  • base64 – Base64 编码。
  • latin1 – 一种把 Buffer 编码成一字节编码的字符串的方式。
  • binary – latin1 的别名。
  • hex – 将每个字节编码为两个十六进制字符。

组包

  • 创建 Buffer 类

    • Buffer 提供了以下 API 来创建 Buffer 类:
    • Buffer.alloc(size[, fill[, encoding]]):返回一个指定大小的 Buffer 实例,如果没有设置 fill,则默认填满 0
    • Buffer.allocUnsafe(size):返回一个指定大小的 Buffer 实例,但是它不会被初始化,所以它可能包含敏感的数据
    • Buffer.allocUnsafeSlow(size)
    • Buffer.from(array):返回一个被 array 的值初始化的新的 Buffer 实例(传入的 array 的元素只能是数字,不然就会自动被 0 覆盖)
    • Buffer.from(arrayBuffer[, byteOffset[, length]]):返回一个新建的与给定的 ArrayBuffer 共享同一内存的 Buffer。
    • Buffer.from(buffer):复制传入的 Buffer 实例的数据,并返回一个新的 Buffer 实例
    • Buffer.from(string[, encoding]):返回一个被 string 的值初始化的新的 Buffer 实例

所谓组包,就是把对应的 js 对象,变成二进制数据,然后推送给服务端

  • 这里写一个简单的组包
    const obj = {
        header:{
            datalength:123,
            sessiontype:1,
            cmdid:xxx,},
         body:{
            content:"hello",
            sessionid:xxx,
            fromid:xxx,
            toid:xxx
                }
            }
    将上面的 js 对象转化成 buf 后,推送给服务端 
    tcp.write(buf,cb)
    
    cb 是一个异步回掉,当数据推送完后才会调用。

下面给出常用的 buffer 操作api

  • 方法参考手册
  • 以下列出了 Node.js Buffer 模块常用的方法(注意有些方法在旧版本是没有的):
  • 序号 方法 & 描述
  • 1 new Buffer(size)

    • 分配一个新的 size 大小单位为 8 位字节的 buffer。注意, size 必须小于 kMaxLength,否则,将会抛出异常 RangeError。废弃的: 使用 Buffer.alloc() 代替(或 Buffer.allocUnsafe())。
  • 2 new Buffer(buffer)

    • 拷贝参数 buffer 的数据到 Buffer 实例。废弃的: 使用 Buffer.from(buffer) 代替。
  • 3 new Buffer(str[, encoding])

分配一个新的 buffer,其中包含着传入的 str 字符串。encoding 编码方式默认为 ‘utf8’。废弃的: 使用 Buffer.from(string[, encoding]) 代替。

  • 4 buf.length

    • 返回这个 buffer 的 bytes 数。注意这未必是 buffer 里面内容的大小。length 是 buffer 对象所分配的内存数,它不会随着这个 buffer 对象内容的改变而改变。
  • 5 buf.write(string[, offset[, length]][, encoding])

    • 根据参数 offset 偏移量和指定的 encoding 编码方式,将参数 string 数据写入 buffer。offset 偏移量默认值是 0, encoding 编码方式默认是 utf8。length 长度是将要写入的字符串的 bytes 大小。返回 number 类型,表示写入了多少 8 位字节流。如果 buffer 没有足够的空间来放整个 string,它将只会只写入部分字符串。length 默认是 buffer.length – offset。这个方法不会出现写入部分字符。
  • 6 buf.writeUIntLE(value, offset, byteLength[, noAssert])

将 value 写入到 buffer 里,它由 offset 和 byteLength 决定,最高支持 48 位无符号整数,小端对齐,例如:

const buf = Buffer.allocUnsafe(6);

buf.writeUIntLE(0x1234567890ab, 0, 6);

// 输出: <Buffer ab 90 78 56 34 12>
console.log(buf);
noAssert 值为 true 时,不再验证 value 和 offset 的有效性。默认是 false。
  • 7 buf.writeUIntBE(value, offset, byteLength[, noAssert])

    • 将 value 写入到 buffer 里,它由 offset 和 byteLength 决定,最高支持 48 位无符号整数,大端对齐。noAssert 值为 true 时,不再验证 value 和 offset 的有效性。默认是 false。
const buf = Buffer.allocUnsafe(6);

buf.writeUIntBE(0x1234567890ab, 0, 6);

// 输出: <Buffer 12 34 56 78 90 ab>
console.log(buf);
  • 8 buf.writeIntLE(value, offset, byteLength[, noAssert])

    • 将 value 写入到 buffer 里,它由 offset 和 byteLength 决定,最高支持 48 位有符号整数,小端对齐。noAssert 值为 true 时,不再验证 value 和 offset 的有效性。默认是 false。
  • 9 buf.writeIntBE(value, offset, byteLength[, noAssert])

    • 将 value 写入到 buffer 里,它由 offset 和 byteLength 决定,最高支持 48 位有符号整数,大端对齐。noAssert 值为 true 时,不再验证 value 和 offset 的有效性。默认是 false。
  • 10 buf.readUIntLE(offset, byteLength[, noAssert])

    • 支持读取 48 位以下的无符号数字,小端对齐。noAssert 值为 true 时,offset 不再验证是否超过 buffer 的长度,默认为 false。
  • 11 buf.readUIntBE(offset, byteLength[, noAssert])

    • 支持读取 48 位以下的无符号数字,大端对齐。noAssert 值为 true 时,offset 不再验证是否超过 buffer 的长度,默认为 false。
  • 12 buf.readIntLE(offset, byteLength[, noAssert])

    • 支持读取 48 位以下的有符号数字,小端对齐。noAssert 值为 true 时,offset 不再验证是否超过 buffer 的长度,默认为 false。
  • 13 buf.readIntBE(offset, byteLength[, noAssert])

    • 支持读取 48 位以下的有符号数字,大端对齐。noAssert 值为 true 时,offset 不再验证是否超过 buffer 的长度,默认为 false。
  • 14 buf.toString([encoding[, start[, end]]])

    • 根据 encoding 参数(默认是 ‘utf8’)返回一个解码过的 string 类型。还会根据传入的参数 start (默认是 0) 和 end (默认是 buffer.length)作为取值范围。
  • 15 buf.toJSON()

    • 将 Buffer 实例转换为 JSON 对象。
  • 16 buf[index]

    • 获取或设置指定的字节。返回值代表一个字节,所以返回值的合法范围是十六进制 0x00 到 0xFF 或者十进制 0 至 255。
  • 17 buf.equals(otherBuffer)

    • 比较两个缓冲区是否相等,如果是返回 true,否则返回 false。
  • 18 buf.compare(otherBuffer)

    • 比较两个 Buffer 对象,返回一个数字,表示 buf 在 otherBuffer 之前,之后或相同。
  • 19 buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]])

    • buffer 拷贝,源和目标可以相同。targetStart 目标开始偏移和 sourceStart 源开始偏移默认都是 0。sourceEnd 源结束位置偏移默认是源的长度 buffer.length。
  • 20 buf.slice([start[, end]])

    • 剪切 Buffer 对象,根据 start(默认是 0) 和 end (默认是 buffer.length) 偏移和裁剪了索引。负的索引是从 buffer 尾部开始计算的。
  • 21 buf.readUInt8(offset[, noAssert])

    • 根据指定的偏移量,读取一个无符号 8 位整数。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。如果这样 offset 可能会超出 buffer 的末尾。默认是 false。
  • 22 buf.readUInt16LE(offset[, noAssert])

    • 根据指定的偏移量,使用特殊的 endian 字节序格式读取一个无符号 16 位整数。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 23 buf.readUInt16BE(offset[, noAssert])

    • 根据指定的偏移量,使用特殊的 endian 字节序格式读取一个无符号 16 位整数,大端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 24 buf.readUInt32LE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个无符号 32 位整数,小端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 25 buf.readUInt32BE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个无符号 32 位整数,大端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 26 buf.readInt8(offset[, noAssert])

    • 根据指定的偏移量,读取一个有符号 8 位整数。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 27 buf.readInt16LE(offset[, noAssert])

    • 根据指定的偏移量,使用特殊的 endian 格式读取一个 有符号 16 位整数,小端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 28 buf.readInt16BE(offset[, noAssert])

根据指定的偏移量,使用特殊的 endian 格式读取一个 有符号 16 位整数,大端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。

  • 29 buf.readInt32LE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个有符号 32 位整数,小端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 30 buf.readInt32BE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个有符号 32 位整数,大端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 31 buf.readFloatLE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个 32 位双浮点数,小端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 32 buf.readFloatBE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个 32 位双浮点数,大端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 33 buf.readDoubleLE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个 64 位双精度数,小端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 34 buf.readDoubleBE(offset[, noAssert])

    • 根据指定的偏移量,使用指定的 endian 字节序格式读取一个 64 位双精度数,大端对齐。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 offset 可能会超出 buffer 的末尾。默认是 false。
  • 35 buf.writeUInt8(value, offset[, noAssert])

    • 根据传入的 offset 偏移量将 value 写入 buffer。注意:value 必须是一个合法的无符号 8 位整数。若参数 noAssert 为 true 将不会验证 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则不要使用。默认是 false。
  • 36 buf.writeUInt16LE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个合法的无符号 16 位整数,小端对齐。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 37 buf.writeUInt16BE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个合法的无符号 16 位整数,大端对齐。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 38 buf.writeUInt32LE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式 (LITTLE-ENDIAN: 小字节序) 将 value 写入 buffer。注意:value 必须是一个合法的无符号 32 位整数,小端对齐。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 39 buf.writeUInt32BE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式 (Big-Endian: 大字节序) 将 value 写入 buffer。注意:value 必须是一个合法的有符号 32 位整数。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 40 buf.writeInt8(value, offset[, noAssert])
  • 41 buf.writeInt16LE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个合法的 signed 16 位整数。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 42 buf.writeInt16BE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个合法的 signed 16 位整数。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 43 buf.writeInt32LE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个合法的 signed 32 位整数。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 44 buf.writeInt32BE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个合法的 signed 32 位整数。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 45 buf.writeFloatLE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:当 value 不是一个 32 位浮点数类型的值时,结果将是不确定的。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 46 buf.writeFloatBE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:当 value 不是一个 32 位浮点数类型的值时,结果将是不确定的。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 47 buf.writeDoubleLE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个有效的 64 位 double 类型的值。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 48 buf.writeDoubleBE(value, offset[, noAssert])

    • 根据传入的 offset 偏移量和指定的 endian 格式将 value 写入 buffer。注意:value 必须是一个有效的 64 位 double 类型的值。若参数 noAssert 为 true 将不会验证 value 和 offset 偏移量参数。这意味着 value 可能过大,或者 offset 可能会超出 buffer 的末尾从而造成 value 被丢弃。除非你对这个参数非常有把握,否则尽量不要使用。默认是 false。
  • 49 buf.fill(value, offset)

    • 使用指定的 value 来填充这个 buffer。如果没有指定 offset (默认是 0) 并且 end (默认是 buffer.length),将会填充整个 buffer。

最后的总结:

  • 实时通讯,特别是三端加密和消息同步这块,是非常复杂的,本人大概只写到了 10 分之 1
  • 组包和拆包,具体要根据你特定的业务场景还有公司定制的协议去具体操作,这里只是一个大概阐述
  • 后台的代码以后我会尽力用 golangnode.js各写一份。
  • 海量高并发场景,机房部署,整体架构这里都没有写,因为确实太多了,一旦并发量上来了,无论前后端要做的事情都非常多
  • 有幸公司昨天在深圳万象城请到了 Bilibili 的架构师毛剑先生给我们培训,他让我对后端的认识又深刻了不少,特别是IM 整体架构和优化这块,以后总结好了,也会给大家分享
  • 如果有写得不对的地方,请指出,谢谢。
  • 加解密的过程没有写上来,这块也是非常核心。
退出移动版