- TCP與HTTP
- HTTP報文格式
- 請求報文
- 請求行
- 請求頭
- 請求體
- 響應報文
- 響應行
- 響應頭
- 響應體
- 關于url
- 關于method
- 關于請求體
- 關于狀態碼
- 2xx 請求正常
- 3xx 緩存和重定向
- 4xx 客戶端錯誤
- 5xx 服務端錯誤
- 在node中獲取請求報文
- 在node中創建http服務器與客戶端
- 創建一個服務器
- 創建一個客戶端
- 響應注意事項
- 關于響應實體和Content-Type
- 用tcp實現一個簡單的http服務器
- 注冊響應回調
- parser分離請求報文與發射request
- 解析請求頭
- demo代碼
- http其它
- 非短連接
- 管線化
[TOC]
## TCP與HTTP
首先,`HTTP` 是基于 `TCP` 協議的,只有當tcp連接順利建立時,瀏覽器客戶端才能向服務器發送http請求。(詳見TCP三次握手)
當`TCP` 讓讓一臺pc端對端的連接上另一臺pc后,兩臺機器之間可以互通數據,但這個數據并沒有經過什么額外的加工,是純粹的數據,即用戶輸入什么數據,服務器就會拿到什么數據。
而 `HTTP` 有些許不一樣, 一個http請求會將用戶的輸入經過瀏覽器包裝后再發送給服務端,而包裝后的數據即是我們說的 `http請求報文`。對應的,服務器要向瀏覽器回復響應,也需要經過一層包裝,包裝成 `http響應報文`再響應給客戶端。
從傳輸層面上來講,http僅僅是tcp的一項子集,一種再封裝。
## HTTP報文格式
HTTP報文格式 是對 http協議最直觀的闡釋,也是我們學習http協議最有效率的手段。
HTTP報文主要分文兩大類,`請求報文` 和 `響應報文`,請求報文和響應報文又都分為`行`、 `頭` 和 `體` 三部分,并且頭和體這兩部分之間有 `空行` 隔開。
### 請求報文

#### 請求行
請求行分為以下三部分,每個部分之間用空格隔開
- method:
主要是用來標識是要傳數據還是獲取數據
- path:
url地址
- protocol
http的協議版本號
#### 請求頭
請求頭和請求行不一樣,它是多行的,每一行都是一組鍵值對,鍵和值之間用`:`和`空格`隔開。
- 請求首部:
Host:xxx.com
- 通用首部:
請求和響應都有的,比如 `Connection:keep-alive`
- 實體首部:
以 `Content-`開頭的
- 其它
#### 請求體
正經的,用戶想要傳給服務器的數據
### 響應報文

#### 響應行
- protocol
http協議版本號
- statusCode
狀態碼
- statusCode-reason
原因短語,狀態碼的解釋
#### 響應頭
同請求頭
#### 響應體
同請求體
### 關于url
客戶端封裝請求報文時會將url中的hash給去掉(query是會保留的)

故服務器端是永遠接收不到客戶端的hash值的
### 關于method
GET 獲取資源
POST 想服務器端發送數據,傳輸實體主體
PUT 傳輸文件 , RESTful中是更新修改操作
HEAD 獲取報文首部
DELETE 刪除文件
OPTIONS 詢問支持的方法 ,試探方法,比如跨域,會先詢問服務端能否跨域
TRACE 追蹤路徑
### 關于請求體
當提交的表單只包含一條數據時,且表單類型為默認時,請求報文長這樣

如果是多條數據,會用空行隔開

但如果是`multipart/form-data`編碼時,請求體中多端數據間則是用特殊的分隔符來隔開的
即使只有一段數據也會用特殊的分隔符包裹住

多段數據時

