小作坊的服务器购买须知

如今大多数同学在学习编程的道路上,都会选择购买自己的服务器,然而由于国内环境,并不能访问到Google等,所以很多同学会选择,国外服务器,或者香港服务器。我推荐如果只是学习编程实践没有访问外网的需要,还是用国内的阿里云服务,质量还是有保证的。本人使用的是香港的小作坊服务器UFOVPS,遭受多次攻击。很沉痛。在此来告诫各位同学

ssh登陆不上,被告知流量异常

提交工单回复
後台系統檢測到您的服務器不停地往外發送大量異常數據包,和攻擊行為的特徵很類似,在短時間內消耗了大量的流量,流量記錄您可以在“監測記錄”看到,請問是否是您本人使用?如果不是您本人使用,可能是由下面的原因造成的:

1
2
使用了不夠複雜的密碼 ,或者已經被洩露的常用密碼,導致服務器被入侵或者惡意利用;
使用了第三方的破解版軟件或者沒有驗證來源的軟件 ,例如shadowsock、銳速、ZetaTCP等破解軟件,導致被利用後門入侵或劫持。

但是我只知道并非如此,通过查看后台流量;来源我发现是我的服务器大量的向外请求无效数据包,被当成肉鸡了,我立刻将防火墙Iptables打开,关闭向外访问权限,限制请求频率才解决。
关于如何设置防火墙请查看我另一个记录文档。

Iptables 安装与设置

安装

先检查是否安装了iptables

1
$ service iptables status

安装iptables

1
$ yum install -y iptables 

升级iptables

1
$ yum update iptables

安装iptables-services

1
$ yum install iptables-services

如果系统本身安装了firewalld服务,需要关闭该服务

1
2
3
4
$ 停止firewalld服务
systemctl stop firewalld
$ 禁用firewalld服务
systemctl mask firewalld

规则设置

查看现有规则

1
$ iptables -L -n

查看现有规则

1
$ iptables -L -n

查看现有规则

1
$ iptables -L -n

允许来自于lo接口的数据包(本地访问)

1
$ iptables -A INPUT -i lo -j ACCEPT

开放XXXX端口

1
2
3
4
5
$ iptables -A INPUT -p tcp --dport XXXX -j ACCEPT

-A默认是插入到尾部的,可以-I来插入到指定位置

iptables -I INPUT 3 -p tcp -m tcp --dport xxxx -j ACCEPT

允许ping

1
$ iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT

要封停一个IP,使用下面这条命令:

1
$ iptables -I INPUT -s ***.***.***.*** -j DROP

要解封一个IP,使用下面这条命令:

1
$ iptables -D INPUT -s ***.***.***.*** -j DROP

其他入站一律丢弃

1
$ iptables -P INPUT DROP

所有出站一律绿灯

1
$ iptables -P OUTPUT ACCEPT

所有转发一律丢弃

1
$ iptables -P FORWARD DROP

如果要添加内网ip信任(接受其所有TCP请求)

1
$ iptables -A INPUT -p tcp -s 45.96.174.68 -j ACCEPT

访问外网

1
$ iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

防止DDOS攻击

屏蔽 SYN_RECV 的连接

1
$ iptables -A FORWARD -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m limit --limit 1/sec -j ACCEPT

限制IP碎片,每秒钟只允许100个碎片,用来防止DoS***

1
$ iptables -A FORWARD -f -m limit --limit 100/sec --limit-burst 100 -j ACCEPT

限制ping包每秒一个,10个后重新开始

1
$ iptables -A FORWARD -p icmp -m limit --limit 1/sec --limit-burst 10 -j ACCEPT

###限制ICMP包回应请求每秒一个

1
$ iptables -A FORWARD -p icmp -m icmp --icmp-type 8 -m limit --limit 1/sec -j ACCEPT

删除规则

删除INPUT规则第三行例子

1
$ iptables -D INPUT 3

SSH无法链接linux

SSH无法链接Linux


某次误操作导致linux虚拟机服务器链接不上,报错/var/empty/sshd must be owned by root and not group or world-writable.

发现是权限问题

问题查找

1
2
3
$ rpm -V    检查到ssh软件包正常,但是某个目录属性错误
经查看发现这个目录的属主不是root,所以启动ssh报错
修改为root属主,启动成功

问题解决

1
2
3
chown -R root:root /var/empty/sshd
chmod 744 /var/empty/sshd
service sshd restart

问题总结

