nodejs+express搭建一个高性能WebServer
由于最近的项目中要用到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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现