### 關于狀態碼
狀態碼主要分為五大類
- 1xx imformational(信息狀態碼) websocket
- 2xx Success
- 3xx Redirect
- 4xx Client Error
- 5xx Server Error
#### 2xx 請求正常
- 200 OK 客戶端發送過來的數據被正常處理
- 204 Not Content 正常響應,沒有實體
- 206 Partial Content 范圍請求,返回部分數據,響應報文中由Content-Range指定內容
#### 3xx 緩存和重定向
- 301 Moved Permanently 永久重定向
- 302 Found 臨時重定向 不一定去哪 跳轉到不同的地方 Nginx
- 303 See Other和302類似,但必須用GET方法
- 304 Not Modified 狀態未改變 需要和(if-Match、if-Modified-since、if-None_Match、if-Range、if-Unmodified-since)配合使用
- 307 Temporary Redirect 臨時重定向,不改變請求方法
#### 4xx 客戶端錯誤
- 400 Bad Request 請求報文語法錯誤
- 401 unauthorized 需要認證
- 403 Forbidden 服務器拒絕訪問對應的資源
- 404 Not Found 服務器上無法找到資源
#### 5xx 服務端錯誤
- 500 Internal Server Error 服務器故障
- 503 Service Unavailable 服務器處于超負載或正在停機維護
## 在node中獲取請求報文
```
console.log(req.method); //請求方法
console.log(req.url); //url地址
console.log(req.httpVersion); //http協議版本
console.log(req.headers); //請求頭
```
```
// 獲取請求體
req.on('data',function(data){
console.log(data.toString());
})
```
## 在node中創建http服務器與客戶端
### 創建一個服務器
```
let http = require('http');
let server = http.createServer();
server.on('request',function(req,res){
res.end('ok');
});
server.listen(8080);
```
你可以可以這樣簡寫
```
let http = require('http');
let server = http.createServer(function(req,res){
res.end('ok');
});
server.listen(8080);
```
### 創建一個客戶端
```
let http = require('http');
let options = {
host:'localhost'
,port:8080
,method:'POST'
,headers:{
'Content-Type':'application/x-www-form-urlencoded'
// ,'Content-Length':15 //一般來說這個數值會自動計算
}
}
let req = http.request(options);
req.write('id=999');
// 只有調用end才會真正向服務器發送請求
req.end();
// 當客戶端收到服務器響應的時候觸發
req.on('response',function(res){ //只有一個參數
console.log(res.statusCode);
console.log(res.headers);
let result = [];
res.on('data',function(data){
result.push(data);
})
res.on('end',function(data){
let str = Buffer.concat(result);
console.log(str.toString());
})
})
```
還可以把`request()`和`on('response')`合在一起寫,不過此時無法像服務端主動發送頭以外的數據(只有調用`http.request(opt)`才會返回req,才能調用`write()`)。
```
http.get(options,function(res){
...
res.on('data',function(chunk){
...
});
res.on('end',function(){
...
})
})
```
#### 響應注意事項
end后無法繼續寫入(可寫流規定)
```
res.write()
res.end()
<<<
Erorr::write after end!
```
設置狀態碼以后 會自動補全狀態碼文本描述
```
res.statusCode = 200; //默認
```
我們不僅可以設置,也可以刪除一個準備發送給客戶端的響應頭
```
res.setHeader('Content-Type','text/plain');
res.setHeader('name','ahhh');
res.removeHeader('name'); //刪除一個準備設置的頭
```
`writeHead`相較于`setHead`能同時設置多個頭,并且連狀態碼一起設置。但它和setHeader最大的不同在于,writeHeader一旦調用會立刻發送。
```
console.log(res.headersSent) //false
res.writeHead(200,{'Content-Type':'text/plain'}); //writeHead設置完后不能再調用res.setHeader,因為調用writeHead會直接把頭發送出去
// res.setHeader('name','zfpx'); //Can't set headers after they are sent.
console.log(res.headersSent) //true
```
而`setHeader`設置的頭是在調用write方法之后才會發送,另外需要注意的一點是頭必須在write之前設置。
```
console.log('--- --- ---')
console.log(res.headersSent); //false
res.setHeader('name','ahhh');
console.log(res.headersSent) //false
res.write('ok');
console.log(res.headersSent) //true
res.end('end');
console.log(res.headersSent) //true
console.log('--- --- ---')
```
## 關于響應實體和Content-Type
客戶端發送請求和服務端回以響應時都需要設置這個`Content-Type`頭,
對于服務端來說,它需要拿這個頭解析客戶端發送過來的**實體數據**,(縱然不少情況下,請求都沒有實體部分,比如get請求)。
```
let buffers = [];
req.on('data',function(chunk){
buffers.push(chunk);
})
req.on('end',function(){
let content = Buffer.concat(buffers).toString();
if(contentType === 'application/json'){
console.log(JSON.parse(content).name);
}else if(contentType === 'application/x-www-form-urlencoded'){
let queryString = require('querystring');
console.log(queryString.parse(content).name);
}
})
```
>實際情況下,如果有請求體(實體數據),可能會很復雜。(前面的請求體部分)
并且服務端響應客戶端數據時也需要發給它這么一個頭以便客戶端解析數據,而這個`Content-Type`往往和要返回給客戶端的資源文件的后綴名是相關聯的,So我們一般使用一個npm包幫我們進行轉換,
```
...
let mime = require('mime');
...
res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
```
## 用tcp實現一個簡單的http服務器
http服務器相較于tcp服務器其實就多做了一件事,即解析請求頭,剩下的請求體部分該`on data`還是一樣on data監聽即可。
但需要注意的是,data,即請求體是什么時候 `發射` 的呢?嗯,是在分離出請求頭并解析完畢請求頭后發射的。

