├── .gitignore ├── README.md ├── index.js ├── lib ├── easy_sock.js ├── easy_tcp_server.js └── easy_udp.js ├── package.json ├── server.js └── test ├── lib ├── simple-seq-client.js └── simple-seq-server.js ├── tcp.test.js └── test_udp.js /.gitignore: -------------------------------------------------------------------------------- 1 | \.idea 2 | node_modules 3 | \.nyc_output 4 | coverage 5 | .vscode 6 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easy_sock — Make socket use easier 2 | 3 | 快速开发基于tcp连接的二进制网络协议接口的nodejs模块 4 | 5 | A fast way to create TCP socket API in nodejs 6 | 7 | 0.3.0 updated : easy_sock supports UDP now! See test_udp.js 8 | 9 | ------ 10 | easy_sock帮你快速开发基于tcp协议的接口,快速打通nodejs跟其他私有协议server的交互。让你做到像调用本地接口一样调用server api。 11 | 12 | easy_sock helps you to build a reliable, friendly used socket Api, with any kinds of binary protocols. 13 | 14 | easy_sock主要解决以下问题: 15 | 16 | easy_sock helps you to solve the follow probrams: 17 | 18 | - 处理socket发送、接收数据,以及数据包完整性校验 19 | Take care of send/receive message, and check whether the package is complete 20 | - 封装网络请求中各种复杂的异步调用以及中间过程,屏蔽tcp连接细节 21 | Take care of asyn calls in socket use. You don't need to call "connect()" before use. 22 | - 支持长连接、socket复用以及并发请求 23 | Keep alive mode is available. Send multi request at the same time in a single socket. 24 | - 自动管理连接状态,在合适的时候帮你断开连接或重新连接 25 | Connect and close socket Automatically 26 | - 各种异常处理 27 | Call you when error occurs 28 | 29 | ## 什么场景下适合使用 30 | 31 | 如果你的网络协议符合以下特性,便可以使用easy_sock: 32 | In what circumstances can I use easy_sock: 33 | 34 | - 基于tcp连接,通过二进制或文本协议封包 35 | Base on tcp connect, binary or text protocols 36 | - 符合一来一回的应答试请求 37 | Send one request and get one response 38 | - 支持通过请求序列号的方式来接收和返回并发请求包(大部分网络协议都支持,否则无法支持并发) 39 | The Server side support concurrent request, use sequence number to identify certain request 40 | 41 | ## 安装 Install 42 | 43 | ```bash 44 | npm install easy_sock 45 | ``` 46 | 47 | ## 使用简单,任性 Easy to use 48 | 下面通过一个基于easy_sock封装的cmem接口,来演示如何使用: 49 | (下面不翻译了,自己看代码吧) 50 | ```javascript 51 | var Cmem = require("cmem_core"); 52 | var client = new Cmem({ 53 | ip:"127.0.0.1", 54 | port:9101, 55 | keepAlive : false 56 | }); 57 | 58 | client.getData("key1",function(err, data){...}); 59 | client.getData("key2",function(err, data){...}); 60 | client.getData("key3",function(err, data){ 61 | client.getData("key4",function(err, data){...}); 62 | }); 63 | ``` 64 | 这是一个短连接的例子(keepAlive=false)。 65 | 可以看到,代码里没有connect、close等方法的调用,因为easy_sock已经帮你封装好了,会在第一次请求的时候建立连接,在没有任何请求后断开连接。注意到代码里面实际包含了并发请求和串行请求,不用担心会同时发起多个tcp connection,所有请求只会共享一个tcp连接。 66 | 67 | 注意到key4是在key3回调后才发起请求的,这时候有可能因为前三个请求已经返回而断开连接。遇到这种情况下,程序会再次发起一个新连接完成请求,然后断开。 68 | 当keepAlive=true时,会使用长连接方式,这时候必须主动调用close()方法来断开连接。 69 | 70 | ## 三步完成接口开发 71 | 你只需要实现以下3个方法,便可完成任何一种tcp网络请求: 72 | 73 | All you need to do is finish the following functions, then an Api is accomplish: 74 | 75 | - **encode**:将需要发送的数据按协议进行二进制编码 76 | - **decode**:将接收到的包进行解码,转换成js可操作的格式 77 | - **isReceiveComplete**:判断当前数据包是否完整 78 | 79 | 你可以通过C代码来实现这三个方法。任何类型的tcp协议,只要实现了这3个接口,剩下的事情都一样。这就是为什么easy_sock能存在的原因:) 80 | 81 | You can make a gyp to do this. As a matter of fact, no matter what protocol it is, all works are the same except these three functions. That's why easy_sock is written. 82 | 83 | ## 简单例子 84 | 85 | 下面通过一个demo演示各接口的使用方法: 86 | 87 | ```javascript 88 | function createSocket(){ 89 | var easysock = new EasySock(); 90 | easysock.setConfig({ 91 | ip : "127.0.0.1", 92 | port : 9101, 93 | keepAlive : false, 94 | timeout : 50 //0 by default 95 | }); 96 | 97 | //check if the package is received complete 98 | easysock.isReceiveComplete = function(packet){ 99 | var len = 0; 100 | //your code here.. 101 | 102 | /* 103 | * Check if the package is received complete. If not, return 0. 104 | * Otherwise return length of the FIRST complete package. 105 | * If the buffer contains more than one package--it usually happens when package size is small--, 106 | * just return the size of first one(not total). 107 | */ 108 | return len; 109 | }; 110 | 111 | //encode the data to binary 112 | easysock.encode = function(data, seq){ 113 | var packet = new Buffer(100); 114 | packet.writeInt32BE(seq, 0); 115 | //your code here.. 116 | 117 | //Translate the "data"(usually is a json or string) into a Buffer, and return the Buffer 118 | return packet; 119 | }; 120 | 121 | //decode the buffer 122 | easysock.decode = function(packet){ 123 | //The packet is a Buffer with a complete response. So decode the buffer to other type of data. 124 | var seq = packet.readInt32BE(0); 125 | //do sth else 126 | 127 | //must return the result and seq 128 | return { 129 | result : {}, 130 | seq : seq 131 | }; 132 | 133 | }; 134 | return easysock; 135 | } 136 | 137 | var client = createSocket(); 138 | 139 | client.write({ 140 | key : "" 141 | }, 142 | function(err, data){ 143 | if (err){ 144 | //err is a string 145 | console.log("fail:" + err); 146 | } 147 | else{ 148 | console.log("success"); 149 | console.dir(data); 150 | } 151 | } 152 | ); 153 | ``` 154 | 155 | 如有任何意见欢迎交流 vicyao#tencent.com 156 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/easy_sock'); 2 | module.exports.Udp = require('./lib/easy_udp'); -------------------------------------------------------------------------------- /lib/easy_sock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 一个方便进行socket网络操作的模块,解决socket复用,长连接、短连接,并发请求等问题 3 | * @author vicyao 4 | * 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var net = require('net'); 10 | var debug = require("debug")('easy_sock'); 11 | 12 | //需要通过创建实例来使用 13 | var Easysock = module.exports = function(conf){ 14 | //并发请求时的会话标识 15 | this.seq = 0; 16 | 17 | //保存请求的回调函数 18 | this.context = {}; 19 | 20 | //全局唯一的一个socket 21 | this.socket = null; 22 | 23 | this.between_connect = false; 24 | this.between_close = false; 25 | 26 | this.calling_close = false; 27 | 28 | this.currentSession = 0; 29 | this.tmpGetTaskList = []; 30 | 31 | this.config = { 32 | ip:"", 33 | port : 0, 34 | /* 35 | * 是否保持连接状态,如果为false,则每次socket空闲下来后就会关闭连接 36 | */ 37 | keepAlive : false, 38 | timeout : 0 39 | }; 40 | 41 | if (conf){ 42 | this.setConfig(conf); 43 | } 44 | 45 | this.isReceiveComplete = null; 46 | this.encode = null; 47 | this.decode = null; 48 | }; 49 | 50 | /** 51 | * 设置配置信息 52 | * @param {[Object]} obj [description] 53 | */ 54 | Easysock.prototype.setConfig = function(conf){ 55 | this.config = this.config || {}; 56 | 57 | if(typeof(conf) == 'object'){ 58 | for(var key in conf){ 59 | this.config[key] = conf[key]; 60 | } 61 | } 62 | }; 63 | 64 | /** 65 | * 当前是否已连接(或正在连接) 66 | * @type {Bool} 67 | */ 68 | Easysock.prototype.isAlive = false; 69 | 70 | /** 71 | * 对外的获取数据的接口方法 72 | * @param {[Array]} data [任意类型,会直接传给encode函数] 73 | * @param {[Function]} callback [回调函数(err, data)] 74 | * @return {[string]} [发送方式,wait等待socket连接,create创建socket,write复用socket] 75 | */ 76 | Easysock.prototype.write = function(data, callback){ 77 | var self = this; 78 | //当在这两个状态的时候,先保留请求,等连接成功后再执行 79 | if (this.between_connect || this.between_close){ 80 | 81 | this.tmpGetTaskList.push(function(err){ 82 | if (err){ 83 | callback(err); 84 | } 85 | else{ 86 | self.write(data, callback); 87 | } 88 | }); 89 | return 'wait'; 90 | } 91 | 92 | if (!this.config || !this.config.ip || !this.config.port){ 93 | callback(new Error("needs config info:ip,port")); 94 | } 95 | else{ 96 | 97 | if (this.socket){ 98 | //并发情况下靠这个序列标识哪个返回是哪个请求 99 | var seq = this.seq = (this.seq+1) % 10000; 100 | 101 | //编码 102 | try { 103 | var buf = this.encode(data, this.seq); 104 | } catch(e) { 105 | return callback(e); 106 | } 107 | if (!Buffer.isBuffer(buf)){ 108 | callback(new Error("encode error")); 109 | return; 110 | } 111 | 112 | var timer = null; 113 | if(this.config.timeout){ 114 | timer = setTimeout(function(){ 115 | //返回超时 116 | self.context[seq] = null; 117 | self.currentSession--; 118 | 119 | tryCloseSocket(self); 120 | callback(new Error("request or decode timeout(" + self.config.timeout + "ms)")); 121 | }, this.config.timeout); 122 | } 123 | 124 | //保存当前上下文,都是为了并发 125 | this.context[this.seq] = { 126 | seq : this.seq, 127 | cb : function(err, result){ 128 | if (timer){ 129 | clearTimeout(timer); 130 | } 131 | callback(err, result); 132 | } 133 | }; 134 | this.currentSession++; 135 | 136 | //真正的写socket 137 | this.socket.write(buf); 138 | return 'write'; 139 | } 140 | else{ 141 | //第一次请求,初始化 142 | this.tmpGetTaskList.push(function(err){ 143 | if (err) { 144 | callback(err); 145 | } 146 | else { 147 | self.write(data, callback); 148 | } 149 | }); 150 | initSocket(self); 151 | return 'create' 152 | } 153 | } 154 | }; 155 | 156 | /** 157 | * 关闭连接 158 | */ 159 | Easysock.prototype.close = function(){ 160 | if (this.socket && this.currentSession == 0 && this.tmpGetTaskList.length == 0 && (!this.between_close)){ 161 | this.between_close = true; 162 | this.isAlive = false; 163 | this.socket.end(); 164 | } 165 | else{ 166 | //等所有请求处理完再关闭 167 | this.calling_close = true; 168 | } 169 | } 170 | 171 | /** 172 | * 初始化socket方法 173 | */ 174 | function initSocket(cur){ 175 | var totalData = new Buffer(''); 176 | 177 | var socket = cur.socket = new net.Socket({ 178 | writable : true, 179 | readable : true 180 | }); 181 | 182 | //socket.setTimeout(cur.config.timeout); 183 | socket.setKeepAlive(cur.config.keepAlive); 184 | 185 | var errorCall = function(msg){ 186 | //Timeout while connection or some connection error 187 | debug(msg); 188 | clearTimeout(connect_timer); 189 | 190 | //actually, I don't know which request is error and which cb function I shall call. So call them all. 191 | var cb; 192 | while(cb = cur.tmpGetTaskList.shift()){ 193 | cb(msg); 194 | } 195 | 196 | for (var key in cur.context){ 197 | var ctx = cur.context[key]; 198 | if (ctx && typeof(ctx.cb) == "function"){ 199 | ctx.cb(msg); 200 | cur.context[key] = null; 201 | cur.currentSession--; 202 | } 203 | } 204 | 205 | socket.destroy(); 206 | }; 207 | 208 | var connect_timeout = cur.config.timeout ? cur.config.timeout * 3 : 3000; 209 | 210 | var connect_timer = setTimeout(function(){ 211 | errorCall(new Error("easy_sock:TCP connect timeout(" + connect_timeout + ")")); 212 | cur.socket = null; 213 | cur.between_connect = false; 214 | }, connect_timeout); 215 | 216 | 217 | socket.on('connect',function(){ 218 | //连接成功,把等待的数据发送掉 219 | debug("easy_sock connected"); 220 | clearTimeout(connect_timer); 221 | cur.between_connect = false; 222 | 223 | //外部有可能会在发起连接但还没完成的时候发起请求,所以,把积累的请求都发了 224 | /* let get; 225 | while(get = cur.tmpGetTaskList.shift()) { 226 | get(); 227 | } */ 228 | 229 | function asyncWhile() { 230 | cur.tmpGetTaskList.shift()(); 231 | setImmediate(function () { 232 | if (cur.tmpGetTaskList.length) { 233 | asyncWhile(); 234 | } 235 | }) 236 | } 237 | asyncWhile(); 238 | 239 | }).on('data', function(data) { 240 | if (!data || !Buffer.isBuffer(data) || data.length <= 0 ){ 241 | //error 242 | debug("buffer error:" + data); 243 | errorCall(new Error("receive error, illegal data")); 244 | socket.end(); 245 | } 246 | else{ 247 | 248 | totalData = Buffer.concat([totalData, data]); 249 | var packageSize = cur.isReceiveComplete(totalData); 250 | if(packageSize){ 251 | //网络有可能一次返回n个结果包,需要做判断,是不是很bt。。 252 | var totalSize = totalData.length; 253 | if (packageSize == totalSize){ 254 | //只有一个包,这是大多数情况 255 | handleData(cur, totalData, errorCall); 256 | } 257 | else{ 258 | //存在多个包,这里要做一些buffer复制的操作,会消耗一定性能 259 | 260 | while(true){ 261 | var buf = totalData.slice(0, packageSize); 262 | handleData(cur, buf, errorCall); 263 | totalData = totalData.slice(packageSize, totalData.length); 264 | packageSize = cur.isReceiveComplete(totalData); 265 | 266 | if (packageSize >= totalData.length){ 267 | //last one 268 | handleData(cur, totalData, errorCall); 269 | break; 270 | } 271 | else if (packageSize == 0){ 272 | //包还没接收完 273 | return; 274 | } 275 | } 276 | } 277 | 278 | //清空buffer,给下一次请求使用 279 | totalData = new Buffer(''); 280 | } 281 | else{ 282 | //没接收完的话继续接收 283 | //console.log("keep looking"); 284 | } 285 | 286 | } 287 | 288 | }).on('error',function(e){ 289 | errorCall(e); 290 | socket.destroy(); 291 | cur.socket = null; 292 | cur.between_connect = false; 293 | 294 | }).on('close',function(){ 295 | cur.between_close = false; 296 | cur.socket = null; 297 | cur.isAlive = false; 298 | cur.currentSession = 0; 299 | debug("easy_sock closed"); 300 | if (cur.tmpGetTaskList.length){ 301 | //刚关闭socket,又来新请求 302 | cur.tmpGetTaskList.shift()(); 303 | } 304 | }); 305 | 306 | //连接也有可能会超时阻塞 307 | socket.connect({ 308 | port : cur.config.port, 309 | host : cur.config.ip 310 | }); 311 | 312 | cur.between_connect = true; 313 | cur.isAlive = true; 314 | } 315 | 316 | /** 317 | * 处理返回数据,回调 318 | */ 319 | function handleData(cur, buf, errorCal){ 320 | var obj; 321 | try { 322 | obj = cur.decode(buf); 323 | } catch(e) { 324 | //error 325 | debug("decode error:"); 326 | cur.socket.destroy(); 327 | } 328 | 329 | if (typeof obj != "object"){ 330 | //error 331 | debug("easy_sock:handle error:" + obj); 332 | cur.socket.destroy(); 333 | return; 334 | } 335 | 336 | var ctx = cur.context[obj.seq]; 337 | if (!ctx){ 338 | //找不到上下文,可能是因为超时,callback已执行,直接放弃当前数据 339 | //console.log("Can't find context. This should never happened!" + obj.seq); 340 | //socket.destroy(); 341 | return; 342 | } 343 | 344 | cur.context[obj.seq] = null; 345 | cur.currentSession--; 346 | 347 | tryCloseSocket(cur); 348 | 349 | //遵循nodejs最佳实践,第一个参数是err,第二个才是返回结果 350 | if (!obj.result && obj.error) { 351 | ctx.cb(obj.error, null); 352 | 353 | } else { 354 | ctx.cb(null, obj.result); 355 | } 356 | } 357 | 358 | /** 359 | * 尝试关闭socket 360 | */ 361 | function tryCloseSocket(cur){ 362 | 363 | if ((cur.calling_close || !cur.config.keepAlive) && cur.currentSession == 0 && cur.tmpGetTaskList.length == 0){ 364 | cur.between_close = true; 365 | cur.calling_close = false; 366 | cur.isAlive = false; 367 | //调用end()之后sock会自动close,消息回调会先触发end,再触发close 368 | cur.socket.end(); 369 | } 370 | } -------------------------------------------------------------------------------- /lib/easy_tcp_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const debug = require("debug")('easysock-server'); 3 | 4 | /** 5 | * @param config 6 | * encode 7 | * decode 8 | * check 9 | * @param handleResponse:async fn 10 | * 11 | * 12 | * @returns {bindsocket(socket)} 13 | */ 14 | let exportee = module.exports = function (config, handleResponse) { 15 | let {encode, decode, check} = config; 16 | if (!check && config.isReceiveComplete) check = config.isReceiveComplete; 17 | 18 | // 缓冲区 19 | let buffer = null; 20 | let handleData = async function (buffer) { 21 | let param = null; 22 | let result = null; 23 | 24 | try { 25 | param = decode(buffer); 26 | result = await handleResponse(param.error, param.result); 27 | return encode(result, param.seq); 28 | } catch (e) { 29 | return debug(e); 30 | } 31 | }; 32 | 33 | return function bindsocket(socket) { 34 | debug('socket connected', socket.connection); 35 | 36 | socket.on('data', data=> { 37 | debug('socket ondata'); 38 | 39 | buffer = (buffer && buffer.length > 0) ? 40 | Buffer.concat([buffer, data]) : // 有遗留数据才做拼接操作 41 | data; 42 | let checkLength = null; 43 | while (buffer && (checkLength = check(buffer))) { 44 | let requestdata = null; 45 | let responsedata = null; 46 | if (checkLength == buffer.length) { 47 | requestdata = buffer; 48 | buffer = null; 49 | 50 | } else { 51 | requestdata = buffer.slice(0, checkLength); 52 | buffer = buffer.slice(checkLength); 53 | } 54 | 55 | (async()=> { 56 | debug('handling data'); 57 | responsedata = await handleData(requestdata); 58 | debug('response data'); 59 | try { 60 | responsedata && socket.write(responsedata); 61 | } catch (e) { 62 | debug('write error'); 63 | } 64 | })(); 65 | } 66 | debug('remain', buffer && buffer.length) 67 | }); 68 | 69 | socket.on('end', e=> { 70 | debug('socket end'); 71 | }); 72 | 73 | return socket; 74 | }; 75 | }; 76 | 77 | const net = require("net"); 78 | /** 79 | * 直接创建一个server 80 | * @param config 包含encode、decode、check 81 | * @param handlerResponse 一个async function,参数为收到的请求结构体,返回是回包的结构体 82 | */ 83 | exportee.server = function (config, handlerResponse) { 84 | let handleSocket = exportee(config, handlerResponse); 85 | let socketList = []; 86 | 87 | return Object.assign( 88 | net.createServer(function (socket) { 89 | handleSocket(socket); 90 | socketList.push(socket); 91 | }), 92 | 93 | { 94 | closeAllSocket: function () { 95 | socketList.forEach(socket=> { 96 | if (!socket.destroyed) { 97 | socket.destroy(); 98 | 99 | } 100 | }) 101 | } 102 | } 103 | ) 104 | }; -------------------------------------------------------------------------------- /lib/easy_udp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @fileoverview 封装UDP请求的相关逻辑,只适用于标准应答试请求,即一个发包一个回包。 5 | * 重要提示:跟TCP不同,对于UDP请求,不能用一个socket做并发请求。原因是nodejs进程有可能来不及处理所有的收包响应,导致下一个udp响应包替换前一个包,造成丢包,从而造成回调函数没有调用。 6 | * 所以,每次请求都重新创建一个socket来发包收包。 7 | * 注意该代码只能在node 4.2以上版本执行,node 0.1版本的udp存在bug。 8 | * 在内网下测试,每1000个并发请求耗时190ms 9 | * @author vicyao 10 | */ 11 | 12 | var dgram = require("dgram"); 13 | 14 | //需要通过创建实例来使用 15 | var Easyudp = exports = module.exports = function(conf){ 16 | //并发请求时的会话标识,实际上这里并不需要seq,只是为了防止回调混淆 17 | this.seq = 0; 18 | 19 | this.config = { 20 | ip:"", 21 | port : 0, 22 | timeout : 5000 23 | }; 24 | 25 | if (conf){ 26 | this.setConfig(conf); 27 | } 28 | 29 | //输入(object,seq),输出buffer 30 | this.encode = null; 31 | 32 | //输入buffer,输出object 33 | this.decode = null; 34 | } 35 | 36 | /** 37 | * 设置配置信息 38 | * @param {[Object]} obj [description] 39 | */ 40 | Easyudp.prototype.setConfig = function(conf){ 41 | this.config = this.config || {}; 42 | 43 | if(typeof(conf) == 'object'){ 44 | for(var key in conf){ 45 | this.config[key] = conf[key]; 46 | } 47 | } 48 | }; 49 | 50 | /** 51 | * 对外的发送数据的接口方法,其实用send更合适,但为了跟easysock统一,兼容下write 52 | * @param {[Array]} data [任意类型,会直接传给encode函数] 53 | * @param {[Function]} callback [回调函数(err, data)] 54 | * @return {[void]} [void] 55 | */ 56 | Easyudp.prototype.write = Easyudp.prototype.send = function(data, callback){ 57 | 58 | var self = this; 59 | this.seq = (this.seq + 1) % 100000; 60 | var cur_seq = this.seq; 61 | 62 | var buf = this.encode(data, cur_seq); 63 | 64 | if (!Buffer.isBuffer(buf)){ 65 | callback("encode error"); 66 | return; 67 | } 68 | 69 | //每次都新建一个socket,不复用 70 | var socket = dgram.createSocket("udp4"); 71 | 72 | socket.bind({ 73 | port : 0, 74 | exclusive : true //当exclusive为true时不会复用socket句柄,否则会导致回调函数混乱。但在0.1中,即使将exclusive设置为true,nodejs还是会重复分配端口,这是个坑 75 | },function(){ 76 | //注意,虽然这个bind有回调,但实际上不需要等bind的回调执行,就可以开始发送数据了 77 | //var address = socket.address(); 78 | //console.log("bind success. port=" + address.port); 79 | }); 80 | 81 | //超时处理 82 | var timer = null; 83 | if (this.config.timeout){ 84 | timer = setTimeout(function(){ 85 | if (socket){ 86 | try{ 87 | socket.close(); 88 | } 89 | catch(e){ 90 | //nodejs 0.1版本出现过 91 | callback("close error:" + e); 92 | } 93 | } 94 | callback("easyudp: Request timeout(" + self.config.timeout + "ms)!"); 95 | 96 | }, self.config.timeout); 97 | } 98 | 99 | //接收回包 100 | socket.on("message", function(msg, rinfo){ 101 | 102 | //如果这里有异常,直接往外抛出 103 | var result = self.decode(msg); 104 | if (result && result.seq != null){ 105 | 106 | if (timer){ 107 | //清除超时的timer 108 | clearTimeout(timer); 109 | } 110 | 111 | //回调正确性检测 112 | if (cur_seq != result.seq){ 113 | //发送的请求跟回调不匹配,多个socket并发的时候互相混淆,这个bug在nodejs 0.1版本出现过,4.2之后应该不会了 114 | callback("easyudp: 发送的请求跟回调不匹配. send seq=" + cur_seq + ", received seq=" + result.seq); 115 | } 116 | else { 117 | callback(null, result); 118 | } 119 | } 120 | else{ 121 | //result不正确 122 | callback("decode error"); 123 | } 124 | 125 | //完成后即销毁socket 126 | socket.close(); 127 | }); 128 | 129 | socket.on("error", function(err){ 130 | if (timer){ 131 | //清除超时的timer 132 | clearTimeout(timer); 133 | } 134 | //如果数据已正常反馈,再发生错误,callback会被调用2次,但实际上这种情况应该不会发生。 135 | callback(err); 136 | }); 137 | 138 | socket.on("close", function(err){ 139 | socket = null; 140 | }); 141 | 142 | //真正发送请求 143 | socket.send(buf, 0, buf.length, this.config.port, this.config.ip, function(err, bytes){ 144 | if (err) { 145 | //按照文档,这里只有可能出现DNS解析错误,通过ip发送应该不会报错 146 | callback(err); 147 | } 148 | }); 149 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy_sock", 3 | "version": "0.4.1", 4 | "description": "A easy and foolish way to develop a socket module, which is easy to use as well. Now support both TCP and UDP.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "ava test/tcp.test.js" 8 | }, 9 | "keywords": [ 10 | "socket" 11 | ], 12 | "author": "vicyao", 13 | "license": "ISC", 14 | "_shasum": "77ea2bfa6c10379f0792e3eb8116bab8d316be62", 15 | "_from": "easy_sock@", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ysbcc/easy_sock.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/ysbcc/easy_sock/issues" 22 | }, 23 | "homepage": "https://github.com/ysbcc/easy_sock#readme", 24 | "dependencies": { 25 | "debug": "^2.2.0" 26 | }, 27 | "devDependencies": { 28 | "ava": "^0.19.1", 29 | "nyc": "^10.3.2", 30 | "protocol-buffers": "^3.2.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require("./lib/easy_tcp_server"); -------------------------------------------------------------------------------- /test/lib/simple-seq-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EasySock = require("../../index"); 3 | 4 | module.exports = function (port, timeout) { 5 | let easysock = new EasySock(); 6 | easysock.setConfig({ 7 | ip: "127.0.0.1", 8 | port: port, 9 | keepAlive: true, 10 | timeout: timeout || 200 //0 by default 11 | }); 12 | Object.assign(easysock, { 13 | encode: function (data, seq) { 14 | // console.log('client encode', data); 15 | let body = new Buffer(data, 'utf-8'); 16 | 17 | // 假设encode需要50ms 18 | let start = Date.now(); 19 | while(Date.now() - start < 50) { 20 | continue 21 | } 22 | 23 | let packet = new Buffer(20); 24 | //包头-start 25 | packet.writeInt8(81, 0); // Q 26 | packet.writeInt8(86, 1); // A 27 | 28 | packet.writeInt16BE(1, 2); // version 29 | packet.writeInt32BE(body.length + 4, 4); // 长度 30 | packet.writeInt32BE(seq, 8); 31 | packet.writeInt32BE(10001, 12); 32 | packet.writeInt32BE(body.length, 16); 33 | //包头-end 34 | 35 | packet = Buffer.concat([packet, body]); 36 | 37 | return packet; 38 | }, 39 | 40 | decode: function (packet) { 41 | // console.log('client decode', data); 42 | let seq = packet.readUInt32BE(8); 43 | 44 | let body = packet.slice(32); 45 | let msg = body.toString('utf-8'); 46 | return { 47 | result: msg, 48 | seq: seq, 49 | error: null 50 | }; 51 | }, 52 | 53 | isReceiveComplete: function (packet) { 54 | if (packet.length < 28) 55 | return 0; 56 | 57 | var len = packet.readUInt32BE(4); 58 | if (packet.length < len + 28) 59 | return 0; 60 | return len + 28; 61 | } 62 | }); 63 | 64 | easysock.writePromise = (function (write) { 65 | 66 | return function (data) { 67 | return new Promise(function (resolve, reject) { 68 | write.call(easysock, data, function (err, data) { 69 | err ? reject(err) : resolve(data); 70 | }) 71 | }); 72 | }; 73 | })(easysock.write); 74 | 75 | return easysock 76 | }; -------------------------------------------------------------------------------- /test/lib/simple-seq-server.js: -------------------------------------------------------------------------------- 1 | const easyServer = require("../../lib/easy_tcp_server"); 2 | 3 | function create(port, callback = function() {}) { 4 | let server = easyServer.server( 5 | { 6 | decode: function (request) { 7 | // console.log('server decode', request); 8 | // 解包头与包体 9 | let seq = request.readInt32BE(8); 10 | return { 11 | error: null, 12 | result: request.slice(20).toString('utf-8'), 13 | seq 14 | } 15 | 16 | }, 17 | encode: function (data, seq) { 18 | // console.log('server encode', data); 19 | body = new Buffer(data, 'utf-8'); 20 | // 编好包头返回 21 | let header = Buffer.alloc(32); 22 | header.writeUInt8(0x51, 0); 23 | header.writeUInt8(0x41, 1); 24 | header.writeUInt32BE(body.length + 4, 4); 25 | header.writeUInt32BE(seq, 8); 26 | return Buffer.concat([header, body]) 27 | 28 | }, 29 | isReceiveComplete: function (buffer) { 30 | if (buffer.length < 16) {return} // 包头至少16 31 | let length = buffer.readInt32BE(4); // 读出包体长度 32 | if (buffer.length >= length + 16) return length + 16; 33 | 34 | return 0 35 | } 36 | 37 | }, 38 | 39 | async function handleRequest(err, data) { 40 | 41 | await new Promise((resolve, reject)=> { 42 | setTimeout(resolve, 50); 43 | }); 44 | 45 | return data 46 | } 47 | 48 | ); 49 | 50 | server.listen(port, function (err) { 51 | process.send('listened'); 52 | callback() 53 | }); 54 | return server; 55 | } 56 | 57 | if (process.env.PORT) { 58 | create(process.env.PORT); 59 | } 60 | 61 | module.exports = create; 62 | -------------------------------------------------------------------------------- /test/tcp.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ava = require("ava"); 3 | const cp = require("child_process"); 4 | const createClient = require("./lib/simple-seq-client"); 5 | 6 | function forkServer(port) { 7 | return new Promise(resolve=> { 8 | let pcs = cp.fork(__dirname + '/lib/simple-seq-server.js', { 9 | env: { 10 | PORT: port 11 | } 12 | }); 13 | pcs.on('message', data=> { 14 | if (data.toString().indexOf('listened') != -1) { 15 | resolve({ 16 | close: function () { 17 | return new Promise((resolve, reject)=> { 18 | pcs.kill(); 19 | pcs.on('close', resolve) 20 | }); 21 | } 22 | }); 23 | } 24 | }) 25 | }); 26 | } 27 | 28 | ava.serial('正常通信', async function (t) { 29 | let port = 9091; 30 | 31 | // 创建服务器和easy_sock 32 | let server = await forkServer(port); 33 | let easysock = createClient(port); 34 | 35 | for (var i = 0; i < 10; i++) { 36 | let data = await easysock.writePromise('hehe'); 37 | t.is(data, 'hehe'); 38 | } 39 | 40 | easysock.close(); 41 | await server.close(); 42 | }); 43 | 44 | ava.serial('服务器进程重启', async function (t) { 45 | let port = 9092; 46 | 47 | // 创建服务器和easy_sock 48 | let server = await forkServer(port); 49 | let easysock = createClient(port); 50 | 51 | for (let i = 0; i < 5; i++) { 52 | let data = await easysock.writePromise('hehe'); 53 | t.is(data, 'hehe'); 54 | } 55 | 56 | // 服务器关闭 57 | await server.close(); 58 | // console.log('start'); 59 | for (let i = 0; i < 5; i++) { 60 | try { 61 | let error = await easysock.writePromise('hehe'); 62 | } catch(error) { 63 | continue; 64 | } 65 | t.is(false, true) 66 | // t.is(error.message, 'easy_sock:TCP connect timeout(600)'); 67 | } 68 | 69 | // 服务器重启完毕 70 | server = await forkServer(port); 71 | 72 | for (let i = 0; i < 5; i++) { 73 | let data = await easysock.writePromise('hehe'); 74 | t.is(data, 'hehe'); 75 | } 76 | }); 77 | 78 | let createServer = require("./lib/simple-seq-server"); 79 | ava.serial('服务器主动关闭连接,然后客户端重连', async function (t) { 80 | let port = 9093; 81 | let server = null; 82 | 83 | await new Promise(resolve=> { 84 | server = createServer(port, resolve) 85 | }); 86 | let easysock = createClient(port); 87 | 88 | for (let i = 0; i < 5; i++) { 89 | var prom = easysock.writePromise('hehe'); 90 | let data = await prom 91 | t.is(data, 'hehe'); 92 | } 93 | 94 | server.closeAllSocket(); 95 | await new Promise((resolve, reject)=> { 96 | setTimeout(resolve, 1500) 97 | }); 98 | 99 | for (let i = 0; i < 5; i++) { 100 | let data = await easysock.writePromise('hehe'); 101 | t.is(data, 'hehe'); 102 | } 103 | }); 104 | 105 | ava.serial('并行请求,有超时失败率', async function (t) { 106 | let port = 9094; 107 | await forkServer(port); 108 | 109 | let easysock = createClient(port, 120); // tcp有可能拼两个包一起返回 110 | 111 | const Number = 5; 112 | let failedTime = 0; 113 | t.plan(Number) 114 | 115 | for (let i = 0; i < Number; i++) { 116 | easysock.write('hehe' + i, function (err, data) { 117 | console.log(err, data) 118 | if (err) { 119 | failedTime += 1; 120 | } 121 | t.pass(); 122 | }) 123 | } 124 | 125 | return await new Promise(rs => { 126 | setTimeout(() => { 127 | console.log(`失败率:${failedTime/Number}`); 128 | rs() 129 | }, 1000); 130 | }) 131 | }) 132 | 133 | ava.serial('第一次连接超时,在close之前创建新连接,触发死循环', async function (t) { 134 | let port = 9095; 135 | await forkServer(port); 136 | 137 | let easysock = createClient(port, 100); 138 | const Number = 5; 139 | t.plan(Number) 140 | 141 | easysock.write('first', function (err, data) { 142 | // 第一个连接超时 143 | console.log('first', err, data); 144 | }) 145 | 146 | // 卡CPU,保证在触发第一个连接的close事件前创建第二个socket 147 | let start = Date.now(); 148 | while (Date.now() - start < 400) { 149 | continue 150 | } 151 | setTimeout(() => { 152 | console.log('client socket begin write ...') 153 | for (let i = 0; i < Number; i++) { 154 | easysock.write('hehe' + i, function (err, data) { 155 | console.log('请求有回调,没有死循环', err, data); 156 | t.pass() 157 | }) 158 | } 159 | }) 160 | 161 | 162 | await new Promise(rs => { 163 | setTimeout(() => { 164 | rs() 165 | }, 1000); 166 | }) 167 | 168 | }) -------------------------------------------------------------------------------- /test/test_udp.js: -------------------------------------------------------------------------------- 1 | var Easyudp = require("./../index.js").Udp; 2 | 3 | var easyudp = new Easyudp({ 4 | ip: "127.0.0.1", 5 | port: 1990, 6 | timeout: 100 7 | }); 8 | 9 | var encode = function (data, seq) { 10 | var buf = new Buffer(8); 11 | buf.writeInt32BE(data.num, 0); 12 | buf.writeInt32BE(seq, 4); 13 | return buf; 14 | }; 15 | easyudp.encode = encode; 16 | 17 | var decode = function (buf) { 18 | var num = buf.readInt32BE(0); 19 | var seq = buf.readInt32BE(4); 20 | return { 21 | num: num, 22 | seq: seq 23 | } 24 | }; 25 | easyudp.decode = decode; 26 | 27 | 28 | //server start 29 | var dgram = require("dgram"); 30 | var server = dgram.createSocket("udp4"); 31 | 32 | if (process.version.indexOf('v0.1') >= 0) { 33 | server.bind(1990); 34 | } 35 | else { 36 | server.bind({ 37 | port: 1990 38 | }); 39 | } 40 | 41 | server.on("message", function (buf, rinfo) { 42 | 43 | //在这个测试程序中,如果并发量大的话接受就会丢包,node还是处理不过来 44 | var res = decode(buf); 45 | res.num *= res.num; 46 | //console.dir(rinfo) { address: '127.0.0.1', family: 'IPv4', port: 44175, size: 8 } 47 | 48 | var ret = encode(res, res.seq); 49 | 50 | server.send(ret, 0, ret.length, rinfo.port, rinfo.address, function (err, bytes) { 51 | if (err) { 52 | console.log("server error"); 53 | console.log(err); 54 | } 55 | }); 56 | }); 57 | //server end 58 | 59 | var succ_cnt = 0; 60 | var send = function (num) { 61 | easyudp.send({ 62 | num: num 63 | }, function (err, result) { 64 | if (err) { 65 | //在nodejs 0.1版本下运行会偶尔出现这类错,即是促发了nodejs的bug “easyudp: 发送的请求跟回调不匹配. send seq=26, received seq=23” 66 | console.log(err); 67 | } 68 | else { 69 | console.log((num * num == result.num ? "YES " : "NO!!!!! ") + "send" + num + ", return " + result.num); 70 | } 71 | succ_cnt++; 72 | 73 | if (succ_cnt == 200) { 74 | server.close(); 75 | server = null; 76 | } 77 | }); 78 | } 79 | 80 | //并发测试 81 | var n = 0; 82 | while (n++ < 200) { 83 | send(n); 84 | } 85 | 86 | 87 | 88 | 89 | --------------------------------------------------------------------------------