文章概述
本篇文章记录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 | # 【查看默认配置】 |
配置全局安装目录
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 | // 命令生成,会有一些配置,按提示默认填写即可; |
config.json配置文件内容:
1 | { |
包安装命令
npm可以安装的工具包可在npmjs网站搜索;
npm命令安装工具包,一般安装的工具包信息会配置到config.json文件的dependencies(会发布到生产环境)配置项或devDependencies(仅用于开发环境)配置项。
- 安装最新版本的工具包,如安装koa工具包
1 | // 写法一: |
- 安装指定版本的工具包:
1 | $ npm install --save koa@2.8.1 |
简写命令
1 | npm i 简写为:npm install |
yarn包管理
Yarn是Facebook提供的替代npm的工具,可以加速node模块的下载。React Native的命令行工具用于执行创建、初始化、更新项目、运行打包服务(packager)等任务。
安装与更新
1 | //npm安装yarn |
npm相关配置
1 | # 【查看默认配置】 |
配置国内镜像
不推荐配置,配置镜像后有些包下载不下来
还原默认镜像地址
yarn默认的配置如下,如果配置了一些国内镜像后,下载包出问题,请设置默认配置:1
2
3
4# 【registry】
$ yarn config set registry https://registry.yarnpkg.com
# 【distUrl】
$ yarn config set disturl undefined
常用命令
1 | //初始化package.json文件(=npm init) |
nodejs简介
在命令行模式下,可以执行node进入Node交互式环境,也可以执行node hello.js运行一个.js文件。
执行js文件
node命令可以直接执行一个已经写好的js文件;
1 | $ node hello.js |
node命令行
输入node命令,会进入nodejs命令行交互环境,此模式下可以直接执行js代码。
模块
CommonJs规范
模块加载机制被称为CommonJS规范。在这个规范下,每个.js文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突。
模块加载实质
模块加载,实际上是将代码包装到了匿名函数内部,所以模块之间成员名互不冲突。
1 | (function () { |
导入导出
模块可以导出一些成员,提供给其他模块导入使用;
导入
1 | var greet = require('./hello'); |
导出
方法1:module.exports赋值(推荐);
1 | 可以直接对module.exports赋值; |
1 | // hello.js |
方法二:直接使用exports;
1 | // hello.js |
1 | 【注意】 |
综合示例
以下hello模块导出成员提供给world模块使用;
hello.js模块
1 | ; |
world.js模块
1 | ; |
基本模块
global
- JavaScript有且仅有一个全局对象,在浏览器中,叫window对象。
- node.js环境中也有一个全局对象叫global。
console
1 | node命令行输入global.console会直接打印出所有控制台输出方法; |
文件相关
- __dirname:全局变量,存储的是文件所在的文件目录;
- __filename:全局变量,存储的是全路径文件名;
process
- process也是Node.js提供的一个对象,它代表当前Node.js进程。
- 通过process对象可以拿到node运行环境信息.
1 | > process === global.process; |
相关方法
nextTick
process.nextTick():不是立刻执行,而是要等到下一次事件循环执行,nodejs会把此方法的逻辑放到任务队列最后,等到任务执行完成再执行。
1 | 1> process.nextTick()方法是将某个逻辑插入到当前循环任务队列的末尾,早于setTimeout(function(){},0)执行。 |
1 | // test.js |
进程事件回调
- 程序即将退出时的回调;
1 | process.on('exit', function (code) { |
js运行环境
- 很多JavaScript代码既能在浏览器中执行,也能在Node环境执行,但有些时候,程序本身需要判断自己到底是在什么环境下执行的,常用的方式就是根据浏览器和Node环境提供的全局变量名称来判断:
1 | if (typeof(window) === 'undefined') { |
异步文件操作库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 | // Buffer -> String |
1 | // String -> Buffer |
文件信息
- fs.stat(),返回一个Stat对象,用于同步获取文件信息;
- fs.statSync(),返回一个Stat对象,用于同步获取文件信息;
stat属性
- 是否是文件:stat.isFile();
- 是否是目录:stat.isDirectory();
- 文件大小:stat.size;
- 创建时间(Date对象):stat.birthtime;
- 修改时间(Date对象): stat.mtime;
示例
1 | let fs = require('fs'); |
文件的读写
同步和异步读写
- 由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。
- 服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。
异步读文件
1 | 异步读取时,回调函数接收两个参数:第一个参数代表错误信息,第二个参数代表结果。 |
读取文本文件
1 | let fs = require('fs'); |
读二进制文件
- 当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。
- Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不同);
1 | let fs = require('fs'); |
同步读文件
除了标准的异步读取模式外,fs也提供相应的同步读取函数。同步读取的函数和异步函数相比,多了一个Sync后缀,并且不接收回调函数,函数直接返回结果。
- 同步读取文件需要用try…catch捕获该错误;
1 | let fs = require('fs'); |
异步写文件
- writeFile创建并写入文件;
- writeFile()的参数依次为:文件名、数据和回调函数。
1 | - writeFile传入的参数如果数据是String,默认按UTF-8编码写入文本文件; |
1 | let fs = require('fs'); |
同步写文件
- writeFileSync()用于同步写文件;
1 | ; |
文件目录
fs提供读取文件目录下所有文件的方法,同读文件类似,提供了同步和异步,方法将返回一个包含“指定目录下所有文件名称”的数组对象。;
- fs.readdir(path):异步读;
- fs.readdirSync(path):同步读;
stream
- stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。
- 流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。
- 流的方式读写文件是一点一点儿处理文件,用过的部分会被GC,所以占内存少。
- 所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable。
流API
读写流都通过fs模块来创建;
- 创建读取流:fs.createReadStream(path,options);
1 | 注意:options可以配置highWaterMark设置读取流的缓冲区大小,单位字节; |
- 创建写入流:fs.createWriteStream(path,options);
流处理回调
流对象的处理回调事件:
- data事件表示流的数据已经可以读取了,data事件回调可能会有很多次;
- end事件表示这个流已经到末尾了,没有数据可以读取了;
- error事件表示出错了;
示例
读流
1 | ; |
写流
1 | ; |
pipe
一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe。
读写文件
- pipe管道完成读写文件的过程,实际上是文件复制的过程;
1 | ; |
- 默认情况下,当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 | ; |
文件路径模块
处理本地文件目录需要使用Node.js提供的path模块,它可以方便地构造目录:
1 | ; |
HTTP服务器
一个简单的http服务器,通过http.createServer(function(request,response))方法创建;
1 | // 导入http模块: |
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 ;
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
5var querystring = require('querystring');
var postData = querystring.stringify({
'param1' : 'test',
'param2':'1'
});
GET
- get请求: options是带参数的全路径
1 | var http = require('http'); |
- 单独配置查询参数的get请求
1 | var queryString = require('querystring'); |
POST
post请求带参数,需要用write将请求参数写入才能完成请求;
1 | var http = require('http'); |
加解密
Nodejs用C/C++实现通用的加密和哈希算法通过cypto模块暴露为js接口,运行速度快。
MD5和SHA1
MD5和sha1是常用的哈希算法,用于给任意数据一个“签名”,签名结果通常用一个十六进制的字符串表示;
- update()方法默认字符串编码为UTF-8,也可以传入Buffer。
- 如果要计算SHA1,只需要把’md5’改成’sha1’,还可以使用更安全的sha256和sha512。
1 | const crypto = require('crypto'); |
Hmac
- Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥;
- 只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。
1 | const crypto = require('crypto'); |
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
3createCipheriv(algorithm: string, key: any, iv: any)
【参数说明】
- iv:表示一个初始向量,aes-256-gcm是一个8位的Buffer或字符串;
aes192
1 | const crypto = require('crypto'); |
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 | var crypto = require('crypto'); |
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
24const 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 | var mysql = require('mysql'); |
连接配置
在建立新连接时,可以设置以下参数: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)
终止连接
终止连接有两种方式:
- 调用end()方法,安全,可以确保此前的操作完成,该方法可以有回调函数作参数;
- 调用destroy(),立即终止销毁连接,不安全,此前的操作可能还没执行,该方法没有回调参数;
1 | connection.end(function(err) { |
连接池
使用连接池连接可以更容易地共享某个连接,也可以管理多个连接。
- 当连接完成后,调用connection.release()方法使连接返回到连接池,以便其他人可以再次使用。
- 如果你想关闭连接并从连接池中删除它,就要使用connection.destroy()方法。在下次需要时连接池会再创建一个新的连接。
- 当从连接池中恢复之前的某个连接时,会给服务器发送一个ping包以检查连接是否正常。
参数配置
接受所有与connection相同的配置参数,除此配置外,连接池还支持一些额外的参数:
- acquireTimeout(获取超时时间): 获取连接时,触发连接超时之前的毫秒数。这与connectTimeout略有不同,因为从连接池获取连接并不总会创建连接 (默认值:10000)
- waitForConnections(连接等待时间): 当无连接可用或连接数达到上限的时候,判定连接池动作。如果为true,连接池会将请求加入队列,待可用之时再触发操作;如为false,连接池将立即返回错误 (默认值:true)
- connectionLimit(连接数限制): 所允许立即创建的最大连接数量 (默认值: 10)
- queueLimit(队列数量限制): 在调用getConnection返回错误之前,连接池所允许入队列的最大请求数量。如设置为0, 则不限制。 (默认值: 0)
事件
连接池事件:
1 | //获取连接 |
使用
1 | var mysql = require('mysql'); |
连接池集群
详情见官方文档
sql防注入转义
为了防止SQL注入,每当需要在SQL查询中使用用户数据时,应该对查询相关信息进行转义。
查询值转义
- 占位符转义:将查询的值用?占位;
- escape转义:通过mysql.escape(), connection.escape()或pool.escape()方法参数传入查询值(注意:pool只能用于线程池建立的连接);
不同类型的值转义区别
1 | - 数字不会被转义 |
占位符转义值
查询的值用?作为查询值得占位符
注意:sql语句中的任何?,包括注释和字符串都会被转义替换;
1 | connection.query('SELECT * FROM user WHERE id=?', 2,function (error, results, fields) { |
escape转义值
注入示例
1 | connection.query('SELECT * FROM user WHERE id=?', ['2 or id=3'],function (error, results, fields) { |
mysql.escape(), connection.escape()或pool.escape()防注入写法都类似:
1 | connection.query('SELECT * FROM user WHERE id='+connection.escape('2 or id=3'),function (error, results, fields) { |
查询标识转义
如果用户提供了不可信的查询标识(数据库名、表名、列名),需要转义:
- 用mysql.escapeId(identifier), connection.escapeId(identifier) 或 pool.escapeId(identifier) 方法对它进行转义;
1 | connection.query('SELECT * FROM user ORDER BY' + connection.escapeId('create_time'),function (error, results, fields) { |
- 用 ?? 作为占位符来替代你想要转义的标识:
1 | connection.query('SELECT * FROM user ORDER BY ??','create_time',function (error, results, fields) { |
Sequelize
Sequelize是一个基于promise的Node.js ORM,目前支持Postgres,MySQL, SQLite和Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 读取和复制等功能.
参考
安装
注意sequelize 4.x使用的是mysql2
安装:
1 | $ npm install --save sequelize |
特性
- ORM全称Object-Relational Mapping,即对象关系映射,可以把关系数据库的表结构映射到对象上。
- 支持原始sql查询;
- sequelize操作数据库返回的是promise对象,所有可以使用promise的特性写法,也可以使用ES7的async写法配合koa2使用;
连接数据库
Sequelize将在初始化时设置连接池:
- 单个进程连接到数据库,你最好每个数据库只创建一个实例。
- 多个进程连接到数据库,则必须为每个进程创建一个实例,但每个实例应具有“最大连接池大小除以实例数”的最大连接池大小。 因此,如果您希望最大连接池大小为90,并且有3个工作进程,则每个进程的实例应具有30的最大连接池大小。
- 定义config.js文件配置数据库信息:
1 | module.exports = { |
- 创建数据库连接:
1 | const Sequelize = require('sequelize'); |
使用步骤
- 定义模型对象:定义数据表映射对象模型;
- 调用操作数据库的方法;
定义模型
要定义模型和表之间的映射,请使用define方法。
- Sequelize将自动添加createdAt和updatedAt属性;
语法
1 | sequelize.define(modelName, attributes, options) |
示例
1 | let User = sequelize.define('user', { |
操作数据库
增加
【语法】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 | // 查询单个实例 |
删除
- 删除单个
1 | (async () => { |
- 批量删除
1 | (async () => { |
修改
- 修改单个实例
1 | (async () => { |
- 批量修改
1 | (async () => { |
API常用方法
Sequelize ORM对象方法:
- authenticate(): 测试连接是否成功;
1 | sequelize |
封装
以项目根目录为当前目录:
- 创建config目录作为各种环境数据库配置文件存放目录,创建config-test.js文件用于测试环境的数据库配置,config-prod.js作为生产环境数据库配置:
1 | module.exports = { |
- 创建config.js文件用于判断加载哪个数据库配置文件:
1 | const prodConfig = './config/config-prod.js'; |
- 创建db.js文件,用于创建数据库连接和定义公共数据库映射模板:
1 | const Sequelize = require('sequelize'); |
- 创建models文件夹用于存放数据库操作模板,如:User.js:
1 | const Sequelize = require('sequelize'); |
- 可以选择性创建model.js文件,用来加载所有model文件夹的模板:
1 | const fs = require('fs'); |
- 操作数据库测试:
1 | const User = require('../models/User.js'); |
WebSocket
HTTP协议是一个请求->响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。
HTML5推出了WebSocket标准,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。
WebSocket协议
- 浏览器发起请求:
WebSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求。客户端浏览器建立WebSocket连接的http请求格式:1
2
3
4
5
6
7GET 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 | 1. GET请求的地址不是类似/path/,而是以ws://开头的地址; |
- 服务器响应请求:
1 | //101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。 |
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
- 安装ws:
1 | $ npm install --save ws |
- 创建server端:
1 | // 导入WebSocket模块: |
- 创建client端:
1 | const WebSocket = require('ws'); |
至此一个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处理,流程图如下:
绑定统一端口的关键代码如下:
1 | let server = app.listen(3000); |
识别用户身份
把用户登录后的身份写入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 | proxy_set_header Upgrade $http_upgrade; |
Nginx即可理解该连接将使用WebSocket协议。
一个示例配置文件内容如下:
1 | server { |