## 4.1 计算机网络基础知识
### 4.1.1 HTTP协议
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。HTTP是一个客户端和服务器端请求和应答的标准(TCP)。
**HTTPS**
HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单来说就是安全版的http。
HTTPS和HTTP的区别主要为以下四点:
* https协议需要到ca申请证书,一般免费证书很少,需要交费。
* http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
* http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
* http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
**HTTP2.0**
HTTP 2.0即超文本传输协议 2.0,是下一代HTTP协议。
新特性如下:
* 多路复用 (Multiplexing):多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。
* 二进制分帧:在不改动 HTTP/1.x 的语义、方法、状态码、URI 以及首部字段….. 的情况下,在应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码。
* 首部压缩(Header Compression):HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法。
* 服务端推送(Server Push)服务端推送是一种在客户端请求之前发送数据的机制。在 HTTP/2 中,服务器可以对客户端的一个请求发送多个响应。
**HTTP内容**
HTTP头:发送的是一些附加的信息:内容类型服务器发送响应的日期,HTTP状态码。
正文信息:用户提交的表单信息。
### 4.1.2 打开一个网站的流程(example.com为例)
**协议通信流程**
> http客户端发起请求,创建端口
> http服务器在端口监听客户请求
> http服务器向客户端返回状态和内容
1. 域名解析
①浏览器搜索浏览器自身的DNS缓存。
②如果浏览器没有找到自身的DNS缓存或之前的缓存已失效,那么浏览器会搜索操作系统自身的DNS缓存。
③如果操作系统的DNS缓存也没有找到,那么系统会尝试在本地的HOST文件去找。
④如果在HOST里依然没有找到,浏览器会发起一个DNS的系统调用,即一般向本地的宽带运营商发起**域名解析请求**。
2. 域名解析请求:
①宽带运营商服务器会首先查看自身的缓存,看是否有结果
②如果没有,那么运营商服务器会发起一个**迭代DNS解析请求**(根域,顶级域,域名注册商),最终会返回对DNS解析的结果。
③运营商服务器然后把结果返回给操作系统内核(同时也缓存在自己的缓存区),然后操作系统把结果返回给浏览器。
以上的最终结果,是让浏览器拿到example.com的IP地址,DNS解析完成。
3. 三次握手:建立TCP/IP连接。
4. 在TCP/IP连接建立起来后,浏览器就可以向服务器发送HTTP请求了。比如,用HTTP的GET方法请求一个根域里的某个域名,协议可以采用HTTP 1.0 。
5. 服务器端接受这个请求,根据路径参数,经过后端的一些处理之后,把处理后的一个结果以数据的形式返回给浏览器,如果是example.com网站的页面,服务器就会把完整的HTML页面代码返回给浏览器。
6. 浏览器拿到了example.com这个网站的完整HTML页面代码,在解析和渲染这个页面的时候,里面的Javascript、CSS、图片等静态资源,它们同样也是一个个HTTP请求,都需要经过上面的步骤来获取。
7. 浏览器根据拿到的资源对页面进行渲染,最终把一个完整的页面呈现出来。
## 4.2 网络监听(chrome浏览器)
通过开发者工具的网络监听,我们可以完成:
* 查看页面全部资源网络请求,包括请求耗时、状态码、头信息
* Ajax请求返回Json数据格式化查看
* 支持按请求类型分组过滤查看
* 查看页面完全加载耗时,总请求个数
### 4.2.1 以[新浪网](www.sina.com.cn)为例
F12打开开发者工具,点开网络监听,可以看到一系列信息。
![](https://box.kancloud.cn/ffedc4c6ece01b8645d591123539e3dd_1513x544.png)
**Preview**
![](https://box.kancloud.cn/6e18891a5b3748974c429545a5d63de4_925x185.png)
预览查看,如果是json文件,可以看到Ajax请求的格式化,如果是图片,可以看到图片本身。
**Timing**
![](https://box.kancloud.cn/d369733cd1fc4d973ed945ddc09d4864_1407x371.png)
Timing显示资源加载所要耗费的时间线。
* Stalled:等待时机,浏览器要发生请求,到能发出请求的时间。不包括DNS查询和连接建立时间
* Proxy negotiation:代理协商的时间
* Request sent:请求时间。从请求报文的第一个字节发出,到最后一个字节发送完毕的时间
* Waiting(TTFB):请求发出后至收到第一个字节响应的时间
* Content Download:从接受到响应第一个字节开始到最后一个字节结束花费的时间
**Headers**
![](https://box.kancloud.cn/d8ba10622ef03b7c5e14652ea3b87523_586x201.png)
简单作用说明:
* 静态资源请求状态码可用于分析是否使用了缓存
* 请求头信息可用于查看请求携带cookie信息
* 响应头信息科用于分析服务器配置信息
* 单个请求耗时与总请求耗时可用于网络优化
headers查看全部的头信息,下面介绍一下常见的请求方法和状态码:
* http请求方法:
Get:获取,读取数据
Post:提交资源
Put:更新
Delete:删除
Head 与get方法相同,但服务器不传回资源
* 状态码(服务器端返回浏览器,告知浏览器请求成功或失败的信息)
1XX:请求已经接受
2XX:请求成功并处理成功
3XX:重定向
4XX:客户端错误
5XX:服务器端错误
200:OK,请求成功
400:客户端请求有语法错误
401:请求未经授权
403:收到请求,但不提供服务
404:资源未找到
500:服务器端未知错误
503:服务器端当前不能处理请求
## 4.3 网络操作
不了解网络编程的程序员不是好前端,而NodeJS恰好提供了一扇了解网络编程的窗口。通过NodeJS,除了可以编写一些服务端程序来协助前端开发和测试外,还能够学习一些HTTP协议与Socket协议的相关知识,这些知识在优化前端性能和排查前端故障时说不定能派上用场。
使用NodeJS内置的http模块简单实现一个HTTP服务器:
~~~javascript
var http = require('http');
http.createServer( (request, response) => {
// 发送 HTTP 头部
// HTTP 状态值: 200 : OK
// 内容类型: text/plain
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应数据 "Hello World"
response.end('Hello World\n');
}).listen(8888);
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');
~~~
### 4.3.1 HTTP
http模块提供两种使用方式:
* 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。
* 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。
首先来看看服务端模式下如何工作。如前面的例子所示,首先需要使用`.createServer`方法创建一个服务器,然后调用`.listen`方法监听端口。之后,每当来了一个客户端请求,创建服务器时传入的回调函数就被调用一次。可以看出,这是一种**事件机制**。
HTTP请求本质上是一个数据流,由请求头(headers)和请求体(body)组成。
完整的HTTP请求数据内容:
~~~javascript
Object {host: "127.0.0.1:8888", connection: "keep-alive", cache-control: "max-age=0", upgrade-insecure-requests: "1", user-agent: "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/5…", …}
accept:"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
accept-encoding:"gzip, deflate, br"
accept-language:"zh-CN,zh;q=0.8"
cache-control:"max-age=0"
connection:"keep-alive"
host:"127.0.0.1:8888"
upgrade-insecure-requests:"1"
user-agent:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36"
__proto__:Object {__defineGetter__: , __defineSetter__: , hasOwnProperty: , …}
~~~
HTTP请求在发送给服务器时,可以认为是按照从头到尾的顺序一个字节一个字节地以数据流方式发送的。而http模块创建的HTTP服务器在接收到完整的请求头后,就会调用回调函数。在回调函数中,除了可以使用request对象访问请求头数据外,还能把request对象当作一个只读数据流来访问请求体数据。
~~~javascript
http.createServer( (request, response) => {
var body = [];
console.log(request.method);
console.log(request.headers);
request.on('data', function (chunk) {
body.push(chunk);
});
request.on('end', function () {
body = Buffer.concat(body);
console.log(body.toString());
});
}).listen(8888);
~~~
HTTP响应本质上也是一个数据流,同样由响应头(headers)和响应体(body)组成。
~~~javascript
Connection:keep-alive
Content-Type:text/plain
Date:Sun, 23 Jul 2017 13:11:54 GMT
Transfer-Encoding:chunked
~~~
在回调函数中,除了可以使用response对象来写入响应头数据外,还能把response对象当作一个只写数据流来写入响应体数据。例如在以下例子中,服务端原样将客户端请求的请求体数据返回给客户端。
~~~javascript
http.createServer(function (request, response) {
response.writeHead(200, { 'Content-Type': 'text/plain' });
request.on('data', function (chunk) {
response.write(chunk);
});
request.on('end', function () {
response.end();
});
}).listen(8888);
~~~
接下来看看客户端模式下如何工作。为了发起一个客户端HTTP请求,我们需要指定目标服务器的位置并发送请求头和请求体.
~~~javascript
var options = {
hostname: 'www.example.com',
port: 8888,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
var request = http.request(options, function (response) {});
request.write('Hello World');
request.end();
~~~
因为大多数请求都是 GET 请求且不带请求主体,所以 Node.js 提供了该便捷方法。 该方法与 `http.request() `唯一的区别是它设置请求方法为` GET `且自动调用 `req.end()`。
> 注意,响应数据必须在回调中被消耗,原因详见 http.ClientRequest 。
callback 被调用时只传入一个参数,该参数是 http.IncomingMessage 的一个实例。
一个获取 JSON 的例子:
~~~javascript
http.get('http://nodejs.org/dist/index.json', (res) => {
const { statusCode } = res;
const contentType = res.headers['content-type'];
let error;
if (statusCode !== 200) {
error = new Error('请求失败。\n' +
`状态码: ${statusCode}`);
} else if (!/^application\/json/.test(contentType)) {
error = new Error('无效的 content-type.\n' +
`期望 application/json 但获取的是 ${contentType}`);
}
if (error) {
console.error(error.message);
// 消耗响应数据以释放内存
res.resume();
return;
}
res.setEncoding('utf8');
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
console.log(parsedData);
} catch (e) {
console.error(e.message);
}
});
}).on('error', (e) => {
console.error(`错误: ${e.message}`);
});
~~~
### 4.3.2 HTTPS
HTTPS 是 HTTP 基于 TLS/SSL 的版本。在 Node.js 中,它被实现为一个独立的模块。
在服务端模式下,创建一个HTTPS服务器的示例如下。
~~~javascript
// curl -k https://localhost:8000/
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};
https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
~~~
可以看到,与创建HTTP服务器相比,多了一个options对象,通过key和cert字段指定了HTTPS服务器使用的私钥和公钥。
另外,NodeJS支持SNI技术,可以根据HTTPS客户端请求使用的域名动态使用不同的证书,因此同一个HTTPS服务器可以使用多个域名提供服务。接着上例,可以使用以下方法为HTTPS服务器添加多组证书。
~~~javascript
server.addContext('foo.com', {
key: fs.readFileSync('./ssl/foo.com.key'),
cert: fs.readFileSync('./ssl/foo.com.cer')
});
server.addContext('bar.com', {
key: fs.readFileSync('./ssl/bar.com.key'),
cert: fs.readFileSync('./ssl/bar.com.cer')
});
~~~
在客户端模式下,发起一个HTTPS客户端请求与http模块几乎相同,示例如下。
~~~javascript
var options = {
hostname: 'www.example.com',
port: 443,
path: '/',
method: 'GET'
};
var request = https.request(options, function (response) {});
request.end();
~~~
### 4.3.3 URL
处理HTTP请求时url模块使用率超高,因为该模块允许解析URL、生成URL,以及拼接URL。首先来看看一个完整的URL的各组成部分。
~~~
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ href │
├──────────┬──┬─────────────────────┬─────────────────────┬───────────────────────────┬───────┤
│ protocol │ │ auth │ host │ path │ hash │
│ │ │ ├──────────────┬──────┼──────────┬────────────────┤ │
│ │ │ │ hostname │ port │ pathname │ search │ │
│ │ │ │ │ │ ├─┬──────────────┤ │
│ │ │ │ │ │ │ │ query │ │
" https: // user : pass @ sub.host.com : 8080 /p/a/t/h ? query=string #hash "
│ │ │ │ │ hostname │ port │ │ │ │
│ │ │ │ ├──────────────┴──────┤ │ │ │
│ protocol │ │ username │ password │ host │ │ │ │
├──────────┴──┼──────────┴──────────┼─────────────────────┤ │ │ │
│ origin │ │ origin │ pathname │ search │ hash │
├─────────────┴─────────────────────┴─────────────────────┴──────────┴────────────────┴───────┤
│ href │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
~~~
我们可以使用new构造方法或`.parse`来将一个URL字符串转换为URL对象。
~~~javascript
const { URL } = require('url');
const myURL = new URL('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');
//或者
const myURL = url.parse('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');
/* =>
{ protocol: 'https:',
auth: 'user:pass',
host: 'sub.host.com:8080',
port: '8080',
hostname: 'sub,host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'http://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash' }
*/
~~~
传给`.parse`方法的不一定要是一个完整的URL,例如在HTTP服务器回调函数中,`request.url`不包含协议头和域名,但同样可以用`.parse`方法解析。
~~~javascript
http.createServer(function (request, response) {
var tmp = request.url; // => "/foo/bar?a=b"
url.parse(tmp);
/* =>
{ protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?a=b',
query: 'a=b',
pathname: '/foo/bar',
path: '/foo/bar?a=b',
href: '/foo/bar?a=b' }
*/
}).listen(8888);
~~~
`.parse`方法还支持第二个和第三个布尔类型可选参数。第二个参数等于true时,该方法返回的URL对象中,query字段不再是一个字符串,而是一个经过querystring模块转换后的参数对象。第三个参数等于true时,该方法可以正确解析不带协议头的URL,例如`//www.example.com/foo/bar。`
反过来,format方法允许将一个URL对象转换为URL字符串,示例如下。
~~~javascript
url.format({
protocol: 'http:',
host: 'www.example.com',
pathname: '/p/a/t/h',
search: 'query=string'
});
/* =>
'http://www.example.com/p/a/t/h?query=string'
*/
~~~
另外,.resolve方法可以用于拼接URL,示例如下。
~~~
url.resolve('http://www.example.com/foo/bar', '../baz');
/* =>
http://www.example.com/baz
*/
~~~
### 4.3.4 Query String
querystring 模块提供了一些实用工具,用于解析与格式化 URL 查询字符串。
`querystring.parse() `方法能把一个 URL 查询字符串(str)解析成一个键值对的集合。
~~~javascript
//查询字符串 'foo=bar&abc=xyz&abc=123' 被解析成:
{
foo: 'bar',
abc: ['xyz', '123']
}
~~~
`querystring.stringify()` 方法通过遍历对象的自有属性,从一个给定的 obj 产生一个 URL 查询字符串。
~~~javascript
querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
// 返回 'foo=bar&baz=qux&baz=quux&corge='
~~~
### 4.3.5 Zlib
zlib模块提供通过 Gzip 和 Deflate/Inflate 实现的压缩功能。当我们处理HTTP请求和响应时,可能需要用到这个模块。
压缩或者解压数据流(例如一个文件)通过zlib流将源数据流传输到目标流中来完成。
~~~javascript
const gzip = zlib.createGzip();
const fs = require('fs');
const inp = fs.createReadStream('input.txt');
const out = fs.createWriteStream('input.txt.gz');
inp.pipe(gzip).pipe(out);
~~~
数据的压缩或解压缩也可以只用一个步骤完成:
~~~javascript
const input = '.................................';
zlib.deflate(input, (err, buffer) => {
if (!err) {
console.log(buffer.toString('base64'));
} else {
// 错误处理
}
});
const buffer = Buffer.from('eJzT0yMAAGTvBe8=', 'base64');
zlib.unzip(buffer, (err, buffer) => {
if (!err) {
console.log(buffer.toString());
} else {
// 错误处理
}
});
~~~
再看看一个使用zlib模块压缩HTTP响应体数据的例子。这个例子中,判断了客户端是否支持gzip,并在支持的情况下使用zlib模块返回gzip之后的响应体数据。
~~~javascript
http.createServer(function (request, response) {
var i = 1024,
data = '';
while (i--) {
data += '.';
}
if ((request.headers['accept-encoding'] || '').indexOf('gzip') !== -1) {
zlib.gzip(data, function (err, data) {
response.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Encoding': 'gzip'
});
response.end(data);
});
} else {
response.writeHead(200, {
'Content-Type': 'text/plain'
});
response.end(data);
}
}).listen(8888);
~~~
接着我们看一个使用zlib模块解压HTTP响应体数据的例子。这个例子中,判断了服务端响应是否使用gzip压缩,并在压缩的情况下使用zlib模块解压响应体数据。
~~~javascript
var options = {
hostname: 'www.example.com',
port: 80,
path: '/',
method: 'GET',
headers: {
'Accept-Encoding': 'gzip, deflate'
}
};
http.request(options, function (response) {
var body = [];
response.on('data', function (chunk) {
body.push(chunk);
});
response.on('end', function () {
body = Buffer.concat(body);
if (response.headers['content-encoding'] === 'gzip') {
zlib.gunzip(body, function (err, data) {
console.log(data.toString());
});
} else {
console.log(data.toString());
}
});
}).end();
~~~
### 4.3.6 Net
net模块可用于创建Socket服务器或Socket客户端。由于Socket在前端领域的使用范围还不是很广,这里只简单演示一下如何从Socket层面来实现HTTP请求和响应。
首先我们来看一个使用Socket搭建一个很不严谨的HTTP服务器的例子。这个HTTP服务器不管收到啥请求,都固定返回相同的响应。
~~~javascript
net.createServer(function (conn) {
conn.on('data', function (data) {
conn.write([
'HTTP/1.1 200 OK',
'Content-Type: text/plain',
'Content-Length: 11',
'',
'Hello World'
].join('\n'));
});
}).listen(8888);
~~~
接着我们来看一个使用Socket发起HTTP客户端请求的例子。这个例子中,Socket客户端在建立连接后发送了一个HTTP GET请求,并通过data事件监听函数来获取服务器响应。
~~~javascript
var options = {
port: 8888,
host: 'www.example.com'
};
var client = net.connect(options, function () {
client.write([
'GET / HTTP/1.1',
'User-Agent: curl/7.26.0',
'Host: www.baidu.com',
'Accept: */*',
'',
''
].join('\n'));
});
client.on('data', function (data) {
console.log(data.toString());
client.end();
});
~~~