Fork me on GitHub

nodejs笔记

文章概述

本篇文章记录nodejs的笔记。

开发环境

安装nodejs

下载安装
  • nodejs官网下载推荐版本,安装时记得添加到环境变量(Add to Path);
  • nodejs自带npm,是Node.js的包管理工具;
查看安装的版本

执行如下命令,会显示相应版本,说明安装成功

1
2
3
4
// node的版本
$ node -v
// npm的版本
$ npm -v

语法提示

类型定义文件

安装类型定义文件类型定义文件配置node支持,来支持node的语法提示;

用npm命令配置到package.json里;

1
$ npm install --save-dev @types/node
webstorm
1
file->settings->Node.js and NPM ->coding assistance->enable

npm

npm是nodejs自带的包管理工具,用于下载和管理工具包,并配置到config.json文件中;

更新

1
$ npm i npm@latest -g

npm相关配置

1
2
# 【查看默认配置】
$ npm config ls
配置全局安装目录

1> 为prefix指定一个本地目录:

1
$ npm config set prefix E:\pathName

2> 配置环境变量

1
windows系统在环境变量path中追加E:\ProgramFile\dev\nodejs\npm_global_modules
配置国内镜像

不推荐配置,配置镜像后有些包下载不下来

还原默认镜像地址

npm默认的配置如下,如果配置了一些国内镜像后,下载包出问题,请设置默认配置:

1
2
3
4
# 【registry】
$ npm config set registry https://registry.npmjs.org/
# 【distUrl】
$ npm config set disturl undefined

config.json

npm命令生成config.json文件:

1
2
// 命令生成,会有一些配置,按提示默认填写即可;
$ npm init

config.json配置文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "webtechstack",
"version": "1.0.0",
"description": "test",
// dependencies里的插件是需要发布到生产环境的。
"dependencies": {},
// devDependencies里的插件只用于开发环境,不用于生产环境。
"devDependencies": {
"babel-cli": "^6.26.0"
},
// 使用npm run x执行的脚本
"scripts": {},
"repository": {
"type": "git",
"url": "https://gitee.com/cnlius/WebTechStack.git"
},
"author": "jason"
}

包安装命令

npm可以安装的工具包可在npmjs网站搜索;

npm命令安装工具包,一般安装的工具包信息会配置到config.json文件的dependencies(会发布到生产环境)配置项或devDependencies(仅用于开发环境)配置项。

  • 安装最新版本的工具包,如安装koa工具包
1
2
3
4
5
6
7
8
9
10
11
// 写法一:
// 安装到dependencies:
$ npm install koa --save
// 安装到devDependencies:
$ npm install koa --save-dev

// 写法二:
// 安装到dependencies:
$ npm install --save koa
// 安装到devDependencies:
$ npm install --save-dev koa
  • 安装指定版本的工具包:
1
$ npm install --save koa@2.8.1
简写命令
1
2
3
npm i 简写为:npm install
--save 简写为:-S;
--save-dev 简写为:-D;

yarn包管理

Yarn是Facebook提供的替代npm的工具,可以加速node模块的下载。React Native的命令行工具用于执行创建、初始化、更新项目、运行打包服务(packager)等任务。

安装与更新

1
2
3
4
//npm安装yarn
$ npm install -g yarn
//npm更新yarn
$ npm install yarn@latest -g

npm相关配置

1
2
# 【查看默认配置】
$ yarn config ls
配置国内镜像

不推荐配置,配置镜像后有些包下载不下来

还原默认镜像地址

yarn默认的配置如下,如果配置了一些国内镜像后,下载包出问题,请设置默认配置:

1
2
3
4
# 【registry】
$ yarn config set registry https://registry.yarnpkg.com
# 【distUrl】
$ yarn config set disturl undefined

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
//初始化package.json文件(=npm init)
$ yarn init
//安装package.json文件配置的工具包到node_module(=npm install)
$ yarn
//对比npm:
//安装工具库
$ yarn global add x@v.v.v // npm i x@v.v.v -g
$ yarn add x@v.v.v // npm i x@v.v.v --save
$ yarn add x@v.v.v --dev // npm i x@v.v.v --save-dev
//卸载工具库
$ yarn remove x //npm uninstall x --save(-dev)
//执行package.json文件的script脚本配置项
$ yarn run x // npm run x

nodejs简介

在命令行模式下,可以执行node进入Node交互式环境,也可以执行node hello.js运行一个.js文件。

执行js文件

node命令可以直接执行一个已经写好的js文件;

1
$ node hello.js

node命令行

输入node命令,会进入nodejs命令行交互环境,此模式下可以直接执行js代码。

模块

CommonJs规范

模块加载机制被称为CommonJS规范。在这个规范下,每个.js文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突。

模块加载实质

模块加载,实际上是将代码包装到了匿名函数内部,所以模块之间成员名互不冲突。

1
2
3
4
5
6
7
8
(function () {
// 读取的hello.js代码:
var s = 'Hello';
var name = 'world';

console.log(s + ' ' + name + '!');
// hello.js代码结束
})();

导入导出

模块可以导出一些成员,提供给其他模块导入使用;

导入
1
var greet = require('./hello');
导出

方法1:module.exports赋值(推荐);

1
可以直接对module.exports赋值;
1
2
3
4
5
6
7
8
9
10
11
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
module.exports = {
hello: hello,
greet: greet
};

方法二:直接使用exports;

1
2
3
4
5
6
7
8
9
10
11
12
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
function hello() {
console.log('Hello, world!');
}
exports.hello = hello;
exports.greet = greet;
1
2
3
4
5
6
【注意】
不可以直接对exports赋值,类似下面这样是不允许的:
exports = {
hello: hello,
greet: greet
};

