分布式

特征

  • 分布性 分布式系统中的计算机会依据业务或其他需求在空间中随意分布,并且可以灵活变动
  • 对等性 整个系统中的计算机节点都是对等的,没有物理上的主从之分
  • 并发性 系统中的多节计算机节点可能会并发操作共享资源
  • 没有全局时钟 因为多个节点在空间上的随意分布,没有办法设置全局的时钟序列
  • 故障率 在由大量计算机组成的分布式系统中,故障是无法避免的

CAP定理

在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼

  • 强一致性 分布式系统会存在多个数据副本,要求所有的用户都可以读到最新的数据
  • 可用性 系统对用户的每个操作都能在有限的时间内返回正常的结果
  • 分区容错性 组成分布式系统的子网由于网络故障不能通信,导致整个系统被分成若干孤立的子网,但仍要保证致性和可用性服务

BASE理论

对CAP中可用性和一致性权衡的结果,通过牺牲强一致性来获取可用性,核心思想是:每个应用可以根据自身的特点,采用适当的方式达到最终一致性

  • 基本可用(Basically Available)是当系统出现故障时,可以牺牲部分可用性,如响应时间、功能减少,从而保证系统运行
  • 软状态(Soft state)允许不同节点的数据副本在同步时存在延迟,并认为该状态不会影响系统整体的可用性;
  • 最终一致性(Eventually consistent)强调系统所有的数据副本最终能达到一致状态,但不需要实时保证数据一致性,而所有客户端能获取到最新的值

一致性协议

2PC 两段提交

将事务的提交过程分成了两个阶段来进行处理,它对每个事务使用先尝试后提交的方式来处理,是一种强一致性算法。它的优点是原理简单,实现方便,而缺点是同步阻塞,单点问题,数据不一致

Smaller icon

阶段一:提交事务请求

  1. 事务询问:向所有参与者发送事务内容,并询问事务提交是否可以执行,然后等待参与者响应
  2. 执行事务:参与者执行事务,并在日志中记录Undo和Redo信息
  3. 参与者反馈响应:如果参与者成功执行事务操作就反馈Yes,否则反馈No

执行事务提交

  1. 发送提交请求:向参与者发送Commit
  2. 事务提交:参与者收到Commit后,执行事务提交,并在完成之后释放占用的事务资源
  3. 反馈提交结果:参与者完成事务提交后,反馈Ack消息
  4. 完成事务:所有参与者反馈Ack消息,完成事务

Paxos算法

Paxos是一种基于消息传递且具有高度容错性的一致性算法,是解决分布式一致性问题最有效的算法之一,能避免2PC和3PC存在的问题。

Raft协议

  1. 将一致性协议划分为领导者选举,Log复制和安全性
  2. 将paxos的p2p模式改成Master-slave模式

三种常见的通信机制:序列化和远程过程调用,消息队列,多播通信

序列化和远程过程调用

网络中处于不同机器上的进程,通过远程过程调用(Remote Procedure Call,rpc)相互通信
Smaller icon

当机器A上的进程调用B上的进程时,A的调用进程被挂起,B上被调用的进程开始执行,执行完再将结果返回。通常RPC框架使用JSON或XML序列化数据,以提高数据存取和通信

采用Pub-Sub机制的分布式消息系统,具有极高的消息吞吐量,支持消息传递至少到达一次

整体架构

由三类角色组成:消息生产者(producer),代理服务器(Broker),消息消费者(Consumer)

当组件是被connect绑定redux后,可以使用getWrappedInstance()获取组件的原型,但需要给connect添加{ withRef: true }选项才能使用

  • connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

其中mergeProps(stateProps, dispatchProps, ownProps)是用于特定情况下,修改提供给组件的props

1
2
3
4
function mergeProps(stateProps, dispatchProps, ownProps) {
//默认情况下返回
return Object.assign({}, ownProps, stateProps, dispatchProps)
}

需要添加的{ withRef: true }选项要放在connect的第四个参数

###example

