nodejs+express搭建一個高性能WebServer

  因爲最近的項目中要用到nodejs作一個WebServer服務器,因此最近學習了一下nodejs的語法和express框架。學習的過程當中也參考了許多文章博客,同時也有一些本身的心得體會,如今都一一記錄下來。html

  首先,第一個問題,爲何選了nodejs來作WebServer?或者換一種說法,用nodejs作WebServer與其餘語言相比有哪些優點?nodejs是運行chrome的V8上的JavaScript,採用事件驅動、非阻塞異步IO模型,最重要的是,它是單線程的(固然並非真正的單線程,這裏的單線程指的是主線程只有一個,而底層的工做線程有多個,要否則怎麼實現異步的IO對吧),站在開發者的角度上講,你在nodejs上寫的全部邏輯,都是運行在一個主線程上的。單線程的好處是很顯而易見的,內存佔用少,較多線程而言CPU切換的開銷小,編寫單線程程序簡單,絕對的線程安全,也不用考慮多線程之間的內存共享和同步的問題。然而單線程的劣勢也是很明顯的,沒法利用CPU多核的優點,沒法處理CPU密集型的任務,由於容易形成主線程因爲長時間耗在計算任務上而出現線程的阻塞。然而這些劣勢在WebServer服務器上都是能夠避免的,WebServer服務器原本就應該避免複雜的計算,計算是留給後臺作的,WebServer要關注的問題始終是怎樣實現一個較高的IO效率,如何實現一個較大的吞吐率以及怎樣將前端過來的請求以最快的速度響應給前端,nodejs的異步IO剛好能夠實現這一點。關於單線程沒法利用多核CPU的問題,其實若是真的要把CPU充分利用起來的話,能夠在一臺服務器上開多個nodejs服務,監聽不一樣的端口,再用一個負載均衡將請求輪詢分發到這些端口上,這裏要保證每份nodejs服務都是同樣的且無狀態的,若是要實現session機制的話,能夠用共享的一個redis來實現。因爲個人WebServer是部署在雲端的,因此我在開發的過程當中用了一個clb來作負載均衡,在雲上申請了一個redis來作共享的session存儲。前端

  下面,來說講個人具體實現。因爲個人WebServer服務器要與前端的微信小程序交互,根據微信小程序的官方開發文檔,小程序、開發者服務器與微信的服務接口之間的交互應該是這樣的:node

  首先,微信小程序前端獲取code,再把code發送給開發者服務器,開發者服務器拿到這個code以後,再帶上小程序的appid和appsecret,去調微信的接口,拿到openid和session_key,而後自定義一個本身的登錄態(就至關於再作一層session,根據openid和session_key經過某種算法生成一個本身的sessionid,由於openid和session_key是不該該直接給前端的,會形成安全問題),最後把這個sessionid返回給小程序前端,前端拿到這個sessionid以後,之後每次請求都要帶上這個sessionid,後臺檢測這個sessionid是否合法,這就至關於在微信的登錄機制上定義了一個本身的登錄態。具體的實現代碼以下(個人WebServer服務器如今充當了上圖中開發者服務器的角色):redis

配置文件config.js:算法

 1 var config={
 2     //redis配置
 3     redisPort:6379,
 4     redisHost:'127.0.0.1',
 5     redisPasswd:'xxxxx',
 6     //微信小程序配置
 7     appid:'12345678',
 8     secret:'1234567890',
 9     wxAddress:'https://api.weixin.qq.com/sns/jscode2session',
10 }
11 module.exports=config;

server端代碼:chrome

 1 var app=require('express')();
 2 var request=require('request');
 3 var querystring=require('querystring');
 4 var redis=require('redis');
 5 var crypto=require('crypto');
 6 var bodyParser=require('body-parser');
 7 var config=require('./config');
 8 
 9 //鏈接redis
