因為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