Node.js進階篇-koa、鉤子函數、websocket、嵌入式開發

代碼地址以下:
http://www.demodashi.com/demo/12932.htmljavascript

1、簡介

    koa是由Express原班人馬打造的,致力於成爲一個更小、更富有表現力、更健壯的Web框架,Koa不定製路由,無冗餘的中間件,開發設計方案趨向定製化,因此很適合對業務和技術有靈活要求的web場景。css


2、應用

    因爲restful、加解密、跨域、參數解析、中間件等比較基礎,且文檔豐富,本小節將直接跳過,側重於分享如下幾點:html


一、路由轉發時,如何利用鉤子函數機制作到controller層業務解耦
二、在socket通訊中如何動態加載protobuf進行數據格式交換
三、如何基於websocket綁定相同的端口
四、如何利用c++編寫node擴展庫

  • 2.1 業務解耦

    中間件及鉤子函數機制皆爲業務解耦的有效實現方式,其中中間件模式因其實現方便而應用普遍, 如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', 再次請求接口,能夠發如今需求變更狀況對源碼修改極少,代碼可維護性提高數據庫


  • 2.2 protobuf數據協議

    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分別在終端打印結果

  • 2.3 websocket

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")

koa_3

  • 2.1 C++插件

    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

koa_4
koa_5

D) 定義路由並調用接口

koa_6


項目文件目錄結構截圖

3、參考

代碼地址以下:
http://www.demodashi.com/demo/12932.html

注:本文著做權歸做者,由demo大師代發,拒絕轉載,轉載須要做者受權

相關文章
相關標籤/搜索