10 var opts={auth_pass : config.redisPasswd};
11 var redisStore=redis.createClient(config.redisPort, config.redisHost, opts);
12 redisStore.on('connect', function(){
13     console.log('redis connect successful');
14 });
15 //使用JSON解析工具
16 app.use(bodyParser.urlencoded({extended: false}));
17 app.use(bodyParser.json());
18 //監聽登陸請求
19 app.get('/onLogin', function(req, res){
20     let code=req.query.code;
21     console.log("onLogin: code:"+code);
22     var getData=querystring.stringify({
23         appid: config.appid,
24         secret: config.secret,
25         js_code:code,
26         grant_type:'authorization_code'
27     });
28     var url=config.wxAddress+"?"+getData;
29     var session_id="";
30     request.get(url, function(err, req){
31         if(!err && req.statusCode===200){
32             var json=JSON.parse(req.body);
33             var openid=json.openid;
34             var session_key=json.session_key;
35             console.log('openid: '+openid);
36             console.log('session_key: '+session_key);
37             if(openid && session_key){
38                 //根據openid和session_key用md5算法生成session_id
39                 var hash=crypto.createHash('md5');
40                 hash.update(openid+session_key);
41                 session_id=hash.digest('hex');
42                 console.log('session_id:'+session_id);
43                 //將session_id存入redis並設置超時時間爲30分鐘
44                 redisStore.set(session_id, openid+":"+session_key);
45                 redisStore.expire(session_id, 1800);
46                 將session_id傳遞給客戶端
47                 res.set("Content-Type", "application/json");
48                 res.json({sessionid: session_id});
49             }else{
50                 res.json({warning: 'code is invalid'});
51             }
52         }else{
53             console.log(err);
54         }
55     });
56 });
57 var server=app.listen(8889, function(){
58     var host=server.address().address;
59     var port=server.address().port;
60     console.log('address is http://%s:%s', host, port);
61 });

登錄功能完成後,就能夠寫其餘的接口了,在接口中判斷sessionid是否有效,若是有效就提供服務,無效直接拒絕:express

 1 app.get('/products', function(req, res){
 2     let session_id=req.header('sessionid');
 3     let session_val=redisStore.get(session_id);
 4     if(session_val){
 5         console.log('sessionid is not ok');
 6         ...
 7     }else{
 8         console.log('sessionid is ok');
 9         res.json({warning: 'sessionid is invalid'});
10     }
11 });

  有時候,爲了確保安全,咱們還須要採用https與前端進行通訊,這個時候就須要有ca證書了,固然若是尚未來得及申請,咱們也能夠本身手動生成一個證書供測試用。咱們可使用openssl來生成證書:json

一、生成私鑰文件:小程序

  openssl genrsa 1024 > private.pem微信小程序

二、經過私鑰文件生成CSR證書籤名:

  openssl req -new -key private.pem -out csr.pem

三、經過私鑰文件和證書籤名生成證書文件:

  openssl x509 -req -days 365 -in csr.pem -signkey private.pem -out file.crt

填完國家、省份、公司名等信息以後證書就製做完成了。

  加入https後,server端的代碼就變成了這樣:

 1 var app=require('express')();
 2 var request=require('request');
 3 var querystring=require('querystring');
 4 var redis=require('redis');
 5 var crypto=require('crypto');
 6 var bodyParser=require('body-parser');
 7 var config=require('./config');
 8 var fs=require('fs');
 9 var http=require('http');
10 var https=require('https');
11 
12 //讀取https證書
13 var privateKey=fs.readFileSync('./private.pem', 'utf8');
14 var certificate=fs.readFileSync('./file.crt', 'utf8');
15 var credentials={key: privateKey, cert: certificate};
16 //鏈接redis
17 var opts={auth_pass : config.redisPasswd};
18 var redisStore=redis.createClient(config.redisPort, config.redisHost, opts);
19 redisStore.on('connect', function(){
20     console.log('redis connect successful');
21 });
22 //使用JSON解析工具
23 app.use(bodyParser.urlencoded({extended: false}));
24 app.use(bodyParser.json());
25 //開啓監聽
26 var httpsServer=https.createServer(credentials, app);
27 httpsServer.listen(config.httpsPort, function(){
28     console.log('HTTPS server is running on https://localhost:%s', config.httpsPort);
29 });
30 //監聽登陸請求
31 app.get('/onLogin', function(req, res){
32     let code=req.query.code;
33     console.log('Request path:'+req.path);
34     console.log("code:"+code);
35     var getData=querystring.stringify({
36         appid: config.appid,
37         secret: config.secret,
38         js_code:code,
39         grant_type:'authorization_code'
40     });
41     var url=config.wxAddress+"?"+getData;
42     var session_id="";
43     request.get(url, function(err, req){
44         if(!err && req.statusCode===200){
45             var json=JSON.parse(req.body);
46             var openid=json.openid;
47             var session_key=json.session_key;
48             console.log('openid: '+openid);
49             console.log('session_key: '+session_key);
50             if(openid && session_key){
51                 //根據openid和session_key用md5算法生成session_id
52                 var hash=crypto.createHash('md5');
53                 hash.update(openid+session_key);
54                 session_id=hash.digest('hex');
55                 console.log('session_id:'+session_id);
56                 //將session_id存入redis並設置超時時間爲30分鐘
57                 redisStore.set(session_id, openid+":"+session_key);
58                 redisStore.expire(session_id, 1800);
59                 //將session_id傳遞給客戶端
60                 res.set("Content-Type", "application/json");
61                 res.json({sessionid: session_id});
62             }else{
63                 res.json({warning: 'code is invalid'});
64             }
65         }else{
66             console.log(err);
67         }
68     });
69 });
70 app.get('/products', function(req, res){
71     let session_id=req.header('sessionid');
72     let session_val=redisStore.get(session_id);
73     if(session_val){
74         console.log('sessionid is not ok');
75         ...
76     }else{
77         console.log('sessionid is ok');
78         res.json({warning: 'sessionid is invalid'});
79     }
80 });

  咱們還能夠把本身生成的sessionid放在cookie裏面,這樣前端登錄後,之後每次發請求就不用在參數裏面帶上sessionid了,由於咱們能夠直接從cookie裏面獲取sessionid,而後進行校驗。因此最終的代碼就變成了這樣:

 1 var app=require('express')();
 2 var request=require('request');
 3 var querystring=require('querystring');
 4 var redis=require('redis');
 5 var crypto=require('crypto');
 6 var bodyParser=require('body-parser');
 7 var config=require('./config');
 8 var fs=require('fs');
 9 var http=require('http');
