因为TCP协议是流协议,在收发数据的时候会有粘包的问题。本例使用自定义的SPtcp封包协议对TCP数据再进行一次封装,解决了粘包问题。
注:其性能仍有待优化。优化方向:使用TCP自带的接收窗口缓存。
- sptcp.js
/**
* script: sptcp.js
* description: 简单封包协议SPtcp类
* authors: alwu007@sina.cn
* date: 2016-04-14
*/
var util = require('util');
function SPtcp(socket) {
//解析所处的阶段
var _sp_parse_step = SPtcp.SP_PARSE_STEP.HEADER;
//接收缓存
var _sp_rcv_buf = new Buffer(0);
//包头
var _sp_header = null;
//包体
var _sp_body = null;
//套接字
this.socket = socket;
//解析整包方法
function _spParseSPPacket(func){
if (_sp_rcv_buf.length >= SPtcp.SP_HEADER_LENGTH) {
//解析包头
_sp_header = {bodyLength: _sp_rcv_buf.readUInt16LE(0, true)};
//裁剪接收缓存
_sp_rcv_buf = _sp_rcv_buf.slice(SPtcp.SP_HEADER_LENGTH);
//解析包体
_sp_parse_step = SPtcp.SP_PARSE_STEP.BODY;
_spParseBody(func);
}
};
//解析包体方法
function _spParseBody(func){
if (_sp_rcv_buf.length >= _sp_header.bodyLength) {
var packet = _sp_rcv_buf.toString('utf8', 0, _sp_header.bodyLength);
util.log('['+socket.remoteAddress+']->['+socket.localAddress+'] receive: '+packet);
//裁剪接收缓存
_sp_rcv_buf = _sp_rcv_buf.slice(_sp_header.bodyLength);
//处理消息
try {
var msg = JSON.parse(packet);
func(msg);
} catch(e) {
util.log(e);
}
//清空包头和包体
_sp_header = null;
_sp_body = null;
//解析下一个包
_sp_parse_step = SPtcp.SP_PARSE_STEP.HEADER;
_spParseSPPacket(func);
}
};
//接收数据
this.spReceiveData = (data, func) => {
if (!func) func = msg => undefined;
//合并新旧数据
_sp_rcv_buf = Buffer.concat([_sp_rcv_buf, data]);
//解析处理数据
if (_sp_parse_step == SPtcp.SP_PARSE_STEP.HEADER) {
_spParseSPPacket(func);
} else if (_sp_parse_step == SPtcp.SP_PARSE_STEP.BODY) {
_spParseBody(func);
}
};
//发送数据
this.spSendData = msg => {
var packet = JSON.stringify(msg);
var body_buf = new Buffer(packet);
var head_buf = new Buffer(SPtcp.SP_HEADER_LENGTH);
head_buf.writeUInt16LE(body_buf.length);
var snd_buf = Buffer.concat([head_buf, body_buf]);
this.socket.write(snd_buf);
};
//销毁方法
this.spDestroy = () => {
delete this.socket;
};
}
//包头长度,单位字节
SPtcp.SP_HEADER_LENGTH = 4;
//解析所处的阶段
SPtcp.SP_PARSE_STEP = {
HEADER: 0, //解析包头阶段
BODY: 1, //解析包体阶段
};
exports.SPtcp = SPtcp;
- spsvr.js
/**
* script: spsvr.js
* description: SPtcp服务器端
* authors: alwu007@sina.cn
* date: 2016-04-15
*/
var util = require('util');
var net = require('net');
var SPtcp = require('./sptcp').SPtcp;
var server = net.createServer(client => {
util.log('client connected: ' + client.remoteAddress);
//套接字继承SPtcp
SPtcp.call(client, client);
//监听data事件
client.on('data', data => {
client.spReceiveData(data, msg => {
util.log('susl msg: ' + util.inspect(msg));
client.spSendData(msg);
});
});
//监听结束事件
client.on('end', () => {
util.log('disconnected from client: ' + client.remoteAddress);
client.spDestroy();
});
//监听错误事件
client.on('error', err => {
util.log(err);
client.end();
});
});
var listen_options = {
host: '172.16.200.26',
port: 6200,
};
util.log('listen options: ' + util.inspect(listen_options));
server.listen(listen_options, () => {
util.log('server bound');
});
- spcli.js
/**
* script: spcli.js
* description: SPtcp客户端
* authors: alwu007@sina.cn
* date: 2016-04-15
*/
var util = require('util');
var net = require('net');
var SPtcp = require('./sptcp').SPtcp;
var connect_options = {
host: '172.16.200.26',
port: 6200,
localPort: 6201,
};
util.log('connect options: ' + util.inspect(connect_options));
var client = net.connect(connect_options, ()=>{
//套接字继承SPtcp
SPtcp.call(client, client);
//监听data事件
client.on('data', data => {
client.spReceiveData(data, msg => {
util.log('susl msg: ' + util.inspect(msg));
});
});
//监听结束事件
client.on('end', () => {
util.log('disconnected from server: ' + client.remoteAddress);
client.spDestroy();
});
//监听错误事件
client.on('error', err => {
util.log(err);
client.end();
});
//发送消息
for (var i=0; i<10; i++) {
var msg = {op:'test', msg:'hello, 草谷子!', times:i};
client.spSendData(msg);
}
//关闭连接
client.end();
});
优化方案1:接收缓存_sp_rcv_buf改为Buffer数组,并记录数组元素的长度和_sp_rcv_length。这样做的好处有两点,一点是不用每次收到数据就执行一次concat方法分配一块新的内存;一点是在执行concat方法时直接传入长度参数,加快该方法的执行速度。——于2016-04-16
优化方案2:将类的方法定义在prototype原型对象上,这样该类的所有实例就共用同一个方法副本,节约资源。——于2016-04-19