代碼地址以下:
http://www.demodashi.com/demo/12932.htmljavascript
koa是由Express原班人馬打造的,致力於成爲一個更小、更富有表現力、更健壯的Web框架,Koa不定製路由,無冗餘的中間件,開發設計方案趨向定製化,因此很適合對業務和技術有靈活要求的web場景。css
因爲restful、加解密、跨域、參數解析、中間件等比較基礎,且文檔豐富,本小節將直接跳過,側重於分享如下幾點:html
一、路由轉發時,如何利用鉤子函數機制作到controller層業務解耦 二、在socket通訊中如何動態加載protobuf進行數據格式交換 三、如何基於websocket綁定相同的端口 四、如何利用c++編寫node擴展庫
中間件及鉤子函數機制皆爲業務解耦的有效實現方式,其中中間件模式因其實現方便而應用普遍, 如koa、express、sails中都曾大量用到,
而鉤子函數機制在node生態中被大量用到ORM對數據庫的操做,如mongoose、waterline,鮮有在controller層的普遍應用,本小節則嘗試分享
一個簡易的Hooks實現方式,並應用在koa框架中。java
編寫koa-hooks, 並提交到npmnode
const hooks = require('hooks') class ApiHooks { constructor(ctx, next, cb) { this._ctx = ctx this._next = next this._cb = cb this._listenerTree = {} this.addListenerTree() } addListenerTree() { for (let fn in hooks) { this[fn] = hooks[fn] } } addHooks(listeners) { const self = this try { listeners.map(listener => { const [method, hooksFn] = listener.split('.') if(hooksFn.match('before')) self.addFn(method, hooksFn, 'pre') if(hooksFn.match('after')) self.addFn(method, hooksFn, 'post') }) } catch (err) { console.log('err:', err) } } addFn(method, hooksFn, hook) { const self = this self[hook](method, async (next) => { await self[hooksFn](self._ctx, next, self._cb) }) } } module.exports = ApiHooks
編寫一個restful風格接口/v1/verb/get
,繼承ApiHooks, 添加對應的鉤子函數beforeVerbCheckLogin實現登陸檢查c++
/** * Created by Joseph on 18/09/2017. */ const Api = require('koa-hooks').Api const VerbService = require('../../services/verb.js') class VerbApi extends Api { constructor(ctx, next, cb) { super(ctx, next, cb) this.addHooks([ 'verbGetOnThisRequest.beforeVerbCheckLogin', 'verbPostOnThisRequest.beforeVerbCheckLogin', 'verbPutOnThisRequest.beforeVerbCheckLogin', 'verbDeleteOnThisRequest.beforeVerbCheckLogin', ]) } async beforeVerbCheckLogin(ctx, next, cb) { const data = await VerbService.beforeVerbCheckLogin(ctx, next) data ? cb(ctx, data) : await next() } async verbGetOnThisRequest(ctx, next, cb) { const data = await VerbService.verbGetOnThisTest(ctx, next) data ? cb(ctx, data) : await next() } async verbPostOnThisRequest(ctx, next, cb) { const data = await VerbService.verbPostOnThisTest(ctx, next) data ? cb(ctx, data) : await next() } async verbPutOnThisRequest(ctx, next, cb) { const data = await VerbService.verbPutOnThisTest(ctx, next) data ? cb(ctx, data) : await next() } async verbDeleteOnThisRequest(ctx, next, cb) { const data = await VerbService.verbDeleteOnThisTest(ctx, next) data ? cb(ctx, data) : await next() } } module.exports = (ctx, next, cb) => new VerbApi(ctx, next, cb)
啓動服務,請求接口http://127.0.0.1:3000/v1/verb/get
,能夠發現此鉤子函數已經生效web
註釋掉//'verbGetOnThisRequest.beforeVerbCheckLogin', 再次請求接口,能夠發如今需求變更狀況對源碼修改極少,代碼可維護性提高數據庫
protobuf是谷歌開源的是一種輕便高效的結構化數據存儲格式, 且平臺無關、語言無關、可擴展,一般用在tcp編程對數據傳輸要求較高的場
景,protobuf兼有json的可讀性,且傳輸效率遠大於json、xml等,很是適合流式數據交換。express
A) 根據文件名及message動態加載protobufnpm
const protobuf = require('protobufjs') const protoPath = '/Users/dreamboad/Projects/koa-service/message/' class Proto { async loadByName(protoName, messageName, obj, type) { return new Promise((resolve, reject) => { protobuf.load(`${protoPath}${protoName}.proto`, (err, root) => { if (err) { return console.log(err) || resolve() } const data = root.lookupType(`${protoName}.${messageName}`) if (type === 'encode' && data.verify(obj)) { return console.log('encode err') || resolve() } switch (type) { case 'decode': return resolve(data.toObject(data.decode(obj), { objects: true })) case 'encode': return resolve(data.encode(data.create(obj) || '').finish()) } }) }) } async deserialize(protoName, messageName, obj) { return await this.loadByName(protoName, messageName, obj, 'decode') } async serialize(protoName, messageName, obj) { return await this.loadByName(protoName, messageName, obj, 'encode') } } module.exports = new Proto()
B) 編寫soket client
/** * 一、動態加載protobuf * 二、socket數據流斷包、粘包處理(TODO) * 三、心跳機制、及斷線重連 */ const net = require('net') const [HOST, PORT] = ['127.0.0.1', 9999] const client = new net.Socket() const connection = () => { client.connect(PORT, HOST, () => { console.log('CONNECTED TO: ' + HOST + ':' + PORT)}) } client.on('data', (data) => { console.log(`${HOST}:${PORT} CONNECT DATA: `, data) }) client.on('error', (e) => { console.log(`${HOST}:${PORT} CONNECT ERROR: ` + e) }) client.on('timeout', (e) => { console.log(`${HOST}:${PORT} CONNECT TIMEOUT: ` + e) }) client.on('end', (e) => { console.log(`${HOST}:${PORT} CONNECT END: ` + e) }) client.on('close', (e) => { console.log(`${HOST}:${PORT} CONNECT CLOSE: ` + e) if (client.destroyed) { client.destroy() } setTimeout(connection, 3000) }) process.on('exit', () => { client.destroy() client.on('close', () => { console.log('Connection closed') }) }) // 鏈接 客戶端 module.exports = { connection, client }
C) 在soket通訊中序列化/反序列化json數據
/** * 序列化、反序列化 */ const crypto = require('crypto') const Proto = require('./protobuf') class SocketProto { async doTranslation(obj, protoName, messageName, operation) { try { switch (operation) { case 'decode': return await Proto.deserialize(obj, protoName, messageName) case 'encode': return await Proto.serialize(obj, protoName, messageName) } } catch (error) { console.log(error) } } async decode(obj, protoName, messageName) { return await this.doTranslation(obj, protoName, messageName, 'decode') } async encode(obj, protoName, messageName) { return await this.doTranslation(obj, protoName, messageName, 'encode') } } module.exports = new SocketProto()
D) 鏈接服務器,讀寫流式數據,並用proto解析
const { connection, client } = require('./socket_client') const SocketProto = require('./socket_protobuf') const config = require('../config/').msgIdConfig connection() const writer = module.exports.writer = async (protoName, messageName, obj) => { const w = await SocketProto.encode(protoName, messageName, obj) return client.write(w) } const reader = module.exports.reader = async (protoName, messageName, obj) => { const r = await SocketProto.decode(protoName, messageName, obj) return r } client.on('data', (buf) => { chooseFnByMsg('', 'basemsg', buf) }) const chooseFnByMsg = (msgId, type, obj) => { if (msgId) { if (!config[msgId] || !config[msgId].req || !config[msgId].res) { return console.log('noting to do: ', msgId) } } switch (type) { case 'basemsg': return reader(config.head.res.pName, config.head.res.mName, obj) case 'write': return writer(config[msgId].req.pName, config[msgId].req.mName, obj) case 'read': return reader(config[msgId].res.pName, config[msgId].res.mName, obj) default: console.log('noting to do default: ', msgId) break } } chooseFnByMsg(1, 'write', { Field: "String" }) module.exports = chooseFnByMsg
E) server及client分別在終端打印結果
A) koa server
const app = new Koa() // web socket const server = require('http').Server(app.callback()) const io = require('socket.io')(server) io.on('connection', client => { console.log('new connection:') client.on('news', (data, cb) => { console.log('news:', data) }) client.on('disconnect', () => { console.log('disconnect:') }) })
B) websocket client
const client = require('socket.io-client').connect('http://localhost:3000') client.emit('news', "hello world")
IO異步及高併發是Node的優點,但若在須要密集計算、集成基於C++的第三方SDK等場景時,Node的劣勢則顯現出來,此時能夠基於node-gyp來嵌入集成C++解決以上等問題。
A) 安裝node-gyp
cnpm install -g node-gyp
A) 編輯binding.gyp、C++、Node調用模塊
{ "targets": [ { "target_name": "demo", "sources": ["src/demo.cc"] }, { "target_name": "test_params_nocb", "sources": ["src/test_params_nocb.cc"] }, { "target_name": "test_function_nocb", "sources": ["src/test_function_nocb.cc"] }, { "target_name": "test_params_function_nocb", "sources": ["src/test_params_function_nocb.cc"] } ] }
// test_function_nocb.cc #include <node.h> namespace demo { using v8::Function; using v8::FunctionCallbackInfo; using v8::Isolate; using v8::Local; using v8::Null; using v8::Object; using v8::String; using v8::Value; void RunCallback(const FunctionCallbackInfo<Value>& args) { Isolate* isolate = args.GetIsolate(); Local<Function> cb = Local<Function>::Cast(args[0]); Local<Value> argv[1] = { String::NewFromUtf8(isolate, "hello world") }; cb->Call(Null(isolate), 1, argv); } void Init(Local<Object> exports, Local<Object> module) { NODE_SET_METHOD(module, "exports", RunCallback); } NODE_MODULE(test_function_nocb, Init) } // namespace demo
module.exports.embeddedProxy = (cb, params) => { return new Promise((resolve, reject) => { try { return cb((data) => { resolve(data) }, params) } catch (err) { return resolve({ data: "調用失敗", code: -1 }) } }) }
C) 編譯C++
node-gyp configure node-gyp build
D) 定義路由並調用接口
注:本文著做權歸做者,由demo大師代發,拒絕轉載,轉載須要做者受權