综合示例

以下hello模块导出成员提供给world模块使用;

hello.js模块

1
2
3
4
5
6
'use strict';
let title = 'Hello';
function greet(name) {
console.log(title + ', ' + name + '!');
}
module.exports = greet;

world.js模块

1
2
3
4
5
'use strict';
// 引入hello模块:
let greet = require('./hello');
let name='world';
greet(name);

基本模块

global

  • JavaScript有且仅有一个全局对象,在浏览器中,叫window对象。
  • node.js环境中也有一个全局对象叫global。
console
1
node命令行输入global.console会直接打印出所有控制台输出方法;
文件相关
  • __dirname:全局变量,存储的是文件所在的文件目录;
  • __filename:全局变量,存储的是全路径文件名;

process

  • process也是Node.js提供的一个对象,它代表当前Node.js进程。
  • 通过process对象可以拿到node运行环境信息.
1
2
3
4
5
6
7
8
9
10
11
> process === global.process;
true
> process.version; //node.js的版本
'v5.2.0'
> process.platform; //当前运行的系统平台
'darwin'
> process.arch; //当前系统平台架构
'x64'
> process.cwd(); //返回当前工作目录
'/Users/michael'
> process.env.NODE_ENV //node的运行环境,NODE_ENV可能是undefined,所以仅用值production用来判断是否是生产环境;
相关方法
nextTick

process.nextTick():不是立刻执行,而是要等到下一次事件循环执行,nodejs会把此方法的逻辑放到任务队列最后,等到任务执行完成再执行。

1
2
3
1> process.nextTick()方法是将某个逻辑插入到当前循环任务队列的末尾,早于setTimeout(function(){},0)执行。
2> 可以理解为setTimeout 0是在下次循环开始,而process.nextTick结束后本次循环才完成。
3> Promise.resolve()是在process.nextTick之后执行的。
1
2
3
4
5
6
7
8
9
10
// test.js
// process.nextTick()将在下一轮事件循环中调用:
process.nextTick(function () {
console.log('nextTick callback!');
});
console.log('nextTick was set!');
/* 输出:
nextTick was set!
nextTick callback!
*/
进程事件回调
  • 程序即将退出时的回调;
1
2
3
process.on('exit', function (code) {
console.log('about to exit with code: ' + code);
});

js运行环境

  • 很多JavaScript代码既能在浏览器中执行,也能在Node环境执行,但有些时候,程序本身需要判断自己到底是在什么环境下执行的,常用的方式就是根据浏览器和Node环境提供的全局变量名称来判断:
1
2
3
4
5
if (typeof(window) === 'undefined') {
console.log('node.js');
} else {
console.log('browser');
}

异步文件操作库mz

mz仓库地址

介绍

mz提供的API和Node.js的fs模块完全相同,但fs模块使用回调,而mz封装了fs对应的函数,并改为Promise。这样,我们就可以非常简单的用await调用mz的函数,而不需要任何回调。

API

  • 判断文件是否存在:fs.exists(filename)

文件模块

  • Node.js内置的fs模块就是文件系统模块,负责读写文件。
  • fs模块同时提供了异步和同步的方法;

fs模块

fs模块引用:

1
let fs = require('fs');

文件相关API

Buffer对象
  • 多用于表示二进制文件;

Buffer对象和String作转换:

1
2
3
// Buffer -> String
var text = data.toString('utf-8');
console.log(text);
1
2
3
// String -> Buffer
var buf = Buffer.from(text, 'utf-8');
console.log(buf);

文件信息

  • fs.stat(),返回一个Stat对象,用于同步获取文件信息;
  • fs.statSync(),返回一个Stat对象,用于同步获取文件信息;
stat属性
  • 是否是文件:stat.isFile();
  • 是否是目录:stat.isDirectory();
  • 文件大小:stat.size;
  • 创建时间(Date对象):stat.birthtime;
  • 修改时间(Date对象): stat.mtime;
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let fs = require('fs');
fs.stat('data.txt', function (err, stat) {
if (err) {
console.log(err);
} else {
// 是否是文件:
console.log('isFile: ' + stat.isFile());
// 是否是目录:
console.log('isDirectory: ' + stat.isDirectory());
if (stat.isFile()) {
// 文件大小:
console.log('size: ' + stat.size);
// 创建时间, Date对象:
console.log('birth time: ' + stat.birthtime);
// 修改时间, Date对象:
console.log('modified time: ' + stat.mtime);
console.log('年:'+stat.mtime.getFullYear());
}
}
});
/*结果如下:
isFile: true
isDirectory: false
size: 12
birth time: Thu May 17 2018 17:46:35 GMT+0800 (中国标准时间)
modified time: Thu May 17 2018 18:22:49 GMT+0800 (中国标准时间)
2018*/

文件的读写

同步和异步读写
  • 由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。
  • 服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。
异步读文件
1
2
3
异步读取时,回调函数接收两个参数:第一个参数代表错误信息,第二个参数代表结果。
1> 当正常读取时,err参数为null,data参数为读取到的String。
2> 当读取发生错误时,err参数代表一个错误对象,data为undefined。
读取文本文件
1
2
3
4
5
6
7
8
let fs = require('fs');
fs.readFile('data.txt', 'utf-8', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
读二进制文件
  • 当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。
  • Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不同);
1
2
3
4
5
6
7
8
9
let fs = require('fs');
fs.readFile('image.jpg', function (err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
console.log(data.length + ' bytes');
}
});
同步读文件

除了标准的异步读取模式外,fs也提供相应的同步读取函数。同步读取的函数和异步函数相比,多了一个Sync后缀,并且不接收回调函数,函数直接返回结果。

  • 同步读取文件需要用try…catch捕获该错误;
