├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── History.md ├── LICENSE ├── README.md ├── example ├── client.js └── server.js ├── lib └── base.js ├── package.json └── test ├── close_wait.test.js ├── error.test.js ├── index.test.js └── support ├── server.js ├── server_end.js └── server_immidiate_end.js /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | examples/**/app/public 4 | logs 5 | run -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | * **Node Version**: 16 | * **Platform**: 17 | 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Checklist 12 | 13 | 14 | - [ ] `npm test` passes 15 | - [ ] tests and/or benchmarks are included 16 | - [ ] documentation is changed or added 17 | - [ ] commit message follows commit guidelines 18 | 19 | ##### Affected core subsystem(s) 20 | 21 | 22 | 23 | ##### Description of change 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'javascript' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Use only 'java' to analyze code written in Java, Kotlin or both 36 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | pull_request: 8 | branches: [ master ] 9 | 10 | workflow_dispatch: {} 11 | 12 | jobs: 13 | Job: 14 | name: Node.js 15 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 16 | with: 17 | os: 'ubuntu-latest' 18 | version: '14, 16, 18, 20' 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | workflow_dispatch: {} 8 | 9 | jobs: 10 | release: 11 | name: Node.js 12 | uses: artusjs/github-actions/.github/workflows/node-release.yml@v1 13 | secrets: 14 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 15 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 16 | with: 17 | checkTest: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | npm-debug.log 5 | .logs 6 | logs 7 | *.swp 8 | run 9 | *-run 10 | .idea 11 | .DS_Store 12 | .tmp 13 | 14 | .* 15 | !.github 16 | !.eslintignore 17 | !.eslintrc 18 | !.gitignore 19 | !.travis.yml 20 | package-lock.json 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.2.0](https://github.com/node-modules/tcp-base/compare/v3.1.1...v3.2.0) (2023-08-30) 4 | 5 | 6 | ### Features 7 | 8 | * show tips on send error ([#16](https://github.com/node-modules/tcp-base/issues/16)) ([8f4fb4c](https://github.com/node-modules/tcp-base/commit/8f4fb4ca57f043c5e9efee5809fdfa7ec7985c9a)) 9 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 3.1.1 / 2022-09-01 3 | ================== 4 | 5 | **fixes** 6 | * [[`be4eee2`](http://github.com/node-modules/tcp-base/commit/be4eee2c8bc3f96b6de11639c5e346a08e92fdf0)] - fix: socket.on('error') instead of once (#13) (hyj1991 <>) 7 | 8 | **others** 9 | * [[`d0a814a`](http://github.com/node-modules/tcp-base/commit/d0a814a17da2d5dc910e3eee3706646972d9396c)] - 🤖 TEST: Run ci on GitHub Action (#14) (fengmk2 <>) 10 | 11 | 3.1.0 / 2017-12-01 12 | ================== 13 | 14 | **features** 15 | * [[`e59388a`](http://github.com/node-modules/tcp-base/commit/e59388a564304803d0b85222bce1dc3e945f6fac)] - feat: support unix-domain-socket (#11) (fisher <>) 16 | 17 | **others** 18 | * [[`f9c56ee`](http://github.com/node-modules/tcp-base/commit/f9c56ee64afb38b4062e5f1a247db45cbce4192c)] - chore: bump to 3.0.0 (xiaochen.gaoxc <>), 19 | 20 | 3.0.0 / 2017-04-20 21 | ================== 22 | 23 | * fix: invoke oneway after socket closed (#7) 24 | * refactor: [BREAKING-CHANGE] not support reconnect 25 | 26 | 2.0.0 / 2017-02-17 27 | ================== 28 | 29 | * refactor: [BREAKING-CHANGE] upgrade sdk-base (#5) 30 | 31 | 1.1.0 / 2016-11-30 32 | ================== 33 | 34 | * feat: add heartbeat timeout server host 35 | * feat: add socket write status 36 | 37 | 1.0.2 / 2016-11-11 38 | ================== 39 | 40 | * fix: use heartbeat logic to handle that connection stuck in CLOSE_WAIT status (#3) 41 | 42 | 1.0.1 / 2016-10-20 43 | ================== 44 | 45 | * fix: callback exception should not crash tcp-base (#2) 46 | 47 | 1.0.0 / 2016-09-18 48 | ================== 49 | 50 | * feat: implement tcp-base 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 node-modules and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tcp-base 2 | 3 | A base class for tcp client with basic functions 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![Node.js CI](https://github.com/node-modules/tcp-base/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/tcp-base/actions/workflows/nodejs.yml) 7 | [![Test coverage][codecov-image]][codecov-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/tcp-base.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/tcp-base 12 | [codecov-image]: https://codecov.io/gh/node-modules/tcp-base/branch/master/graph/badge.svg 13 | [codecov-url]: https://codecov.io/gh/node-modules/tcp-base 14 | [download-image]: https://img.shields.io/npm/dm/tcp-base.svg?style=flat-square 15 | [download-url]: https://npmjs.org/package/tcp-base 16 | 17 | ## Install 18 | 19 | ```bash 20 | npm install tcp-base --save 21 | ``` 22 | 23 | Node.js >= 6.0.0 required 24 | 25 | ## Usage 26 | 27 | A quick guide to implement a tcp echo client 28 | 29 | Client: 30 | 31 | ```js 32 | const TCPBase = require('tcp-base'); 33 | 34 | /** 35 | * A Simple Protocol: 36 | * (4B): request id 37 | * (4B): body length 38 | * ------------------------------ 39 | * body data 40 | */ 41 | class Client extends TCPBase { 42 | getHeader() { 43 | return this.read(8); 44 | } 45 | 46 | getBodyLength(header) { 47 | return header.readInt32BE(4); 48 | } 49 | 50 | decode(body, header) { 51 | return { 52 | id: header.readInt32BE(0), 53 | data: body, 54 | }; 55 | } 56 | 57 | // heartbeat packet 58 | get heartBeatPacket() { 59 | return Buffer.from([ 255, 255, 255, 255, 0, 0, 0, 0 ]); 60 | } 61 | } 62 | 63 | const client = new Client({ 64 | host: '127.0.0.1', 65 | port: 8080, 66 | }); 67 | 68 | const body = Buffer.from('hello'); 69 | const data = Buffer.alloc(8 + body.length); 70 | data.writeInt32BE(1, 0); 71 | data.writeInt32BE(body.length, 4); 72 | body.copy(data, 8, 0); 73 | 74 | client.send({ 75 | id: 1, 76 | data, 77 | timeout: 5000, 78 | }, (err, res) => { 79 | if (err) { 80 | console.error(err); 81 | } 82 | console.log(res.toString()); // should echo 'hello' 83 | }); 84 | ``` 85 | 86 | Server: 87 | 88 | ```js 89 | 'use strict'; 90 | 91 | const net = require('net'); 92 | 93 | const server = net.createServer(socket => { 94 | let header; 95 | let bodyLen; 96 | 97 | function readPacket() { 98 | if (bodyLen == null) { 99 | header = socket.read(8); 100 | if (!header) { 101 | return false; 102 | } 103 | bodyLen = header.readInt32BE(4); 104 | } 105 | 106 | if (bodyLen === 0) { 107 | socket.write(header); 108 | } else { 109 | const body = socket.read(bodyLen); 110 | if (!body) { 111 | return false; 112 | } 113 | socket.write(Buffer.concat([ header, body ])); 114 | } 115 | bodyLen = null; 116 | return true; 117 | } 118 | 119 | socket.on('readable', () => { 120 | try { 121 | let remaining = false; 122 | do { 123 | remaining = readPacket(); 124 | } 125 | while (remaining); 126 | } catch (err) { 127 | console.error(err); 128 | } 129 | }); 130 | }); 131 | server.listen(8080); 132 | ``` 133 | 134 | [MIT](LICENSE) 135 | -------------------------------------------------------------------------------- /example/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const TCPBase = require('../lib/base'); 4 | 5 | /** 6 | * A Simple Protocol: 7 | * (4B): request id 8 | * (4B): body length 9 | * ------------------------------ 10 | * body data 11 | */ 12 | class Client extends TCPBase { 13 | getHeader() { 14 | return this.read(8); 15 | } 16 | 17 | getBodyLength(header) { 18 | return header.readInt32BE(4); 19 | } 20 | 21 | decode(body, header) { 22 | return { 23 | id: header.readInt32BE(0), 24 | data: body, 25 | }; 26 | } 27 | 28 | // heartbeat packet 29 | get heartBeatPacket() { 30 | return Buffer.from([ 255, 255, 255, 255, 0, 0, 0, 0 ]); 31 | } 32 | } 33 | 34 | const client = new Client({ 35 | host: '127.0.0.1', 36 | port: 8080, 37 | }); 38 | 39 | const body = Buffer.from('hello'); 40 | const data = Buffer.alloc(8 + body.length); 41 | data.writeInt32BE(1, 0); 42 | data.writeInt32BE(body.length, 4); 43 | body.copy(data, 8, 0); 44 | 45 | client.send({ 46 | id: 1, 47 | data, 48 | timeout: 5000, 49 | }, (err, res) => { 50 | if (err) { 51 | console.error(err); 52 | } 53 | console.log(res.toString()); // should echo 'hello' 54 | }); 55 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | 5 | const server = net.createServer(socket => { 6 | let header; 7 | let bodyLen; 8 | 9 | function readPacket() { 10 | if (bodyLen == null) { 11 | header = socket.read(8); 12 | if (!header) { 13 | return false; 14 | } 15 | bodyLen = header.readInt32BE(4); 16 | } 17 | 18 | if (bodyLen === 0) { 19 | socket.write(header); 20 | } else { 21 | const body = socket.read(bodyLen); 22 | if (!body) { 23 | return false; 24 | } 25 | socket.write(Buffer.concat([ header, body ])); 26 | } 27 | bodyLen = null; 28 | return true; 29 | } 30 | 31 | socket.on('readable', () => { 32 | try { 33 | let remaining = false; 34 | do { 35 | remaining = readPacket(); 36 | } 37 | while (remaining); 38 | } catch (err) { 39 | console.error(err); 40 | } 41 | }); 42 | }); 43 | server.listen(8080); 44 | -------------------------------------------------------------------------------- /lib/base.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const is = require('is-type-of'); 3 | const assert = require('assert'); 4 | const Base = require('sdk-base'); 5 | 6 | const addressKey = Symbol('address'); 7 | const defaultOptions = { 8 | noDelay: true, 9 | connectTimeout: 3000, 10 | responseTimeout: 3000, 11 | heartbeatInterval: 5000, 12 | needHeartbeat: true, 13 | concurrent: 0, 14 | logger: console, 15 | }; 16 | const noop = () => {}; 17 | let seed = 0; 18 | 19 | class TCPBase extends Base { 20 | /** 21 | * A base class for tcp client with basic functions 22 | * 23 | * @param {Object} options 24 | * - {String} host - server host 25 | * - {Number} port - server port 26 | * - {Number} headerLength - length of the packet header, this field is optional, 27 | * but if you not provider, you must override getHeader method 28 | * - {Boolean} [noDelay] - whether use the Nagle algorithm or not,defaults to true 29 | * - {Number} [concurrent] - the number of concurrent packet, defaults to zero, means no limit 30 | * - {Number} [responseTimeout] - limit the maximum time for waiting a response 31 | * - {Logger} [logger] - the logger client 32 | * @class 33 | */ 34 | constructor(options) { 35 | super(); 36 | 37 | this.options = Object.assign({}, defaultOptions, options); 38 | if (!this.options.path) { 39 | assert(this.options.host, 'options.host is required'); 40 | assert(this.options.port, 'options.port is required'); 41 | } 42 | 43 | if (this.options.needHeartbeat) { 44 | assert(this.heartBeatPacket, 'heartBeatPacket getter must be implemented if needHeartbeat'); 45 | } 46 | 47 | this.clientId = ++seed; 48 | this._heartbeatTimer = null; 49 | this._socket = null; 50 | this._header = null; 51 | this._bodyLength = null; 52 | this._lastError = null; 53 | this._queue = []; 54 | this._invokes = new Map(); 55 | this[addressKey] = this.options.host + ':' + this.options.port; 56 | this._lastHeartbeatTime = 0; 57 | this._lastReceiveDataTime = 0; 58 | 59 | this._connect(); 60 | this.ready(err => { 61 | if (!err && this.options.needHeartbeat) { 62 | this._startHeartbeat(); 63 | } 64 | }); 65 | } 66 | 67 | /** 68 | * get packet header 69 | * 70 | * @return {Buffer} header 71 | */ 72 | getHeader() { 73 | return this.read(this.options.headerLength); 74 | } 75 | 76 | /* eslint-disable valid-jsdoc, no-unused-vars */ 77 | 78 | /** 79 | * get body length from header 80 | * 81 | * @param {Buffer} header - header data 82 | * @return {Number} bodyLength 83 | */ 84 | getBodyLength(header) { 85 | throw new Error('not implement'); 86 | } 87 | 88 | /** 89 | * return a heartbeat packet 90 | * 91 | * @property {Buffer} TCPBase#heartBeatPacket 92 | */ 93 | get heartBeatPacket() { 94 | throw new Error('not implement'); 95 | } 96 | 97 | /** 98 | * send heartbeat packet 99 | * 100 | * @return {void} 101 | */ 102 | sendHeartBeat() { 103 | this._socket.write(this.heartBeatPacket); 104 | } 105 | 106 | /** 107 | * deserialze method, leave it to implement by subclass 108 | * 109 | * @param {Buffer} buf - binary data 110 | * @return {Object} packet object 111 | */ 112 | decode(buf) { 113 | throw new Error('not implement'); 114 | } 115 | 116 | /* eslint-enable valid-jsdoc, no-unused-vars */ 117 | 118 | /** 119 | * if the connection is writable, also including flow control logic 120 | * 121 | * @property {Boolean} TCPBase#_writable 122 | */ 123 | get _writable() { 124 | if (this.options.concurrent && this._invokes.size >= this.options.concurrent) { 125 | return false; 126 | } 127 | 128 | return this.isOK; 129 | } 130 | 131 | /** 132 | * if the connection is healthy or not 133 | * 134 | * @property {Boolean} TCPBase#isOK 135 | */ 136 | get isOK() { 137 | return this._socket && this._socket.writable; 138 | } 139 | 140 | /** 141 | * remote address 142 | * 143 | * @property {String} TCPBase#address 144 | */ 145 | get address() { 146 | return this[addressKey]; 147 | } 148 | 149 | /** 150 | * logger 151 | * 152 | * @property {Logger} TCPBase#logger 153 | */ 154 | get logger() { 155 | return this.options.logger; 156 | } 157 | 158 | /** 159 | * Pulls some data out of the socket buffer and returns it. 160 | * If no data available to be read, null is returned 161 | * 162 | * @param {Number} n - to specify how much data to read 163 | * @return {Buffer} - data 164 | */ 165 | read(n) { 166 | return this._socket.read(n); 167 | } 168 | 169 | /** 170 | * send packet to server 171 | * 172 | * @param {Object} packet 173 | * - {Number} id - packet id 174 | * - {String} [tips] - packet tips, show tips on error 175 | * - {Buffer} data - binary data 176 | * - {Boolean} [oneway] - oneway or not 177 | * - {Number} [timeout] - the maximum time for waiting a response 178 | * @param {Function} [callback] - Call this function,when processing is complete, optional. 179 | * @return {void} 180 | */ 181 | send(packet, callback = noop) { 182 | if (!this._socket) { 183 | const err = new Error(`[TCPBase] The socket was closed. (address: ${this[addressKey]})`); 184 | err.id = packet.id; 185 | err.tips = packet.tips; 186 | err.data = packet.data.toString('base64'); 187 | if (packet.oneway) { 188 | err.oneway = true; 189 | callback(); 190 | this.emit('error', err); 191 | } else { 192 | callback(err); 193 | } 194 | return; 195 | } 196 | if (packet.oneway) { 197 | this._socket.write(packet.data); 198 | callback(); 199 | return; 200 | } 201 | if (!this._writable) { 202 | this._queue.push([ packet, callback ]); 203 | return; 204 | } 205 | const meta = { 206 | id: packet.id, 207 | tips: packet.tips, 208 | dataLength: packet.data.length, 209 | bufferSize1: this._socket.bufferSize, 210 | bufferSize2: -1, 211 | startTime: Date.now(), 212 | endTime: -1, 213 | }; 214 | let endTime; 215 | meta.writeSuccess = this._socket.write(packet.data, () => { 216 | endTime = Date.now(); 217 | }); 218 | const timeout = packet.timeout || this.options.responseTimeout; 219 | this._invokes.set(packet.id, { 220 | meta, 221 | packet, 222 | timer: setTimeout(() => { 223 | if (!this._socket) { 224 | return; 225 | } 226 | 227 | meta.bufferSize2 = this._socket.bufferSize; 228 | meta.endTime = endTime; 229 | this._finishInvoke(packet.id); 230 | const err = new Error(`Server no response in ${timeout}ms, address#${this[addressKey]}`); 231 | err.socketMeta = meta; 232 | err.name = 'ResponseTimeoutError'; 233 | callback(err); 234 | }, timeout), 235 | callback, 236 | }); 237 | } 238 | 239 | /** 240 | * thunk style api of send(packet, callback) 241 | * 242 | * @param {Object} packet 243 | * - {Number} id - packet id 244 | * - {Buffer} data - binary data 245 | * - {Boolean} [oneway] - oneway or not 246 | * - {Number} [timeout] - the maximum time for waiting a response 247 | * @return {Function} thunk function 248 | */ 249 | sendThunk(packet) { 250 | return callback => this.send(packet, callback); 251 | } 252 | 253 | sendPromise(packet) { 254 | return new Promise((resolve, reject) => { 255 | this.send(packet, (err, data) => { 256 | if (err) return reject(err); 257 | resolve(data); 258 | }); 259 | }); 260 | } 261 | 262 | _finishInvoke(id) { 263 | this._invokes.delete(id); 264 | if (this._writable) { 265 | this._resume(); 266 | } 267 | } 268 | 269 | _errorCallback(callback, err) { 270 | if (!err) { 271 | err = new Error(`The socket was closed. (address: ${this[addressKey]})`); 272 | err.name = 'SocketCloseError'; 273 | } 274 | callback && callback(err); 275 | } 276 | 277 | // mark all invokes timeout 278 | _cleanInvokes(err) { 279 | for (const id of this._invokes.keys()) { 280 | const req = this._invokes.get(id); 281 | clearTimeout(req.timer); 282 | this._errorCallback(req.callback, err); 283 | } 284 | this._invokes.clear(); 285 | } 286 | 287 | // clean up the queue 288 | _cleanQueue(err) { 289 | let args = this._queue.pop(); 290 | while (args) { 291 | // args[0] 是packet, args[1]是callback 292 | this._errorCallback(args[1], err); 293 | args = this._queue.pop(); 294 | } 295 | } 296 | 297 | _resume() { 298 | const args = this._queue.shift(); 299 | if (args) { 300 | this.send(args[0], args[1]); 301 | } 302 | } 303 | 304 | // read data from socket,and decode it to packet object 305 | _readPacket() { 306 | if (is.nullOrUndefined(this._bodyLength)) { 307 | this._header = this.getHeader(); 308 | if (!this._header) { 309 | return false; 310 | } 311 | this._bodyLength = this.getBodyLength(this._header); 312 | } 313 | 314 | let body; 315 | if (this._bodyLength > 0) { 316 | body = this.read(this._bodyLength); 317 | if (!body) { 318 | return false; 319 | } 320 | } 321 | this._bodyLength = null; 322 | const entity = this.decode(body, this._header); 323 | // the schema of entity 324 | // { 325 | // id: 'request id', 326 | // isResponse: true, 327 | // data: {} // deserialized object 328 | // } 329 | let type = 'request'; 330 | if (!entity.hasOwnProperty('isResponse')) { 331 | entity.isResponse = this._invokes.has(entity.id); 332 | } 333 | if (entity.isResponse) { 334 | type = 'response'; 335 | const invoke = this._invokes.get(entity.id); 336 | if (invoke) { 337 | this._finishInvoke(entity.id); 338 | clearTimeout(invoke.timer); 339 | process.nextTick(() => { 340 | invoke.callback(entity.error, entity.data); 341 | }); 342 | } 343 | } 344 | if (entity.data) { 345 | process.nextTick(() => { 346 | this.emit(type, entity, this[addressKey]); 347 | }); 348 | } 349 | return true; 350 | } 351 | 352 | /** 353 | * close the socket 354 | * 355 | * @param {Error} err - the error which makes socket closed 356 | * @return {void} 357 | */ 358 | close(err) { 359 | if (!this._socket) { 360 | return Promise.resolve(); 361 | } 362 | this._socket.destroy(err); 363 | return this.await('close'); 364 | } 365 | 366 | _handleClose() { 367 | if (!this._socket) { 368 | return; 369 | } 370 | this._socket.removeAllListeners(); 371 | this._socket = null; 372 | 373 | this._cleanInvokes(this._lastError); 374 | // clean timer 375 | if (this._heartbeatTimer) { 376 | clearInterval(this._heartbeatTimer); 377 | this._heartbeatTimer = null; 378 | } 379 | this._cleanQueue(this._lastError); 380 | this.emit('close'); 381 | } 382 | 383 | _handleReadable() { 384 | this._lastReceiveDataTime = Date.now(); 385 | try { 386 | let remaining = false; 387 | do { 388 | remaining = this._readPacket(); 389 | } while (remaining); 390 | } catch (err) { 391 | this.close(err); 392 | } 393 | } 394 | 395 | _connect(done) { 396 | if (!done) { 397 | done = err => { 398 | this.ready(err ? err : true); 399 | }; 400 | } 401 | const { port, host, localAddress, localPort, family, hints, lookup, path } = this.options; 402 | const socket = this._socket = net.connect({ 403 | port, host, localAddress, localPort, family, hints, lookup, path, 404 | }); 405 | socket.setNoDelay(this.options.noDelay); 406 | socket.on('readable', () => { this._handleReadable(); }); 407 | socket.once('close', () => { this._handleClose(); }); 408 | socket.on('error', err => { 409 | err.message += ' (address: ' + this[addressKey] + ')'; 410 | this._lastError = err; 411 | if (err.code === 'ECONNRESET') { 412 | this.logger.warn('[TCPBase] socket is closed by other side while there were still unhandled data in the socket buffer'); 413 | } else { 414 | this.emit('error', err); 415 | } 416 | }); 417 | socket.setTimeout(this.options.connectTimeout, () => { 418 | const err = new Error(`[TCPBase] socket connect timeout (${this.options.connectTimeout}ms)`); 419 | err.name = 'TcpConnectionTimeoutError'; 420 | err.host = this.options.host; 421 | err.port = this.options.port; 422 | this.close(err); 423 | }); 424 | 425 | socket.once('connect', () => { 426 | // set timeout back to zero after connected 427 | socket.setTimeout(0); 428 | this.emit('connect'); 429 | }); 430 | 431 | Promise.race([ 432 | this.await('connect'), 433 | this.await('error'), 434 | ]).then(done, done); 435 | } 436 | 437 | _startHeartbeat() { 438 | this._heartbeatTimer = setInterval(() => { 439 | const duration = this._lastHeartbeatTime - this._lastReceiveDataTime; 440 | if (this._lastReceiveDataTime && duration > this.options.heartbeatInterval) { 441 | const err = new Error(`server ${this[addressKey]} no response in ${duration}ms, maybe the socket is end on the other side.`); 442 | err.name = 'ServerNoResponseError'; 443 | this.close(err); 444 | return; 445 | } 446 | // flow control 447 | if (this._invokes.size > 0 || !this.isOK) { 448 | return; 449 | } 450 | this._lastHeartbeatTime = Date.now(); 451 | this.sendHeartBeat(); 452 | }, this.options.heartbeatInterval); 453 | } 454 | } 455 | 456 | module.exports = TCPBase; 457 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcp-base", 3 | "version": "3.2.0", 4 | "description": "A base class for tcp client with basic functions", 5 | "main": "lib/base.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "lint": "eslint --ext .js .", 11 | "test": "npm run lint && npm run test-local", 12 | "test-local": "egg-bin test", 13 | "cov": "egg-bin cov", 14 | "ci": "npm run lint && npm run cov" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/node-modules/tcp-base.git" 19 | }, 20 | "keywords": [ 21 | "tcp" 22 | ], 23 | "author": "gxcsoccer ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/node-modules/tcp-base/issues" 27 | }, 28 | "homepage": "https://github.com/node-modules/tcp-base#readme", 29 | "engines": { 30 | "node": ">= 6.0.0" 31 | }, 32 | "devDependencies": { 33 | "egg-bin": "^5.2.0", 34 | "eslint": "^8.23.0", 35 | "eslint-config-egg": "^12.0.0", 36 | "mm": "^2.1.0", 37 | "mz-modules": "^1.0.0", 38 | "pedding": "^1.1.0" 39 | }, 40 | "dependencies": { 41 | "is-type-of": "^1.0.0", 42 | "sdk-base": "^3.1.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/close_wait.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const pedding = require('pedding'); 3 | const TCPBase = require('../'); 4 | const server = require('./support/server_end'); 5 | 6 | describe('test/close_wait.test.js', () => { 7 | class Client extends TCPBase { 8 | constructor(options) { 9 | Object.assign(options, { 10 | headerLength: 8, 11 | heartbeatInterval: 3000, 12 | }); 13 | 14 | super(options); 15 | } 16 | 17 | getBodyLength(header) { 18 | return header.readInt32BE(0); 19 | } 20 | 21 | decode(buf, header) { 22 | let data; 23 | if (buf) { 24 | data = JSON.parse(buf); 25 | } 26 | return { 27 | id: header.readInt32BE(4), 28 | data, 29 | }; 30 | } 31 | 32 | get heartBeatPacket() { 33 | return makeRequest(1).data; 34 | } 35 | } 36 | 37 | function makeRequest(id, content, oneway) { 38 | const header = Buffer.alloc(8); 39 | header.fill(0); 40 | const body = Buffer.from(JSON.stringify(content || { 41 | id, 42 | message: 'hello', 43 | })); 44 | header.writeInt32BE(body.length, 0); 45 | header.writeInt32BE(id, 4); 46 | return { 47 | id, 48 | oneway, 49 | data: Buffer.concat([ header, body ]), 50 | }; 51 | } 52 | 53 | describe('end by server', () => { 54 | before(done => server.start(9600, true, done)); 55 | after(() => server.close()); 56 | 57 | it('should close if end by server', done => { 58 | const client = new Client({ 59 | host: '127.0.0.1', 60 | port: 9600, 61 | }); 62 | client.on('close', done); 63 | }); 64 | }); 65 | 66 | describe('long time no response', () => { 67 | before(done => server.start(9600, false, done)); 68 | after(() => server.close()); 69 | 70 | it('should close if long time no response', done => { 71 | done = pedding(done, 2); 72 | const client = new Client({ 73 | host: '127.0.0.1', 74 | port: 9600, 75 | }); 76 | client.on('close', done); 77 | client.on('error', err => { 78 | assert(err); 79 | console.log(err); 80 | assert(err.name === 'ServerNoResponseError'); 81 | assert(/server 127.0.0.1:9600 no response in \d+ms, maybe the socket is end on the other side/i.test(err.message)); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/error.test.js: -------------------------------------------------------------------------------- 1 | const TCPBase = require('../'); 2 | const server = require('./support/server_immidiate_end'); 3 | 4 | describe('test/error.test.js', () => { 5 | class Client extends TCPBase { 6 | constructor(options) { 7 | Object.assign(options, { 8 | headerLength: 8, 9 | heartbeatInterval: 3000, 10 | }); 11 | 12 | super(options); 13 | } 14 | 15 | getBodyLength(header) { 16 | return header.readInt32BE(0); 17 | } 18 | 19 | decode(buf, header) { 20 | let data; 21 | if (buf) { 22 | data = JSON.parse(buf); 23 | } 24 | return { 25 | id: header.readInt32BE(4), 26 | data, 27 | }; 28 | } 29 | 30 | get heartBeatPacket() { 31 | return makeRequest(1).data; 32 | } 33 | } 34 | 35 | function makeRequest(id, content, oneway) { 36 | const header = Buffer.alloc(8); 37 | header.fill(0); 38 | const body = Buffer.from(JSON.stringify(content || { 39 | id, 40 | message: 'hello', 41 | })); 42 | header.writeInt32BE(body.length, 0); 43 | header.writeInt32BE(id, 4); 44 | return { 45 | id, 46 | oneway, 47 | data: Buffer.concat([ header, body ]), 48 | }; 49 | } 50 | 51 | describe('should not throw uncaughtExeception', () => { 52 | it('when socket has been destroyed', done => { 53 | server.start(9090, () => { 54 | const client = new Client({ 55 | host: '127.0.0.1', 56 | port: 9090, 57 | }); 58 | 59 | client.once('connect', () => { 60 | client.send({ id: 'foo', data: 'bar1' }); 61 | server.close(); 62 | client.send({ id: 'foo', data: 'bar2' }); 63 | }); 64 | client.on('error', err => err); 65 | client.on('close', () => console.log('close')); 66 | setTimeout(done, 5000); 67 | }); 68 | }); 69 | 70 | it('when receive error multiple times', done => { 71 | server.start(9090, () => { 72 | const client = new Client({ 73 | host: '127.0.0.1', 74 | port: 9090, 75 | }); 76 | 77 | client.once('connect', () => { 78 | server.close(); 79 | const error = new Error('ECONNRESET'); 80 | error.code = 'ECONNRESET'; 81 | client._socket && client._socket.emit('error', error); 82 | client._socket && client._socket.emit('error', error); 83 | }); 84 | client.on('error', err => err); 85 | client.on('close', () => console.log('close')); 86 | setTimeout(done, 5000); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const mm = require('mm'); 2 | const os = require('os'); 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | const pedding = require('pedding'); 6 | const { sleep } = require('mz-modules'); 7 | const server = require('./support/server'); 8 | const TCPBase = require('../'); 9 | 10 | describe('test/index.test.js', () => { 11 | class Client extends TCPBase { 12 | constructor(options) { 13 | Object.assign(options, { 14 | headerLength: 8, 15 | heartbeatInterval: 3000, 16 | }); 17 | super(options); 18 | } 19 | 20 | getBodyLength(header) { 21 | return header.readInt32BE(0); 22 | } 23 | 24 | decode(buf, header) { 25 | let data; 26 | if (buf) { 27 | data = JSON.parse(buf); 28 | } 29 | return { 30 | id: header.readInt32BE(4), 31 | data, 32 | }; 33 | } 34 | 35 | get heartBeatPacket() { 36 | return makeRequest(1).data; 37 | } 38 | } 39 | 40 | class Client2 extends Client { 41 | constructor(options) { 42 | Object.assign(options, { 43 | headerLength: 0, 44 | needHeartbeat: false, 45 | }); 46 | 47 | super(options); 48 | } 49 | 50 | getHeader() { 51 | return this.read(8); 52 | } 53 | } 54 | 55 | class Client3 extends Client2 { 56 | decode() { 57 | throw new Error('mock error'); 58 | } 59 | } 60 | 61 | function makeRequest(id, content, oneway, tips) { 62 | const header = Buffer.alloc(8); 63 | header.fill(0); 64 | const body = Buffer.from(JSON.stringify(content || { 65 | id, 66 | message: 'hello', 67 | })); 68 | header.writeInt32BE(body.length, 0); 69 | header.writeInt32BE(id, 4); 70 | return { 71 | id, 72 | tips, 73 | oneway, 74 | data: Buffer.concat([ header, body ]), 75 | }; 76 | } 77 | 78 | let client; 79 | let client2; 80 | const sockPath = path.join(os.tmpdir(), `tcp-base-test-${Date.now()}.sock`); 81 | const port = 12211; 82 | before(done => { 83 | server.start(port, err => { 84 | if (err) { 85 | return done(err); 86 | } 87 | client = new Client({ 88 | host: '127.0.0.1', 89 | port, 90 | }); 91 | 92 | client.ready(() => { 93 | if (os.platform() === 'win32') { 94 | done(); 95 | } else { 96 | server.start(sockPath, err => { 97 | if (err) { 98 | return done(err); 99 | } 100 | client2 = new Client({ 101 | path: sockPath, 102 | }); 103 | 104 | client2.ready(done); 105 | }); 106 | } 107 | }); 108 | }); 109 | }); 110 | 111 | afterEach(mm.restore); 112 | 113 | after(async () => { 114 | await client.close(); 115 | await client.close(); 116 | if (client2) { 117 | client2.close(); 118 | } 119 | server.close(); 120 | }); 121 | 122 | it('client should be ok', () => { 123 | assert(client.isOK); 124 | }); 125 | 126 | it('should get address ok', () => { 127 | assert(client.address === `127.0.0.1:${port}`); 128 | }); 129 | 130 | it('should send request ok', done => { 131 | client.send(makeRequest(1), (err, res) => { 132 | assert.ifError(err); 133 | assert.deepEqual(res, { 134 | id: 1, 135 | message: 'hello', 136 | }); 137 | done(); 138 | }); 139 | }); 140 | 141 | if (client2) { 142 | it('client2 should be ok', () => { 143 | assert(client2.isOK); 144 | }); 145 | it('client2 should send request ok', done => { 146 | client2.send(makeRequest(1001), (err, res) => { 147 | assert.ifError(err); 148 | assert.deepEqual(res, { 149 | id: 1001, 150 | message: 'hello', 151 | }); 152 | done(); 153 | }); 154 | }); 155 | } 156 | 157 | it('should send promise ok', async () => { 158 | const res = await client.sendPromise(makeRequest(2)); 159 | assert.deepEqual(res, { 160 | id: 2, 161 | message: 'hello', 162 | }); 163 | }); 164 | 165 | it('should send oneway ok', async () => { 166 | await client.sendPromise(makeRequest(3, { 167 | id: 3, 168 | noResponse: true, 169 | }, true)); 170 | assert(client._invokes.size === 0); 171 | }); 172 | 173 | it('should emit error if connect timeout', done => { 174 | const client = new Client({ 175 | host: '127.0.0.1', 176 | port: 12000, 177 | }); 178 | client.on('error', err => { 179 | assert(err); 180 | assert(err.message.includes('connect ECONNREFUSED 127.0.0.1:12000')); 181 | done(); 182 | }); 183 | }); 184 | 185 | it('should emit close if socket is destroyed', done => { 186 | let client = new Client({ 187 | host: '127.0.0.1', 188 | port, 189 | }); 190 | 191 | client.on('close', () => { 192 | client = new Client({ 193 | host: '127.0.0.1', 194 | port, 195 | }); 196 | }); 197 | 198 | client._socket.destroy(); 199 | 200 | setTimeout(() => { 201 | client.send(makeRequest(1), (err, res) => { 202 | assert.ifError(err); 203 | assert.deepEqual(res, { 204 | id: 1, 205 | message: 'hello', 206 | }); 207 | done(); 208 | }); 209 | }, 1000); 210 | }); 211 | 212 | // it('should emit close in the same tick', async () => { 213 | // let client = new Client({ 214 | // host: '127.0.0.1', 215 | // port, 216 | // }); 217 | 218 | // client.on('close', () => { 219 | // client = new Client({ 220 | // host: '127.0.0.1', 221 | // port, 222 | // }); 223 | // }); 224 | 225 | // client.close(); 226 | 227 | // const res = await client.sendPromise(makeRequest(2)); 228 | // assert.deepEqual(res, { 229 | // id: 2, 230 | // message: 'hello', 231 | // }); 232 | // }); 233 | 234 | it('should emit error if socket has been closed', done => { 235 | const client = new Client3({ 236 | host: '127.0.0.1', 237 | port, 238 | }); 239 | 240 | client.close(); 241 | 242 | client.send(makeRequest(1), err => { 243 | assert(err); 244 | assert(err.name === 'SocketCloseError'); 245 | done(); 246 | }); 247 | }); 248 | 249 | it('should emit error if parse header error', done => { 250 | done = pedding(2, done); 251 | 252 | const client = new Client3({ 253 | host: '127.0.0.1', 254 | port, 255 | }); 256 | 257 | client.on('error', err => { 258 | assert(err); 259 | assert(err.message.includes('mock error')); 260 | done(); 261 | }); 262 | 263 | client.send(makeRequest(1), err => { 264 | assert(err); 265 | assert(err.message.includes('mock error')); 266 | done(); 267 | }); 268 | }); 269 | 270 | it('should emit error if parse body error', done => { 271 | done = pedding(2, done); 272 | 273 | const client = new Client3({ 274 | host: '127.0.0.1', 275 | port, 276 | }); 277 | 278 | client.on('error', err => { 279 | assert(err); 280 | assert(err.message.includes('mock error')); 281 | done(); 282 | }); 283 | 284 | client.send(makeRequest(1), err => { 285 | assert(err); 286 | assert(err.message.includes('mock error')); 287 | done(); 288 | }); 289 | }); 290 | 291 | it('should process request timeout well', done => { 292 | client.send(makeRequest(4, { 293 | id: 4, 294 | noResponse: true, 295 | }, false), err => { 296 | assert(err); 297 | [ 'id', 'dataLength', 'bufferSize1', 'bufferSize2', 'startTime', 'endTime', 'writeSuccess' ] 298 | .forEach(p => { 299 | assert(err.socketMeta.hasOwnProperty(p)); 300 | }); 301 | assert(err.socketMeta.writeSuccess); 302 | assert(err.message === `Server no response in 3000ms, address#127.0.0.1:${port}`); 303 | done(); 304 | }); 305 | }); 306 | 307 | it('should wait for drain if buffer is full', async () => { 308 | const queue = []; 309 | const content = require('../package.json'); 310 | for (let i = 5; i < 10000; i++) { 311 | const req = makeRequest(i, { 312 | id: i, 313 | content, 314 | }); 315 | req.timeout = 10000; 316 | queue.push(client.sendPromise(req)); 317 | } 318 | await Promise.all(queue); 319 | }); 320 | 321 | it('should clean all invoke if client is close', done => { 322 | const cli = new Client({ 323 | host: '127.0.0.1', 324 | port, 325 | }); 326 | cli.ready() 327 | .then(() => { 328 | cli.send(makeRequest(2, { 329 | id: 2, 330 | timeout: 2000, 331 | }), err => { 332 | assert(err); 333 | assert(err.name === 'SocketCloseError'); 334 | }); 335 | cli._queue.push([ makeRequest(1, { 336 | id: 1, 337 | timeout: 2000, 338 | }), err => { 339 | assert(err); 340 | assert(err.name === 'SocketCloseError'); 341 | } ]); 342 | cli.close(); 343 | }) 344 | .catch(err => done(err)); 345 | cli.on('close', () => { 346 | assert(cli._invokes.size === 0); 347 | assert(cli._queue.length === 0); 348 | done(); 349 | }); 350 | }); 351 | 352 | it('should override getHeader ok', async () => { 353 | const client = new Client2({ 354 | host: '127.0.0.1', 355 | port, 356 | }); 357 | await client.ready(); 358 | const res = await client.sendPromise(makeRequest(1)); 359 | assert.deepEqual(res, { 360 | id: 1, 361 | message: 'hello', 362 | }); 363 | }); 364 | 365 | it('should send heartbeat request', async () => { 366 | const client = new Client({ 367 | host: '127.0.0.1', 368 | port, 369 | }); 370 | await client.ready(); 371 | await sleep(100); 372 | assert(client._heartbeatTimer); 373 | mm(client._socket, 'write', buf => { 374 | assert.deepEqual(buf, client.heartBeatPacket); 375 | client.emit('heartbeat'); 376 | }); 377 | await client.await('heartbeat'); 378 | await client.close(); 379 | }); 380 | 381 | it('should override sendHeartBeat', async () => { 382 | const client = new Client({ 383 | host: '127.0.0.1', 384 | port, 385 | }); 386 | 387 | mm(client, 'sendHeartBeat', () => { 388 | client.send(makeRequest(10), (err, res) => { 389 | client.emit('heartbeat', res); 390 | }); 391 | }); 392 | 393 | await client.ready(); 394 | await sleep(100); 395 | 396 | assert(client._heartbeatTimer); 397 | const heartbeat = await client.await('heartbeat'); 398 | assert(heartbeat.id === 10); 399 | await client.close(); 400 | }); 401 | 402 | it('should support concurrent', async () => { 403 | const concurrent = 3; 404 | 405 | class Client5 extends Client2 { 406 | constructor(options) { 407 | Object.assign(options, { 408 | concurrent, 409 | }); 410 | super(options); 411 | } 412 | 413 | send(packet, callback) { 414 | assert(this._invokes.size <= concurrent); 415 | return super.send(packet, callback); 416 | } 417 | } 418 | 419 | const client = new Client5({ 420 | host: '127.0.0.1', 421 | port, 422 | }); 423 | const queue = []; 424 | for (let i = 0; i < 20000; i++) { 425 | const req = makeRequest(i); 426 | req.timeout = 10000; 427 | queue.push(client.sendPromise(req)); 428 | } 429 | await Promise.all(queue); 430 | }); 431 | 432 | it('should send before ready', done => { 433 | const client = new Client({ 434 | host: '127.0.0.1', 435 | port, 436 | }); 437 | client.send(makeRequest(1), (err, res) => { 438 | assert.ifError(err); 439 | assert.deepEqual(res, { 440 | id: 1, 441 | message: 'hello', 442 | }); 443 | done(); 444 | }); 445 | }); 446 | 447 | it('should process empty body ok', done => { 448 | const header = Buffer.alloc(8); 449 | header.writeInt32BE(0, 0); 450 | header.writeInt32BE(1000, 4); 451 | const request = { 452 | id: 1000, 453 | oneway: false, 454 | data: header, 455 | }; 456 | client.send(request, (err, res) => { 457 | assert.ifError(err); 458 | assert(!res); 459 | done(); 460 | }); 461 | }); 462 | 463 | it('should not emit PacketParsedError if message is handled with error in event listener', done => { 464 | const listeners = process.listeners('uncaughtException'); 465 | process.removeAllListeners('uncaughtException'); 466 | 467 | const handle = err => { 468 | assert(err.message === 'responseError'); 469 | process.removeAllListeners('uncaughtException'); 470 | listeners.forEach(listener => { 471 | process.on('uncaughtException', listener); 472 | }); 473 | done(); 474 | }; 475 | process.on('uncaughtException', handle); 476 | 477 | let first = false; 478 | client.on('response', () => { 479 | if (!first) { 480 | first = true; 481 | throw new Error('responseError'); 482 | } 483 | }); 484 | 485 | client.send(makeRequest(1), err => { 486 | assert.ifError(err); 487 | }); 488 | }); 489 | 490 | it('send response packet', done => { 491 | mm(client.options, 'concurrent', 1); 492 | // 1. send 正常请求, 493 | // 2. 连续 send oneway 请求. 494 | // 3. 验证 queue 的数量和队列的内容是否符合预期. 495 | done = pedding(4, done); 496 | const c0 = { 497 | id: 10, 498 | message: 'hello', 499 | timeout: 100, 500 | }; 501 | const c1 = { 502 | id: 11, 503 | message: 'hello', 504 | timeout: 500, 505 | }; 506 | const c2 = { 507 | id: 12, 508 | message: 'hello', 509 | timeout: 1000, 510 | }; 511 | client.send(makeRequest(10, c0), (err, res) => { 512 | assert.ifError(err); 513 | assert.deepEqual(res, c0); 514 | done(); 515 | }); 516 | client.send(makeRequest(11, c1), (err, res) => { 517 | assert.ifError(err); 518 | assert.deepEqual(res, c1); 519 | done(); 520 | }); 521 | setTimeout(() => { 522 | // c1 目前已经在 queue 里面, 然后连续添加2个 oneway. 523 | client.send(makeRequest(15, { 524 | id: 5, 525 | oneway: true, 526 | }, true)); 527 | client.send(makeRequest(16, { 528 | id: 6, 529 | oneway: true, 530 | }, true)); 531 | 532 | client.send(makeRequest(17), (err, res) => { 533 | assert.ifError(err); 534 | assert.deepEqual(res, { id: 17, message: 'hello' }); 535 | done(); 536 | }); 537 | 538 | const queue = client._queue; 539 | assert(queue.length === 2); 540 | assert(queue[0][0].id === 12); 541 | assert(queue[1][0].id === 17); 542 | }, 600); 543 | 544 | client.send(makeRequest(12, c2), (err, res) => { 545 | assert.ifError(err); 546 | assert.deepEqual(res, c2); 547 | done(); 548 | }); 549 | }); 550 | 551 | it('should not emit PacketParsedError if error occurred in callback function', done => { 552 | const listeners = process.listeners('uncaughtException'); 553 | process.removeAllListeners('uncaughtException'); 554 | 555 | const handle = err => { 556 | assert(err.message === 'callbackError'); 557 | process.removeAllListeners('uncaughtException'); 558 | listeners.forEach(listener => { 559 | process.on('uncaughtException', listener); 560 | }); 561 | done(); 562 | }; 563 | process.on('uncaughtException', handle); 564 | 565 | client.on('error', () => { 566 | assert(false, 'should not run'); 567 | }); 568 | 569 | client.send(makeRequest(1), err => { 570 | assert.ifError(err); 571 | throw new Error('callbackError'); 572 | }); 573 | }); 574 | 575 | it('should not emit Error if socket ECONNRESET', async () => { 576 | const client = new Client({ 577 | host: '127.0.0.1', 578 | port, 579 | }); 580 | await client.ready(); 581 | const err = new Error('123'); 582 | err.code = 'ECONNRESET'; 583 | client._socket.on('error', () => { 584 | client._socket.destroy(); 585 | }); 586 | await Promise.race([ 587 | client.await('error'), 588 | client.await('close'), 589 | client._socket.emit('error', err), 590 | ]); 591 | }); 592 | 593 | it('should handle connect timeout ok', async () => { 594 | const client = new Client({ 595 | host: '1.1.1.1', 596 | port: 12200, 597 | connectTimeout: 300, 598 | }); 599 | try { 600 | await client.ready(); 601 | assert(false, 'should not run here'); 602 | } catch (err) { 603 | assert(err.name === 'TcpConnectionTimeoutError'); 604 | assert(err.message.includes('[TCPBase] socket connect timeout (300ms)')); 605 | } 606 | }); 607 | 608 | it('should get error if socket is closed', async () => { 609 | const client = new Client({ 610 | host: '127.0.0.1', 611 | port, 612 | connectTimeout: 300, 613 | }); 614 | await client.ready(); 615 | await client.close(); 616 | try { 617 | await client.sendPromise(makeRequest(1, {}, false, 'useful tips here')); 618 | assert(false, 'should not run here'); 619 | } catch (err) { 620 | assert.equal(err.tips, 'useful tips here'); 621 | assert(err.message === `[TCPBase] The socket was closed. (address: 127.0.0.1:${port})`); 622 | } 623 | try { 624 | await Promise.race([ 625 | client.await('error'), 626 | client.sendPromise(makeRequest(2, 'haha', true)), 627 | ]); 628 | assert(false, 'should not run here'); 629 | } catch (err) { 630 | assert(err && err.message === `[TCPBase] The socket was closed. (address: 127.0.0.1:${port})`); 631 | assert(err.oneway === true); 632 | } 633 | }); 634 | }); 635 | -------------------------------------------------------------------------------- /test/support/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | let server; 5 | let header; 6 | let bodyLen = null; 7 | 8 | module.exports = { 9 | start(port, callback) { 10 | server = net.createServer(c => { // 'connection' listener 11 | function handle() { 12 | if (!bodyLen) { 13 | header = c.read(8); 14 | if (!header) { 15 | return; 16 | } 17 | bodyLen = header.readInt32BE(0); 18 | } 19 | 20 | if (bodyLen === 0) { 21 | c.write(header); 22 | return; 23 | } 24 | 25 | const body = c.read(bodyLen); 26 | if (!body) { 27 | return; 28 | } 29 | 30 | const obj = JSON.parse(body); 31 | 32 | if (obj.oneway) { 33 | header = null; 34 | bodyLen = null; 35 | handle(); 36 | return; 37 | } 38 | 39 | if (obj.timeout) { 40 | const buf = Buffer.concat([ header, body ]); 41 | setTimeout(() => c.write(buf), obj.timeout); 42 | } else if (!obj.noResponse) { 43 | c.write(Buffer.concat([ header, body ])); 44 | } 45 | bodyLen = null; 46 | header = null; 47 | handle(); 48 | } 49 | 50 | c.on('readable', () => handle()); 51 | c.on('error', () => {}); 52 | }); 53 | server.listen(port, callback); 54 | }, 55 | close() { 56 | server.close(); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /test/support/server_end.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | 3 | let server; 4 | exports.start = function(port, end, cb) { 5 | server = net.createServer({ 6 | allowHalfOpen: true, 7 | }, socket => { 8 | socket.write(Buffer.from('0000001a000000007b226964223a302c226d657373616765223a2268656c6c6f227d', 'hex')); 9 | console.log('socket connect', socket.remoteAddress); 10 | socket.on('data', data => { 11 | console.log('receive', data.toString()); 12 | }); 13 | 14 | if (end) { 15 | setTimeout(() => { 16 | console.log('end the socket'); 17 | socket.end(); 18 | }, 3000); 19 | } 20 | }); 21 | server.listen(port, cb); 22 | }; 23 | 24 | exports.close = function() { 25 | server && server.close(); 26 | }; 27 | -------------------------------------------------------------------------------- /test/support/server_immidiate_end.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | 5 | let server; 6 | const connections = []; 7 | 8 | exports.start = function(port, cb) { 9 | server = net.createServer({ 10 | allowHalfOpen: true, 11 | }, socket => { 12 | socket.write(Buffer.from('0000001a000000007b226964223a302c226d657373616765223a2268656c6c6f227d', 'hex')); 13 | console.log('socket connect', socket.remoteAddress); 14 | socket.on('data', data => { 15 | console.log('receive', data.toString()); 16 | }); 17 | 18 | connections.push(socket); 19 | }); 20 | server.listen(port, cb); 21 | }; 22 | 23 | exports.close = function() { 24 | connections.forEach(conn => conn.destroy()); 25 | server && server.close(); 26 | }; 27 | --------------------------------------------------------------------------------