JAVA程序員如何轉node_04

前言

這個系列的文章已經拖了好久。我一直想着我應該寫點什麼比較好。想着想着就覺得算了,明天吧,可能明天就有新的思路。我應該寫上手一些框架的步驟?這可能比較簡單,剛入門上手框架也確實容易對這門語言產生自己的印象。但是現在網站上這種教程沒有嗎?我想,只要瀏覽一下cnode,你能很快找到各種各樣的教程。

我想分享的是一種體會,一種從無快速上手的體會,一種先入爲主導致種種問題而產生的體會,這纔是我寫這些東西的初心。可能他不會很容易懂,需要你稍微做過一點你才能知道我走過的這些坑是真實存在的。

稍微總結一下之前寫的文章內容。第一篇,我寫的是什麼是異步,以及在代碼層面上怎麼實現異步。我也說了,是回調函數實現的異步。第二篇,我簡單說了一下node的異步處理邏輯,使用的是事件循環機制。此外因爲回調函數會產生多層回調,所以爲了去除他,說了如何用promise將回調函數包裝成async。第三篇,我介紹了node中的包管理工具npm的用法以及簡單的使用了一下koa來生成一個網頁服務應用。

第四篇說什麼呢,咱們繼續來說說網頁服務應用,因爲這是node後端程序員接觸的最多的一部分。


如果你寫的是一個javaweb應用,最原始的就是使用servlet。首先你會寫一個servlet繼承httpServlet,然後在類的下面編寫doGet方法doPost方法等。寫完了這些你需要在web.xml中編寫urlPartern來將url對應到你的servlet類中。這些弄完了你會將他打包,放在apache目錄下,開啓apache服務。這裏面,servlet就是mvc中的controller,web.xml實現的就是一個路由轉發的功能。

我們再來看看node原生怎麼實現一個網頁服務。

1、在一個目錄下新建一個文件app.js,輸入以下代碼

const http = require('http');
http.createServer((req,res) => {
    res.end('hello world');
}).listen(8888);
console.log('server is running on 127.0.0.1:8888');

2、命令行中node app.js 啓動應用。

這樣瀏覽器訪問127.0.0.1:8888就可以看到hello world。就這樣簡單的4行代碼(不算最後一行),就可以實現很高的qps了(每秒8000次左右),node默認是單線程工作,如果開啓多線程,那麼就可達到一萬多的每秒請求。作爲參考,apache的qps大概在5000次,go和node差不多,Nginx可達幾萬。

要知道,網絡io是io,硬盤查詢也是io。對於網絡io,大家都是採用輪詢的方式掃描端口,在這一處的io影響是不大的。我個人認爲,系統內部的硬盤io纔是node對於io處理的優勢之處。舉個例子,同樣發送8000個請求,在沒有涉及硬盤存儲,直接從內存獲取數據返回的時候,大家比較的就只是網絡的io。但如果這個時候請求涉及到數據的存儲,這時候apache這種傳統同步服務器在單個請求中會阻塞到其他的請求,而如果是node的話就能進行異步訪問從而達到並行處理的效果。

關於這一點我在第一篇中說過

使用node的話是非阻塞IO,調用了IO操作之後不要求數據直接就能返回,cpu直接就開始處理下一個操作,等到了IO操作結束之後,IO操作會去通知cpu執行接下來的操作。這就使計算機的IO處理速度大大提升。

也就是說,如果增加了數據的存儲操作,可能node就是會變慢一點(7000)次左右,而apache會迅速降到(1000)次左右。

我們來看一眼這幾行代碼,其中最主要的就是這一句。

http.createServer((req,res) => {res.end('hello world'); })

其中(req,res) => {res.end(‘hello world’); }就是以下的縮寫

function func1(request,response) {
    res.end('hello world');
}

也就是說,將寫一個帶有兩個參數的函數放入http.createServer()中就能生成一個服務器對象。

http

爲了不讓大家太迷糊,我儘量簡單講一下http模塊都做了什麼。

你將函數放入http.createServer()中之後,http會給你生成一個服務器對象,一直監聽着8888這個端口,當他發現端口有連接事件(connect)的時候,他按兵不動(不會觸發你的那個函數)。只有當端口收到了一個有效請求的時候,這時候http會生成一個request對象和一個response對象,將這兩個對象放入你的函數之中。你的函數處理完之後就會返回給原請求的地址。

有了這個,我們就可以在request對象中獲取我們需要的信息,如get請求中地址欄攜帶的信息、post中body存放的信息、請求地址等等。有了這些你就可以實現一個網絡應用了。

但是,此時你的網絡應用寫起來會零零散散,看起來像這樣。

const http = require('http');
const url = require('url');
const qs = require('querystring');
http.createServer((req,res) => {
    // console.log(req.url, req.method);
    const method = req.method;
    let { pathname: path, query } = url.parse(req.url);
    // GET /index1 請求
    if(path === '/index1' && method === 'GET') {
        query = qs.parse(query);
        res.end(`處理來自${method} ${path},數據爲 ${JSON.stringify(query)} 的請求`);
        return ;
    }
    // POST /index2
    if(path === '/index2' && method === 'POST') {
        let data = '';
        req.on('data', (chunk) => {
            data += chunk;  
        });
        req.on('end', () => {
            res.end(`處理來自${method} ${path},數據爲 ${JSON.stringify(qs.parse(data))}`)
        })
        return;
    }
    res.statusCode = 404;
    res.end('404 NOT FOUND')
}).listen(8888);
console.log('server is running on 127.0.0.1:8888');