1
2
3
4
5
6
7
8
let fs = require('fs');
try {
var data = fs.readFileSync('sample.txt', 'utf-8');
console.log(data);
} catch (err) {
// 出错了
console.log(err);
}
异步写文件
  • writeFile创建并写入文件;
  • writeFile()的参数依次为:文件名、数据和回调函数。
1
2
3
- writeFile传入的参数如果数据是String,默认按UTF-8编码写入文本文件;
- 如果传入的参数是Buffer,则写入的是二进制文件。
- 回调函数由于只关心成功与否,因此只需要一个err参数。
1
2
3
4
5
6
7
8
9
let fs = require('fs');
var data = 'how are you!';
fs.writeFile('data.txt', data, function (err) {
if (err) {
console.log(err);
} else {
console.log('ok.');
}
});
同步写文件
  • writeFileSync()用于同步写文件;
1
2
3
4
'use strict';
var fs = require('fs');
var data = 'Hello, Node.js';
fs.writeFileSync('output.txt', data);

文件目录

fs提供读取文件目录下所有文件的方法,同读文件类似,提供了同步和异步,方法将返回一个包含“指定目录下所有文件名称”的数组对象。;

  • fs.readdir(path):异步读;
  • fs.readdirSync(path):同步读;

stream

  • stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。
  • 流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。
  • 流的方式读写文件是一点一点儿处理文件,用过的部分会被GC,所以占内存少。
  • 所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable。
流API

读写流都通过fs模块来创建;

  1. 创建读取流:fs.createReadStream(path,options);
1
2
注意:options可以配置highWaterMark设置读取流的缓冲区大小,单位字节;
let rs = fs.createReadStream('data.txt',{highWaterMark:3,encoding:'utf-8'});
  1. 创建写入流:fs.createWriteStream(path,options);
流处理回调

流对象的处理回调事件:

  • data事件表示流的数据已经可以读取了,data事件回调可能会有很多次;
  • end事件表示这个流已经到末尾了,没有数据可以读取了;
  • error事件表示出错了;
示例
读流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';

var fs = require('fs');
// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');
rs.on('data', function (chunk) {
console.log('DATA:')
console.log(chunk);
});
rs.on('end', function () {
console.log('END');
});
rs.on('error', function (err) {
console.log('ERROR: ' + err);
});
写流
1
2
3
4
5
6
7
8
9
10
11
12
'use strict';

var fs = require('fs');
var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end();

var ws2 = fs.createWriteStream('output2.txt');
ws2.write(new Buffer('使用Stream写入二进制数据...\n', 'utf-8'));
ws2.write(new Buffer('END.', 'utf-8'));
ws2.end();

pipe

一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe。

读写文件
  • pipe管道完成读写文件的过程,实际上是文件复制的过程;
1
2
3
4
5
6
'use strict';

let fs = require('fs');
let rs = fs.createReadStream('sample.txt');
let ws = fs.createWriteStream('copied.txt');
rs.pipe(ws);
  • 默认情况下,当Readable流的数据读取完毕,end事件触发后,将自动关闭Writable流。如果我们不希望自动关闭Writable流,需要传入参数:
1
readable.pipe(writable, { end: false });

http

Node.js自带的http模块对http请求解析,http模块提供的request和response对象和http协议进行通信。

  • request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;
  • response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

相关API

request
  • response对象本身是一个Readable Stream;
属性
  • url: 请求地址;
response
  • response对象本身是一个Writable Stream;

URL模块

解析URL需要用到Node.js提供的url模块,它使用起来非常简单,通过parse()将一个字符串解析为一个Url对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';

var url = require('url');
console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));

/*结果如下:
Url {
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/path/to/file',
path: '/path/to/file?query=string',
href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash'
}
*/

文件路径模块

处理本地文件目录需要使用Node.js提供的path模块,它可以方便地构造目录:

1
2
3
4
5
6
7
8
'use strict';

var path = require('path');
// 解析当前文件目录:
var workDir = path.resolve('.'); // '/Users/michael'
// 文件路径组合: 当前目录+'pub'+'index.html':
var filePath = path.join(workDir, 'pub', 'index.html');
// '/Users/michael/pub/index.html'

HTTP服务器

一个简单的http服务器,通过http.createServer(function(request,response))方法创建;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 导入http模块:
var http = require('http');
var url = require('url');

// 创建http server,并传入回调函数:
var server = http.createServer(function (request, response) {
var urlInfo = url.parse(request.url, true);
// 根据请求url的相对路径来处理请求的逻辑
if(urlInfo.pathname === '/test') {
// 将HTTP响应200写入response
response.writeHead(200, {'Content-Type': 'text/plain'});
// 将HTTP响应的HTML内容写入response:
response.end('<h1>Hello world!</h1>');
}
}).listen(8080);

// 让服务器监听8080端口:
//server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');
server
  • server支持链式调用;
方法
  • listen(x): server监听的端口;
文件服务器

简单的http服务器处理文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
'use strict';

var
fs = require('fs'),
url = require('url'),
path = require('path'),
http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');
console.log('Static root dir: ' + root);

// 创建服务器:
var server = http.createServer(function (request, response) {
// 获得URL的path,类似 '/css/bootstrap.css':
var pathname = url.parse(request.url).pathname;
console.log("url: "+request.url);
console.log("pathname: "+pathname);
// 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
var filepath = path.join(root, pathname);
// 获取文件状态:
fs.stat(filepath, function (err, stats) {
if (!err && stats.isFile()) {
// 没有出错并且文件存在:
console.log('200 ' + request.url);
// 发送200响应:
response.writeHead(200);
// 将文件流导向response:
fs.createReadStream(filepath).pipe(response);
} else {
// 出错了或者文件不存在:
console.log('404 ' + request.url);
// 发送404响应:
response.writeHead(404);
response.end('404 Not Found');
}
});
});

