由於最近的項目中要用到nodejs做一個WebServer服務器,所以最近學習了一下nodejs的語法和express框架。學習的過程中也參考了許多文章博客,同時也有一些自己的心得體會,現在都一一記錄下來。
首先,第一個問題,為什么選了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服務器要與前端的微信小程序交互,根據微信小程序的官方開發文檔,小程序、開發者服務器與微信的服務接口之間的交互應該是這樣的:
首先,微信小程序前端獲取code,再把code發送給開發者服務器,開發者服務器拿到這個code之后,再帶上小程序的appid和appsecret,去調微信的接口,拿到openid和session_key,然后自定義一個自己的登陸態(就相當於再做一層session,根據openid和session_key通過某種算法生成一個自己的sessionid,因為openid和session_key是不應該直接給前端的,會造成安全問題),最后把這個sessionid返回給小程序前端,前端拿到這個sessionid之后,以后每次請求都要帶上這個sessionid,后台檢測這個sessionid是否合法,這就相當於在微信的登陸機制上定義了一個自己的登陸態。具體的實現代碼如下(我的WebServer服務器現在充當了上圖中開發者服務器的角色):
配置文件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端代碼:
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是否有效,如果有效就提供服務,無效直接拒絕:
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來生成證書:
1、生成私鑰文件:
openssl genrsa 1024 > private.pem
2、通過私鑰文件生成CSR證書簽名:
openssl req -new -key private.pem -out csr.pem
3、通過私鑰文件和證書簽名生成證書文件:
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 });
參考文章:
1、https://blog.csdn.net/xjtroddy/article/details/51388655
2、https://www.jianshu.com/p/853099ae2edd
3、https://blog.csdn.net/itKingOne/article/details/79259490
4、https://blog.csdn.net/qq_38125123/article/details/71196853
5、https://www.cnblogs.com/linzhanfly/p/9082895.html
6、https://www.zhihu.com/question/61337684
7、https://blog.csdn.net/night_emperor/article/details/78909249
8、https://www.cnblogs.com/tugenhua0707/p/9098132.html