## 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打開開發者工具,點開網絡監聽,可以看到一系列信息。

**Preview**

預覽查看,如果是json文件,可以看到Ajax請求的格式化,如果是圖片,可以看到圖片本身。
**Timing**

Timing顯示資源加載所要耗費的時間線。
* Stalled:等待時機,瀏覽器要發生請求,到能發出請求的時間。不包括DNS查詢和連接建立時間
* Proxy negotiation:代理協商的時間
* Request sent:請求時間。從請求報文的第一個字節發出,到最后一個字節發送完畢的時間
* Waiting(TTFB):請求發出后至收到第一個字節響應的時間
* Content Download:從接受到響應第一個字節開始到最后一個字節結束花費的時間
**Headers**

簡單作用說明:
* 靜態資源請求狀態碼可用于分析是否使用了緩存
* 請求頭信息可用于查看請求攜帶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();
});
~~~