server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');

http请求

  • nodejs的http模块可以直接发送网络请求,请求外部服务器;
  • http模块提供post或者是get方法来发起网络请求;
  • 查询的参数配置,使用queryString模块;
queryString模块

queryString用作http请求查询参数;

1
2
3
4
5
var querystring = require('querystring');
var postData = querystring.stringify({
'param1' : 'test',
'param2':'1'
});

GET
  1. get请求: options是带参数的全路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var http = require('http');

http.get('http://v.juhe.cn/weixin/query?key=f16af393a63364b729fd81ed9fdd4b7d&pno=1&ps=10', function (response) {
var body = [];
console.log(response.statusCode);
console.log(response.headers);
console.log(response);
response.on('data', function (chunk) {
body.push(chunk);
});

response.on('end', function () {
body = Buffer.concat(body);
console.log(body.toString());
});
});
  1. 单独配置查询参数的get请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var queryString = require('querystring');
var http = require('http');

var params = queryString.stringify({
key : 'f16af393a63364b729fd81ed9fdd4b7d',
pno:1,
ps:10
});

var options = {
hostname: 'v.juhe.cn',
path: '/weixin/query?'+params,
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
};

var req = http.get(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
res.setEncoding('utf8');
// 设置读取的大小
res.read(1024*1024*111);

res.on('data', (chunk) => {
console.log("----------request data-----------");
console.log(`BODY: ${chunk}`);

});
res.on('end', () => {
console.log("----------request end-----------");
})
});

req.on('error', (e) => {
console.error(e);
});

req.end();
POST

post请求带参数,需要用write将请求参数写入才能完成请求;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var http = require('http');

var querystring = require('querystring');

var postData = querystring.stringify({
'key' : 'f16af393a63364b729fd81ed9fdd4b7d',
'pno':'1',
'ps':10
});

var options = {
hostname: 'v.juhe.cn',
path: '/weixin/query',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};

var req = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
res.setEncoding('utf8');
// 设置读取的大小
res.read(1024*1024*111);

res.on('data', (chunk) => {
console.log("----------request data-----------");
console.log(`BODY: ${chunk}`);

});
res.on('end', () => {
console.log("----------request end-----------");
})
});

req.on('error', (e) => {
console.log(`problem with request: ${e.message}`);
});

// 需要写入请求参数,才能执行请求
req.write(postData);
req.end();

加解密

Nodejs用C/C++实现通用的加密和哈希算法通过cypto模块暴露为js接口,运行速度快。

MD5和SHA1

MD5和sha1是常用的哈希算法,用于给任意数据一个“签名”,签名结果通常用一个十六进制的字符串表示;

  • update()方法默认字符串编码为UTF-8,也可以传入Buffer。
  • 如果要计算SHA1,只需要把’md5’改成’sha1’,还可以使用更安全的sha256和sha512。
1
2
3
4
5
6
7
8
const crypto = require('crypto');
const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544

Hmac

  • Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥;
  • 只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。
1
2
3
4
5
6
const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');
console.log(hmac.digest('hex')); // 80f7e22570...

AES

  • AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用;
  • AES有很多不同的算法,如aes192,aes-128-ecb,aes-256-cbc等,AES除了密钥外还可以指定IV(Initial Vector),不同的系统只要IV不同,用相同的密钥加密相同的数据得到的加密结果也是不同的。
  • 加密结果通常有两种表示方法:hex和base64。
  • AES加密,加解密秘钥的创建createCipher、createDecipher方法在新的API中废弃,推荐使用createCipheriv,createDecipheriv来创建加解密秘钥;
    1
    2
    3
    createCipheriv(algorithm: string, key: any, iv: any)
    【参数说明】
    - iv:表示一个初始向量,aes-256-gcm是一个8位的Buffer或字符串;
aes192
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const crypto = require('crypto');

/**
* AES加密
* @param data
* @param key
* @returns {string}
*/
function aesEncrypt(data, key) {
// 秘钥
const cipher = crypto.createCipher('aes192', key);
let encrypted = cipher.update(data, 'utf8', 'hex');
// 加上剩余的加密内容
encrypted += cipher.final('hex');
return encrypted;
}

/**
* AES解密
* @param encrypted
* @param key
* @returns {string}
*/
function aesDecrypt(encrypted, key) {
const decipher = crypto.createDecipher('aes192', key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

var data = 'Hello';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);

console.log("打印支持的cipher算法:"+crypto.getCiphers());
console.log('old text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text: ' + decrypted);
aes-256-gcm

AES加密使用gcm加密算法时, 需要cipher.getAuthTag()在cipher.final()方法完全加密后调用,获取加密秘钥的tag,解密前用decipher.setAuthTag给解密秘钥设置这个tag;

key长度
  • aes-256-gcm/aes-128-gcm/aes-192-gcm算法的key长度是256或128或192除以8;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var crypto = require('crypto');

var algorithm = 'aes-256-gcm';
var password = '3zTvzr3p67VC61jmV54rIYu1545x4TlY';
var iv = crypto.randomBytes(8);

function encrypt(text) {
var cipher = crypto.createCipheriv(algorithm, password, iv);
var encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
var tag = cipher.getAuthTag();
return {
content: encrypted,
tag: tag
};
}

function decrypt(encrypted) {
var decipher = crypto.createDecipheriv(algorithm, password, iv);
decipher.setAuthTag(encrypted.tag);
var dec = decipher.update(encrypted.content, 'hex', 'utf8');
dec += decipher.final('utf8');
return dec;
}

var hw = encrypt("hello world");
// outputs hello world
console.log(decrypt(hw));

Diffie-Hellman

DH算法是一种密钥交换协议,它可以让双方在不泄漏密钥的情况下协商出一个密钥来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const crypto = require('crypto');

// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();

// 素数
var prime = ming.getPrime();
var generator = ming.getGenerator();

console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));

// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();

// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_secret = hong.computeSecret(ming_keys);

// print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));

操作数据库

mysql

目前使用最广泛的MySQL Node.js驱动程序是开源的node-mysql,可以直接使用npm安装。

注意:目前最新的sequelize不支持mysql库,支持mysql2

安装

1
$ npm install mysql --save

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var mysql = require('mysql');
// 1. 创建数据库连接
var connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : '******',
database : 'db_test',
port : 3306
});
//2.连接数据库
//connection.connect();
connection.connect(function(err) {
if (err) {
console.error('error connecting: ' + err.stack);
return;
}
console.log('connected ' + connection.threadId);
});
//3.执行sql语句
connection.query('SELECT * FROM `user`', function (error, results, fields) {
if (error) throw error;
console.log(results);
});
//4.关闭连接(end方法会确保执行完此前的所有查询)
//connection.end();
connection.end(function(err) {
if (err) {
console.error('end error connecting: ' + err.stack);
return;
}
console.log('end connected ' + connection.threadId);
});
连接配置

在建立新连接时,可以设置以下参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- host:连接的数据库地址。(默认:localhost)
- port:连接地址对应的端口。(默认:3306)
- localAddress: 源IP地址使用TCP连接。(可选)
- socketPath:当主机和端口参数被忽略的时候,可以填写一个Unix的Socket地址。
- user: mysql的连接用户名。
- password: 对应用户的密码。
- database: 所需要连接的数据库的名称。(可选)
- charset: 连接的编码形式。这就是mysql中的整理。(例如:utf8_general_ci)如果被指定,则作为默认的整理排序规则。(默认:utf8_general_ci)
- timezone:用来保存当前本地的时区。(默认:local)
- connectTimeout: 设置在连接的时候,超过多久以后未响应则返回失败。(默认:10000)
- stringifyObjects: stringify对象代替转换值。issue# 501。(默认:false)
- insecureAuth:使用旧(不安全)的连接方式去连接MySQL。(默认:false)
- typeCast: 确定列值是否需要转换为本地JavaScript类型。(默认:true)
- queryFormat:自定义查询的方式。地址:Custom format.
- supportBigNumbers: 如果你使用了BIGINT和DECIMAL格式的表列,那么需要开启这个参数来支持。(默认:false)只有当他们超过JavaScript所能表达的最长的字节的时候,如果没有设置这个参数,则会将过长的数字作为字符串传递。否则,返回对象的长度。如果supportBigNumbers参数被忽略,则这个参数也会被忽略。
- dateStrings:一些日期类型(TIMESTAMP, DATETIME, DATE)会以Strings的类型返回,然后转换成JavaScript的日期对象。(默认:false)
- debug:是否把连接情况打印到文件。(默认:false)
- trace: 生成错误的堆栈跟踪,包括库入口的调用位置(“长堆栈的轨迹”)。一般会造成轻微的性能损失。(默认:true)

终止连接

终止连接有两种方式:

  1. 调用end()方法,安全,可以确保此前的操作完成,该方法可以有回调函数作参数;
  2. 调用destroy(),立即终止销毁连接,不安全,此前的操作可能还没执行,该方法没有回调参数;
1
2
3
4
5
connection.end(function(err) {
// 连接终止
});

connection.destroy();

连接池

使用连接池连接可以更容易地共享某个连接,也可以管理多个连接。

  • 当连接完成后,调用connection.release()方法使连接返回到连接池,以便其他人可以再次使用。
  • 如果你想关闭连接并从连接池中删除它,就要使用connection.destroy()方法。在下次需要时连接池会再创建一个新的连接。
  • 当从连接池中恢复之前的某个连接时,会给服务器发送一个ping包以检查连接是否正常。
参数配置

接受所有与connection相同的配置参数,除此配置外,连接池还支持一些额外的参数:

  • acquireTimeout(获取超时时间): 获取连接时,触发连接超时之前的毫秒数。这与connectTimeout略有不同,因为从连接池获取连接并不总会创建连接 (默认值:10000)
  • waitForConnections(连接等待时间): 当无连接可用或连接数达到上限的时候,判定连接池动作。如果为true,连接池会将请求加入队列,待可用之时再触发操作;如为false,连接池将立即返回错误 (默认值:true)
  • connectionLimit(连接数限制): 所允许立即创建的最大连接数量 (默认值: 10)
  • queueLimit(队列数量限制): 在调用getConnection返回错误之前,连接池所允许入队列的最大请求数量。如设置为0, 则不限制。 (默认值: 0)
事件

连接池事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//获取连接
pool.on('acquire', function (connection) {
console.log('Connection %d acquired', connection.threadId);
});

//连接事件
pool.on('connection', function (connection) {
connection.query('SET SESSION auto_increment_increment=1')
});

//队列中等待可用连接
pool.on('enqueue', function () {
console.log('Waiting for available connection slot');
});

//连接释放回池
pool.on('release', function (connection) {
console.log('Connection %d released', connection.threadId);
});

//连接池中关闭所有连接:一旦pool.end()被调用,pool.getConnection及其它操作将不再被执行!
pool.end(function (err) {
// all connections in the pool have ended
});
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var mysql = require('mysql');
var pool = mysql.createPool({
connectionLimit : 10,
host : 'localhost',
user : 'root',
password : 'gogogo15820',
database : 'db_test',
port : 3306
});