使用connect绑定redux的后test组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {connect} from 'react-redux'
import {Component} from 'react'
export default class Test extends Component {
constructor(props) {
super(props);
}
testFunction =(test='1')=>{
alert(test)
}
render() {
return (
<div>test</div>
)
}
}
function mergeProps(stateProps, dispatchProps, ownProps) {
return Object.assign({}, ownProps, stateProps, dispatchProps)
}
export default connect({}, {},mergeProps,{withRef:true})(Test)

获取test组件的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {Component} from 'react'
import Test from './Test'
export default class Test2 extends Component {
constructor(props) {
super(props);
}
componentWillMount(){
//使用connect提供的getWrappedInstance()
this.refs.test.getWrappedInstance().testFunction('test')
}
render() {
return (
<div><Test ref='test' /></div>
)
}
}

image概念

Smaller icon

Remote docker hub 集中存储镜像的web服务器地址

Namespace 命名空间,存放一类镜像

Repository 一个镜像仓库,使用tag区分

Tag 类似的git中的Tag,表示镜像版本

使用image

  • 列出本机镜像:
1
2
3
4
$ docker images
```
可以使用--filter过滤结果,例如--filter "dangling=true"选出悬挂镜像(没有对应名称和tag,其上层不会被任何镜像依赖,通常可删除)

//删除悬挂镜像
$ docker image –filter “dangling=true” -q|xargs docker rmi
```

创建镜像

使用DockerFile built: docker build -t nginx-node:6.9.2 .

将容器commit: 使用docker ps查看容器id,在docker commit id nginx:version

导入镜像

下载镜像: docker pull busybox

导出镜像: docker save nginx:version >nginx.tar

导入镜像: docker import(导入包含根文件系统的归档),docker load(一般导入由docker save导出的镜像)

find

1
2
3
4
5
db.c.find() 匹配集合所有文档
db.users.find({age:27}) 查找特定键值的文档
db.user.find({},{username:1,email:1}) 返回指定的键 默认有_id,加_id:0剔除
操作符 含义
$lt <
$lte <=
$gt >
$gte >=
$OR
$in
$not 元条件句,用在其他条件之上
$inc 自增 {$inc:{age:1}} 自增一
$all 使用多个元素匹配数组
$slice 返回指定位置的数据 {$slice:”10”}

查询内嵌文档

精确匹配 查询条件与子文档完全匹配

点表示法 进入内嵌文档内部查询 db.people.find({name.first:’joe’,name.last:something})

模糊查找 $elemMatch 对内嵌文档使用操作符查找 db.blog.find({comments:{$elemMatch:{author:joe,score:{$gte:5}}}})

skip

  • 当数据量大时,使用skip会很慢,mongo还不支持在索引中保存太多的元数据

利用上次结果来计算下次的查询条件

koa-csrf

Cross-Site Request Forgery 跨站请求伪造

koa-csrf

原理

它在session中保存一个secret字段,然后使用它生成token,其中salt是随机生成的字符串,长度可自定。每次请求时都会重新生成salt,于是每次token也会不同。

1
2
3
4
5
var hash = crypto
.createHash('sha1')
.update(salt + '-' + secret, 'ascii')
.digest('base64');
token = salt + '-' + hash;

验证的时候,只需要取出token头部的salt,再从session中取出secret,再生成expected,与token对比

默认不检查GET,HEAD,OPTIONS请求

使用

与session配合使用,在请求时附带字段_csrf:token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var koa = require('koa')
var csrf = require('koa-csrf')
var session = require('koa-session')
var Router = require('koa-router');
var bodyParser = require('koa-bodyparser');
var app = koa();
var router = Router();
app.use(bodyParser());
app.keys = ['session secret']
session(app)
app.use(csrf());
router.get('/', async ctx => {
ctx.body = ctx.csrf;
})
router.post('/', ctx => {
ctx.body = ctx.request.body
});

中间件

  • 封装底层细节,为上层提供更方便的服务

node的http模块提供了应用层协议网络的封装,需要中间件来处理http请求的细节。因为node异步,所以需要在当前中间件处理完后通知下一中间件执行,例如Connect的尾触发next()