10 var https=require('https');
11 var cookieParser=require('cookie-parser');
12 
13 //讀取https證書
14 var privateKey=fs.readFileSync('./private.pem', 'utf8');
15 var certificate=fs.readFileSync('./file.crt', 'utf8');
16 var credentials={key: privateKey, cert: certificate};
17 //鏈接redis
18 var opts={auth_pass : config.redisPasswd};
19 var redisStore=redis.createClient(config.redisPort, config.redisHost, opts);
20 redisStore.on('connect', function(){
21     console.log('redis connect successful');
22 });
23 //使用JSON解析工具
24 app.use(bodyParser.urlencoded({extended: false}));
25 app.use(bodyParser.json());
26 //使用cookie
27 app.use(cookieParser());
28 //開啓監聽
29 var httpsServer=https.createServer(credentials, app);
30 httpsServer.listen(config.httpsPort, function(){
31     console.log('HTTPS server is running on https://localhost:%s', config.httpsPort);
32 });
33 //監聽登陸請求
34 app.get('/onLogin', function(req, res){
35     let code=req.query.code;
36     console.log('Request path:'+req.path);
37     console.log("code:"+code);
38     var getData=querystring.stringify({
39         appid: config.appid,
40         secret: config.secret,
41         js_code:code,
42         grant_type:'authorization_code'
43     });
44     var url=config.wxAddress+"?"+getData;
45     var session_id="";
46     request.get(url, function(err, req){
47         if(!err && req.statusCode===200){
48             var json=JSON.parse(req.body);
49             var openid=json.openid;
50             var session_key=json.session_key;
51             console.log('openid: '+openid);
52             console.log('session_key: '+session_key);
53             if(openid && session_key){
54                 //根據openid和session_key用md5算法生成session_id
55                 var hash=crypto.createHash('md5');
56                 hash.update(openid+session_key);
57                 session_id=hash.digest('hex');
58                 console.log('session_id:'+session_id);
59                 //將session_id存入redis並設置超時時間爲20分鐘
60                 redisStore.set(session_id, openid+":"+session_key);
61                 redisStore.expire(session_id, 1200);
62                 //將session_id存入cookie設置超時時間爲20分鐘
63                 res.cookie('sessionid', session_id, {maxAge: 20*60*1000});
64                 res.json({sessionid: session_id, errorCode: 0});
65             }else{
66                 res.json({msg: 'code is invalid', code: 8001});
67                 console.log('code is invalid, errorCode: 8001');
68             }
69         }else{
70             res.json({msg: 'unknow errro', code: 9001});
71             console.log('unknow error, errorCode: 9001, err:'+err);
72         }
73     });
74 });
75 app.get('/products', function(req, res){
76     let session_id=req.cookies.sessionid;
77     let session_val=redisStore.get(session_id);
78     if(session_val){
79         console.log('sessionid is not ok');
80     }else{
81         console.log('sessionid is ok');
82         res.json({warning: 'sessionid is invalid'});
83     }
84 });

 

 

參考文章:

一、http://www.javashuo.com/article/p-nrxwnsrb-k.html

二、https://www.jianshu.com/p/853099ae2edd

三、http://www.javashuo.com/article/p-twdsgyec-z.html

四、https://blog.csdn.net/qq_38125123/article/details/71196853

五、http://www.javashuo.com/article/p-chaxjhmh-gg.html

六、https://www.zhihu.com/question/61337684

七、http://www.javashuo.com/article/p-ecmrxuia-t.html

八、http://www.javashuo.com/article/p-axvnbpzd-gw.html

相關文章
相關標籤/搜索