// 直接用pool查询
pool.query('SELECT * FROM user', function(err, results, fields) {
if (err) throw err;
console.log(results);
});

// 使用共享连接查询
pool.getConnection(function(err, connection) {
connection.query('SELECT * FROM user ORDER BY ??','create_time',function (error, results, fields) {
// 释放连接回连接池
connection.release();
// 销毁连接
// connection.destroy();
if (error) throw error;
console.log(results);
});
});

连接池集群

详情见官方文档

sql防注入转义

为了防止SQL注入,每当需要在SQL查询中使用用户数据时,应该对查询相关信息进行转义。

查询值转义
  • 占位符转义:将查询的值用?占位;
  • escape转义:通过mysql.escape(), connection.escape()或pool.escape()方法参数传入查询值(注意:pool只能用于线程池建立的连接);
不同类型的值转义区别
1
2
3
4
5
6
7
8
9
10
- 数字不会被转义
- 布尔值会被转移成 true / false
- Date 对象会被转义成形如 'YYYY-mm-dd HH:ii:ss' 的字符串
- Buffer 会被转义成十六进制字符串,如: X'0fa5'
- 字符串会被安全地转义
- 数组会被转义成列表,例如: ['a', 'b'] 会被转义成 'a', 'b'
- 嵌套数组会被转义成多个列表(在大规模插入时),如: [['a', 'b'], ['c', 'd']] 会被转义成 ('a', 'b'), ('c', 'd')
- 对象的所有可遍历属性会被转义成键值对。如果属性的值是函数,则会被忽略;如果属性值是对象,则会使用其 toString() 方法的返回值。
- undefined / null 会被转义成 NULL
- NaN / Infinity 将会被原样传入。由于MySQL 并不支持这些值,在它们得到支持之前,插入这些值将会导致MySQL报错。
占位符转义值

查询的值用?作为查询值得占位符

注意:sql语句中的任何?,包括注释和字符串都会被转义替换;

1
2
3
4
connection.query('SELECT * FROM user WHERE id=?', 2,function (error, results, fields) {
if (error) throw error;
console.log(results);
});
escape转义值

注入示例

1
2
3
4
connection.query('SELECT * FROM user WHERE id=?', ['2 or id=3'],function (error, results, fields) {
if (error) throw error;
console.log(results);
});

mysql.escape(), connection.escape()或pool.escape()防注入写法都类似:

1
2
3
4
connection.query('SELECT * FROM user WHERE id='+connection.escape('2 or id=3'),function (error, results, fields) {
if (error) throw error;
console.log(results);
});
查询标识转义

如果用户提供了不可信的查询标识(数据库名、表名、列名),需要转义:

  • 用mysql.escapeId(identifier), connection.escapeId(identifier) 或 pool.escapeId(identifier) 方法对它进行转义;
1
2
3
4
connection.query('SELECT * FROM user ORDER BY' + connection.escapeId('create_time'),function (error, results, fields) {
if (error) throw error;
console.log(results);
});
  • 用 ?? 作为占位符来替代你想要转义的标识:
1
2
3
4
connection.query('SELECT * FROM user ORDER BY ??','create_time',function (error, results, fields) {
if (error) throw error;
console.log(results);
});

Sequelize

Sequelize是一个基于promise的Node.js ORM,目前支持Postgres,MySQL, SQLite和Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 读取和复制等功能.

参考

中文文档

博客整理文档

安装

注意sequelize 4.x使用的是mysql2

安装:

1
2
3
4
5
$ npm install --save sequelize
$ npm install --save-dev @types/sequelize
//需要mysql2
$ npm install --save mysql2
$ npm install --save-dev @types/mysql2

特性

  • ORM全称Object-Relational Mapping,即对象关系映射,可以把关系数据库的表结构映射到对象上。
  • 支持原始sql查询;
  • sequelize操作数据库返回的是promise对象,所有可以使用promise的特性写法,也可以使用ES7的async写法配合koa2使用;

连接数据库

Sequelize将在初始化时设置连接池:

  • 单个进程连接到数据库,你最好每个数据库只创建一个实例。
  • 多个进程连接到数据库,则必须为每个进程创建一个实例,但每个实例应具有“最大连接池大小除以实例数”的最大连接池大小。 因此,如果您希望最大连接池大小为90,并且有3个工作进程,则每个进程的实例应具有30的最大连接池大小。
  1. 定义config.js文件配置数据库信息:
1
2
3
4
5
6
7
module.exports = {
database: 'db_test', // 使用哪个数据库
username: 'root', // 用户名
password: '******', // 口令
host: 'localhost', // 主机名
port: 3306 // 端口号,MySQL默认3306
};
  1. 创建数据库连接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Sequelize = require('sequelize');
var mysql = require('mysql');
const config = require('./config');

// 可以简单地使用 uri 连接
// const sequelize = new Sequelize('postgres://user:pass@example.com:5432/dbname');
const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
});

// 测试连接是否成功
sequelize
.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
})
.catch(err => {
console.error('Unable to connect to the database:', err);
});

使用步骤

  1. 定义模型对象:定义数据表映射对象模型;
  2. 调用操作数据库的方法;

定义模型

要定义模型和表之间的映射,请使用define方法。

  • Sequelize将自动添加createdAt和updatedAt属性;