自己在处理文件权限的时候失误操作,命令中使用了 /xxx 导致将所有文件所有权更改导致

Node.js中Http的从请求到响应

Node.js 中,起一个 HTTP server 十分简单,短短数行即可:

1
2
3
4
5
6
7
8
9
'use stirct'
const { createServer } = require('http')
createServer(function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello World\n')
})
.listen(3000, function () {
console.log('Listening on port 3000')
})

1
2
$ curl localhost:3000
Hello World

就这么简单,因为 Node.js 把许多细节都已在源码中封装好了,主要代码在 lib/http*.js 这些文件中,现在就让我们照着上述代码,看看从一个 HTTP 请求的到来直到响应,Node.js 都为我们在源码层做了些什么。

HTTP 请求的来到

在 Node.js 中,若要收到一个 HTTP 请求,首先需要创建一个 http.Server 类的实例,然后监听它的 request 事件。由于 HTTP 协议属于应用层,在下层的传输层通常使用的是 TCP 协议,所以 net.Server 类正是 http.Server 类的父类。具体的 HTTP 相关的部分,是通过监听 net.Server 类实例的 connection 事件封装的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// lib/_http_server.js
// ...
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.addListener('request', requestListener);
}
// ...
this.addListener('connection', connectionListener);
// ...
}
util.inherits(Server, net.Server);

这时,则需要一个 HTTP parser 来解析通过 TCP 传输过来的数据:

1
2
3
4
5
6
7
8
9
10
11
12
// lib/_http_server.js
const parsers = common.parsers;
// ...
function connectionListener(socket) {
// ...
var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
socket.parser = parser;
parser.incoming = null;
// ...
}

值得一提的是,parser 是从一个“池”中获取的,这个“池”使用了一种叫做 free list(wiki)的数据结构,实现很简单,个人觉得是为了尽可能的对 parser 进行重用,并避免了不断调用构造函数的消耗,且设有数量上限(http 模块中为 1000):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// lib/freelist.js
'use strict';
exports.FreeList = function(name, max, constructor) {
this.name = name;
this.constructor = constructor;
this.max = max;
this.list = [];
};
exports.FreeList.prototype.alloc = function() {
return this.list.length ? this.list.pop() :
this.constructor.apply(this, arguments);
};
exports.FreeList.prototype.free = function(obj) {
if (this.list.length < this.max) {
this.list.push(obj);
return true;
}
return false;
};

由于数据是从 TCP 不断推入的,所以这里的 parser 也是基于事件的,很符合 Node.js 的核心思想。使用的是 http-parser 这个库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// lib/_http_common.js
// ...
const binding = process.binding('http_parser');
const HTTPParser = binding.HTTPParser;
const FreeList = require('internal/freelist').FreeList;
// ...
var parsers = new FreeList('parsers', 1000, function() {
var parser = new HTTPParser(HTTPParser.REQUEST);
// ...
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
parser[kOnExecute] = null;
return parser;
});
exports.parsers = parsers;
// lib/_http_server.js
// ...
function connectionListener(socket) {
parser.onIncoming = parserOnIncoming;
}

所以一个完整的 HTTP 请求从接收到完全解析,会挨个经历 parser 上的如下事件监听器:
1
2
3
4
5
> parserOnHeaders:不断解析推入的请求头数据。
> parserOnHeadersComplete:请求头解析完毕,构造 header 对象,为请求体创建 http.IncomingMessage 实例。
> parserOnBody:不断解析推入的请求体数据。
> parserOnExecute:请求体解析完毕,检查解析是否报错,若报错,直接触发 clientError 事件。若请求为 CONNECT 方法,或带有 Upgrade 头,则直接触发 connect 或 upgrade 事件。
> parserOnIncoming:处理具体解析完毕的请求。

所以接下来,我们的关注点自然是 parserOnIncoming 这个监听器,正是这里完成了最终 request 事件的触发,关键步骤代码如下:
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
// lib/_http_server.js
// ...
function connectionListener(socket) {
var outgoing = [];
var incoming = [];
// ...
function parserOnIncoming(req, shouldKeepAlive) {
incoming.push(req);
// ...
var res = new ServerResponse(req);
if (socket._httpMessage) { // 这里判断若为真,则说明 socket 正在被队列中之前的 ServerResponse 实例占用
outgoing.push(res);
} else {
res.assignSocket(socket);
}
res.on('finish', resOnFinish);
function resOnFinish() {
incoming.shift();
// ...
var m = outgoing.shift();
if (m) {
m.assignSocket(socket);
}
}
// ...
self.emit('request', req, res);
}
}

