Node和http:一本通【附tcp實現http小代碼】

  • TCP與HTTP
  • HTTP報文格式
    • 請求報文
      • 請求行
      • 請求頭
      • 請求體
    • 響應報文
      • 響應行
      • 響應頭
      • 響應體
    • 關於url
    • 關於method
    • 關於請求體
    • 關於狀態碼
      • 2xx 請求正常
      • 3xx 緩存和重定向
      • 4xx 客戶端錯誤
      • 5xx 服務端錯誤
  • 在node中獲取請求報文
  • 在node中建立http服務器與客戶端
    • 建立一個服務器
    • 建立一個客戶端
      • 響應注意事項
  • 關於響應實體和Content-Type
  • 用tcp實現一個簡單的http服務器
    • 註冊響應回調
    • parser分離請求報文與發射request
    • 解析請求頭
    • demo代碼
  • http其它
    • 非短鏈接
    • 管線化

pre-notify

previously:html

TCP是HTTP的基石,對tcp還不是灰常清楚的小夥伴能夠看看個人這篇NodeJS和TCP:一本通node

本文主要用於我的知識梳理,可能篇幅較長,So專門在開頭copy了一份目錄以便配合右下方鏈接點擊分段閱讀(づ ̄ 3 ̄)づgit

TCP與HTTP

首先,HTTP 是基於 TCP 協議的,只有當tcp鏈接順利創建時,瀏覽器客戶端才能向服務器發送http請求。(詳見TCP三次握手)github

TCP 讓讓一臺pc端對端的鏈接上另外一臺pc後,兩臺機器之間能夠互通數據,但這個數據並無通過什麼額外的加工,是純粹的數據,即用戶輸入什麼數據,服務器就會拿到什麼數據。web

HTTP 有些許不同, 一個http請求會將用戶的輸入通過瀏覽器包裝後再發送給服務端,而包裝後的數據便是咱們說的 http請求報文。對應的,服務器要向瀏覽器回覆響應,也須要通過一層包裝,包裝成 http響應報文再響應給客戶端。npm

從傳輸層面上來說,http僅僅是tcp的一項子集,一種再封裝。編程

HTTP報文格式

HTTP報文格式 是對 http協議最直觀的闡釋,也是咱們學習http協議最有效率的手段。json

HTTP報文主要分文兩大類,請求報文響應報文,請求報文和響應報文又都分爲 三部分,而且頭和體這兩部分之間有 空行 隔開。跨域

請求報文

如下圖片出自於Android網絡編程隨想錄(2) 瀏覽器

請求行

請求行分爲如下三部分,每一個部分之間用空格隔開

  • method: 主要是用來標識是要傳數據仍是獲取數據
  • path: url地址
  • protocol http的協議版本號

請求頭

請求頭和請求行不同,它是多行的,每一行都是一組鍵值對,鍵和值之間用:空格隔開。

  • 請求首部: Host:xxx.com
  • 通用首部: 請求和響應都有的,好比 Connection:keep-alive
  • 實體首部: 以 Content-開頭的
  • 其它

請求體

正經的,用戶想要傳給服務器的數據

響應報文

如下圖片出自於Android網絡編程隨想錄(2)

響應行

  • 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代碼

倉庫:點我點我!

http其它

非短鏈接

雖然http不想tcp同樣能夠一直保持長鏈接,但咱們說過它畢竟是基於tcp的,因此也具備保持鏈接的能力。

在響應頭中每每會包含 Connection:keep-alive 字樣的字段,就是讓瀏覽器保持鏈接不要中斷,即便接受完響應信息,這個鏈接通常也能保持必定的時間(大概,嗯,2min?)

管線化

http發送請求時若是包含多個,能夠不用等待就能直接發送下一個請求。

Chrome 併發量約爲6個,Firefox 4個。


To be Continue...

相關文章
相關標籤/搜索