previously:html
TCP是HTTP的基石,對tcp還不是灰常清楚的小夥伴能夠看看個人這篇NodeJS和TCP:一本通node
本文主要用於我的知識梳理,可能篇幅較長,So專門在開頭copy了一份目錄以便配合右下方鏈接點擊分段閱讀(づ ̄ 3 ̄)づgit
首先,HTTP
是基於 TCP
協議的,只有當tcp鏈接順利創建時,瀏覽器客戶端才能向服務器發送http請求。(詳見TCP三次握手)github
當TCP
讓讓一臺pc端對端的鏈接上另外一臺pc後,兩臺機器之間能夠互通數據,但這個數據並無通過什麼額外的加工,是純粹的數據,即用戶輸入什麼數據,服務器就會拿到什麼數據。web
而 HTTP
有些許不同, 一個http請求會將用戶的輸入通過瀏覽器包裝後再發送給服務端,而包裝後的數據便是咱們說的 http請求報文
。對應的,服務器要向瀏覽器回覆響應,也須要通過一層包裝,包裝成 http響應報文
再響應給客戶端。npm
從傳輸層面上來說,http僅僅是tcp的一項子集,一種再封裝。編程
HTTP報文格式 是對 http協議最直觀的闡釋,也是咱們學習http協議最有效率的手段。json
HTTP報文主要分文兩大類,請求報文
和 響應報文
,請求報文和響應報文又都分爲行
、 頭
和 體
三部分,而且頭和體這兩部分之間有 空行
隔開。跨域
如下圖片出自於Android網絡編程隨想錄(2) 瀏覽器
請求行分爲如下三部分,每一個部分之間用空格隔開
請求頭和請求行不同,它是多行的,每一行都是一組鍵值對,鍵和值之間用:
和空格
隔開。
Connection:keep-alive
Content-
開頭的正經的,用戶想要傳給服務器的數據
如下圖片出自於Android網絡編程隨想錄(2)
同請求頭
同請求體
客戶端封裝請求報文時會將url中的hash給去掉(query是會保留的)
故服務器端是永遠接收不到客戶端的hash值的GET 獲取資源
POST 想服務器端發送數據,傳輸實體主體
PUT 傳輸文件 , RESTful中是更新修改操做
HEAD 獲取報文首部
DELETE 刪除文件
OPTIONS 詢問支持的方法 ,試探方法,好比跨域,會先詢問服務端可否跨域
TRACE 追蹤路徑
當提交的表單只包含一條數據時,且表單類型爲默認時,請求報文長這樣
若是是多條數據,會用空行隔開
但若是是multipart/form-data
編碼時,請求體中多端數據間則是用特殊的分隔符來隔開的
即便只有一段數據也會用特殊的分隔符包裹住
多段數據時
狀態碼主要分爲五大類
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());
})
複製代碼
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
頭,
對於服務端來講,它須要拿這個頭解析客戶端發送過來的實體數據,(縱然很多狀況下,請求都沒有實體部分,好比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');
複製代碼
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
}
複製代碼
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};
}
複製代碼
倉庫:點我點我!
雖然http不想tcp同樣能夠一直保持長鏈接,但咱們說過它畢竟是基於tcp的,因此也具備保持鏈接的能力。
在響應頭中每每會包含 Connection:keep-alive
字樣的字段,就是讓瀏覽器保持鏈接不要中斷,即便接受完響應信息,這個鏈接通常也能保持必定的時間(大概,嗯,2min?)
http發送請求時若是包含多個,能夠不用等待就能直接發送下一個請求。
Chrome 併發量約爲6個,Firefox 4個。
To be Continue...