Node + Express/Koa2 + PM2 + MySQL/Redis
- Nodejs,一个js运行环境
- 运行在服务端,作为web server
- 运行在本地,作为打包,构建工具
- API
- 数据存储
- 登录
- 日志
- 安全
- HTTP
- Stream
- Session
- MySQL/Redis
- Nginx
- PM2
- Node介绍 --- Node安装 Node和js区别
- 服务端特点--- 服务端特点 服务端和前端的区别
- 案例分析和设计--- 博客项目的需求分析和技术方案设计
- 使用原生代码开发案例项目
- 实现API和数据存储,使用MySQL数据库
- 从0实现登录,并使用redis存储登录信息
- 安全,日志记录和日志分析
- 使用框架开发案例项目
- 分别使用express 和Koa2
- 中间件机制
- 常用插件
- 中间件原理
- 线上环境
- PM2介绍和配置
- PM2多进程模型
- 关于服务器运维
- API和数据存储
- 登录和Redis
- 安全和日志
- Express 和 Koa2
- 中间件和插件
- 中间件原理
- PM2介绍和配置
- PM2多进程模型
- 服务器运维
1.普通方式 nodejs.cn 官网
2.NVM node版本管理工具
Mac brew install nvm
Window github搜nvm-windows
nvm list # 查看当前所有的node版本
nvm install v10.15.0 # 安装制定版本
nvm use --delete-prefix 10.13.0 # 切换到指定版本
-
ESMAScript -- 语法规范
定义了语法 ,写js的node必须遵守 变量定义,循环,判断,函数 原型和原型链、作用域和闭包、异步 不能操作DOM,不能监听click时间,不能发送ajax请求 不能处理http请求,不能操作文件 只有ESMAScript,无法做项目
-
Javascript
使用ESMAScript语法规范,外加Web API,缺一不可(ESMAScript + Web API) DOM操作,BOM,事件,Ajax... 两者结合,即可完成浏览器端的操作
-
NodeJs
使用ESMAScript语法规范,外加Node API,缺一不可(ESMAScript + Node API) 处理http,处理文件等API 两者结合,即可完成server端的操作
-
补充:Commonjs & Node debugger
-
Commonjs
# 导出
module.exports = 变量
module.exports = 对象
# 引入1
const {a,b} = require('./a); //解构引入
# 引入2
const opt = require('./a);
const a = opt.a
const b = opt.b
- Node bugger
利用vscode自带的debugger功能,会根据
package.json里的main字段去找入口
- server 端可能会遭受各种恶意攻击和误操作
- 单个客户端可以意外挂掉,但是服务端不能
- 可以使用PM2做进程守护
- 日志记录
- 安全
- 集群和服务拆分
- 开发博客系统,具有博客的基本功能
- 之开发server端,不关心前端
- 首页、作者主页、博客详情页
- 登陆页
- 管理中心、新建页、编辑页
- 博客
| id | title | content | createtime | author |
|---|---|---|---|---|
| 1 | 标题1 | 博客1 | 创建时间xxx-xxx-xxx | xxx |
| 2 | 标题2 | 博客2 | 创建时间xxx-xxx-xxx2 | xxx2 |
- 用户
| id | username | password | realname |
|---|---|---|---|
| 1 | zhangsan | 123 | 张三 |
| 2 | lisi | 456 | 李四 |
| 描述 | 接口 | 方法 | url参数 | 备注 |
|---|---|---|---|---|
| 获取博客列表 | /api/blog/list | get | author作者,keyword搜索关键字 | 参数为空,则不进行查询过滤 |
| 获取一篇博客的内容 | /api/blog/detail | get | id | |
| 新增一篇博客 | /api/blog/new | post | post中有新增的信息 | |
| 更新一篇博客 | /api/blog/update | post | id | postData中有更新的内容 |
| 删除一篇博客 | /api/blog/del | post | id | |
| 登录 | /api/user/login | post | postData中有用户名和密码 |
- 业界有统一的解决方案,一般不用再重新设计
- 域名 -- > ip+port
- get请求,即客户端要向server端获取数据,如查询博客列表
- 通过querystring传递数据,如 a.html?a=100&b=200
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req,res)=>{
console.log(req.method)
const url = req.url;
console.log(url);
const queryStr = url.split('?')[1]; //根据问号拆分参数
req.query = querystring.parse(queryStr);
res.end(JSON.stringify(req.query));
});
server.listen(8000);- 客户端向服务端传递数据,如新建博客
- 通过post data传递数据
- 浏览器无法直接模拟,需要手写js,或者使用postman
const http = require('http');
const log = console.log;
const server = http.createServer((req,res)=>{
if(req.method ==="POST"){
// req 数据格式
log('req content-type',req.headers['content-type']);
// 接收数据
let postData = '';
req.on('data',chunk=>{
postData += chunk.toString();
});
req.on('end',()=>{
log('postData',postData);
res.end('hello post')
})
}
});
server.listen(8001);1.Chrome上安装postman插件 2.地址栏输入访问地址 http://localhost:8001,修改请求为POST 3.点击下方 “Body” ---> raw 右侧选择 JSON ---> 下方空白区 输入请求参数json
解析req.url 里的/username /password等
const http = require('http');
const querystring = require('querystring');##
const log = console.log;
const server = http.createServer((req,res)=>{
const method = req.method;
const url = req.url;
const query = querystring.parse(url.split('?')[1]);
const path = url.split('?')[0]
res.setHeader('Content-type','application/json');
const resData = {
method,
url,
query,
path
}
if(method==='GET'){
res.end(JSON.stringify(resData))
}
if(method==='POST'){
let postData = '';
req.on('data',chunk => {
postData += chunk
});
req.on('end',()=>{
log('postData',postData);
resData.postData = postData
res.end(JSON.stringify(resData));
})
}
});
server.listen(8002);- 从0开始搭建,不使用任何框架
- 使用nodemon监测文件变化,自动重启node
- 使用cross-env 设置环境变量,兼容mac/linux/windows
- 初始化路由:根据之前技术方案的设计,做出路由
- 返回假数据:将路由和数据分离,以符合设计原则
├── app.js -- 主要服务
├── bin
│ └── www.js --- 创建服务
├── controllers --- 放假数据
│ └── index.js
├── models --- 返回正确/错误 统一数据格式 Model
│ └── index.js
├── package-lock.json
├── package.json
└── routers -- 路由文件,解析请求
├── blog.js
└── user.js
# blog.js
const handleBlogRouter = (req,res) =>{
const method = req.method;
const path = req.url.split('?')[0];
//获取博客列表 /api/blog/list
if( method==="GET" && path === "/api/blog/list"){
return {
msg:'/api/blog/list'
}
}
//获取一篇博客的内容 /api/blog/detail
if( method==="GET" && path === "/api/blog/detail"){
return {
msg:'/api/blog/detail'
}
}
//新增一篇博客 /api/blog/new
if( method==="POST" && path === "/api/blog/new"){
return {
msg:'/api/blog/new'
}
}
//更新一篇博客 /api/blog/update
if( method==="POST" && path === "/api/blog/update"){
return {
msg:'/api/blog/update'
}
}
//删除一篇博客 /api/blog/del
if( method==="POST" && path === "/api/blog/update"){
return {
msg:'/api/blog/update'
}
}
}
module.exports = handleBlogRouter;# user.js
const handleUserRouter = (req,res) =>{
const method = req.method;
const path = req.url.split('?')[0];
//获取博客列表 /api/user/login
if( method==="POST" && path === "/api/user/login"){
return {
"msg":"/api/user/login"
}
}
}
module.exports = handleUserRouter;# app.js
const handleBlogRouter= require('./routers/blog');
const handleUserRouter = require('./routers/user');
const serverHandle = (req,res) => {
//设置返回的JSON
res.setHeader('Content-type','application/json');
const blogData = handleBlogRouter(req,res);
if(blogData){
res.end(JSON.stringify(blogData));
}
const userData = handleUserRouter(req,res);
if(userData){
res.end(JSON.stringify(userData));
}
}
module.exports = serverHandle;# app.js
const queryString = require('querystring');
const handleBlogRouter= require('./routers/blog');
const handleUserRouter = require('./routers/user');
//解析post data,返回promise对象
const getPostData = (req) => {
const promise = new Promise((resolve,reject) => {
if(req.method === "POST" && req.headers['content-type'] === 'application/json'){
let postData = "";
req.on('data',chunk => postData += chunk.toString());
req.on("end",()=>{
if(postData){
resolve(JSON.parse(postData));
}else{
resolve({});
return;
}
})
}else{
resolve({});
return;
}
});
return promise;
}
const serverHandle = (req,res) => {
//设置返回的JSON
res.setHeader('Content-type','application/json');
//获取path
const url = req.url;
req.path = url.split('?')[0];
req.query = queryString.parse(url.split('?')[1])
//解析post数据
getPostData(req).then(postData => {
req.body = postData;
//处理博客的路由
const blogData = handleBlogRouter(req,res);
if(blogData){
res.end(JSON.stringify(blogData));
}
//处理用户登录的路由
const userData = handleUserRouter(req,res);
if(userData){
res.end(JSON.stringify(userData));
}
})
}
module.exports = serverHandle;# 建立返回的数据格式Model models/index.js
class BaseModel {
constructor(data,message){
if(typeof data === "string"){
this.message = data;
data = null;
message = null
}
if(data){
this.data = data;
}
if(message){
this.message = message;
}
}
}
class SuccessModel extends BaseModel {
constructor(data,message){
super(data,message);
this.errno = 0;
}
}
class ErrorModel extends BaseModel {
constructor(data,message){
super(data,message);
this.errno = -1;
}
}
module.exports = {
SuccessModel,
ErrorModel
}# 解析 博客路由 routers/blog.js
const {
getList,
getDetail,
newBlog,
updateBlog,
delBlog
} = require('../controllers/blog')
const { SuccessModel,ErrorModel } = require('../models')
const handleBlogRouter = (req,res) =>{
const method = req.method;
const path = req.path;
const query = req.query;
const id = query && query.id;
//获取博客列表 /api/blog/list
if( method==="GET" && path === "/api/blog/list"){
const { author , keyword } = query;
if(author && keyword){
const resData = getList(author,keyword);
return new SuccessModel(resData);
}else {
return new ErrorModel('获取博客列表失败,请传正确的参数 author & keyword');
}
}
//获取一篇博客的内容 /api/blog/detail
if( method==="GET" && path === "/api/blog/detail"){
if(id){
const resData = getDetail(id);
return new SuccessModel(resData);
}else{
return new ErrorModel('获取博客内容失败,请传正确的参数id');
}
}
//新增一篇博客 /api/blog/new
if( method==="POST" && path === "/api/blog/new"){
const resData = newBlog(req.body);
return new SuccessModel(resData);
}
//更新一篇博客 /api/blog/update
if( method==="POST" && path === "/api/blog/update"){
const resData = updateBlog(id,req.body);
if(resData){
return new SuccessModel('更新博客成功');
}else {
return new ErrorModel('更新博客失败')
}
}
//删除一篇博客 /api/blog/del
if( method==="POST" && path === "/api/blog/del"){
if(id){
const resData = delBlog(id);
if(resData){
return new SuccessModel('删除博客成功');
}
}
}
}
module.exports = handleBlogRouter;# 解析用户登录路由 routers/user.js
const { loginCheck } = require('../controllers/user');
const { SuccessModel,ErrorModel } = require('../models')
const handleUserRouter = (req,res) =>{
const method = req.method;
const path = req.path;
//获取博客列表 /api/user/login
if( method==="POST" && path === "/api/user/login"){
const { username,password } = req.body;
const result = loginCheck(username,password);
if(result){
return new SuccessModel('登陆成功!');
}
return new ErrorModel('登录失败!');
}
}
module.exports = handleUserRouter;# 处理数据 controllers/blog.js
const getList = (author,keyword) => {
console.log(author,keyword)
return [
{
id:1,
title:'博文A',
content:"内容A",
author:"zhangsan"
},
{
id:2,
title:'博文B',
content:"内容B",
author:"lisi"
}
]
}
const getDetail = (id) => {
return {
id:1,
title:'博文A',
content:"内容A",
author:"zhangsan"
}
}
const newBlog = (blogData = {}) => {
return {
id:3
}
}
const updateBlog = (id,blogData = {}) => {
// id 要更新的博客id
// blogData 博客对象 包含title content对象
console.log('update blog',id,blogData);
return true;
}
const delBlog = (id) => {
//id 要删除的博客id
return true;
}
module.exports = {
getList,
getDetail,
newBlog,
updateBlog,
delBlog
}# 处理数据 controllers/user.js
const loginCheck = (username,password) => {
if(username === 'zhangsan' && password === '123'){
return true;
}
return false;
}
module.exports = {
loginCheck
}- 如何建库,建表
- 建表时常用的数据类型(int,bight,varchar,longtext)
- sql 语句实现增删改查
- 建库 --> 创建myblog数据库 --> 执行show databases查询
- 建表
创建blogs博客表
| id | title | content | createtime | author |
|---|---|---|---|---|
| 1 | 标题1 | 博客1 | 创建时间xxx-xxx-xxx | xxx |
| 2 | 标题2 | 博客2 | 创建时间xxx-xxx-xxx2 | xxx2 |
-------------------------blogs博客表结构设计
| column | datatype | pk主键 | nn不为空 | AI自动增加 | Deafult |
|---|---|---|---|---|---|
| id | int(整数类型) | Y(不重复) | Y | Y (自增) | |
| title | varchar(50)(字符串) | Y | |||
| content | longtext(20) | Y | |||
| createtime | bigint(20)(长整数) | Y | 0 | ||
| author | varchar(50) | Y |
创建users表
| id | username | password | realname |
|---|---|---|---|
| 1 | zhangsan | 123 | 张三 |
| 2 | lisi | 456 | 李四 |
------------------------users用户表结构设计
| column | datatype | pk主键 | nn不为空 | AI自动增加 |
|---|---|---|---|---|
| id | int(整数类型) | Y(不重复) | Y | Y (自增) |
| username | varchar(20)(字符串) | Y | ||
| password | varchar(20) | Y | ||
| realname | varchar(10) | Y |
3.操作表
# 使用myblog库
use myblog;
# 显示所有表
show tables;
# 注释
-- show tables;
# 增
insert into users (username,`password`,realname) values ('zhangsan','123','张三') # 向users表插入username,`password`关键词加反引号,realname
# 查
select * from users; # 查询users表的所有内容
select id,username from users; # 查询users表的id,username
## 条件查询 同时满足
select * from users where username='zhangsan' and `password`='123'
## 条件查询 并集查询 满足其一的就显示
select * from users where username='zhangsan' or `password`='123'
## 模糊查询
select * from users where username like '%zhang%';
## 排序查询(正序),模糊查询有1的结果按照id排序
select * from users where `password` like '%1%' order by id ;
## 排序查询(倒序),模糊查询有1的结果按照id排序
select * from users where `password` like '%1%' order by id desc;
# 更新
update users set relname='李四2' where username='lisi'
## 如果触发MySQL安全机制,可以关闭,再执行更新语句
SET SQL_SAFE_UPDATES=0;
# 删除(一定带where条件)
delete from users where username="lisi";
## 通过字段来表示删除,而不是真实delete删除
select * from users where state='1';
-- select * from users where state<>'0';(另一种写法 <>0 表示不等于0)
update users set state='0' where username='lisi' # 设置state=0表示删除(软删除)
# 查看mysql版本号
select version();npm i mysql
const mysql = require('mysql');
//创建数据库对象
const connection = mysql.createConnection({
host:'localhost',
port:'3306',
user:'xxx',
password:'xxx',
database:'myblog'
});
//开始链接
connection.connect();
//执行sql语句
connection.query(sql语句,(err,results)=>{
if (err) throw err;
console.log(results)
})
//关闭
connection.end()## 遇到的问题 -- 安装完无法连接mysql,错误信息
Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
## 解决方法
mysql -u root -p #登录mysql
Enter password: 输入你当前的mysql密码
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_new_password';
mysql> FLUSH PRIVILEGES;
mysql> quit
重新连接node--node mysql.js
[ RowDataPacket { id: 1, username: 'zhangsan', password: '123', realname: '张三' },
RowDataPacket { id: 2, username: 'lisi', password: '456', realname: '李四2' } ]
# 创建db/conf.js 利用process.env.NODE_ENV来区分线上和本地版本的数据库配置
const env = process.env.NODE_ENV //环境参数
//配置
let MYSQL_CONF
if ( env === 'dev' ) {
MYSQL_CONF = {
host:"localhost",
port:"3306",
user:"root",
password:"xxx",
database:"myBlogs"
}
}
if ( env === 'production' ) {
MYSQL_CONF = {
host:"xxx.xxx.xxx.xxx",
port:"3306",
user:"root",
password:"xxx",
database:"myBlogs"
}
}
module.exports = {
MYSQL_CONF
}# 创建db/mysql.js 启动node和mysql建立连接 封装统一执行SQL的函数doSQL
const mysql = require('mysql');
const { MYSQL_CONF } = require('./conf');
const connection = mysql.createConnection(MYSQL_CONF);
connection.connect();
//统一执行sql函数
function doSQL(sql){
const promise = new Promise((resolve,reject)=>{
connection.query(sql,(err,res)=>{
if(err) reject(err);
resolve(res);
})
});
return promise;
}
module.exports = {
doSQL
}# controllers/blogs.js 使用SQL等数据库操作,对假数据进行替换 --- 博客列表
const { doSQL } = require('../db/mysql');
/**
* 获取博客列表数据 GET
* http://localhost:8000/api/blog/list
* http://localhost:8000/api/blog/list?author=zhangsan
* http://localhost:8000/api/blog/list?author=zhangsan&keyword=A
*/
const getList = (author,keyword) => {
console.log(author,keyword);
let sql = `select * from blogs where 1=1`
if(author){
sql += ` and author='${author}'`
}
if(keyword){
sql += ` and title like '%${keyword}%'` //注意and 前面的空格
}
sql += ` order by 'createtime' desc;` //注意order 前面的空格
//返回promise
return doSQL(sql);
}
/**
* 根据id获取单个博客详情 GET
* http://localhost:8000/api/blog/detail?id=1
* http://localhost:8000/api/blog/detail?id=2
*/
const getDetail = (id) => {
let sql = `select * from blogs where id=${id}`;
return doSQL(sql);
}
/**
* 创建新的博客内容 POST
* http://localhost:8000/api/blog/new
* body 内容
{
"title":"博客xx",
"content":"内容xx"
}
*/
const newBlog = (blogData = {}) => {
const { title,content,author} = blogData;
let createtime = Date.now();
let sql = `insert into blogs (title,content,createtime,author) values ('${title}','${content}',${createtime},'${author}');` //注意执行顺序
return doSQL(sql).then(insertData=>{
console.log('insertData==>',insertData);
/*OkPacket {
fieldCount: 0,
affectedRows: 1,
insertId: 3,
serverStatus: 2,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 0 }*/
return {
id:insertData.insertId //通过insertId来判断是否加载成功
}
});
}
/**
* 创建新的博客内容 POST
* http://localhost:8000/api/blog/update?id=3
* body 内容
{
"title":"博客xxxx",
"content":"内容xxxxx"
}
*/
const updateBlog = (id,blogData = {}) => {
// id 要更新的博客id
// blogData 博客对象 包含title content对象
console.log('update blog',id,blogData);
const { title , content } = blogData;
let sql = `update blogs set title='${title}', content='${content}' where id=${id};` //注意 ,空格 content='${content}'
return doSQL(sql).then(updateRes=>{
if(updateRes.affectedRows > 0){ //根据affectedRows>0来判断是否更新成功
return true;
}
return false;
})
}
/**
* 创建新的博客内容 POST
* http://localhost:8000/api/blog/del?id=3
*/
const delBlog = (id,author) => {
//id 要删除的博客id
let sql = `delete from blogs where id='${id}' and author='${author}';` //安全机制,保证作者一致
return doSQL(sql).then(updateRes=>{
if(updateRes.affectedRows > 0){ //根据affectedRows>0来判断是否删除成功
return true;
}
return false;
})
}
module.exports = {
getList,
getDetail,
newBlog,
updateBlog,
delBlog
}# controllers/user.js 使用SQL等数据库操作,对假数据进行替换 --- 用户列表
const { doSQL } = require('../db/mysql');
/**
* 用户登录注册
* http://localhost:8000/api/user/login
*
{
"username":"lisi",
"password":"45655"
}
*/
const loginCheck = (username,password) => {
let sql = `select username,realname from users where username='${username}' and password='${password}';`
return doSQL(sql).then(usersArr => {
return usersArr[0] || {}
})
}
module.exports = {
loginCheck
}# routers/blogs 对数据进行路由返回处理 -- 博客列表
const {
getList,
getDetail,
newBlog,
updateBlog,
delBlog
} = require('../controllers/blog')
const { SuccessModel,ErrorModel } = require('../models')
const handleBlogRouter = (req,res) =>{
const method = req.method;
const path = req.path;
const query = req.query;
const id = query && query.id;
//获取博客列表 /api/blog/list
if( method==="GET" && path === "/api/blog/list"){
const { author , keyword } = query;
// if(author && keyword){
// // const resData = getList(author,keyword);
// // return new SuccessModel(resData);
// return getList(author,keyword).then(resData=>{
// return new SuccessModel(resData);
// })
// }else {
// return new ErrorModel('获取博客列表失败,请传正确的参数 author & keyword');
// }
return getList(author,keyword).then(resData=>{
return new SuccessModel(resData);
})
}
//获取一篇博客的内容 /api/blog/detail
if( method==="GET" && path === "/api/blog/detail"){
if(id){
return getDetail(id).then(resData=>{
return new SuccessModel(resData);
})
}
}
//新增一篇博客 /api/blog/new
if( method==="POST" && path === "/api/blog/new"){
req.body.author = "zhangsan" ; //假数据,TODO:登录注册
return newBlog(req.body).then(resData=>{
return new SuccessModel(resData);
});
}
//更新一篇博客 /api/blog/update
if( method==="POST" && path === "/api/blog/update"){
return updateBlog(id,req.body).then(resData=>{
if(resData){
return new SuccessModel('更新博客成功');
}else {
return new ErrorModel('更新博客失败')
}
});
}
//删除一篇博客 /api/blog/del
if( method==="POST" && path === "/api/blog/del"){
req.body.author = "zhangsan" ; //假数据,TODO:登录注册
return delBlog(id,req.body.author).then(resData=>{
if(resData){
return new SuccessModel('删除博客成功');
}else{
return new ErrorModel('删除博客失败');
}
});
}
}
module.exports = handleBlogRouter;# routers/users 对数据进行路由返回处理 -- 用户列表
const { loginCheck } = require('../controllers/user');
const { SuccessModel,ErrorModel } = require('../models')
const handleUserRouter = (req,res) =>{
const method = req.method;
const path = req.path;
//获取博客列表 /api/user/login
if( method==="POST" && path === "/api/user/login"){
const { username,password } = req.body;
return loginCheck(username,password).then(loginRes => {
if(loginRes.username){
return new SuccessModel('登陆成功!');
}
return new ErrorModel('登录失败!');
});
}
}
module.exports = handleUserRouter;# app.js 对结果进行前端返回
const queryString = require('querystring');
const handleBlogRouter= require('./routers/blog');
const handleUserRouter = require('./routers/user');
//解析post data,返回promise对象
const getPostData = (req) => {
const promise = new Promise((resolve,reject) => {
if(req.method === "POST" && req.headers['content-type'] === 'application/json'){
let postData = "";
req.on('data',chunk => postData += chunk.toString());
req.on("end",()=>{
if(postData){
resolve(JSON.parse(postData));
}else{
resolve({});
return;
}
})
}else{
resolve({});
return;
}
});
return promise;
}
const serverHandle = (req,res) => {
//设置返回的JSON
res.setHeader('Content-type','application/json');
//获取path
const url = req.url;
req.path = url.split('?')[0];
req.query = queryString.parse(url.split('?')[1])
//解析post数据
getPostData(req).then(postData => {
req.body = postData;
//处理博客的路由
// const blogData = handleBlogRouter(req,res);
// if(blogData){
// res.end(JSON.stringify(blogData));
// }
const blogRes = handleBlogRouter(req,res);
if(blogRes){
blogRes.then(blogData=>{
res.end(JSON.stringify(blogData));
});
return;
}
//处理用户登录的路由
// const userData = handleUserRouter(req,res);
// if(userData){
// res.end(JSON.stringify(userData));
// }
const userRes = handleUserRouter(req,res);
if(userRes){
userRes.then(userData=>{
res.end(JSON.stringify(userData));
});
return;
}
})
}
module.exports = serverHandle;1.什么是cookie
- 存储在浏览器的一段字符串(最大5k)
- 跨域不共享
- 格式如:k1=v1;k2=v2;k3=v3; 可存储结构化数据
- 每次发送http请求,会将请求域的cookie一起发送给server
- server可以修改cookie并返回给浏览器
- 浏览器中也可通过js修改cookie(限制)
2.js操作cookie,浏览器中查看cookie
- 客户端查看cookie三种方式
- Chrome控制台---> Network--->请求中的Request Header
- Chrome控制台---> Application --> Storage -->Cookies
- Chrome控制台---> Console --->输入 'document.cookie'(js查看)
- js修改cookie(有限制) - document.cookie='k1=100;' - document.cookie='k2=100;' - cookie会累加--> 结果: document.cookie --> "k1=100;k2=100;"
3.server端操作cookie,实现登录验证
- 查看cookie -- req.headers.cookie
- 修改cookie -- res.setHeader('Set-Cookie',
key1=v1;key2=v2;path=/) - 模拟实现登录验证 原理:通过get请求输入账号和密码,第一次登陆完,server端在客户端种一个cookie,如果下次再登录,Request Header里会携带上次的cookie
//在serverHandle函数里 解析cookie
req.cookie = {}; //挂载在req上
const cookieStr = req.headers.cookie || ''; // k1=v1;k2=v2;
cookieStr.split(';').forEach(item=>{
let key = item.split('=')[0] || "";
let value = item.split('=')[1] || "";
req.cookie[key] = value;
})
console.log('req.cookie',req.cookie);const { loginCheck } = require('../controllers/user');
const { SuccessModel,ErrorModel } = require('../models')
const handleUserRouter = (req,res) =>{
const method = req.method;
const path = req.path;
//获取博客列表 /api/user/login
if( method==="GET" && path === "/api/user/login"){ //这里修改为GET来模拟
// const { username,password } = req.body;
const { username,password } = req.query;
return loginCheck(username,password).then(loginRes => {
if(loginRes.username){
res.setHeader('Set-Cookie',`username=${loginRes.username};path=/;`)
return new SuccessModel('登陆成功!');
}
return new ErrorModel('登录失败!');
});
}
}
module.exports = handleUserRouter;# test URL: http://localhost:8000/api/user/login?username=zhangsan&password=123
会在浏览器种一个cookie
Response Header:
Connection: keep-alive
Content-Length: 37
Content-type: application/json
Date: Tue, 30 Jul 2019 14:11:56 GMT
Set-Cookie: username=zhangsan;path=/; # 这个是服务端种上的
# 再次请求
Request Header:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: max-age=0
Connection: keep-alive
Cookie: username=zhangsan #这里会携带上次种上的的cookie
Host: localhost:8000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
4.cookie安全性 4.1 httpOnly 场景:对于登录的lisi,如果通过前端修改document.cookie="zhangsan",会导致zhangsan的数据泄漏,为了防止这种情况,服务端需要进行安全限制,防止前端修改cookie
# 设置httpOnly
res.setHeader('Set-Cookie',`username=${loginRes.username};path=/; httpOnly`);
4.2 设置cookie过期时间 场景:对于长时间登录的状态,需要定时让用户登录,防止他人获取登录后的用户信息
//设置cookie过期时间 GMT格式
const setCookieExpires = () => {
const date = new Date();
date.setTime(date.getTime() + (24*60*60*1000));
return date.toGMTString();
}
res.setHeader('Set-Cookie',`username=${loginRes.username};path=/; httpOnly; expires=${setCookieExpires()}`);- 知道cookie的定义和特点
- 前后端如何查看和修改cookie
- 如何使用cookie实现登录验证
- 解决方案:cookie中存储userid,server端对应username,
- 原理 session,即server端存储用户信息,server端给客户端cookie种一个只有server端认识的userid,根据这个去验证username是否是一个人
# 核心实现代码 代码不分文件和执行顺序
const SESSION_DATA = {}; //存储session
//根据cookie解析session
let userId = req.cookie.userid;
let needSetCookie = false;
if(userId){ //判断是否已经有userId,来决定是否是同一个用户 或是 在前端种一个自己能识别的userid
if(!SESSION_DATA[userId]){ //表示有userid,但是没有找到对应的username等数据,清空存储当前userid用户的对象
SESSION_DATA[userId] = {};
}
} else { //在前端种一个自己能识别的userid
needSetCookie = true;
userId = `${Date.now()}_${Math.random()}`;
SESSION_DATA[userId] = {};
}
/**
* 存储当前用户的userId 数据结构是
* SESSION_DATA = {
* 1:{username:xxx,relaname:xxx},
* 2:{username:xxx,relaname:xxx},
* '1565508588565_0.41190501274115565': { username: 'zhangsan', realname: '张三' }
* }
*/
req.session = SESSION_DATA[userId];
console.log('SESSION_DATA',SESSION_DATA);
//根据变量里的userid来判断是否在前端种cookie
if(needSetCookie){
res.setHeader('Set-Cookie',`userid=${userId};path=/; httpOnly; expires=${setCookieExpires()}`);
}
//设置session,搜索数据库,将真实的用户名和userid对应
if(loginRes.username){
// res.setHeader('Set-Cookie',`username=${loginRes.username};path=/; httpOnly; expires=${setCookieExpires()}`);
req.session.username = loginRes.username;
req.session.realname = loginRes.realname;
console.log('req.session is', req.session);
return new SuccessModel('登陆成功!');
} # app.js完整代码
const queryString = require('querystring');
const handleBlogRouter= require('./routers/blog');
const handleUserRouter = require('./routers/user');
const SESSION_DATA = {}; //存储session
//设置cookie过期时间 GMT格式
const setCookieExpires = () => {
const date = new Date();
date.setTime(date.getTime() + (24*60*60*1000));
return date.toGMTString();
}
//解析post data,返回promise对象
const getPostData = (req) => {
const promise = new Promise((resolve,reject) => {
if(req.method === "POST" && req.headers['content-type'] === 'application/json'){
let postData = "";
req.on('data',chunk => postData += chunk.toString());
req.on("end",()=>{
if(postData){
resolve(JSON.parse(postData));
}else{
resolve({});
return;
}
})
}else{
resolve({});
return;
}
});
return promise;
}
const serverHandle = (req,res) => {
//设置返回的JSON
res.setHeader('Content-type','application/json');
//获取path
const url = req.url;
req.path = url.split('?')[0];
//解析query
req.query = queryString.parse(url.split('?')[1])
//解析cookie
req.cookie = {};
const cookieStr = req.headers.cookie || ''; // k1=v1;k2=v2;
cookieStr.split(';').forEach(item=>{
if(!item) return;
const arr = item.split('=');
const key = arr[0].trim() || "";
const value = arr[1].trim() || "";
req.cookie[key] = value;
})
console.log('req.cookie',req.cookie);
//根据cookie解析session
let userId = req.cookie.userid;
let needSetCookie = false;
if(userId){ //判断是否已经有userId,来决定是否是同一个用户 或是 在前端种一个自己能识别的userid
if(!SESSION_DATA[userId]){ //表示有userid,但是没有找到对应的username等数据,清空存储当前userid用户的对象
SESSION_DATA[userId] = {};
}
} else { //在前端种一个自己能识别的userid
needSetCookie = true;
userId = `${Date.now()}_${Math.random()}`;
SESSION_DATA[userId] = {};
}
/**
* 存储当前用户的userId 数据结构是
* SESSION_DATA = {
* 1:{username:xxx,relaname:xxx},
* 2:{username:xxx,relaname:xxx},
* '1565508588565_0.41190501274115565': { username: 'zhangsan', realname: '张三' }
* }
*/
req.session = SESSION_DATA[userId];
console.log('SESSION_DATA',SESSION_DATA);
//解析post数据
getPostData(req).then(postData => {
req.body = postData;
//处理博客的路由
// const blogData = handleBlogRouter(req,res);
// if(blogData){
// res.end(JSON.stringify(blogData));
// }
const blogRes = handleBlogRouter(req,res);
if(blogRes){
blogRes.then(blogData=>{
if(needSetCookie){
res.setHeader('Set-Cookie',`userid=${userId};path=/; httpOnly; expires=${setCookieExpires()}`);
}
res.end(JSON.stringify(blogData));
});
return;
}
//处理用户登录的路由
// const userData = handleUserRouter(req,res);
// if(userData){
// res.end(JSON.stringify(userData));
// }
const userRes = handleUserRouter(req,res);
if(userRes){
userRes.then(userData=>{
if(needSetCookie){
res.setHeader('Set-Cookie',`userid=${userId};path=/; httpOnly; expires=${setCookieExpires()}`);
}
res.end(JSON.stringify(userData));
});
return;
}
})
}
module.exports = serverHandle;# user.js完整代码
const { loginCheck } = require('../controllers/user');
const { SuccessModel,ErrorModel } = require('../models');
//设置cookie过期时间 GMT格式
const setCookieExpires = () => {
const date = new Date();
date.setTime(date.getTime() + (24*60*60*1000));
return date.toGMTString();
}
const handleUserRouter = (req,res) =>{
const method = req.method;
const path = req.path;
//获取博客列表 /api/user/login
if( method==="GET" && path === "/api/user/login"){
// const { username,password } = req.body;
const { username,password } = req.query;
return loginCheck(username,password).then(loginRes => {
if(loginRes.username){
// res.setHeader('Set-Cookie',`username=${loginRes.username};path=/; httpOnly; expires=${setCookieExpires()}`);
req.session.username = loginRes.username;
req.session.realname = loginRes.realname;
console.log('req.session is', req.session);
return new SuccessModel('登陆成功!');
}
return new ErrorModel('===>登录失败!');
});
}
//测试:通过session来登录验证
if( method==="GET" && path === "/api/user/login-test"){
console.log('login-test,req.session==>',req.session);
console.log('login-test,req.cookie==>',req.cookie);
if(req.session.username){
return Promise.resolve(
new SuccessModel({
session:req.session
})
);
}
return Promise.resolve(new ErrorModel('登录失败===>!'));
}
}
module.exports = handleUserRouter;目前session 直接是js变量,放在nodejs进程内存中(Node进程有限)
- 第一,进程内存有限,访问量过大,内存暴增怎么办?(用户访问如果增多)
- 第二,正式线上运行是多进程的,进程之间内存无法共享(当前用户1的数据存在进程1,但是如果下次访问用户1,进到里进程2,进程2不存在用户1的数据,就会导致进程2重复创建数据)
解决方案--redis
- web server最常用的缓存数据库,数据存储在内存中
- 相比于mysql,访问速度快(内存和硬盘数据库不是一个数量级)
- 但成本更高,可存储的数据量更小(内存的硬伤)
- 将web server和redis拆分成两个单独的服务(关键!!!)
- 双方都是独立的,都是可扩展的(例如 都扩展成集群)
- 包括mysql,也是一个单独的服务,也可扩展
为什么session适用redis?
- session访问频繁,对性能要求极高
- session可不考虑断电丢失数据的问题
- session数据量不会太大
为什么网站数据不适合用redis?
- 操作频率不是太高
- 断电不能丢失,必须保留
- 数据量太大,内存成本太高
windows安装redis
Mac安装 brew install redis
# redis使用
redis-server #启动服务
redis-cli # 新建窗口
set key1 value1 # 设置key:value
get key1 # 获取key的值
keys * # 获取所有key
del key1 # 删除当前keynpm install redis --save
const redis = require('redis');
//创建客户端
const redisClient = redis.createClient(6379,'127.0.0.1');
redisClient.on('error',err=>{
console.log(err);
});
//test-设置值,取值
redisClient.set('myname','zhangsan2',redis.print);
redisClient.get('myname',(err,val)=>{
if(err){
console.error(err);
return;
}
console.log('val',val);
redisClient.quit();
});node index.js # 通过node连接redis 读写操作
Reply: OK
val zhangsan2
# redis客户端查看是否生成key
redis-cli
127.0.0.1:6379> keys *
1) "myname"
##### db/conf.js #####
let REDIS_CONF;
if ( env === 'dev' ) {
REDIS_CONF = {
host:'127.0.0.1',
port:6379
}
}
if ( env === 'production' ) {
REDIS_CONF = {
host:'127.0.0.1',
port:6379
}
}
module.exports = {
REDIS_CONF
}
##### db/redis.js 封装get & set 方法 #####
const redis = require('redis');
const { REDIS_CONF } = require('./conf');
const { port , host } = REDIS_CONF;
//创建客户端
const redisClient = redis.createClient(port,host);
redisClient.on('error',err => {
console.error(err);
});
//封装 redis set方法
function redisSet(key,val){
if(typeof val === 'object'){
val = JSON.stringify(val);
}
redisClient.set(key,val,redis.print);
}
//封装 redis get方法
function redisGet(key){
const promise = new Promise((resolve,reject) =>{
redisClient.get(key,(err,val) =>{
if(err){
reject(err);
return;
}
if(val === null){
resolve(null);
return;
}
try {
resolve(JSON.parse(val)); //try/catch 不是为了抓异常,而是兼容对象的值
}catch(e){
resolve(val);
}
});
});
return promise;
}
module.exports = {
redisSet,
redisGet
}
##### app.js #######
const { redisSet, redisGet } = require('./db/redis');
//将session存储到redis中
let userId = req.cookie.userid;
let needSetCookie = false;
if(!userId){
needSetCookie = true;
userId = `${Date.now()}_${Math.random()}`;
redisSet(userId,{});
}
// 获取当前userid对应的username,relaname
req.sessionId = userId;
redisGet(req.sessionId).then(sessionData => {
if(sessionData == null){
redisSet(req.sessionId,{});
req.session = {};
} else {
req.session = sessionData;
}
});
##### routers/user.js #####
const { redisSet } = require('../db/redis');
req.session.username = loginRes.username;
req.session.realname = loginRes.realname;
redisSet(req.sessionId,req.session);# router/blog.js
const {
getList,
getDetail,
newBlog,
updateBlog,
delBlog
} = require('../controllers/blog');
const { SuccessModel,ErrorModel } = require('../models');
// 新增:统一登录验证函数
const loginCheck = (req) => {
if(!req.session.username){
return Promise.resolve(
new ErrorModel('尚未登录')
)
}
}
//新增:验证是否登录
const loginCheckResFunc = (req) => {
const loginCheckRes = loginCheck(req);
if(loginCheckRes){
return loginCheckRes;
}
}
const handleBlogRouter = (req,res) =>{
const method = req.method;
const path = req.path;
const query = req.query;
const id = query && query.id;
//获取博客列表 /api/blog/list
if( method==="GET" && path === "/api/blog/list"){
const { author , keyword } = query;
return getList(author,keyword).then(resData=>{
return new SuccessModel(resData);
})
}
//获取一篇博客的内容 /api/blog/detail
if( method==="GET" && path === "/api/blog/detail"){
if(id){
return getDetail(id).then(resData=>{
return new SuccessModel(resData);
})
}
}
//新增一篇博客 /api/blog/new
if( method==="POST" && path === "/api/blog/new"){
loginCheckResFunc(req); //新增
req.body.author = req.session.username; //新增,不再是假数据
return newBlog(req.body).then(resData=>{
return new SuccessModel(resData);
});
}
//更新一篇博客 /api/blog/update
if( method==="POST" && path === "/api/blog/update"){
loginCheckResFunc(req); //新增
return updateBlog(id,req.body).then(resData=>{
if(resData){
return new SuccessModel('更新博客成功');
}else {
return new ErrorModel('更新博客失败')
}
});
}
//删除一篇博客 /api/blog/del
if( method==="POST" && path === "/api/blog/del"){
loginCheckResFunc(req); //新增
req.body.author = req.session.username; //新增
return delBlog(id,req.body.author).then(resData=>{
if(resData){
return new SuccessModel('删除博客成功');
}else{
return new ErrorModel('删除博客失败');
}
});
}
}
module.exports = handleBlogRouter;# routers/user.js
const { loginCheck } = require('../controllers/user');
const { SuccessModel,ErrorModel } = require('../models');
const { redisSet } = require('../db/redis');
const handleUserRouter = (req,res) =>{
const method = req.method;
const path = req.path;
//获取博客列表 /api/user/login
if( method==="POST" && path === "/api/user/login"){
const { username,password } = req.body;
return loginCheck(username,password).then(loginRes => {
if(loginRes.username){
req.session.username = loginRes.username;
req.session.realname = loginRes.realname;
redisSet(req.sessionId,req.session);
return new SuccessModel('登陆成功!');
}
return new ErrorModel('登录失败!');
});
}
}
module.exports = handleUserRouter;- 登录功能依赖cookie,必须用浏览器来联调
- cookie跨域不共享,前端和server端必须同域
- 需要用到nginx做代理,让前后端同域
- admin.html
- edit.html
- login.html
- detail.html
- index.html
- new.html
npm install http-server -g
http-server -p 8001 # 创建前端静态资源服务端口号为8001,可查看前端html文件
- 前端端口:8001
- 后端端口:8000
nginx介绍
- 高性能的web服务器,开源免费
- 一般用于做静态服务,负载均衡
- 反向代理
- 正向代理:客户端控制的代理
- 反向代理:对于客户端是一个黑盒的server端代理
nginx反向代理
- 浏览器---> localhost/index.html ---> nginx ---> / ---> html
- 浏览器---> localhost/index.html ---> nginx ---> /api/... ---> nodejs
nginx下载
Windows
Mac brew install nginx
nginx配置 Windows:C:\nginx\conf\nginx.conf Mac:/usr/local/etc/nginx/nginx.conf
nginx命令
nginx -t # 测试配置文件格式是否正确
nginx -s reload # 启动nginx 重启
nginx -s stop # 停止# vim /usr/local/etc/nginx/nginx.conf
worker_processes 2; # 启动几个内核
location / {
proxy_pass http://localhost:8001;
}
location /api/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
}
# nginx -t 测试是否配置代理成功
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful- cookie是什么? session是什么?如何实现登录?
- redis扮演什么角色?有什么核心价值?
- nginx的反向代理配置,联调过程中的作用?
- 系统没有日志,就等于人没有眼睛
- 每日流量? QPS多少(QPS:每秒访问量)?
- 第一,访问日志 access log (server端最重要的日志)
- 客户端信息,页面信息
- GET/POST 请求
- 浏览器信息,占比
- 第二,自定义日志(包括自定义事件、错误记录等);
- 怎么记录日志?
- 怎么拆分日志?分析日志?
1.目录
- Nodejs 文件操作 --- Stream 流(为了节省CPU内存)
- 日志要存储到文件中 -- 需要拷贝到其他文件,减少操作成本
- 为何不存储到mysql中? -- 硬盘数据库,适合多表联动的查表操作场景
- 为何不存储到redis中? -- 内存数据库,成本太高,日志操作不频繁
- 日志功能开发和使用
- 日志文件拆分,日志内容分析
2.文件读写,是否存在
- fs.readFile
- fs.writeFile
- fs.exist
3.IO操作的性能瓶颈
- IO 包括"网络IO" 和 "文件IO"
- 相对于CPU计算和内存读写,IO的突出特点就是:慢!
- 如何在有限的硬件资源下提高IO的操作效率?
4.stream--解放CPU内存
- 标准输入输出,pipe就是管道(符合水流管道的模型图)
- process.stdin获取数据,直接通过管道传递给 process.stdout
process.stdin.pipe(process.stdout)
node index.js
### 结果 输入123 回车就是对应的内容
123
123
231231231
231231231- 文件IO操作
# 将 1.txt文件内容 复制到 2.txt
const fs = require('fs');
const path = require('path');
const filename1 = path.resolve(__dirname,'./1.txt');
const filename2 = path.resolve(__dirname,'./2.txt');
const readStream = fs.createReadStream(filename1);
const writeStream = fs.createWriteStream(filename2);
//将文件1.txt的内容通过流的方式 复制到 2.txt
readStream.pipe(writeStream);
//监听变化
readStream.on('data',chunk => {
console.log(chunk.toString());
});
readStream.on('end',() => {
console.log('copy done');
});- 网络IO操作
# 网络IO操作,将1.txt文本内容通过流返回至客户端
const http = require('http');
const fs = require('fs');
const path = require('path');
//将文件1的内容通过stream输出返回至客户端 -- 1
const filename1 = path.resolve(__dirname,'./1.txt');
const server = http.createServer((req,res)=>{
if(req.method === "GET"){
const readStream = fs.createReadStream(filename1); //2
readStream.pipe(res); //3
}
})
server.listen(8005);# utils/log.js
const fs =require('fs');
const path = require('path');
//生成write stream
function createWriteStream(fileName){
const fullFileName = path.resolve(__dirname,'./','../','logs',fileName);
const writeStream = fs.createWriteStream(fullFileName,{
flags:'a' //append 追加写日志
});
return writeStream;
}
const accessWriteStream = createWriteStream('access.log');
function writeLog(writeStream,log){
writeStream.write(log + '\n'); //关键
}
function access(log){
writeLog(accessWriteStream,log);
}
module.exports = {
access
}
# 使用
const { access } = require('./utils/log');
//记录 access log
access(`${req.method} -- ${req.url} -- ${req.headers['user-agent']} -- ${Date.now()}`);- 日志内容会慢慢积累,放在一个文件中不好处理
- 按时间划分日志文件,如 2019-08-13.access.logs
- 实现方式: linux的crontab命令,即定时任务
- 设置定时任务 格式 *****command ==> minute/hour/day/month/week command 顺序:分 时 日 月 周 shell脚本 例如
5****command 表示 每5分钟执行该命令
*1***command 表示 每天的第1个小时执行该命令
**5**command 表示 每个月5号执行该命令
***5*command 表示 5月1号执行该命令
****5command 表示 每周5执行该命令再比如
## 分钟 ##
* * * * * command 每1分钟执行一次command
3,15 * * * * command 每小时的第3和第15分钟执行
## 小时 ##
* */1 * * * /etc/init.d/smb restart 每一小时重启smb
* 23-7/1 * * * /etc/init.d/smb restart 晚上11点到早上7点之间,每隔一小时重启smb
30 21 * * * /etc/init.d/smb restart 每晚的21:30重启smb
0,30 18-23 * * * /etc/init.d/smb restart 每天18 : 00至23 : 00之间每隔30分钟重启smb
3,15 8-11 * * * command 在上午8点到11点的第3和第15分钟执行
## 日期 ##
3,15 8-11 */2 * * command 每隔两天的上午8点到11点的第3和第15分钟执行
45 4 1,10,22 * * /etc/init.d/smb restart 每月1、10、22日的4 : 45重启smb
## 月份 ##
0 4 1 jan * /etc/init.d/smb restart 一月一号的4点重启smb
## 周 ##
3,15 8-11 * * 1 command 每个星期一的上午8点到11点的第3和第15分钟执行
10 1 * * 6,0 /etc/init.d/smb restart 每周六、周日的1:10重启smb
0 11 4 * mon-wed /etc/init.d/smb restart 每月的4号与每周一到周三的11点重启smb- 将access.log拷贝并重命名为2019-11-2.access.log
- 清空access.log文件,继续积累日志
#### 编写shell脚本 ####
#!/bin/sh
cd /Users/yuxiaoyang03/Desktop/learning/Node-Blog/blog-1/logs // 到log的目录下
cp access.log $(date +%Y-%m-%d).access.log // 将access.log根据时间日期重命名
echo "" > access.log // 清空access.log
#### 设置定时任务 ####
crontab -e
#### 编写定时任务 每天凌晨0点0分执行shell脚本 ####
* 0 * * * sh /Users/yuxiaoyang03/Desktop/learning/Node-Blog/blog-1/utils/copyLog.sh
#### 查看已经设置的定时任务 ####
crontab -l
- 如针对access.log日志,分析Chrome的占比
- 日志是按行存储的,一行就是一条日志
- 使用Node的readline(基于stream,效率高)
// readline使用步骤
const readline = require('readline');
const fileName = path.join(__dirname, '../logs/access.log'); //找要分析的日志
const readStream = fs.createReadStream(fileName); // 创建readstream
const rl = readline.createInterface({
input: readStream
});// 创建readline对象
rl.on('line',()=>{});
rl.on('close',()=>{});- 将 http://localhost:8000/api/blog/list 在Chrome/Safari/Firefox里面执行几遍在access.log里产生日志内容
- 创建 utils/readline.js
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// 寻找目标日志文件
const fileName = path.join(__dirname, '../logs/access.log');
// 创建读取流
const readStream = fs.createReadStream(fileName);
// 创建readline对象
const rl = readline.createInterface({
input: readStream
});
// 计数器
let chromeNum = 0;
let sum = 0;
// 逐行读取
rl.on('line', (lineData) => {
if (!lineData) {
return;
};
sum++;
// 获取Chrome浏览器的日志信息
// GET -- /api/blog/list -- Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36 -- 1572693248592
let logArr = lineData.split(' -- '); // 根据--日志格式拆分
if (logArr[2] && logArr[2].indexOf('Chrome') > 0) {
chromeNum++;
}
});
rl.on('close', () => {
let res = parseFloat(chromeNum / sum).toFixed(2) * 100 + "%";
console.log('chrome浏览器占比:' + res);
});- 执行 node ./utils/readline.js
- 日志对于server端很重要,就像眼睛
- IO性能瓶颈,使用stream提高性能
- 使用crontab拆分日志文件
- 使用readline分析日志内容
- SQL注入:窃取数据库内容
- XSS攻击:窃取前端的cookie的内容
- 密码加密:保护用户信息安全(重要!!)
- 最原始、最简单的攻击,从web2.0就有了
- 攻击方式:输入一个sql片段,最终拼接成一段攻击代码
- 预防措施:使用mysql的escape函数处理输入内容即可
# 正常的sql
select username,realname from users where username='zhangsan'and password='123';
# 如果在用户名处输入 “zhangsan' --” 就会导致sql注入
select username,realname from users where username='zhangsan' -- 'and password='123';
# 这样加上“'zhangsan;delete from users -- ”就完蛋了 users表就没了
select username,realname from users where username='zhangsan';delete from users -- 'and password='123';
# 防范 -- 利用mysql.escape()
username = mysql.escape(username)
password = mysql.escape(password)
## escape转义了符号 所以达到了防止sql注入的功能
select username,realname from users where username='zhangsan\' -- 'and password='123';
- 前端同学最熟悉的攻击方式,但server端更应该掌握
- 攻击方式:在页面展示内容中掺杂js代码,以获取网页信息
- 预防措施:转换生成js的特殊字符
// xss攻击 往文本框里输入js代码
<script>alert(document.cookie)</script>
// 转换特殊字符
& --> &
< --> ⁢
> --> >
" --> "
' --> '
/ --> /
// 可以安装xss工具包
npm i xss --save
title = xss(title);
content = xss(content);
- 数据库被用户攻破,最不应该泄漏的就是用户信息
- 攻击方式:获取用户名和密码,再去尝试登录其他系统
- 预防措施:将密码加密,即便拿到密码也不显示明文
// 原理: 自定义的key + 密码 --> 加密的戳
let SELECT_KEY = 'AbcD_123123##';
let content = `password=${password}&key=${SELECT_KEY}`;
let md5Pass = crypto.createHash('md5').update(content).digest('hex'); //最终加密的密码
// 将zhangsan的密码更新为加密之后的'eb0c2f011e4adb48ad2802321a8c132f'
// 更换密码长度 右击users表 --> Alter Table --> VARCHAR(20) 改为 VARCHAR(32)
-
开发了哪些功能模块,完整的流程
- 功能模块
- 处理http接口
- 连接数据库
- 实现登录
- 安全
- 日志
- 上线
- 流程 浏览器 ---> nginx ---> /.. ---> 静态文件html/css/js/img ---> /api/.. ---> 日志记录(日志文件)/路由处理/登陆校验(redis)/用户信息(redis)/数据处理(mysql)
- 功能模块
-
用到了哪些核心知识点
- http,nodejs处理http、处理路由、mysql
- cookie、session、redis、nginx反向代理
- 安全知识:sql注入、xss攻击、密码加密
- 日志、stream、contrab、readline
-
回顾 server和前端的区别
- 服务稳定性
- 内存CPU优化扩展
- 日志记录
- 安全(包括登录验证)
- 集群和服务拆分
- Nodejs最常用的web server
- 下载、安装、使用、express中间件机制
- 开发接口、连接数据库、实现登录、日志记录
- 分析express中间件原理
- 安装--express-generator脚手架
- sudo npm i express-generator -g
- express blog-express
- npm i & npm start
- npm i cross-env nodemon -D
- 初始化代码介绍、处理路由
- 使用中间件
- 各个插件的作用
- 思考更插件的实现原理
- 处理get/post请求
- app.use(中间件函数)
- app.get(中间件函数)
- app.post(中间件函数)
- npm i express-session
// app.js
const session = require('express-session');
app.use(session({
secret: 'AAAbbb_123##',
cookie: {
path: '/', //默认
httpOnly: true,//默认
maxAge: 24 * 60 * 60 * 100
}
}));// user.js
/** 测试session是否成功
* http://localhost:3000/api/user/session-test
* 通过在不同浏览器的返回可以看到不同的数字,说明session配置成功
*/
router.get('/session-test',(req,res,next)=>{
const session = req.session;
if(!session.vieNum){
session.vieNum = 0
}
session.vieNum ++;
res.json({
vieNum:session.vieNum
})
})原理:登陆完在服务端记录session用户信息,在通过login-test验证是否已登录
router.get('/login-get', function(req, res, next) {
const { username,password } = req.query;
return loginCheck(username,password).then(loginRes => {
if(loginRes.username){
req.session.username = loginRes.username;
req.session.realname = loginRes.realname;
res.json(new SuccessModel('登陆成功!'));
return;
}
res.json(new ErrorModel('登录失败!'));
});
});
/** 测试/login是否成功
* http://localhost:3000/api/user/login-get?username=zhangsan&password=123 这里用get模拟post登录
* http://localhost:3000/api/user/login-test 测试是否已登录
*/
router.get('/login-test',(req,res,next)=>{
const session = req.session;
if(session.username){
res.json({
res:'已登录'
})
return
}
res.json({
res:'登录失败'
})
});// 实际登录使用的是post
router.post('/login', function(req, res, next) {
const { username,password } = req.body;
return loginCheck(username,password).then(loginRes => {
if(loginRes.username){
req.session.username = loginRes.username;
req.session.realname = loginRes.realname;
res.json(new SuccessModel('登陆成功!'));
return;
}
res.json(new ErrorModel('登录失败!'));
});
});- npm i redis connect-redis
// 1 获取redis客户端 db/redis.js
const redis = require('redis');
const { REDIS_CONF } = require('./conf');
const { port , host } = REDIS_CONF;
//创建客户端
const redisClient = redis.createClient(port,host);
redisClient.on('error',err => {
console.error(err);
});
module.exports = redisClient;// app.js 连接redis的过程
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redisClient = require('./db/redis');
const sessionStore = new RedisStore({
client:redisClient
})
app.use(session({
store:sessionStore
}))- 测试redis中是否已经存储了用户的key
redis-cli
访问 http://localhost:3000/api/user/login-get?username=zhangsan&password=123
keys *
127.0.0.1:6379> keys *
1) "sess:VKNIoVD6i8u6pTgksuzfCBOQEU67YDJt" # 表示存进去了
# 获取用户信息
get sess:VKNIoVD6i8u6pTgksuzfCBOQEU67YDJt
"{\"cookie\":{\"originalMaxAge\":8640000,\"expires\":\"2019-11-03T11:49:33.851Z\",\"httpOnly\":true,\"path\":\"/\"},\"username\":\"zhangsan\",\"realname\":\"\xe5\xbc\xa0\xe4\xb8\x89\"}" ---> 用户信息const ENV = process.env.NODE_ENV
if(ENV !== 'production'){
// 解析开发环境 日志
app.use(logger('dev'));
// GET /api/blog/list 304 10.089 ms - - 日志格式
}else{
// 解析线上环境 日志
const logFile = path.join(__dirname,'./logs/access.log');
const writeStream = fs.createWriteStream(logFile,{
flags:'a'
});
app.use(logger('combined',{
stream:writeStream
}));
//::1 - - [03/Nov/2019:11:18:04 +0000] "GET /api/blog/list HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36"
}- express中间件是异步回调,koa2原生支持async/await
- 新框架开发框架和系统,都基于koa2,例如egg.js
- aysnc/await 语法介绍 安装和介绍koa2
- 开发接口,连接数据库,实现登录,日志记录
- 分析koa2中间件原理
- aysnc 函数执行返回promise对象
- await 后 可执行promise对象
- await 必须包裹在 async函数内
- try-catch可以截获promise中reject的值
async function getRes() {
try {
const aData = await getFileContent('1.json');
console.log(aData);
const bData = await getFileContent(aData.next);
console.log(bData);
const cData = await getFileContent(bData.next);
console.log(cData);
} catch (err) {
console.error(err);
}
}
getRes()- 安装 使用koa-generator
- npm i koa-generator
- Koa2 my-test
- npm i & npm run dev
- npm i cross-env 脚手架没有配置环境参数,需要手动配置
"dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
"prd": "cross-env NODE_ENV=production pm2 start bin/www",
-
初始化代码 处理路由
- ctx 上下文 --> req,res的集合
- 处理路由 访问 http://localhost:8000/api/user
// routes/user.js const router = require('koa-router')() router.prefix('/api/user') router.get('/', async (ctx, next) => { ctx.body ={ title:'123' } }) module.exports = router
// app.js const user = require('./routes/user'); // 引入路由文件 app.use(user.routes(), user.allowedMethods()); // 注册路由
-
使用中间件
-
实现登录
- koa-gengeric-session 和 koa-redis
- npm i koa-generic-session koa-redis redis
// app.js const session = require('koa-generic-session'); const redisStore = require('koa-redis'); // connect redis to save session info app.keys = ['AAAbbb_123##']; app.use(session({ store: redisStore({ all:'127.0.0.1:6379' //本地写死 }), // 配置cookie cookie: { path: '/', httpOnly: true, maxAge: 24 * 60 * 60 * 1000 } }))
- redis-server 开启redis服务
- 编写测试session的代码
// routes/user.js router.get('/session-test', async (ctx, next) => { if (ctx.session.vieNum == null) { ctx.session.vieNum = 0 } ctx.session.vieNum++; ctx.body = { vieNum: ctx.session.vieNum } })
- 执行http://localhost:8000/api/user/session-test在浏览器中测试
- 在redis中查看是否存入
redis-cli
127.0.0.1:6379> keys *
- "koa:sess:e9n3c4mZ8zvjmkAr_FXGPhcT-DM9GVb_" 127.0.0.1:6379> get "koa:sess:e9n3c4mZ8zvjmkAr_FXGPhcT-DM9GVb_" "{"cookie":{"path":"/","httpOnly":true,"maxAge":86400000,"overwrite":true,"signed":true},"vieCount":1,"vieNum":3}"
-
开发接口
- user用户登录有关
- http://localhost:8000/api/user/login 用户登录 { "username":"lisi", "password":"45655" }
- blog相关
- http://localhost:8000/api/blog/list 获取博客列表数据
- http://localhost:8000/api/blog/detail?id=1 根据id获取单个博客详情
- http://localhost:8000/api/blog/new 创建新的博客内容 POST { "title":"博客xx", "content":"内容xx" }
- http://localhost:8000/api/blog/update?id=2 更新博客内容 POST { "title":"博客xxxx", "content":"内容xxxxx" }
- http://localhost:8000/api/blog/del?id=3 根据id删除博客内容 POST
- user用户登录有关
-
记录日志 -- 继续使用morgan
- 使用koa-smorgan
- 自定义日志使用console.log和console.error
- 日志文件拆分
- 日志内存分析
POST /api/blog/update?id=2 - 34ms --> POST /api/blog/update?id=2 200 39ms 42b <-- GET /api/blog/list GET /api/blog/list - 2ms --> GET /api/blog/list 200 7ms 231b
const logger = require('koa-logger')
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})- npm i koa-morgan
// app.js
const fs = require('fs');
const path = require('path');
const morgan = require('koa-morgan');
// use koa-mogran to save logs
const ENV = process.env.NODE_ENV
if (ENV !== 'production') { // 解析开发环境 日志 格式:GET /api/blog/list 200 8.058 ms - 343
app.use(morgan('dev'));
}
else { // 解析线上环境 日志
const logFile = path.join(__dirname, './logs/access.log');
const writeStream = fs.createWriteStream(logFile, {
flags: 'a'
});
app.use(morgan('combined', {
stream: writeStream
}));
// 最终线上的日志格式 ::1 - - [06/Nov/2019:13:29:19 +0000] "GET /api/blog/list HTTP/1.1" 200 343 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36"
}// test09-koa-test
/** koa -- 洋葱卷模型
1.....第一层洋葱--开始...
3.......第二层洋葱--开始.
5.......第三层洋葱--开始
6.......第三层洋葱--结束
4.....第二层洋葱--结束...
2.....第一层洋葱--结束...
*/
- 1.app.use注册中间件 进行收集
- 2.实现next机制,上一个通过next触发下一个
- 3.不涉及method和path
- 服务器稳定性
- 充分利用服务器硬件资源,以便提高性能
- 线上日志记录
- 进程守护,系统崩溃自动重启
- 启动多进程,充分利用CPU和内存
- 自带日志记录功能
-
- pm2 start app.js
-
- pm2 list
-
- pm2 --version
-
- pm2 restart /
- 如果修改了服务里面的内容,重启服务可以直接使用 pm2 restart 0 / app Use --update-env to update environment variables [PM2] Applying action restartProcessId on app [0](ids: [ '0' ]) [PM2] app ✓
┌────┬─────────────────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app │ 1.0.0 │ fork │ 53536 │ 0s │ 1 │ online │ 0% │ 13.1mb │ yux… │ disabled │
└────┴─────────────────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────┘
-
- pm2 stop / [PM2] Applying action stopProcessId on app [app](ids: [ 0 ]) [PM2] app ✓
┌────┬─────────────────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app │ 1.0.0 │ fork │ 0 │ 0 │ 5 │ stopped │ 0% │ 0b │ yux… │ disabled │
└────┴─────────────────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────┘
-
- pm2 delete / [PM2] Applying action deleteProcessId on app [app](ids: [ 0 ]) [PM2] app ✓
-
- pm2 info / 查看服务的信息
-
- pm2 log / 查看日志
- 0|app | server is listen on port 7000
0|app | cur time 1573129491818
0|app | error 1573129491818
0|app | cur time 1573129491931
0|app | error 1573129491931
0|app | cur time 1573129493350
0|app | error 1573129493350
0|app | cur time 1573129493402
0|app | error 1573129493402
- cat /Users/.../.pm2/logs/app-error.log 查看错误日志
- cat /Users/.../.pm2/logs/app-out.log 查看输出日志
-
- pm2 monit / 查看当前通过pm2运行的进程的状态,比如内存信息,日志等
┌─ Process List ─────────────────────────────────────┐┌── app Logs ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│[ 0] app Mem: 31 MB CPU: 0 % online ││ app > cur time 1573129746959 │
│ ││ app > error 1573129746960 │
│ ││ app > cur time 1573129747028 │
│ ││ app > error 1573129747028 │
│ ││ app > cur time 1573129747806 │
│ ││ app > error 1573129747806 │
│ ││ app > cur time 1573129747846 │
| ││ app > error 1573129747846
// 模拟错误
if(req.url === '/err'){
throw new Error('error')
}重启pm2 pm2 restart 0 访问 http://localhost:7000/err 页面挂掉 pm2 list
┌────┬─────────────────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app │ 1.0.0 │ fork │ 54999 │ 0s │ 3 │ online │ 0% │ 10.8mb │ yux… │ disabled │
└────┴─────────────────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────┘
┌────┬─────────────────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app │ 1.0.0 │ fork │ 55041 │ 6s │ 5 │ online │ 2.3% │ 31.2mb │ yux… │ disabled │
└────┴─────────────────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────┘
发现restart由3 --> 5
- 新建PM2配置文件(包括进程数量,日志文件目录等)
- 修改PM2启动命令,重启
- 访问server,检查日志文件的内容(日志记录是否生效)
{
"apps":{
"name":"pm2-test-server",
"script":"app.js",
"watch":true,
"ignore_watch":[
"node_modules",
"logs"
],
"error_file":"logs/err.log", //修改日志目录
"out_file":"logs/out.log",
"log_date_format":"YYYY-MM-DD HH:mm:ss"
}
}// package.json
"prd:conf":"cross-env NODE_ENV=production pm2 start pm2.conf.json"- 操作系统限制一个进程的内存
- 内存;无法充分利用机器全部内存
- CPU: 无法充分利用多核CPU的优势
- 多进程之间,无法实现内存共享
- 解决: 多进程访问同一个redis,实现数据共享
// 添加 instances属性
{
"apps":{
"name":"pm2-test-server",
"script":"app.js",
"watch":true,
"ignore_watch":[
"node_modules",
"logs"
],
"error_file":"logs/err.log",
"out_file":"logs/out.log",
"log_date_format":"YYYY-MM-DD HH:mm:ss",
"instances":4
}
}npm run prd:conf
┌────┬─────────────────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ pm2-test-server │ 1.0.0 │ cluster │ 56926 │ 27s │ 0 │ online │ 0% │ 33.3mb │ yux… │ enabled │
│ 1 │ pm2-test-server │ 1.0.0 │ cluster │ 56927 │ 27s │ 0 │ online │ 0% │ 33.0mb │ yux… │ enabled │
│ 2 │ pm2-test-server │ 1.0.0 │ cluster │ 56932 │ 27s │ 0 │ online │ 0% │ 32.0mb │ yux… │ enabled │
│ 3 │ pm2-test-server │ 1.0.0 │ cluster │ 56935 │ 27s │ 0 │ online │ 0.3% │ 33.2mb │ yux… │ enabled │
└────┴─────────────────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────
- 服务器运维,有专业的OP人员来处理
- 大公司有自己的运维
- 中小型公司,推荐使用阿里云等云服务