在现代网络通信中,WebSocket和Protobuf已成为构建高效、跨平台通信系统的关键技术。本文将详细介绍如何使用这两种技术来实现一个稳定且高效的网络通信系统。
引言
随着移动设备和Web应用的普及,开发者需要构建能够跨多个平台工作的网络通信系统。WebSocket提供了一种在单个连接上进行全双工通信的方式,而Protobuf(Protocol Buffers)则是一种轻便高效的结构化数据存储格式,广泛应用于数据序列化。
为什么选择WebSocket和Protobuf
WebSocket的优势
WebSocket协议提供了一种在客户端和服务器之间建立持久连接的方法。这种连接允许服务器主动向客户端发送消息,非常适合需要实时数据传输的应用场景。
Protobuf的优势
Protobuf是一种由Google开发的二进制数据格式,它具有以下优点:
- 高效性:Protobuf的二进制格式比JSON等文本格式更加紧凑,减少了网络传输的负载。
- 易于使用:Protobuf提供了丰富的库支持,可以轻松地在不同语言之间进行数据交换。
- 强类型系统:Protobuf支持强类型,有助于在编译时捕捉错误。
系统设计
通信模型
在我们的系统中,通信模型基于请求-响应模式。客户端发送请求,服务器根据请求返回相应的数据。
消息定义
每个通信消息都包含cmd和scmd字段,用于标识消息类型。cmd代表模块号,scmd代表模块下的具体子消息号。
例如,系统模块的消息定义如下:
message C_HEART_BEAT {
int32 cmd = 1 [default = 0];
int32 scmd = 2 [default = 2];
}
message S_HEART_BEAT {
int32 cmd = 1 [default = 0];
int32 scmd = 2 [default = 3];
int64 serverTime = 3; // 服务器时间
}
代码生成
使用protobuf-cil工具,我们可以从Protobuf定义文件生成相应的代码。这里,我们只生成消息的编码(encode)和解码(decode)方法。
/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/
"use strict";
if (typeof (window) == 'undefined') {
var $protobuf = require("protobufjs/minimal");
} else {
var $protobuf = protobuf;
}
var $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
var $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});
$root.SystemPto = (function() {
var SystemPto = {};
SystemPto.C_HEART_BEAT = (function() {
function C_HEART_BEAT(p) {
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
C_HEART_BEAT.prototype.cmd = 0;
C_HEART_BEAT.prototype.scmd = 2;
C_HEART_BEAT.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.cmd != null && Object.hasOwnProperty.call(m, "cmd"))
w.uint32(8).int32(m.cmd);
if (m.scmd != null && Object.hasOwnProperty.call(m, "scmd"))
w.uint32(16).int32(m.scmd);
return w;
};
C_HEART_BEAT.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.SystemPto.C_HEART_BEAT();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.cmd = r.int32();
break;
case 2:
m.scmd = r.int32();
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
return C_HEART_BEAT;
})();
SystemPto.S_HEART_BEAT = (function() {
function S_HEART_BEAT(p) {
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
S_HEART_BEAT.prototype.cmd = 0;
S_HEART_BEAT.prototype.scmd = 3;
S_HEART_BEAT.prototype.serverTime = $util.Long ? $util.Long.fromBits(0,0,false) : 0;
S_HEART_BEAT.encode = function encode(m, w) {
if (!w)
w = $Writer.create();
if (m.cmd != null && Object.hasOwnProperty.call(m, "cmd"))
w.uint32(8).int32(m.cmd);
if (m.scmd != null && Object.hasOwnProperty.call(m, "scmd"))
w.uint32(16).int32(m.scmd);
if (m.serverTime != null && Object.hasOwnProperty.call(m, "serverTime"))
w.uint32(24).int64(m.serverTime);
return w;
};
S_HEART_BEAT.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.SystemPto.S_HEART_BEAT();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.cmd = r.int32();
break;
case 2:
m.scmd = r.int32();
break;
case 3:
m.serverTime = r.int64();
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
return S_HEART_BEAT;
})();
return SystemPto;
})();
消息发送与接收
发送消息
当客户端需要发送一个心跳包给服务器时,可以创建一个心跳消息对象,并使用封装的socket发送。
let heart = new SystemPto.C_HEART_BEAT();
socket.send(ProtoBufEncoder.encode(heart));
消息编码
ProtoBufEncoder统一处理消息的编码和解码。客户端发送的消息以C_开头,服务器响应的消息以S_开头。使用Map数据结构存储消息类。
public static encode(message: IGameMessage): Buffer {
if (!message) {
return;
}
let index = ProtoBufEncoder.getComposeCmdId(message.cmd, message.scmd);
let messageClass = ProtoBufEncoder.protoBufClass.get(index);
ProtoBufEncoder.protoBufWriter.reset();
const writer = messageClass.encode(message, ProtoBufEncoder.protoBufWriter);
return writer.finishWithSysCmd(message.cmd, message.scmd);
}
消息解码
服务器返回的buffer中包含消息号,根据消息号获取对应的消息类,并解码获取消息对象数据。
public static decode(buffer: Buffer, offset: number): IGameMessage {
if (buffer.length < 8) {
console.error("protobuf decode err! buffer长度小于8");
return;
}
let dataView = new DataView(buffer);
const cmd = dataView.getInt32(offset);
const scmd = dataView.getInt32(offset + 4);
if (Buffer.isBuffer(buffer) === false) {
buffer = Buffer.from(buffer);
}
const messageIndex = ProtoBufEncoder.getComposeCmdId(cmd, scmd);
const messageClass = ProtoBufEncoder.protoBufClass.get(messageIndex);
if (!messageClass) {
console.error("无法获取协议:", cmd, scmd);
return null;
}
ProtoBufEncoder.protoBufReader.buf = buffer;
ProtoBufEncoder.protoBufReader.pos = offset + 8;
ProtoBufEncoder.protoBufReader.len = buffer.length;
let result = null;
try {
result = messageClass.decode(ProtoBufEncoder.protoBufReader);
} catch (e) {
console.error("解码消息失败 :", messageClass.name);
}
return result;
}
扩展功能
心跳机制
为了保持连接的活跃性,我们实现了一个心跳机制。客户端定期发送心跳包,服务器收到后返回心跳响应。
// 客户端发送心跳
let heartBeat = new SystemPto.C_HEART_BEAT();
heartBeat.cmd = 0;
heartBeat.scmd = 2;
socket.send(ProtoBufEncoder
Kimi: .encode(heartBeat));
// 服务端接收心跳
socket.on('message', (message) => {
let decodedMessage = ProtoBufEncoder.decode(message);
if (decodedMessage && decodedMessage.cmd === 0 && decodedMessage.scmd === 2) {
let response = new SystemPto.S_HEART_BEAT();
response.cmd = 0;
response.scmd = 3;
response.serverTime = Date.now();
socket.send(ProtoBufEncoder.encode(response));
}
});
安全性考虑
在实际应用中,通信的安全性也是非常重要的。我们可以通过以下方式增强系统的安全性:
- 使用WSS(WebSocket Secure):使用WSS协议可以保证数据在传输过程中的加密。
- 消息签名:在消息中加入签名,服务器在接收到消息后验证签名,确保消息的完整性和来源的合法性。
性能优化
为了提高通信效率,我们还可以进行以下优化:
- 压缩数据:在发送数据前对数据进行压缩,减少传输的数据量。
- 连接复用:尽量复用现有的WebSocket连接,避免频繁地建立和关闭连接。
结论
WebSocket和Protobuf的结合为现代网络通信提供了一个高效、跨平台的解决方案。通过合理的设计和优化,我们可以构建出稳定且高效的通信系统。