语法
1
2
3
4
5
sequelize.define(modelName, attributes, options)
【参数】
- modelName:模型名称;
- attributes:成员属性定义;
- options:可选设置;
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let User = sequelize.define('user', {
id: {
type: Sequelize.INTEGER, // INTEGER
autoIncrement: true, //主键自增
primaryKey: true //主键
},
name: Sequelize.STRING, // VARCHAR(255)
sex: Sequelize.STRING, // VARCHAR(255)
age: Sequelize.INTEGER, // INTEGER
create_time: Sequelize.BIGINT // BIGINT
}, {
// orm自动增加时间戳,可以禁用,禁用后就不会自动插入createdAt和updatedAt时间
timestamps: false,
//也可以手动隐藏创建和更新时间
// createdAt: false,
// updatedAt: false,
tableName: 'user' //表名称默认会加s, 这里可以设置最终的表名;
});

操作数据库

增加

【语法】

1
2
模型对象.create(values?: TAttributes, options?: CreateOptions)
模型对象.create({},{});

【参数】

  • values:为模型的成员属性设置值;
  • options:增加元素的设置;
1
- files:['file_name'] -> 仅增加某条数据的某些字段;
Promise方式

ES6 Promise方式操作数据库

1
2
3
4
5
6
7
8
9
10
11
// promise方式操作数据库
User.create({
name: '李x龙',
sex: '男',
age: 33,
create_time: new Date()
},{ fields: [ 'name' ] }).then(result => {
console.log(JSON.stringify(result));
}).catch(error => {
console.log(error.stack);
});

async异步方式

ES7异步方式操作数据库:

1
2
3
4
5
6
7
8
(async () => {
await User.create({
name:'李云龙',
sex:'男',
age: 33,
create_time:'2018-02-01'
});
})();

查询

查询有两个方法:

  • find: 查询单条数据;
  • findAll: 查询多条数据;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 查询单个实例
(async () => {
let user = await User.find({
where: {
sex: '女'
}

});
console.log(user);
})();
//查询添加筛选条件
(async () => {
let users = await User.findAll({
//设置需要查询的字段,通过嵌套数组给查询的字段重命名(相当于sql中的as)
attributes: [['id','userId'], 'name',[sequelize.fn('COUNT',
//排除某些字段:attributes: { exclude: ['baz'] }
sequelize.col('name')), 'nameCount']],
//查询条件
where: {
name: '李云龙'
},
//id降序排序
order: [['id','DESC']],
//分页设置,跳过2条数据,取5条数据
offset:2,
limit:5

});
console.log(`user count: ${users.length}`);
for (let user of users) {
console.log(JSON.stringify(user));
}
})();

//聚合函数查询
(async () => {
let users = await User.findAll({
//设置需要查询的字段是一个聚合函数,可以使用sql中的函数
attributes: [[sequelize.fn('COUNT', sequelize.col('name')), 'nameCount']],
//查询条件
where: {
name: '李云龙'
}

});
console.log(`user count: ${users.length}`);
for (let user of users) {
console.log(JSON.stringify(user));
}
})();

//查询多少条记录
(async () => {
let users = await User.count({
where:{
name:'李云龙'
}
});
console.log(`user count: ${users}`);
})();
删除
  • 删除单个
1
2
3
4
5
6
7
8
(async () => {
let user=await User.find({
where:{
id:1
}
});
await user.destroy();
})();
  • 批量删除
1
2
3
4
5
6
7
8
(async () => {
let count=await User.destroy({
where:{
name:'李云龙'
}
});
console.log(count);
})();
修改
  • 修改单个实例
1
2
3
4
5
6
7
8
9
(async () => {  
let user = await User.find({
where:{
id:16
}
});
user.name='李寻欢';
await user.save();
})();
  • 批量修改
1
2
3
4
5
(async () => {
let user = await User.update({sex:'女'},{
where:{}
});
})();

API常用方法

Sequelize ORM对象方法:

  • authenticate(): 测试连接是否成功;
1
2
3
4
5
sequelize
.authenticate()
.then(() => {
console.log('successfully.');
});

封装

以项目根目录为当前目录:

  1. 创建config目录作为各种环境数据库配置文件存放目录,创建config-test.js文件用于测试环境的数据库配置,config-prod.js作为生产环境数据库配置:
1
2
3
4
5
6
7
module.exports = {
database: 'db_test', // 使用哪个数据库
username: 'root', // 用户名
password: '', // 口令 *****
host: 'localhost', // 主机名
port: 3306 // 端口号,MySQL默认3306
};
  1. 创建config.js文件用于判断加载哪个数据库配置文件:
1
2
3
4
5
6
7
8
9
10
const prodConfig = './config/config-prod.js';
const testConfig = './config/config-test-db.js';
var config = null;

if (process.env.NODE_ENV === 'test') {
config = require(testConfig);
} else {
config = require(prodConfig);
}
module.exports = config;
  1. 创建db.js文件,用于创建数据库连接和定义公共数据库映射模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const Sequelize = require('sequelize');
const config = require('./config');
console.log('init Sequelize...');

/**
* sequelize数据库连接配置
* @type {sequelize.Sequelize | sequelize}
*/
const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
});

/**
* model模板
* @param name
* @param attributes
* @returns {Model}
*/
function defineModel(name, attributes) {
//模板属性
let attrs = {};
for (let key in attributes) {
let value = attributes[key];
value.allowNull = value.allowNull || false;
attrs[key] = value;
}
//默认的字段
attrs.id = {
type: Sequelize.INTEGER,
primaryKey: true
};
attrs.createdAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.updatedAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.version = {
type: Sequelize.BIGINT,
allowNull: false
};
return sequelize.define(name, attrs, {
tableName: name,
timestamps: false,
//模型生命周期检查钩子
hooks: {
//操作数据库开始时设置默认字段
beforeValidate: function (obj) {
let date = new Date();
let seconds = date.getSeconds();
if (date.getSeconds() < 10) {
seconds = '0' + date.getSeconds();
}
let curTime = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate()
+ ' ' + date.getHours() + ':' + date.getMinutes() + ':' + seconds;
//判断是否是新纪录
if (obj.isNewRecord) {
obj.createdAt = curTime;
obj.updatedAt = curTime;
obj.version = 0;
} else {
obj.updatedAt = curTime;
obj.version++;
}
}
}
});
}
var db = {
defineModel: defineModel
};
module.exports = db;
  1. 创建models文件夹用于存放数据库操作模板,如:User.js:
1
2
3
4
5
6
7
const Sequelize = require('sequelize');
const db = require('../db');

let User=db.defineModel('user', {
name: Sequelize.STRING(100)
})
module.exports =User;
  1. 可以选择性创建model.js文件,用来加载所有model文件夹的模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');

//读models文件夹
let files = fs.readdirSync(__dirname + '/models');

let js_files = files.filter((f)=>{
return f.endsWith('.js');
});

module.exports = {};

for (let f of js_files) {
console.log(`import model from file ${f}`);
let name = f.substring(0, f.length - 3);
module.exports[name] = require(__dirname + '/models/' + f);
}
  1. 操作数据库测试:
1
2
3
4
5
6
7
8
const User = require('../models/User.js');

(async () => {
let user = await User.create({
name:'李二狗'
});
console.log(user);
})();

WebSocket

HTTP协议是一个请求->响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。

HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。

WebSocket协议

  1. 浏览器发起请求:

WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求。客户端浏览器建立WebSocket连接的http请求格式:

1
2
3
4
5
6
7
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13

该请求和普通的HTTP请求有几点不同:

1
2
3
4
1. GET请求的地址不是类似/path/,而是以ws://开头的地址;
2. 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
3. Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
4. Sec-WebSocket-Version指定了WebSocket的协议版本。
  1. 服务器响应请求:
1
2
3
4
5
//101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
CS支持条件

客户端(浏览器)-服务端支持WebSocket的条件;

浏览器

要支持WebSocket通信,浏览器得支持这个协议,这样才能发出ws://xxx的请求。目前,支持WebSocket的主流浏览器如下:

  • Chrome
  • Firefox
  • IE >= 10
  • Sarafi >= 6
  • Android >= 4.4
  • iOS >= 8
服务器

由于WebSocket是一个协议,服务器具体怎么实现,取决于所用编程语言和框架本身。Node.js本身支持的协议包括TCP协议和HTTP协议,要支持WebSocket协议,需要对Node.js提供的HTTPServer做额外的开发。已经有若干基于Node.js的稳定可靠的WebSocket实现,我们直接用npm安装使用即可。

WebSocket连接机制

安全的WebSocket连接机制和HTTPS类似,首先,浏览器用wss://xxx创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。

ws模块

在Node.js中,使用最广泛的WebSocket模块是ws。

使用ws
  1. 安装ws:
1
$ npm install --save ws
  1. 创建server端:
1
2
3
4
5
6
7
8
9
// 导入WebSocket模块:
const WebSocket = require('ws');

// 引用Server类:
const WebSocketServer = WebSocket.Server;
// 实例化:
const wss = new WebSocketServer({
port: 3000
});
  1. 创建client端:
1
2
3
4
5
6
7
8
9
10
11
12
13
const WebSocket = require('ws');
let ws = new WebSocket('ws://localhost:3000/test');

// 打开WebSocket连接后立刻发送一条消息:
ws.on('open', function () {
console.log(`[CLIENT] open()`);
ws.send('Hello!');
});

// 响应收到的消息:
ws.on('message', function (message) {
console.log(`[CLIENT] Received: ${message}`);
});

至此一个CS形式的ws程序就可以运行了…

ws和koa同一端口

把WebSocketServer绑定到同一个端口的关键代码是先获取koa创建的http.Server的引用,再根据http.Server创建WebSocketServer;

  • 浏览器创建WebSocket时发送的仍然是标准的HTTP请求。无论是WebSocket请求,还是普通HTTP请求,都会被http.Server处理;
  • WS请求会直接由WebSocketServer处理;
  • 实际上,3000端口并非由koa监听,而是koa调用Node标准的http模块创建的http.Server监听的。koa只是把响应函数注册到该http.Server中了。类似的,WebSocketServer也可以把自己的响应函数注册到http.Server中,这样,同一个端口,根据协议,可以分别由koa和ws处理,流程图如下:

image

绑定统一端口的关键代码如下:

1
2
3
4
5
6
7
8
let server = app.listen(3000);

// 引用Server类:
const WebSocketServer = WebSocket.Server;
// 实例化:
const wss = new WebSocketServer({
server: server
});
识别用户身份

把用户登录后的身份写入Cookie,在koa中,可以使用middleware解析Cookie,把用户绑定到ctx.state.user上。WS请求也是标准的HTTP请求,所以,服务器也会把Cookie发送过来,这样,我们在用WebSocketServer处理WS请求时,就可以根据Cookie识别用户身份。

配置反向代理

如果网站配置了反向代理,例如Nginx,则HTTP和WebSocket都必须通过反向代理连接Node服务器。HTTP的反向代理非常简单,但是要正常连接WebSocket,代理服务器必须支持WebSocket协议。

我们以Nginx为例,编写一个简单的反向代理配置文件。

详细的配置可以参考Nginx的官方博客:Using NGINX as a WebSocket Proxy

首先要保证Nginx版本>=1.3,然后,通过proxy_set_header指令,设定:

1
2
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Nginx即可理解该连接将使用WebSocket协议。

一个示例配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
listen 80;
server_name localhost;

# 处理静态资源文件:
location ^~ /static/ {
root /path/to/ws-with-koa;
}

# 处理WebSocket连接:
location ^~ /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

# 其他所有请求:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!