可以看出,对于同一个 socket 发来的请求,源码中分别维护了两个队列,用于缓冲 IncomingMessage 实例和对应的 ServerResponse 实例。先来的 ServerResponse 实例先占用 socket ,监听其 finish 事件,从各自队列中释放该 ServerResponse 实例和对应的 IncomingMessage 实例。

比较绕,以一个简化的图示来总结这部分逻辑:

响应该 HTTP 请求

到了响应时,事情已经简单许多了,传入的 ServerResponse 已经获取到了 socket。http.ServerResponse 继承于一个内部类 http.OutgoingMessage,当我们调用 ServerResponse#writeHead 时,Node.js 为我们拼凑好了头字符串,并缓存在 ServerResponse 实例内部的 _header 属性中:

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
// lib/_http_outgoing.js
// ...
OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
// ...
if (headers) {
var keys = Object.keys(headers);
var isArray = Array.isArray(headers);
var field, value;
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
if (isArray) {
field = headers[key][0];
value = headers[key][1];
} else {
field = key;
value = headers[key];
}
if (Array.isArray(value)) {
for (var j = 0; j < value.length; j++) {
storeHeader(this, state, field, value[j]);
}
} else {
storeHeader(this, state, field, value);
}
}
}
// ...
this._header = state.messageHeader + CRLF;
}

紧接着在调用 ServerResponse#end 时,将数据拼凑在头字符串后,添加对应的尾部,推入 TCP ,具体的写入操作在内部方法 ServerResponse#_writeRaw 中:
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
// lib/_http_outgoing.js
// ...
OutgoingMessage.prototype.end = function(data, encoding, callback) {
// ...
if (this.connection && data)
this.connection.cork();
var ret;
if (data) {
this.write(data, encoding);
}
if (this._hasBody && this.chunkedEncoding) {
ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish);
} else {
ret = this._send('', 'binary', finish);
}
if (this.connection && data)
this.connection.uncork();
// ...
return ret;
}
OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) {
if (typeof encoding === 'function') {
callback = encoding;
encoding = null;
}
var connection = this.connection;
// ...
return connection.write(data, encoding, callback);
};

到这,一个请求就已经通过 TCP ,发回给客户端了

Node.js 中 Buffer 的 8KB 池分配规则和固定位数字的读写

8KB 池分配规则

统计一下,当前版本的 Node.js (v6.0)中可以创建一个新 Buffer 类实例的 API 有:

  • new Buffer() (已不推荐使用,可能会泄露内存中潜在的敏感信息,具体例子可以看这里)
  • Buffer.alloc()
  • Buffer.allocUnsafe()(虽然也有泄露内存中敏感信息的可能,但语义上非常明确)
  • Buffer.from()
  • Buffer.concat()
    跟着代码追溯,这些 API 最后都会走进两个内部函数中的一个,来创建 Buffer 实例,这两个内部函数分别是 createBuffer() 和 allocate():
    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
    // lib/buffer.js
    // ...
    Buffer.poolSize = 8 * 1024;
    var poolSize, poolOffset, allocPool;
    function createPool() {
    poolSize = Buffer.poolSize;
    allocPool = createBuffer(poolSize, true);
    poolOffset = 0;
    }
    createPool();
    function createBuffer(size, noZeroFill) {
    flags[kNoZeroFill] = noZeroFill ? 1 : 0;
    try {
    const ui8 = new Uint8Array(size);
    Object.setPrototypeOf(ui8, Buffer.prototype);
    return ui8;
    } finally {
    flags[kNoZeroFill] = 0;
    }
    }
    function allocate(size) {
    if (size === 0) {
    return createBuffer(size);
    }
    if (size < (Buffer.poolSize >>> 1)) {
    if (size > (poolSize - poolOffset))
    createPool();
    var b = allocPool.slice(poolOffset, poolOffset + size);
    poolOffset += size;
    alignPool();
    return b;
    } else {
    return createBuffer(size, true);
    }
    }
    通过代码可以清楚的看到,若最后创建时,走的是 createBuffer() 函数,则不经过 8KB 池,若走 allocate() 函数,当传入的数据大小小于 Buffer.poolSize 有符号右移 1 位后的结果(相当于将该值除以 2 再向下取整,在本例中,为 4 KB),才会使用到 8KB 池(若当前池剩余空间不足,则创建一个新的,并将当前池指向新池)。