### 注冊響應回調
```
// http.createServer(function(req,res){})
server.on('request',function(req,res){
//do somtheing like you are doing at tcp
}
```
### parser分離請求報文與發射request
```
let server = net.createServer(function(socket){
parser(socket,function(req,res){
server.emit('request',req,res);
});
});
server.listen(3000);
```
這里分離請求報文是指將 **請求體** 與其它兩部分(請求行,請求頭)分成兩塊,怎么分?嗯,前面說過,請求體和請求頭之間有一行空行作為分隔,**即 `\r\n\r\n` 或則說 `0x0d 0x0a 0x0d 0x0a`**。
嗯。。原理就是這么個原理咯,但有一個坑。
socket作為一個雙工流,在讀取客戶端發來的數據時和普通的可讀流一樣有一個默認讀取值,So,這可能導致你要讀很多下才摸得到`\r\n\r\n`這個分隔符,
并且可能最終讀到`\r\n\r\n` 時還會多讀出一些不屬于"請求頭"部分數據,我們還需要將這部分多余的屬于請求體的數據**按**回去,以便發射`requset`事件時我們能拿取到完整的 `請求體` 數據。
嗯。。坑比較多,這里就不獻丑貼代碼了,有興趣的小伙伴可以自己去實現以下,有兩點需要注意
- 讀取時使用readable暫停模式來讀取(以便把多余的數據按回去)
- 推薦用`0x0d 0x0a`這種buffer級別的來判斷而不是`\r\n`這種字符級別,因為字符可能會導致亂碼不好判斷,需要處理的情況就更多了。
### 解析請求頭
這里的請求頭 包括 請求行與請求頭
```
function parseHeader(head){
let lines = head.split(/\r\n/);
let start = lines.shift();
let lr = start.split(' ');
let method = lr[0];
let url = lr[1];
let httpVersion = lr[2].split('/')[1];
let headers = {};
lines.forEach(line=>{
let col = line.split(': '); //注意這里的空格
headers[col[0]] = col[1];
});
return {url,method,httpVersion,headers};
}
```
### demo代碼
倉庫:[點我點我!](https://github.com/fancierpj0/ihttp)
## http其它
### 非短連接
雖然http不想tcp一樣可以一直保持長連接,但我們說過它畢竟是基于tcp的,所以也具有保持連接的能力。
在響應頭中往往會包含 `Connection:keep-alive` 字樣的字段,就是讓瀏覽器保持連接不要中斷,即使接受完響應信息,這個連接一般也能保持一定的時間(大概,嗯,2min?)
### 管線化
http發送請求時如果包含多個,可以不用等待就能直接發送下一個請求。
Chrome 并發量約為6個,Firefox 4個?。