多进程架构

依靠node提供的child_process模块,创建工作线程,实现多核cpu的利用

  • 主从模式

主进程和工作进程,在分布式架构中用于并行处理业务的模式,主进程负责调度和管理工作进程,工作进程负责业务处理

创建worker.js:

1
2
3
4
5
6
7
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('Hello World\n');
}).listen(Math.round((1 + Math.random()) * 1000), '127.0.0.1');

创建master.js

1
2
3
4
5
var fork = require('child_process').fork;
var cpus = require('os').cpus();
for (var i = 0; i < cpus.length; i++) {
fork('./worker.js');
}

fork()复制的进程都是独立的进程,有独立的v8,但fork()过程比较长、内存占用多(至少10mb),多进程目的是充分使用cpu资源

创建工作线程

child_process模块提供了4个方法创建工作线程

  1. spawn() 启动一个工作线程执行命令
  2. exec() 启动一个工作线程并且包含回调函数 可以设置超时时间
  3. execFile() 启动一个工作线程来执行可执行文件 可以设置超时时间
  4. fork() 指定js文件来创建工作线程

后面三个方法都是spawn()的延伸

1
2
3
4
5
6
7
8
9
var cp = require('child_process');
cp.spawn('node', ['worker.js']);
cp.exec('node worker.js', function(err, stdout, stderr) {
// some code
});
cp.execFile('worker.js', function(err, stdout, stderr) {
// some code
});
cp.fork('./worker.js');

Smaller icon

通过execFile()执行的js文件,需要在首行加入: #!/usr/bin/env node

进程通信

  • IPC Inter-Process Communication 进程间通信
  • node使用libuv提供的管道(pipe)实现IPC,在window中使用named pipe,*nix使用Unix Domain Socket实现
  • 文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符,读写文件也需要使用文件描述符来指定待读写的文件。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表

在创建工作线程之前会先创建IPC通道并监听,然后通过环境变量(NODE_CHANNEL_FD)告诉工作线程这个IPC通道的文件描述符,工作线程会根据它连接IPC通道完成进程连接。

IPC通道类似socket属于双向通信,直接在系统内核中完成更高效,IPC被node抽象为stream对象,在调用send()时(类似write())发送数据,接收到消息会通过message事件(类似data)触发

句柄传递

  • 句柄是一种可以用来标识资源的引用,包含了指向对象的文件描述符,可以表示一个服务端socket对象,客户端socket对象等

主进程接受到socket请求后,将socket直接发送给工作进程,不用重新与工作进程建立新的socket连接

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
// parent.js
var cp = require('child_process');
var child1 = cp.fork('child.js');
var child2 = cp.fork('child.js');
// Open up the server object and send the handle
var server = require('net').createServer();
server.listen(1337, function() { //主进程发送完句柄后关闭了监听
child1.send('server', server);
child2.send('server', server);
server.close();
});
// child.js
var http = require('http');
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('handled by child, pid is ' + process.pid + '\n');
});
process.on('message', function(m, tcp) {
if (m === 'server') {
tcp.on('connection', function(socket) {
server.emit('connection', socket);
});
}
});
//多个工作线程同时监听相同端口

child_process().fork.send()可以发送的句柄有:

  1. net.Socket tcp套接字
  2. net.Server tcp服务器 建立在tcp服务上的应用层服务都可以
  3. net.Native C++层面的tcp套接字或IPC管道
  4. dgarm.Socket UDP套接字
  5. dgram.Native c++层面的UDP套接字

send()方法在将消息发送到IPC前,会组装成两个对象 一个是handle 一个是message:
{
cmd: ‘NODE_HANDLE’,
type: ‘net.Server’,
msg: message
}

Smaller icon

message会先JSON.stringify()序列化再写入IPC管道(IPC通道只接受字符串),子线程再解析还原为对象,然后触发message事件,这其中还要进行过滤,如果message.cmd的值以NODE_为前缀,就会响应internalMessage事件,例如为NODE_HANDLE,就会取出message.type和得到的文件描述符一起还原