那么现在让我们来看看,这些 API 都走的是哪些方法:

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
// lib/buffer.js
// ...
Buffer.alloc = function(size, fill, encoding) {
// ...
return createBuffer(size);
};
Buffer.allocUnsafe = function(size) {
assertSize(size);
return allocate(size);
};
Buffer.from = function(value, encodingOrOffset, length) {
// ...
if (value instanceof ArrayBuffer)
return fromArrayBuffer(value, encodingOrOffset, length);
if (typeof value === 'string')
return fromString(value, encodingOrOffset);
return fromObject(value);
};
function fromArrayBuffer(obj, byteOffset, length) {
byteOffset >>>= 0;
if (typeof length === 'undefined')
return binding.createFromArrayBuffer(obj, byteOffset);
length >>>= 0;
return binding.createFromArrayBuffer(obj, byteOffset, length);
}
function fromString(string, encoding) {
// ...
if (length >= (Buffer.poolSize >>> 1))
return binding.createFromString(string, encoding);
if (length > (poolSize - poolOffset))
createPool();
var actual = allocPool.write(string, poolOffset, encoding);
var b = allocPool.slice(poolOffset, poolOffset + actual);
poolOffset += actual;
alignPool();
return b;
}
Buffer.concat = function(list, length) {
// ...
var buffer = Buffer.allocUnsafe(length);
// ...
return buffer;
};

挺一目了然的,让我们来总结一下,当在以下情况同时都成立时,创建的新的 Buffer 类实例才会经过内部 8KB 池:

  • 通过 Buffer.allocUnsafe,Buffer.concat,Buffer.from(参数不为一个 ArrayBuffer 实例)和 new Buffer(参数不为一个 ArrayBuffer 实例)创建。
  • 传入的数据大小不为 0 。
  • 且传入数据的大小必须小于 4KB 。

    那些固定位数字读写 API

    当你在阅读 Buffer 的文档时,看到诸如 Buffer#writeUInt32BE,Buffer#readUInt32BE 这样的 API 时,可能会想到 ES6 规范中的 DateView 类提供的那些方法。其实它们做的事情十分相似,Node.js 项目中甚至还有将这些 API 的底层都替换成原生的 DateView 实例来操作的 PR ,但该 PR 目前已被标记为 stalled ,具体原因大致是:
  • 没有显著的性能提升。
  • 会在实例被初始化后又增加新的属性。
  • noAssert 参数将会失效。
    先不管这个 PR ,其实,这些读写操作,若数字的精度在 32 位以下,则对应方法都是由 JavaScript 实现的,十分优雅,利用了 TypeArray 下那些类(Buffer 中使用的是 Uint8Array)的实例中的元素,在位溢出时,会抛弃溢出位的机制。以 writeUInt32LE 和 writeUInt32BE (LE 和 BE 即小端字节序和大端字节序,可以参阅这篇文章)为例,一个 32 位无符号整数需要 4 字节存储,大端字节序时,则第一个元素为直接将传入的 32 位整数无符号右移 24 位,获取到原最左的 8 位,抛弃当下左边的所有位。以此类推,第二个元素为无符号右移 16 位,第三个元素为 8 位,第四个元素无需移动(小端字节序则相反):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Buffer.prototype.writeUInt32BE = function(value, offset, noAssert) {
    value = +value;
    offset = offset >>> 0;
    if (!noAssert)
    checkInt(this, value, offset, 4, 0xffffffff, 0);
    this[offset] = (value >>> 24);
    this[offset + 1] = (value >>> 16);
    this[offset + 2] = (value >>> 8);
    this[offset + 3] = value;
    return offset + 4;
    };
    读操作与之对应,使用了无符号左移后腾出空位再进行 | 操作合并:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Buffer.prototype.readUInt32BE = function(offset, noAssert) {
    offset = offset >>> 0;
    if (!noAssert)
    checkOffset(offset, 4, this.length);
    return (this[offset] * 0x1000000) +
    ((this[offset + 1] << 16) |
    (this[offset + 2] << 8) |
    this[offset + 3]);
    };
    其中的 (this[offset] * 0x1000000) + 相当于 this[offset] << 24 | 。

JavaScript 的深复制

第三方库的实现

Underscore —— _.clone()

