//先直接上代码再解说
let encodeMessage = function(opcode, payload) {
let buf;
let b1 = 0x80 | opcode; // FIN 位(0x80) + 操作码(如 TEXT: 1 → 0x81)
let b2 = 0; // Mask 位为 0(服务器→客户端无需掩码)
let length = payload.length;
if (length < 126) { // 短消息:长度直接写入 b2
buf = Buffer.alloc(payload.length + 2); // 头部 2 字节
b2 |= length; // 设置长度字段
buf.writeUInt8(b1, 0); // 写入 FIN + 操作码
buf.writeUInt8(b2, 1); // 写入 Mask 位 + 长度
payload.copy(buf, 2); // 复制 Payload
} else if (length < (1 << 16)) { // 中等消息:长度占 2 字节,(1<<16)就是65535
buf = Buffer.alloc(payload.length + 4); // 头部 4 字节(2 + 2)
b2 |= 126; // 长度标识符 126
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
buf.writeUInt16BE(length, 2); // 写入 16 位大端长度
payload.copy(buf, 4); // 复制 Payload
} else { // 长消息:长度占 8 字节
buf = Buffer.alloc(payload.length + 10); // 头部 10 字节(2 + 8)
b2 |= 127; // 长度标识符 127
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
buf.writeUInt32BE(0, 2); // 高 32 位(假设长度 ≤ 2^32-1)
buf.writeUInt32BE(length, 6); // 低 32 位
payload.copy(buf, 10); // 复制 Payload
}
return buf;
};
const assert = require('assert');
const opcodes = { TEXT: 1, BINARY: 2, CLOSE: 8, PING: 9, PONG: 10 };
// 测试短消息(长度 < 126)
function testShortMessage() {
const payload = Buffer.from('Hello');
const buf = encodeMessage(opcodes.TEXT, payload);
// 验证头部
assert.strictEqual(buf.readUInt8(0), 0x80 | opcodes.TEXT); // FIN + TEXT opcode
assert.strictEqual(buf.readUInt8(1), payload.length); // 长度字段
// 验证 Payload
assert.deepStrictEqual(buf.slice(2), payload);
}
// 测试中等消息(长度 = 126)
function testMediumMessage() {
const payload = Buffer.alloc(126);
const buf = encodeMessage(opcodes.BINARY, payload);
assert.strictEqual(buf.readUInt8(0), 0x80 | opcodes.BINARY);
assert.strictEqual(buf.readUInt8(1), 126); // 长度标识符 126
assert.strictEqual(buf.readUInt16BE(2), 126); // 实际长度
assert.deepStrictEqual(buf.slice(4), payload);
}
// 测试长消息(长度 > 65535)
function testLongMessage() {
const length = 70000;
const payload = Buffer.alloc(length);
const buf = encodeMessage(opcodes.PING, payload);
assert.strictEqual(buf.readUInt8(0), 0x80 | opcodes.PING);
assert.strictEqual(buf.readUInt8(1), 127); // 长度标识符 127
assert.strictEqual(buf.readUInt32BE(2), 0); // 高 32 位(假设长度 ≤ 2^32-1)
assert.strictEqual(buf.readUInt32BE(6), length); // 低 32 位
assert.deepStrictEqual(buf.slice(10), payload);
}
// 执行测试
testShortMessage();
testMediumMessage();
testLongMessage();
console.log('All tests passed!');
首先,函数定义了一个opcodes对象,包含不同的操作码,比如TEXT是1,BINARY是2等等。然后,encodeMessage函数根据传入的opcode和payload生成一个Buffer。根据payload的长度不同,数据帧的格式也不同:如果长度小于126字节,使用一个字节表示长度;如果在126到65535之间,则用接下来的两个字节表示长度;如果更大,则用接下来的八个字节。同时,第一个字节的最高位(FIN位)设置为0x80,表示这是消息的最后一个帧,后面跟着操作码。第二个字节的最高位是掩码位,这里设置为0,表示数据未掩码,后面跟着长度字段。
接下来,我需要考虑如何测试这个函数是否正确处理不同长度的payload,以及是否正确设置各个字段。可能的测试用例包括:
-
测试一个长度小于126的payload,比如长度为0,或者125的情况,检查生成的Buffer是否正确。
-
测试长度恰好为126的情况,这时候应该使用两个字节表示长度。
-
测试长度大于等于126但小于等于65535的情况,比如127或65535,这时候应使用两个字节的长度。
-
测试长度超过65535的情况,这时候应使用八个字节的长度,并且前四个字节可能为0(因为JavaScript的Buffer最大长度可能受限制,但假设这里处理的是大端序的64位长度)。
-
检查操作码是否正确设置,比如传入不同的opcode时,第一个字节的低四位是否正确。
-
确保mask位是0,因为服务器发送的数据不需要掩码,客户端发送的才需要。所以第二个字节的最高位是否为0。
-
检查payload是否正确复制到Buffer的指定位置。
现在,我需要针对这些情况编写测试代码,可能使用Node.js的assert模块或者其他测试框架如Mocha。例如,对于长度小于126的情况,可以生成一个短字符串作为payload,调用encodeMessage,然后检查返回的Buffer的长度是否正确,各个字段是否正确。
现在逐行分析代码:
let encodeMessage = function(opcode, payload) {
let buf;
let b1 = 0x80 | opcode; // 设置FIN位为1(0x80),操作码为传入的opcode
let b2 = 0; // 初始第二个字节,mask位为0,长度部分初始为0
let length = payload.length;
// 根据长度决定使用哪种格式
if (length < 126) {
// 长度字段占1字节
buf = Buffer.alloc(payload.length + 2 + 0); // 总长度是payload长度+2字节头部
b2 |= length; // 设置长度字段
buf.writeUInt8(b1, 0); // 写入第一个字节
buf.writeUInt8(b2, 1); // 写入第二个字节
payload.copy(buf, 2); // 将payload复制到Buffer的第3个字节开始的位置
} else if (length < (1 << 16)) { // 检查是否小于65536
// 长度字段占2字节
buf = Buffer.alloc(payload.length + 2 + 2); // 额外两个字节用于长度
b2 |= 126; // 设置第二个字节的长度字段为126
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
buf.writeUInt16BE(length, 2); // 大端序写入两个字节的长度
payload.copy(buf, 4); // 从第5个字节开始复制payload
} else {
// 长度字段占8字节
buf = Buffer.alloc(payload.length + 2 + 8); // 额外8字节用于长度
b2 |= 127; // 设置第二个字节的长度字段为127
buf.writeUInt8(b1, 0);
buf.writeUInt8(b2, 1);
// 写入8字节长度,前四个字节为0,后四个字节为实际长度(假设长度不超过32位)
buf.writeUInt32BE(0, 2); // 写入前四个字节为0
buf.writeUInt32BE(length, 6); // 后四个字节为长度的大端序
payload.copy(buf, 10); // 从第11个字节开始复制payload
}
return buf;
};
需要注意的是,对于超过32位的情况,代码可能存在错误。因为如果payload的长度超过2^32-1,这里只能写入后四个字节,前四个字节被写为0,这会导致无法处理非常大的数据。但实际应用中可能这种情况很少,或者受限于Buffer的最大容量。
测试这个函数时,需要覆盖这三种情况:
测试用例1:长度小于126
例如,payload长度为5,opcode为opcodes.TEXT(1)。生成的Buffer前两个字节应该是0x81(0x80 | 1)和0x05。后面跟着5字节的payload。
测试用例2:长度126到65535之间
例如,长度126,opcode为BINARY(2)。第一个字节是0x82,第二个字节是0x7E(126),后面两个字节是0x007E(126的大端),然后payload。
测试用例3:长度大于等于65536
例如,长度65536,opcode为PING(9)。第一个字节0x89,第二个字节0x7F(127)。接下来的四个字节为0,然后是四个字节的0x00010000(65536)。然后payload开始于第11字节。
另外,需要测试边缘情况,如长度125、126、65535、65536等。
测试时,可以编写测试函数,调用encodeMessage,然后检查返回的Buffer的各部分是否符合预期。
例如,对于长度125的payload:
const payload = Buffer.alloc(125, 0xAA); // 填充125个0xAA的Buffer
const buf = encodeMessage(opcodes.TEXT, payload);
// 检查buf的长度是否为125 + 2 = 127
// 第一个字节是否为0x81
// 第二个字节是否为125
// payload是否从第3字节开始正确复制
对于长度126的情况:
const payload = Buffer.alloc(126);
const buf = encodeMessage(opcodes.BINARY, payload);
// buf长度应为126 + 2 + 2 = 130
// 第二个字节是126(0x7E)
// 接下来两个字节是0x007E(大端)
对于大长度:
const payload = Buffer.alloc(70000); // 假设足够大
const buf = encodeMessage(opcodes.PONG, payload);
// 检查第二个字节是否为127(0x7F)
// 接下来的四个字节是0,然后四个字节是70000的大端表示
此外,还需要测试当payload长度为0时的情况,比如空Buffer是否处理正确。
可能的错误点:
- 当使用8字节长度时,是否应该写入64位长度而不是分两次32位?例如,对于非常大的长度(超过2^32),前32位可能不为零,但当前代码将前32位设置为0,这可能有问题。例如,如果payload长度是0x100000000(超过32位),则当前代码会错误地将其表示为0x00000000 00000001,而正确应该是高位32位为0x1,低位为0。但代码中因为使用length参数是payload.length,这在JavaScript中是一个number类型,当超过2^53时精度会丢失。所以实际应用中可能无法处理超过4GB的payload,但代码可能在这种情况下出错。不过对于一般测试,可能不需要考虑这么大的数据。
另一个可能的错误是在处理大端序时,是否正确分割了长度的高位和低位。例如,当长度为65536(0x00010000),用writeUInt32BE(0,2)和writeUInt32BE(length,6),那么第2到5字节是0,第6到9字节是0x00010000,这样整个长度是0x0000000000010000,即65536,是正确的。
此外,需要确认mask位是否为0,即第二个字节的最高位是0。例如,当b2被设置为length或126或127时,因为b2初始为0,且没有设置mask位(0x80),所以是正确的。例如,当payload长度是5,b2是5,即0x05,最高位是0,正确。
综上,测试用例应覆盖所有三个分支,并验证头部各字段以及payload的正确性。对于每个分支,可以编写多个测试用例,使用不同的长度和opcode,然后检查生成的Buffer是否符合WebSocket数据帧的规范。
要测试 encodeMessage 函数,需覆盖不同长度的 payload 和操作码,验证生成的 Buffer 是否符合 WebSocket 协议。以下是测试方法及逐行代码说明:
测试方法
测试用例划分:
短消息(长度 < 126)
中等消息(126 ≤ 长度 ≤ 65535)
长消息(长度 > 65535)
验证内容:
头部字段:FIN 位、操作码、Mask 位、长度字段。
Payload 复制:数据是否正确复制到 Buffer 中。
长度编码:长度是否按协议正确编码(1/2/8 字节)。
测试工具:
使用 Node.js 的 assert 模块或测试框架(如 Mocha)。
关键验证点
FIN 位和操作码:
第一个字节高 4 位为 0x80(FIN=1),低 4 位为操作码(如 1 表示 TEXT)。
Mask 位和长度字段:
第二个字节高 1 位为 0(Mask=0),低 7 位为长度或标识符(126/127)。
长度编码:
短消息:直接写入长度(1 字节)。
中等消息:写入 126 + 2 字节大端长度。
长消息:写入 127 + 8 字节大端长度(高 32 位为 0)。
Payload 复制:
数据从正确位置开始复制(如短消息从第 3 字节开始)。