1
2
3
4
5
6
7
8
9
10
//此处为接受到tcp服务器句柄后工作线程的还原过程
function(message, handle, emit) {
var self = this;
var server = new net.Server();
server.listen(handle, function() {
emit(server);
});
}
// 子进程根据message.type创建对应TCP服务对象,然后监听到文件描述符

node进程之间只有消息传递,没有实例对象传递

端口共同监听
  • 多进程使用相同的文件描述符

node底层对每个端口监听都设置了SO_REUSEADDR,可以让不同进程对相同的网卡和端口监听,即服务端套接字可以被多进程服用

setsockopt(tcp->io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

当独立启动进程时,多个tcp服务器socket套接字的文件描述符互不相同,所以在监听相同端口时会报错。但send()发送句柄再启动的服务,它们引用了相同的文件描述符,并且文件描述符同一时间只能被一个进程使用,所以当网络请求来时,会出现抢占式服务

集群稳定

进程事件

  • error 当工作线程无法被复制创建、杀死、无法发送消息时触发
  • exit 工作线程退出时触发 ,若正常退出,则第一个参数为退出码,若是kill()的,则第二个参数为杀死进程信号(第一个为null)
  • close 工作线程的标准输入输出流中止时触发 参数与exit相同
  • disconnect 父进程、工作线程调用disconnect()方法时触发 将关闭监听IPC

父进程也可以用kill()发送信号 Node提供了kill -l列出的信号对应的事件

1
2
3
4
5
6
7
process.on('SIGTERM', function() {
console.log('Got a SIGTERM, exiting...');
process.exit(1);
});
console.log('sever running with PID:',process.pid);
process.kill(process.pid,'SIGTERM');

自动重启

  • 当工作线程退出时 启动新的工作进程来服务

给 process 对象添加 uncaughtException 事件绑定能够避免发生异常时进程直接退出

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
// master.js
var fork = require('child_process').fork;
var cpus = require('os').cpus();
var server = require('net').createServer();
server.listen(1337);
var workers = {};
var createWorker = function() {
var worker = fork(__dirname + '/worker.js');
// 退出时重新启动新的进程
worker.on('exit', function() {
console.log('Worker ' + worker.pid + ' exited.');
delete workers[worker.pid];
createWorker();
});
// 句柄转发
worker.send('server', server);
workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
};
for (var i = 0; i < cpus.length; i++) {
createWorker();
}
//主进程自己退出时,让所有工作进程退出
process.on('exit', function() {
for (var pid in workers) {
workers[pid].kill();
}
});

当工作线程出现未捕获的异常,就立刻停止接受新的连接,在所有连接断开后退出,主进程帧听到exit后启动新的进程服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// worker.js
var http = require('http');
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('handled by child, pid is ' + process.pid + '\n');
});
var worker;
process.on('message', function(m, tcp) {
if (m === 'server') {
worker = tcp;
worker.on('connection', function(socket) {
server.emit('connection', socket);
});
}
});
process.on('uncaughtException', function() {
//停止接收新的连接
worker.close(function() {
// 所有已有的连接断开后,退出进程
process.exit(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
27
28
29
30
31
32
33
34
35
// worker.js
process.on('uncaughtException', function(err) {
//记录日志
logger.error(err);
// 发送自杀信号
process.send({
act: 'suicide'
});
// 停止接收新的连接
worker.close(function() {
// 所有已有连接断开后,退出进程
process.exit(1);
});
// 5秒后退出进程 此处不graceful
setTimeout(function() {
process.exit(1);
}, 5000);
});
//原本在exit重启新进程的,改为在message事件中
var createWorker = function() {
var worker = fork(__dirname + '/worker.js');
// 启动新的进程
worker.on('message', function(message) {
if (message.act === 'suicide') {
createWorker();
}
});
worker.on('exit', function() {
console.log('Worker ' + worker.pid + ' exited.');
delete workers[worker.pid];
});
worker.send('server', server);
workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
};
限量重启
  • 避免因为在启动过程就出现错误 导致进程一直被重启

限制单位时间内重启次数,超过限制就触发giveup事件,并且对于此事件要着重加日志

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 limit = 10;
// 时间单位
var during = 60000;
var restart = [];
var isTooFrequently = function() {
// 记录重启时间
var time = Date.now();
var length = restart.push(time);
if (length > limit) {
// 取出最后10次记录
restart = restart.slice(limit * -1);
}
// 最后一次重启到前10次重启之间的时间间隔
return restart.length >= limit && restart[restart.length - 1] - restart[0] < during;
};
var workers = {};
var createWorker = function() {
// 检查是否太过频繁
if (isTooFrequently()) {
// 触发giveup事件后,不再重启
process.emit('giveup', length, during);
return;
}
var worker = fork(__dirname + '/worker.js');
worker.on('exit', function() {
console.log('Worker ' + worker.pid + ' exited.');
delete workers[worker.pid];
});
// 重新启动新的进程
worker.on('message', function(message) {
if (message.act === 'suicide') {
createWorker();
}
});
// 句柄转发
worker.send('server', server);
workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
};

负载均衡

  • Round-Robin 轮叫调度

对于Node而言,它繁忙部分有CPU和I/O,影响抢占的是CPU的繁忙度,所以使用抢占式服务可能会导致负载不均衡。

多线程/多进程等待同一个 socket 事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群。可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择)。这种性能浪费现象就是惊群。

轮叫调度是由主线程接受连接,将它依次分发给工作线程,分发策略为:在N个工作线程中,每次选择第i=(i+1)mod n个进程来发送连接

可以在cluster模块中启用:

1
2
3
4
5
6
7
8
// 启用Round-Robin
cluster.schedulingPolicy = cluster.SCHED_RR
// 不启用Round-Robin
cluster.schedulingPolicy = cluster.SCHED_NONE
或者者在环境变量中设置NODE_CLUSTER_SCHED_POLICY的值
export NODE_CLUSTER_SCHED_POLICY=rr
export NODE_CLUSTER_SCHED_POLICY=none

Round-Robin也可以通过代理服务器实现

状态共享

  • 在多个进程中共享数据

将数据放到数据库、文件、缓存,在进程启动时读取进内存。但在数据改变时,需要通知各个工作线程,使它们及时同步状态

定时轮询各个工作线程向第三方定时轮询。但轮询时间设定比较麻烦,过短会有并发处理,且增加性能损耗,过长则会导致数据更新不及时

主动通知 新建通知进程(服务),由它来轮询获取数据更新,并通知工作线程.为了能跨服务器使用,可以使用TCP,UDP来通信。进程在启动时,主动将进程信息注册到通知服务,通知服务依次来通知

Smaller icon

Cluster模块

  • child_process和net模块的组合
  • 提供完善的API,但没有child_process灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// Fork workers
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
} else {
// Workers can share any TCP connection
// In this case its a HTTP server
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}

在进程中判断是主进程还是工作进程:主要通过环境变量的NODE_UNIQUE_ID

1
2
3
cluster.isWorker = ('NODE_UNIQUE_ID' in process.env);
cluster.isMaster = (cluster.isWorker === false);
//使用cluster.setupMaster()设置主进程

Cluster会在内部启动TCP服务器,在fork()工作线程时,将tcp服务器的socket文件描述符发送给工作线程,并行该工作线程的环境变量里存在NODE_UNIQUE_ID。如果工作线程有listen()帧听端口,就会拿到文件描述符,通过SO_REUSEADDR设置实现端口共享。

使用cluster模块后,无法主动指定要共享的socket文件描述符,所以主进程只能管理一组工作进程。而使用child_process则可以创建多个tcp服务器,共享多个socket

Cluster事件

  • fork 复制一个工作线程后触发
  • online 复制工作线程完成后,工作线程主动发送online消息给主进程,主进程收到消息后触发
  • listening 工作线程调用listen()后,会发送listening消息给主进程,主进程收到消息后触发
  • disconnect 主进程和工作线程的IPC通道断开后触发
  • exit 工作线程退出时触发
  • setup cluster.setupMaster()执行后触发

上面的事件大都和child_process相关,是在进程间消息传递的基础上封装完成的

graceful

  • 优雅退出进程

在调用http.createServer().close()关闭server时,它会停止接收新的连接,但对于keepalive的连接,它依然会继续接受,导致close()无法完成

解决方案:

  • 记录所有连接 在关闭时全部销毁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function enableDestroy(server) {
var connections = {}
server.on('connection', function(conn) {
var key = conn.remoteAddress + ':' + conn.remotePort;
connections[key] = conn;
conn.on('close', function() {
delete connections[key];
});
});
server.destroy = function(cb) {
server.close(cb);
for (var key in connections)
connections[key].destroy();
};
}

但是太浪费内存了

req.socket.destroy()

数据上传

node的http模块(HTTP_Parse)只对http头部进行解析,内容部分通过data事件触发,以buffer方式处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var hasBody = function(req) {
return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
};
function(req, res) {
if (hasBody(req)) {
var buffers = [];
req.on('data', function(chunk) {
buffers.push(chunk);
});
req.on('end', function() {
req.rawBody = Buffer.concat(buffers).toString();//buffer.toString()转换成字符串 默认utf-8
handle(req, res);
});
} else {
handle(req, res);
}
}

数据处理

表单数据:Content-Type: application/x-www-form-urlencoded,内容格式和查询字符串相同,foo=bar&baz=val

1
2
3
4
5
6
var handle = function(req, res) {
if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
req.body = querystring.parse(req.rawBody);
}
todo(req, res);
};

json数据:Content-Type: application/json; charset=utf-8 可能附带编码信息

var mime = function(req) {
    var str = req.headers['content-type'] || '';
    return str.split(';')[0];
};
1
2
3
4
5
6
7
8
9
10
11
12
13
var handle = function(req, res) {
if (mime(req) === 'application/json') {
try {
req.body = JSON.parse(req.rawBody);
} catch (e) {
// 异常内容 响应Bad request
res.writeHead(400);
res.end('Invalid JSON');
return;
}
}
todo(req, res);
};

xml数据:Content-Type:application/xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var xml2js = require('xml2js');
var handle = function(req, res) {
if (mime(req) === 'application/xml') {
xml2js.parseString(req.rawBody, function(err, xml) {
if (err) {
// 异常内容 响应Bad request
res.writeHead(400);
res.end('Invalid XML');
return;
}
req.body = xml;
todo(req, res);
});
}
};

附件上传

  • 特殊表单,包含file控件
  • 指定表单属性enctype=”multipart/form-data”

Content-Type: multipart/form-data; boundary=AaB03x Content-Length: 18231
内容可能是由多部分组成,boundary指定每部分内容分界符

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
function(req, res) {
if (hasBody(req)) {
var done = function() {
handle(req, res);
};
if (mime(req) === 'application/json') {
parseJSON(req, done);
} else if (mime(req) === 'application/xml') {
parseXML(req, done);
} else if (mime(req) === 'multipart/form-data') {
parseMultipart(req, done);
}
} else {
handle(req, res);
}
}
//或者使用模块
var formidable = require('formidable');
function(req, res) {
if (hasBody(req)) {
if (mime(req) === 'multipart/form-data') {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
req.body = fields;
req.files = files;
handle(req, res);
});
}
} else {
handle(req, res);
}
}