在 Underscore 中有这样一个方法:.clone(),这个方法实际上是一种浅复制 (shallow-copy),所有嵌套的对象和数组都是直接复制引用而并没有进行深复制。来看一下例子应该会更加直观:

1
2
3
4
5
6
7
8
9
10
11
12
var x = {
a: 1,
b: { z: 0 }
};

var y = _.clone(x);

y === x // false
y.b === x.b // true

x.b.z = 100;
y.b.z // 100

让我们来看一下 Underscore 的源码:
1
2
3
4
5
6
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};


如果目标对象是一个数组,则直接调用数组的slice()方法,否则就是用
.extend()方法。想必大家对extend()方法不会陌生,它的作用主要是将从第二个参数开始的所有对象,按键值逐个赋给第一个对象。而在 jQuery 中也有类似的方法。关于 Underscore 中的 _.extend() 方法的实现可以参考 underscore.js #L1006。

Underscore 的 clone() 不能算作深复制,但它至少比直接赋值来得“深”一些,它创建了一个新的对象。另外,你也可以通过以下比较 tricky 的方法来完成单层嵌套的深复制:

1
2
3
4
5
var _ = require('underscore');
var a = [{f: 1}, {f:5}, {f:10}];
var b = _.map(a, _.clone); // <----
b[1].f = 55;
console.log(JSON.stringify(a)); // [{"f":1},{"f":5},{"f":10}]

jQuery —— $.clone() / $.extend()

在 jQuery 中也有这么一个叫 $.clone() 的方法,可是它并不是用于一般的 JS 对象的深复制,而是用于 DOM 对象。这不是这篇文章的重点,所以感兴趣的同学可以参考jQuery的文档。与 Underscore 类似,我们也是可以通过 $.extend() 方法来完成深复制。值得庆幸的是,我们在 jQuery 中可以通过添加一个参数来实现递归extend。调用$.extend(true, {}, …)就可以实现深复制啦,参考下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};

var y = $.extend({}, x), //shallow copy
z = $.extend(true, {}, x); //deep copy

y.b.f === x.b.f // true
z.b.f === x.b.f // false

在 jQuery的源码 - src/core.js #L121 文件中我们可以找到$.extend()的实现,也是实现得比较简洁,而且不太依赖于 jQuery 的内置函数,稍作修改就能拿出来单独使用。

lodash —— _.clone() / _.cloneDeep()

在lodash中关于复制的方法有两个,分别是.clone()和.cloneDeep()。其中.clone(obj, true)等价于.cloneDeep(obj)。使用上,lodash和前两者并没有太大的区别,但看了源码会发现,Underscore 的实现只有30行左右,而 jQuery 也不过60多行。可 lodash 中与深复制相关的代码却有上百行,这是什么道理呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var $ = require("jquery"),
_ = require("lodash");

var arr = new Int16Array(5),
obj = { a: arr },
obj2;
arr[0] = 5;
arr[1] = 6;

// 1. jQuery
obj2 = $.extend(true, {}, obj);
console.log(obj2.a); // [5, 6, 0, 0, 0]
Object.prototype.toString.call(obj2); // [object Int16Array]
obj2.a[0] = 100;
console.log(obj); // [100, 6, 0, 0, 0]

//此处jQuery不能正确处理Int16Array的深复制!!!

// 2. lodash
obj2 = _.cloneDeep(obj);
console.log(obj2.a); // [5, 6, 0, 0, 0]
Object.prototype.toString.call(arr2); // [object Int16Array]
obj2.a[0] = 100;
console.log(obj); // [5, 6, 0, 0, 0]

通过上面这个例子可以初见端倪,jQuery 无法正确深复制 JSON 对象以外的对象,而我们可以从下面这段代码片段可以看出 lodash 花了大量的代码来实现 ES6 引入的大量新的标准对象。更厉害的是,lodash 针对存在环的对象的处理也是非常出色的。因此相较而言,lodash 在深复制上的行为反馈比前两个库好很多,是更拥抱未来的一个第三方库。

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
/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
arrayTag = '[object Array]',
boolTag = '[object Boolean]',
dateTag = '[object Date]',
errorTag = '[object Error]',
funcTag = '[object Function]',
mapTag = '[object Map]',
numberTag = '[object Number]',
objectTag = '[object Object]',
regexpTag = '[object RegExp]',
setTag = '[object Set]',
stringTag = '[object String]',
weakMapTag = '[object WeakMap]';