理論上,所有的請求都會經過咱們編寫的“這一層函數”。但,難道我們要在這裏依次寫無數個ifelse來判斷request的各個參數、路徑,來決定response的各個響應消息嗎?

不可能吧,一個應用中,你會有日誌功能,配置功能,定時功能,路由管理,mvc這些需求。你全寫在一個文件中,那真的就是面向過程編程了,還不如直接用c語言去寫。

之前我們說過,node中異步函數的調用雖然使用了async和await,但他內部還是使用回調函數,每當有一個事件出現,消息一定是隨着函數作爲參數層層往下,再層層往上。這是node中一個特性,我們能不能根據這個做點什麼呢?

答案已經呼之欲出了,利用回調函數會層層往下又層層往上的特點,我們何不讓將邏輯佈置成一層一層的,讓請求每走一層就處理一部分邏輯?

koa中就是這樣,我們稱之爲洋蔥模型。每一層就是一箇中間件。

中間件

image

洋蔥模型是一個很不錯的組織方式,他天然就實現了面向切片編程。你可以寫一箇中間件,讓某一部分請求通過,這樣就不用在每一個請求中都調用一次。

當然,我不是說洋蔥模型就是完美的,有很多地方依舊用起來會比較彆扭,在某些特定的地方你依然會像以前一樣封裝成工具類這樣調用。但由於回調函數對中間件的天然支持,你能感覺到這種形式的編程還是能給你帶來很多不錯的體驗。

只是說的話會有點抽象,讓我們運行一段這樣的代碼。

const Koa = require('koa');
const app = new Koa();
async function middleWare1(ctx,next){
  console.log('----------middleWare1 start------------');
  await next();
  console.log('----------middleWare1 end------------');
}
async function middleWare2(ctx,next){
  console.log('----------middleWare2 start------------');
  await next();
  console.log('----------middleWare2 end------------');
}
async function middleWare3(ctx,next){
  console.log('----------middleWare3 start------------');
  ctx.body = 'hello world';
  console.log('----------middleWare3 end------------');
}
app.use(middleWare1);
app.use(middleWare2);
app.use(middleWare3);
app.listen(8888);
console.log('Server running at http://127.0.0.1:8888/');

訪問瀏覽器會看到輸出這樣的一段日誌。這說明一個請求進來會進入層層的中間件,再層層的離開。

----------middleWare1 start------------
----------middleWare2 start------------
----------middleWare3 start------------
----------middleWare3 end------------
----------middleWare2 end------------
----------middleWare1 end------------

解釋一下,每個中間件都是一個函數,規定傳入的參數是contxt(上下文)和next(回調函數,調用他就可以執行下一個中間件);

應用中要添加中間件,就要通過app.use(中間件方法)傳入,應用會自動按照其傳入的順序執行。

可以嘗試註釋掉其中的某一個await next(),看看結果是怎麼樣的。

在koa中,中間件大多都是單獨的模塊。我們只需要添加到入口文件app.js中,讓app.use(middleware)添加到應用中即可運用。

比如最簡單的koa-router管理路由的,我們可以看看他是如何管理我們上面原始的粗糙的http請求分發。 這個例子主要用到3個文件

// app.js   主要用於將中間件添加到應用中
const Koa = require('koa');
const app = new Koa();
const router = require('./router');
var bodyParser = require('koa-bodyparser');

app.use(bodyParser());//加入這個 纔可以解析post請求中參數
app
  .use(router.routes()) //將路由添加到應用
  .use(router.allowedMethods());
app.listen(8888);
console.log('server is running on 127.0.0.1:8888');

// router.js 管理路由  
const Router = require('koa-router');
const index = require('./controller/index')
const  router = new Router();
router.get('/index1',index.index1); //路由一般與controller對應
router.post('/index2',index.index2);
module.exports =router;

//index.js  具體處理邏輯的地方 controller層
async function index1(ctx, next) {
    const {method,path,query} = ctx.request;
    ctx.body = `處理來自${method} ${path},數據爲 ${JSON.stringify(query)} 的請求`;
}
async function index2(ctx, next) {
    const {method,path,body} = ctx.request;
    ctx.body = `處理來自${method} ${path},數據爲 ${JSON.stringify(body)} 的請求`;
}
module.exports = {
    index1,
    index2,
}

有了這樣的一個框架,我們就可以方便的對代碼進行模塊化管理、分層管理。

代碼已上傳到Zeeephr/koa-demo ,如果有需要可以看一看。

後記

這個系列後面可能就是一起看源碼了,但是不要慌,node中看源碼的體驗非常好。node編程中有的時候甚至不用去查api或者百度哪裏報錯,直接在node_modules文件夾中點開就能看,最誇張的就是他還可以在引入的包中打斷點,這樣你就能清晰地知道你的數據是如何走向的。

好了這一part就先講到這吧,覺得有用的話可以點點贊,留下你的評論!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章