数据上传与安全

内存限制
  • 限制上传内容大小,超过限制就不接受并发返回400
  • 流式处理,将数据流保存在磁盘,node只保存文件路径

通常node会先保存用户提交的数据,但提交数据过大时,会导致内存占光

Connect对上传数据量的限制方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function(req, res) {
var received = 0,
var len = req.headers['content-length'] ? parseInt(req.headers['content-length'], 10) : null;
// 如果内超过长度限制,返回请求实体过长的状态码
if (len && len > bytes) {
res.writeHead(413);
res.end();
return;
}
// limit
req.on('data', function(chunk) {
received += chunk.length;
if (received > bytes) {
//停止接收数据,触发end()
req.destroy();
}
});
handle(req, res);
};
CSRF
  • Cross-Site Request Forgery 跨站请求伪造

虽然用户通过浏览器访问服务器的Session ID不能被第三方知道,但攻击者可以在另外的网站上构造请求,然后浏览器会根据路由匹配取出cookie并提交到想攻击的服务器

解决方法:添加随机值 在请求时携带

1
2
3
4
5
6
7
8
9
10
11
为每个请求的用户在session中赋予随机值,并在response中告知,然后在请求时返回
function(req, res) {
var token = req.session._csrf || (req.session._csrf = generateRandom(24));
var _csrf = req.body._csrf;
if (token !== _csrf) {
res.writeHead(403);
res.end("禁止访问");
} else {
handle(req, res);
}
}

