From c7104902e3f5a0cdf3577b491b9e53f41dc52180 Mon Sep 17 00:00:00 2001 From: leeching Date: Tue, 26 Jun 2018 19:58:01 +0800 Subject: [PATCH] refactor(nodejs): write nodejs script in javascript way --- README.md | 6 +- binding/nodejs/.prettierrc | 4 + .../__snapshots__/ip2region.spec.js.snap | 8 +- binding/nodejs/ip2region.js | 509 +++++++++--------- binding/nodejs/ip2region.spec.js | 31 +- 5 files changed, 286 insertions(+), 272 deletions(-) create mode 100644 binding/nodejs/.prettierrc diff --git a/README.md b/README.md index 66fdc63..339e275 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ example result: node: ``` -> let ip2region = require('./ip2region.js'); -> let dbService = ip2region.create('./ip2region.db'); -> dbService.binarySearchSync(' 101.105.35.57') +> const IP2Region = require('./ip2region.js'); +> const dbService = new IP2Region({dbPath: './ip2region.db'}); +> dbService.binarySearchSync('101.105.35.57') { city: 0, region: '中国|0|广东|深圳|鹏博士' } ``` diff --git a/binding/nodejs/.prettierrc b/binding/nodejs/.prettierrc new file mode 100644 index 0000000..df60dd8 --- /dev/null +++ b/binding/nodejs/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "tabWidth": 4 +} \ No newline at end of file diff --git a/binding/nodejs/__snapshots__/ip2region.spec.js.snap b/binding/nodejs/__snapshots__/ip2region.spec.js.snap index 790d5f4..b738d52 100644 --- a/binding/nodejs/__snapshots__/ip2region.spec.js.snap +++ b/binding/nodejs/__snapshots__/ip2region.spec.js.snap @@ -1,27 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`binarySearch 1`] = ` +exports[`ip2region binarySearch 1`] = ` Object { "city": 2163, "region": "中国|0|广东省|深圳市|阿里云", } `; -exports[`binarySearch 2`] = ` +exports[`ip2region binarySearch 2`] = ` Object { "city": 0, "region": "0|0|0|内网IP|内网IP", } `; -exports[`should query 1`] = ` +exports[`ip2region should query 1`] = ` Object { "city": 2163, "region": "中国|0|广东省|深圳市|阿里云", } `; -exports[`should query 2`] = ` +exports[`ip2region should query 2`] = ` Object { "city": 0, "region": "0|0|0|内网IP|内网IP", diff --git a/binding/nodejs/ip2region.js b/binding/nodejs/ip2region.js index abedcdd..686fcff 100644 --- a/binding/nodejs/ip2region.js +++ b/binding/nodejs/ip2region.js @@ -4,274 +4,275 @@ * project: https://github.com/lionsoul2014/ip2region * * @author dongyado - * */ -var fs = require('fs'); - -var ipbase = [16777216, 65536, 256, 1]; // for ip2long -var ip2region = {}; -ip2region.db_file_path = null; -ip2region.db_fd = null; - -var totalBlocks = 0; -var firstIndexPtr = 0; -var lastIndexPtr = 0; -var superBlock = new Buffer(8); - -var indexBlockLength = 12; -var totalHeaderLength = 8192; + */ +const fs = require('fs'); +const IP_BASE = [16777216, 65536, 256, 1]; +const INDEX_BLOCK_LENGTH = 12; +const TOTAL_HEADER_LENGTH = 8192; /** - * binary search synchronized - * */ -ip2region.binarySearchSync = function(ip) -{ - var low = 0; - var mid = 0; - var high = totalBlocks; - var dataPos = 0; - var pos = 0; - var sip = 0; - var eip = 0; - var indexBuffer = new Buffer(12); + * Convert ip to long (xxx.xxx.xxx.xxx to a integer) + * + * @param {string} ip + * @return {number} long value + */ +function ip2long(ip) { + const arr = ip.split('.'); + if (arr.length !== 4) { + throw new Error('invalid ip'); + } + return arr.reduce((val, n, i) => { + n = Number(n); + if (!Number.isInteger(n) || n < 0 || n > 255) { + throw new Error('invalid ip'); + } + return val + IP_BASE[i] * n; + }, 0); +} - if( typeof(ip) == 'string' ) ip = ip2long(ip); +/** + * Get long value from buffer with specified offset + * + * @param {Buffer} buffer + * @param {number} offset + * @return {number} long value + */ +function getLong(buffer, offset) { + const val = + (buffer[offset] & 0x000000ff) | + ((buffer[offset + 1] << 8) & 0x0000ff00) | + ((buffer[offset + 2] << 16) & 0x00ff0000) | + ((buffer[offset + 3] << 24) & 0xff000000); + return val < 0 ? val >>> 0 : val; +} - // binary search - while( low <= high ) { - mid = ((low + high) >> 1); - pos = firstIndexPtr + mid * indexBlockLength; - fs.readSync(this.db_fd, indexBuffer, 0, indexBlockLength, pos); - sip = getLong(indexBuffer, 0); +/** + * @typedef {Object} SearchResult + * @property {number} city + * @property {string} region + */ - //console.log( ' sip : ' + sip + ' eip : ' + eip ); - if ( ip < sip) { - high = mid - 1; - } else { - eip = getLong(indexBuffer, 4); - if ( ip > eip ) { +class IP2Region { + constructor(options = {}) { + const { dbPath } = options; + if (!dbPath || !fs.existsSync(dbPath)) { + throw new Error(`[ip2region] db file not exists : ${dbPath}`); + } + + try { + this.dbFd = fs.openSync(dbPath, 'r'); + } catch (e) { + throw new Error( + `[ip2region] Can not open ip2region.db file , path: ${dbPath}` + ); + } + + this.totalBlocks = this.firstIndexPtr = this.lastIndexPtr = 0; + this.calcTotalBlocks(); + + this.headerIndexBuffer = new Buffer(TOTAL_HEADER_LENGTH); + this.headerSip = []; + this.headerPtr = []; + this.headerLen = 0; + this.prepareHeader(); + } + + /** + * @public + */ + destroy() { + fs.closeSync(ip2rObj.dbFd); + } + + /** + * @public + * @param {string} ip + * @return {SearchResult} + */ + binarySearchSync(ip) { + ip = ip2long(ip); + + let low = 0; + let mid = 0; + let high = this.totalBlocks; + let dataPos = 0; + let pos = 0; + let sip = 0; + let eip = 0; + const indexBuffer = new Buffer(12); + + // binary search + while (low <= high) { + mid = (low + high) >> 1; + pos = this.firstIndexPtr + mid * INDEX_BLOCK_LENGTH; + fs.readSync(this.dbFd, indexBuffer, 0, INDEX_BLOCK_LENGTH, pos); + sip = getLong(indexBuffer, 0); + + if (ip < sip) { + high = mid - 1; + } else { + eip = getLong(indexBuffer, 4); + if (ip > eip) { + low = mid + 1; + } else { + dataPos = getLong(indexBuffer, 8); + break; + } + } + } + return this.readData(dataPos); + } + + /** + * @public + * @param {string} ip + * @return {SearchResult} + */ + btreeSearchSync(ip) { + ip = ip2long(ip); + + // first search (in header index) + let low = 0; + let mid = 0; + let high = this.headerLen; + let sptr = 0; + let eptr = 0; + + while (low <= high) { + mid = (low + high) >> 1; + + if (ip == this.headerSip[mid]) { + if (mid > 0) { + sptr = this.headerPtr[mid - 1]; + eptr = this.headerPtr[mid]; + } else { + sptr = this.headerPtr[mid]; + eptr = this.headerPtr[mid + 1]; + } + break; + } + + if (ip < this.headerSip[mid]) { + if (mid == 0) { + sptr = this.headerPtr[mid]; + eptr = this.headerPtr[mid + 1]; + break; + } else if (ip > this.headerSip[mid - 1]) { + sptr = this.headerPtr[mid - 1]; + eptr = this.headerPtr[mid]; + break; + } + high = mid - 1; + } else { + if (mid == this.headerLen - 1) { + sptr = this.headerPtr[mid - 1]; + eptr = this.headerPtr[mid]; + break; + } else if (ip <= this.headerSip[mid + 1]) { + sptr = this.headerPtr[mid]; + eptr = this.headerPtr[mid + 1]; + break; + } low = mid + 1; + } + } + + // match nothing + if (sptr == 0) return null; + + // second search (in index) + const blockLen = eptr - sptr; + const blockBuffer = new Buffer(blockLen + INDEX_BLOCK_LENGTH); + fs.readSync( + this.dbFd, + blockBuffer, + 0, + blockLen + INDEX_BLOCK_LENGTH, + sptr + ); + + low = 0; + high = blockLen / INDEX_BLOCK_LENGTH; + + let p = 0; + let sip = 0; + let eip = 0; + let dataPtr = 0; + + while (low <= high) { + mid = (low + high) >> 1; + p = mid * INDEX_BLOCK_LENGTH; + sip = getLong(blockBuffer, p); + + if (ip < sip) { + high = mid - 1; } else { - dataPos = getLong(indexBuffer, 8); - break; + eip = getLong(blockBuffer, p + 4); + if (ip > eip) { + low = mid + 1; + } else { + dataPtr = getLong(blockBuffer, p + 8); + break; + } } } + return this.readData(dataPtr); + } + + /** + * @private + */ + calcTotalBlocks() { + const superBlock = new Buffer(8); + fs.readSync(this.dbFd, superBlock, 0, 8, 0); + this.firstIndexPtr = getLong(superBlock, 0); + this.lastIndexPtr = getLong(superBlock, 4); + this.totalBlocks = + (this.lastIndexPtr - this.firstIndexPtr) / INDEX_BLOCK_LENGTH + 1; + } + + /** + * @private + */ + prepareHeader() { + fs.readSync( + this.dbFd, + this.headerIndexBuffer, + 0, + TOTAL_HEADER_LENGTH, + 8 + ); + + for (let i = 0; i < TOTAL_HEADER_LENGTH; i += 8) { + const startIp = getLong(this.headerIndexBuffer, i); + const dataPtr = getLong(this.headerIndexBuffer, i + 4); + if (dataPtr == 0) break; + + this.headerSip.push(startIp); + this.headerPtr.push(dataPtr); + this.headerLen++; // header index size count + } } - // read data - if (dataPos == 0) return null; - var dataLen = ((dataPos >> 24) & 0xFF); - dataPos = (dataPos & 0x00FFFFFF); - var dataBuffer = new Buffer(dataLen); + /** + * @private + * @param {number} dataPos + * @return {SearchResult} + */ + readData(dataPos) { + if (dataPos == 0) return null; + const dataLen = (dataPos >> 24) & 0xff; + dataPos = dataPos & 0x00ffffff; + const dataBuffer = new Buffer(dataLen); - fs.readSync(this.db_fd, dataBuffer, 0, dataLen, dataPos); + fs.readSync(this.dbFd, dataBuffer, 0, dataLen, dataPos); - var city_id = getLong(dataBuffer, 0); - var data = dataBuffer.toString('utf8', 4, dataLen); + const city = getLong(dataBuffer, 0); + const region = dataBuffer.toString('utf8', 4, dataLen); - //console.log(city_id); - //console.log(data); - - return { city: city_id, region: data }; + return { city, region }; + } } - -var headerSip = null; -var headerPtr = 0; -var headerLen = 0; - - -/** - * btree search synchronized - * */ -ip2region.btreeSearchSync = function(ip) -{ - var indexBlockBuffer = new Buffer(indexBlockLength); - var headerIndexBuffer = new Buffer(totalHeaderLength); - if( typeof(ip) == 'string' ) ip = ip2long(ip); - - var i = 0; - // header index handler - if (headerSip == null) { - fs.readSync(this.db_fd, headerIndexBuffer, 0, totalHeaderLength, 8); - headerSip = new Array(); - headerPtr = new Array(); - - var startIp = 0; - var dataPtr = 0; - for ( i = 0; i < totalHeaderLength; i += 8) { - startIp = getLong(headerIndexBuffer, i); - dataPtr = getLong(headerIndexBuffer, i + 4); - if ( dataPtr == 0) break; - - headerSip.push(startIp); - headerPtr.push(dataPtr); - headerLen++; // header index size count - } - } - - // first search (in header index) - var low = 0; - var mid = 0; - var high = headerLen; - var sptr = 0; - var eptr = 0; - - while(low <= high) { - mid = ((low + high) >> 1); - - if (ip == headerSip[mid]) { - if ( mid > 0) { - sptr = headerPtr[mid - 1]; - eptr = headerPtr[mid]; - } else { - sptr = headerPtr[mid]; - eptr = headerPtr[mid + 1]; - } - break; - } - - if ( ip < headerSip[mid]) { - if (mid == 0) { - sptr = headerPtr[mid]; - eptr = headerPtr[mid + 1]; - break; - } else if ( ip > headerSip[mid - 1]) { - sptr = headerPtr[mid - 1]; - eptr = headerPtr[mid]; - break; - } - high = mid - 1; - } else { - if ( mid == headerLen - 1) { - sptr = headerPtr[mid - 1]; - eptr = headerPtr[mid]; - break; - } else if ( ip <= headerSip[mid + 1]) { - sptr = headerPtr[mid]; - eptr = headerPtr[mid + 1]; - break; - } - low = mid + 1; - } - } - - // match nothing - if (sptr == 0) return null; - - // second search (in index) - var blockLen = eptr - sptr; - var blockBuffer = new Buffer(blockLen + indexBlockLength); - fs.readSync(this.db_fd, blockBuffer, 0, blockLen + indexBlockLength, sptr); - - low = 0; - high = blockLen / indexBlockLength; - - var p = 0; - var sip = 0; - var eip = 0; - var dataPtr = 0; - - while(low <= high) { - mid = ((low + high) >> 1); - p = mid * indexBlockLength; - sip = getLong(blockBuffer, p); - - if (ip < sip) { - high = mid - 1; - } else { - eip = getLong(blockBuffer, p + 4); - if (ip > eip) { - low = mid + 1; - } else { - dataPtr = getLong(blockBuffer, p + 8); - break; - } - } - } - - // read data - if (dataPtr == 0) return null; - var dataLen = ((dataPtr >> 24) & 0xFF); - var dataPtr = (dataPtr & 0x00FFFFFF); - var dataBuffer = new Buffer(dataLen); - - fs.readSync(this.db_fd, dataBuffer, 0, dataLen, dataPtr); - - var city_id = getLong(dataBuffer, 0); - var data = dataBuffer.toString('utf8', 4, dataLen); - - //console.log(city_id); - //console.log(data); - - return { city: city_id, region: data }; -} - - -/** - * convert ip to long (xxx.xxx.xxx.xxx to a integer) - * */ -function ip2long(ip) -{ - var val = 0; - ip.split('.').forEach(function(ele, i){ - val += ipbase[i] * ele; - }); - - return val; -} - - -/** - * get long value from buffer with specified offset - * */ -function getLong(buffer, offset) -{ - var val = ( - (buffer[offset] & 0x000000FF) | - ((buffer[offset + 1] << 8) & 0x0000FF00) | - ((buffer[offset + 2] << 16) & 0x00FF0000) | - ((buffer[offset + 3] << 24) & 0xFF000000) - ); - - // convert to unsigned int - if (val < 0) { - val = val >>> 0; - } - return val; -} - - -exports.create = function(db_path) -{ - if (typeof(db_path) == "undefined" || fs.exists(db_path) ) { - throw("[ip2region] db file not exists : " + db_path); - } - - ip2region.db_file_path = db_path; - - try { - ip2region.db_fd = fs.openSync(ip2region.db_file_path, 'r'); - } catch(e) { - throw("[ip2region] Can not open ip2region.db file , path : " - + ip2region.db_file_path); - } - - // init basic search environment - if (totalBlocks == 0) { - fs.readSync(ip2region.db_fd, superBlock, 0, 8, 0); - firstIndexPtr = getLong(superBlock, 0); - lastIndexPtr = getLong(superBlock, 4); - totalBlocks = (lastIndexPtr - firstIndexPtr) - / indexBlockLength + 1; - } - - return ip2region; -} - - -exports.destroy = function(ip2rObj) -{ - ip2rObj.db_file_path = null; - fs.closeSync(ip2rObj.db_fd); -} +module.exports = IP2Region; diff --git a/binding/nodejs/ip2region.spec.js b/binding/nodejs/ip2region.spec.js index e6e75de..a7650ee 100644 --- a/binding/nodejs/ip2region.spec.js +++ b/binding/nodejs/ip2region.spec.js @@ -1,15 +1,24 @@ -'use strict'; +const path = require('path'); +const IP2Region = require('./ip2region'); -const ip = require('./ip2region'); +describe('ip2region', () => { + let instance; -const query = ip.create('../../data/ip2region.db'); + beforeAll(() => { + instance = new IP2Region({dbPath: path.join(__dirname, '../../data/ip2region.db')}); + }); -it('should query', () => { - expect(query.btreeSearchSync('120.24.78.68')).toMatchSnapshot(); - expect(query.btreeSearchSync('10.10.10.10')).toMatchSnapshot(); -}); - -it('binarySearch', () => { - expect(query.binarySearchSync('120.24.78.68')).toMatchSnapshot(); - expect(query.binarySearchSync('10.10.10.10')).toMatchSnapshot(); + afterAll(() => { + instance.destroy(); + }); + + test('should query', () => { + expect(instance.btreeSearchSync('120.24.78.68')).toMatchSnapshot(); + expect(instance.btreeSearchSync('10.10.10.10')).toMatchSnapshot(); + }); + + test('binarySearch', () => { + expect(instance.binarySearchSync('120.24.78.68')).toMatchSnapshot(); + expect(instance.binarySearchSync('10.10.10.10')).toMatchSnapshot(); + }); });