var arrayBufferTag = '[object ArrayBuffer]',
float32Tag = '[object Float32Array]',
float64Tag = '[object Float64Array]',
int8Tag = '[object Int8Array]',
int16Tag = '[object Int16Array]',
int32Tag = '[object Int32Array]',
uint8Tag = '[object Uint8Array]',
uint8ClampedTag = '[object Uint8ClampedArray]',
uint16Tag = '[object Uint16Array]',
uint32Tag = '[object Uint32Array]';

借助 JSON 全局对象

相比于上面介绍的三个库的做法,针对纯 JSON 数据对象的深复制,使用 JSON 全局对象的 parse 和 stringify 方法来实现深复制也算是一个简单讨巧的方法。然而使用这种方法会有一些隐藏的坑,它能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。

1
2
3
4
function jsonClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
var clone = jsonClone({ a:1 });

Http通信协议

Http协议是什么

协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的法定活规则,超文本传输协议(HTTP)是一种通信协议,它允许将超文本标记语言文档从web服务器传送到客户端的浏览器。

HTTP协议的特点

  • 支持客户端/服务器模式
  • 无状态,此处的无状态是指对事务无记忆能力,如果后续需要这些信息,那么需要重新传递获取数据。
  • 简单快速,客户端只需要发送请求方法和路径到服务端,请求方式有get,post,head,put,delete,options,trace,关于此处的解释移步此处.
  • 无连接,指每次处理只限制处理一个连接.服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • 灵活,HTTP请求允许发送任意类型的数据。当前请求类型可以查看Content-Type

HTTP请求

1.Date头域
Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date:Mon,31Dec200104:25:57GMT。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区。
2.Pragma头域
Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache。在HTTP/1.1协议中,它的含义和Cache- Control:no-cache相同。
3.Host头域
Host头域指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域,否则系统会以400状态码返回。
4.Referer头域
Referer 头域允许客户端指定请求uri的源资源地址,这可以允许服务器生成回退链表,可用来登陆、优化cache等。他也允许废除的或错误的连接由于维护的目的被 追踪。如果请求的uri没有自己的uri地址,Referer不能被发送。如果指定的是部分uri地址,则此地址应该是一个相对地址。
5.Cache-Control头域 指定请求和响应遵循的缓存机制
Cache- Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置 Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令包括no-cache、no-store、max-age、 max-stale、min-fresh、only-if-cached,响应消息中的指令包括public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age

  • Public指示响应可被任何缓存区缓存。
  • Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
  • no-cache指示请求或响应消息不能缓存
  • no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
  • max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
  • min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
  • max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息
    6.Range头域
    Range头域可以请求实体的一个或者多个子范围。例如:
  • 表示头500个字节:bytes=0-499
  • 表示第二个500字节:bytes=500-999
  • 表示最后500个字节:bytes=-500
  • 表示500字节以后的范围:bytes=500-
  • 第一个和最后一个字节:bytes=0-0,-1
  • 同时指定几个范围:bytes=500-600,601-999
  • 但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200 (OK)。
    7.User-Agent头域
    User-Agent头域的内容包含发出请求的用户信息。HTTP- Version表示支持的HTTP版本,例如为HTTP/1.1。Status- Code是一个三个数字的结果代码。Reason-Phrase给Status-Code提供一个简单的文本描述。Status-Code主要用于机器自 动识别,Reason-Phrase主要用于帮助用户理解。Status-Code的第一个数字定义响应的类别,后两个数字没有分类的作用。浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。

HTTP响应

响应消息行:包含协议/版本,响应状态码,对响应状态码的描述(一切正常)
响应消息头:服务器与客户端通信的暗码,告诉客户端该怎么执行某些操作
响应消息正文:和网页右键“查看源码”看到的内容一样
1.状态行(HTTP/1.1 200 OK)

  • 协议版本号。
  • 状态代码:状态代码由3位数字组成,表示请求是否被理解或被满足。
    • 200 OK 客户端请求成功
    • 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
    • 301 (302) 重定向—要完成请求必须进行更进一步的操作。
    • 304 缓存
    • 401 Unauthonzed 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用
    • 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因()
    • 404 Not Found 请求的资源不存在,例如,输入了错误的URL。
    • 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
    • 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。
  • 状态描述:状态描述给出了关于状态代码的简短的文字描述。
    2.响应头
    Location:响应报头域用于重定向接受者到一个新的位置。
    Server: 响应报头域包含了服务器用来处理请求的软件信息。0