express的解决方案

路由解析

文件路径型

静态文件: url的路径和网站目录路径一致

动态文件: 服务器根据url路径找到对应的文件,再根据后缀找到脚步的解释器

MVC

  • 控制器 Controller 行为集合,根据url找到对应的控制器和行为
  • 模型 Model 数据操作封装
  • 视图 View 视图渲染,调用视图和相关数据进行页面渲染,输出到客户端
手工映射

需要手工配置路由,但url格式灵活

1
2
3
4
5
6
//完全匹配,配置映射
var routes = [];
var use = function (path, action) { routes.push([path, action]);};
use('/user/setting', exports.setting);
use('/setting/user', exports.setting);
  • 正则匹配

在注册路由时将路径转为一个正则表达式,然后用它来匹配

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
var pathRegexp = function(path) {
var keys = [];
path = path
.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function(_, slash, format, key, capture,
optional, star) {
// 将匹配到的键值保存
keys.push(key);
slash = slash || '';
return '' + (optional ? '' : slash) + '(?:'
+ (optional ? slash : '') + (format || '')
+ (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')'
+ (optional || '') + (star ? '(/*)?' : '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.*)');
return {
keys: keys,
regexp: new RegExp('^' + path + '$')
};
}
//修改路由注册
var use = function (path, action) { routes.push([pathRegexp(path), action]);};
//匹配
function(req, res) {
var pathname = url.parse(req.url).pathname;
for (var i = 0; i < routes.length; i++) {
var route = routes[i];
// 正则匹配
var reg = route[0].regexp;
var keys = route[0].keys;
var matched = reg.exec(pathname);
if (matched) {
// 将参数内容抽取设置到req.params
var params = {};
for (var i = 0, l = keys.length; i < l; i++) {
var value = matched[i + 1];c
if (value) {
params[keys[i]] = value;
}
}
req.params = params;
var action = route[1];
action(req, res);
return;
}
}
// 处理404请求
handle404(req, res);
}
自然映射
  • 按照约定的方式去查找
1
2
3
4
5
6
/controller/action/param1/param2/param3
exports.setting = function (req, res, month, year) {
// 如路径为/user/setting/12/1987 那么month为12 year为1987
// TODO
};

RESTful

  • Representational State Transfer (资源)表现层状态转化
  • 将服务器提供的内容看做有个资源,每一个URI代表一种资源
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现对资源的操作
  • 无状态 客户端和服务器交互的过程中是无状态,即服务器不保存用户状态信息(无session)

对服务器上的资源可以用URI(统一资源定位符)指向它,然后通过URI与资源互动,就是http的四个动作:GET、POST、PUT、DELETE

RESTful架构的状态包含两个,一是应用状态,二是资源状态。应用状态是与某一请求相关的状态信息;资源状态是服务器资源在某一时刻的特定状态,任何请求在同一时刻都会获得这一状态。RESTful的无状态是不保存应用状态,需要由请求方在请求时提高

应用无状态的服务器,可以自由调度请求,更好的实现负载均衡,分布式,并减少宕机带来的风险。