TCP
要說http就繞不開tcp,TCP協議對應於傳輸層,而HTTP協議對應於應用層,從本質上來說,二者沒有可比性。但是,http是基於tcp協議的。
TCP/IP 協議七層模型
物理層 | 鏈路層 |
---|---|
將二進位制的0和1和電壓高低,光的閃滅和電波的強弱訊號進行轉換 | 驅動 |
網路層
- 使用 IP 協議,IP 協議基於 IP 轉發分包資料
- IP 協議是個不可靠協議,不會重發
- IP 協議傳送失敗會使用ICMP 協議通知失敗
- ARP 解析 IP 中的 MAC 地址,MAC 地址由網路卡出廠提供
- IP 還隱含鏈路層的功能,不管雙方底層的鏈路層是啥,都能通訊
複製程式碼
傳輸層 通用的 TCP 和 UDP 協議(tcp比udp安全)
TCP 協議面向有連線,能正確處理丟包,傳輸順序錯亂的問題,但是為了建立與斷開連線,需要至少7次的發包收包,資源浪費
UDP 面向無連線,不管對方有沒有收到,如果要得到通知,需要通過應用層
複製程式碼
會話層以上分層 TCP/IP 分層中,會話層,表示層,應用層集中在一起 網路管理通過 SNMP 協議
HTTP
Http協議是建立在TCP協議基礎之上的,當瀏覽器需要從伺服器獲取網頁資料的時候,會發出一次Http請求。Http會通過TCP建立起一個到伺服器的連線通道,當本次請求需要的資料完畢後,Http會立即將TCP連線斷開,這個過程是很短的。所以Http連線是一種短連線,是一種無狀態的連線。
keep-alive http雖然沒有狀態,但是可以通過會話例如session保持連線 連結可複用,節約拆橋時間。
狀態碼
1XX | 2XX | 3XX | 4XX | 5XX |
---|---|---|---|---|
資訊性狀態碼 | 成功狀態碼 | 重定向 | 客戶端錯誤狀態碼 | 服務端錯誤狀態碼 |
少見 | 200 OK | 301 永久性重定向 | 400 請求報文語法錯誤 | 500伺服器請求錯誤 |
204 響應報文不含實體的主體部分 | 302 臨時性重定向(負載均衡) | 401傳送的請求需要有通過 HTTP 認證的認證資訊 307 和302含義相同 | 503 伺服器暫時處於超負載或正在停機維護,無法處理請求 | |
206 範圍請求 | 303 資源存在著另一個 URL,應使用 GET 方法定向獲取資源 | 403 對請求資源的訪問被伺服器拒絕 | ||
304 客戶端已經執行了GET,但檔案未變化。 | 404 伺服器上沒有找到請求的資源 |
console.log錯誤狀態碼
- SyntaxError是解析程式碼時發生的語法錯誤
- Uncaught ReferenceError:引用錯誤
- RangeError:範圍錯誤
- TypeError型別錯誤
- URIError,URL錯誤
- EvalError eval()函式執行錯誤
- 我們今天要說的是網路層的http;我們知道http是用來寫服務端的,那麼他可以做什麼呢? 像ajax互動、狀態碼{304: 伺服器的快取問題 ,206: 範圍請求(部分請求) } 、壓縮{ Content-Encoding:gzip deflate}、圖片的防盜鏈 、加密問題{對稱,非對稱}、伺服器代理(正向代理,反向代理)
寫一個靜態服務(命令列工具)
輸入url到頁面載入都發生了什麼事情?
3次握手
客戶端–傳送帶有SYN標誌的資料包–一次握手–服務端 服務端–傳送帶有SYN/ACK標誌的資料包–二次握手–客戶端 客戶端–傳送帶有帶有ACK標誌的資料包–三次握手–服務端
4次揮手
客戶端-傳送一個FIN,用來關閉客戶端到伺服器的資料傳送 伺服器-收到這個FIN,它發回一個ACK,確認序號為收到的序號加1 。和SYN一樣,一個FIN將佔用一個序號 伺服器-關閉與客戶端的連線,傳送一個FIN給客戶端 客戶端-發回ACK報文確認,並將確認序號設定為收到序號加1
**輸入地址
> 瀏覽器查詢域名的 IP 地址
> 這一步包括 DNS 具體的查詢過程,包括:瀏覽器快取->系統快取->路由器快取...
> 瀏覽器向 web 伺服器傳送一個 HTTP 請求
> 伺服器的永久重定向響應(從 http://example.com 到 http://www.example.com)
> 瀏覽器跟蹤重定向地址
> 伺服器處理請求
> 伺服器返回一個 HTTP 響應
> 瀏覽器顯示 HTML
> 瀏覽器傳送請求獲取嵌入在 HTML 中的資源(如圖片、音訊、視訊、CSS、JS等等)
> . 瀏覽器傳送非同步請求**
複製程式碼
管線化
可以處理併發
URI url urn
uri 統一資源表示符,url 統一資源定位符 location ,統一資源命名符
url 組成
> http://(協議)name:password(登入資訊,認證)@www.fs.ip(伺服器地址):8080(埠號)/dir/index.htm(檔案路徑)?a=a(查詢字串)#asd(片段識別符號)
複製程式碼
我們訪問一個路徑,路徑回去dns域上找對應的ip地址,中間包括查詢瀏覽器快取,本地檔案等等,最後將ip地址返回,http主要針對應用層,應用層會有一些報文,主要通過tcp傳輸,http基於tcp,TCP會將HTTP拆分成很多段,把每個報文可靠的傳給對方,udp是不可靠的會丟包,拼完之後返回伺服器
node之url模組
let url = require('url');
let Obj = url.parse('http://user:passwrd@www.zdl.cn:80/1.html?a=1#aaa')
console.log(Obj);
=>Url {
protocol: 'http:',
slashes: true,
auth: 'user:passwrd',
host: 'www.zdl.cn:80',
port: '80',
hostname: 'www.zdl.cn',
hash: '#aaa',
search: '?a=1',
query: 'a=1',
pathname: '/1.html',
path: '/1.html?a=1',
href: 'http://user:passwrd@www.zdl.cn:80/1.html?a=1#aaa' }
複製程式碼
我們通常要解析字串
let url = require('url');
let {pathname ,query,path} = url.parse('http://user:passwrd@www.zdl.cn:80/1.html?a=1&b=2&c=3&d=4#aaa')
//解析
let str = query;
let obj = {};
str.replace(/([^=&]*)=([^=&])/g,function(){
obj[arguments[1]] = arguments[2];
})
console.log(obj);
//或者
let {pathname ,query,path} = url.parse('http://user:passwrd@www.zdl.cn:80/1.html?a=1&b=2&c=3&d=4#aaa',true) //新增true會直接解析
//解析
console.log(query);
複製程式碼
如上請求報文包括,請求行,請求首部,請求實體(帶content的都是實體),,然後請求還會返回響應頭,請求頭和響應頭都有的叫通用首部欄位,請求頭獨有的叫請求首部欄位。
這個url會給我們解析出一個url物件,包括url個組成部分 http和ttp差的是報文,是一種不儲存但可以保持狀態的協議restful風格
get post put delete head(獲取報文首) options(跨域試探性請求,節約流量) teace(呼叫盞,追蹤路徑)
範圍請求
curl -v --header "Range:bytes=1-200" https://www.baidu.com/img/bd_logo1.png?qua=high
=>subjectAltName: host "www.baidu.com" matched cert's "*.baidu.com"
* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign Organization Validation CA - SHA256 - G2
* SSL certificate verify ok.
> GET /img/bd_logo1.png?qua=high HTTP/1.1
> Host: www.baidu.com
> User-Agent: curl/7.54.0
> Accept: */*
> Range:bytes=1-200
>
< HTTP/1.1 206 Partial Content
< Accept-Ranges: bytes
< Cache-Control: max-age=315360000
< Connection: Keep-Alive
< Content-Length: 200
< Content-Range: bytes 1-200/7877
< Content-Type: image/png
< Date: Sat, 07 Jul 2018 03:56:46 GMT
< Etag: "1ec5-502264e2ae4c0"
< Expires: Tue, 04 Jul 2028 03:56:46 GMT
< Last-Modified: Wed, 03 Sep 2014 10:00:27 GMT
< P3p: CP=" OTI DSP COR IVA OUR IND COM "
< Server: Apache
< Set-Cookie: BAIDUID=B37F40A5E737D32A9475DC95E0925B45:FG=1; expires=Sun, 07-Jul-19 03:56:46 GMT; max-age=31536000; path=/; domain=.baidu.com; version=1
<
PNG
複製程式碼
請求頭中的Range來指定 資源的byte範圍 響應會返回狀態碼206響應報文 對於多重範圍的範圍請求,響應會在首部欄位Content-Type中標明multipart/byteranges
用tcp寫個服務
let http = require('http')
let server = http.createServer();
server.on('request',function(req,res){
console.log("請求到來了");
//req,res都是基於socket的 req可讀流=> 客戶端,res可瀉流 => 服務端
//res.writeHead(200,{...}) 之能寫一次,且和setHeader衝突
res.statusCode = 200;
res.setHeader('Content-Length',2);
res.setHeader('Content-Type','text/html;charset=utf8');
res.end('hello');
//可以通過伺服器,curl ,postman等傳送請求
})
//預設連結成功之後會把socket解析成兩個東西,req,res 解析後觸發事件叫request事件
server.on('connection',function(socket){
//net 中的socket和res一個效果
// socket.write(`
// HTTP/1.1 200 OK
// Content-Length :2
// Content-Type: text/html;charset=utf8
// ok
// `)
// socket.end()
console.log("連結成功");
})
server.listen(3000)
複製程式碼
post 和 get 的區別
- post有請求體,我們可以吧傳遞的內容放到請求體內
- get在url傳遞,
- post看似相對安全,其實不安全
- get只能發64k post可以傳送很多
http應用
//命令列輸入測試
//curl -X POST -v -d "name=zdl" http://localhost:3000/a?a=1&c=4
let http = require('http')
let server = http.createServer();
server.on('request',function(req,res){
console.log(req.method);
console.log(req.url);
console.log(req.httpVersion);
console.log(req.headers);//請求頭物件,要取裡面的引數,可以通過key來取(小寫)
let arr = [];
req.on('data',function(data){//只要是post需要通過監聽事件獲取資料,預設觸發一次64k
arr.push(data)
})
req.on("end",function(){
let str = Buffer.concat(arr);
console.log(str.toString());
res.end('hello');
})
})
server.listen(3000,function(socket){
console.log("server start 3000");
})
複製程式碼
用tcp模擬HTTP
//測試命令列輸入
let net = require('net');
let server = net.createServer();
function parser(socket,callback){
// socket.on("data",function(){
// })//接收
function parserHeader(head){
let obj = {};
let headers = head.split(/\r\n/);
let line = headers.shift();
let [method,path,version] = line.split(' ');
let heads = {};
headers.forEach(line => {
let [key,value] = line.split(': ');
heads[key] = value;
});
obj['method'] = method;
obj['path'] = path;
obj['version'] = version;
obj['headers'] = headers;
return obj;
}
function fn(){
let result = socket.read().toString();//如果read 不傳引數會預設全讀
let [head,content] = result.split(/\r\n\r\n/);
let obj = parserHeader(head);
console.log(obj);
//readble方法會觸發多次,觸發一次後就移除掉
socket.removeListener('readable',fn)
}
socket.on("readable",fn)//預設把快取區填滿
}
server.on('connection',function(socket){
parser(socket,function(req,res){
server.emit('request',req,res);//將socket派發給request
})
})
server.on('request',function(req,res){
console.log(req.method);
console.log(req.url);
console.log(req.httpVersion);
console.log(req.headers);//請求頭物件,要取裡面的引數,可以通過key來取(小寫)
let arr = [];
req.on('data',function(data){//只要是post需要通過監聽事件獲取資料,預設觸發一次64k
arr.push(data)
})
req.on("end",function(){
let str = Buffer.concat(arr);
console.log(str.toString());
res.end('hello');
})
})
server.listen(3000)
複製程式碼
http頭的應用
我們經常用的斷點續傳,多語言還有防盜鏈等等都是基於我們的http來實現的,包括快取,
斷點續傳,分段請求,上面我們提到的分段請求(range:bytes=0-3)
-
客戶端 請求頭 Range:bytes=1-200
-
服務端
響應頭
Accept-Ranges: bytes
< Content-Length: 200
< Content-Range: bytes 1-200/7877
模擬上述功能
range.js
//服務端
let http = require('http');
let path = require('path'); //路徑
let fs = require('fs'); //讀檔案
let p = path.join(__dirname,'1.txt'); //獲取讀檔案的路徑
let {promisify} = require('util');
let stat = promisify(fs.stat); // 將stat方法轉化成promise的方法 可能沒有end預設全部讀取
let server = http.createServer();
server.on('request',async function (req,res) {
//取請求頭,取的到則分段,否則就整體獲取
let range = req.headers['range'];
try{
let s = await stat(p);
let size = s.size;
if (range) {
let [, start, end] = range.match(/(\d*)-(\d*)/);//第一個引數是匹配字串,第二個是第一項,第二個是第二項
start = start ? Number(start) : 0;
end = end ? Number(end)-1 : size-1;
res.statusCode = 206;
// 告訴客戶端當前是範圍請求
res.setHeader('Accept-Ranges','bytes');
// 返回的內容長度
res.setHeader('Content-Length',end-start+1);
res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`);
fs.createReadStream(p,{start,end}).pipe(res); //把讀取的結果傳給res
} else {
// 邊讀邊寫,返回檔案
fs.createReadStream(p).pipe(res);//res是可寫流,在可讀流和可寫流之間加管道,相當於不停的讀檔案不同的調res的write方法
}
}catch(e){
console.log(e);
}
})
server.listen(3000);
//測試curl -v --header "Range:bytes=3-5" http://localhost:3000
複製程式碼
實現下載功能
client.js
//客戶端,需要啟動樓上的服務端,然後在cmd裡node 當前資料夾的名字執行客戶端
let http = require('http');
let fs = require('fs');
let pause = false; // 預設開啟下載模式 true時暫停
let ws = fs.createWriteStream('./download.txt');//希望下載到這個地方去
let options = {
hostname: 'localhost', //主機/路徑
port: 3000, //埠號 還有個頭0-3/3-5等等
}
// 實現下載功能
let start = 0;
//監控輸入
process.stdin.on('data',function (data) {
data = data.toString();
if(data.match(/p/)){
pause = true;
}else{
pause = false;
download();
}
})
function download() {
// 請求之前加個請求頭
options.headers = {
'Range': `bytes=${start}-${start + 9}`
}
start += 10;
// let socket = http.request(options);//每次呼叫時請求的檔案位置累加
// socket.write();
// socket.end()//傳送請求
//等同於
http.get(options, function (res) { //多次傳送請求 get 沒有請求題
let buffers = [];
let total = res.headers['content-range'].split('/')[1];
total = parseInt(total);//58
res.on('data',function(data){
buffers.push(data);
})
res.on('end', function () {
let str = Buffer.concat(buffers).toString();
ws.write(str);//寫到檔案去
if (!pause && start < total) { // 沒有完畢才繼續請求
setTimeout(() => {
download()
}, 1000);
}
});
})
}
download();
複製程式碼
防盜鏈
- 在百度隨便找一張圖片
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script src="main.js"></script>
</head>
<body>
<img src="http://d.hiphotos.baidu.com/video/pic/item/e850352ac65c10385fd7b21fbe119313b17e8945.jpg" alt="">
</body>
</html>
複製程式碼
用http-server啟動伺服器,結果圖片出現裂圖,這就是簡單的防盜,原理很簡單,Referer: 如果當前請求不允許訪問,返回裂圖
檔案結構
index.html
<body>
<img src="http://localhost:3000/2.jpg" alt="">
</body>
複製程式碼
let http = require('http');
let path = require('path');
let url = require('url');
let fs = require('fs');
let {promisify } = require('util');
let stat = promisify(fs.stat);
let whiteList = ['www.zdl1.cn'];
let p = path.resolve(__dirname,'public');
let server = http.createServer(async function(req,res){
let {pathname} = url.parse(req.url); // index.html 2.jpg 1.jpg
let refer = req.headers['referer'] || req.headers['referred'];
try{
let rp = path.join(p,pathname); // 真實的路徑
let s = await stat(rp); // 檔案存在就讀取相應給客戶端
if(refer){
// 如果有refer要判斷是否和法如果 不合法返回一張裂圖
// 現在再哪裡用這張圖 www.zdl2.cn
let hostname = url.parse(refer).hostname;
// 代表當前檔案的主機名 www.zdl1.cn
let host = req.headers['host'].split(':')[0];
if(host != hostname ){
if (whiteList.includes(hostname)){
return fs.createReadStream(path.join(p, '2.jpg')).pipe(res);
}
fs.createReadStream(path.join(p,'1.jpg')).pipe(res);
}else{
fs.createReadStream(path.join(p, '2.jpg')).pipe(res);
}
}else{
fs.createReadStream(rp).pipe(res);
}
}catch(e){
res.end(`NOT Found`);
}
})
server.listen(3000);
複製程式碼
用 http://localhost:8080訪問index檔案,和http://www.zdl1.cn:8080/訪問是一樣的,http://www.zdl2.cn:8080則返回裂圖
域名需要自行配置host檔案
多語言
多語言也是用的頭
- 請求頭格式 Accept-Language: Accept-Language:zh-CN,zh;q=0.9(l瀏覽器預設傳送)
- 響應頭格式 Content-Language:zh-CN
language.js
// 多語言
let pack = {
'zh-CN':'你好',
'zh':'nihao',
'en':'hello',
'fr':'Bonjour'
}
let defaultLanguage = 'en'; // 預設是英語
let http = require('http');
http.createServer(function (req,res) {
let lang = req.headers["accept-language"];
if(lang){ // 如果有多語言
let langs = lang.split(',');//拆分語言,每種語言逗號分割
// [{name:'zh-CN',q:1},{name:'en',q:0.8}]
langs = langs.map(l=>{
let [name,q] = l.split(';');
q = q?Number(q.split('=')[1]):1;
return {name,q}
}).sort((lan1,lan2)=>lan2.q-lan1.q);
for(var i = 0;i<langs.length;i++){ // 迴圈每一種 語言看看包裡有沒有,如果有返回對應的語言
if(pack[langs[i].name]){
res.setHeader('Content-Language', langs[i].name);
res.end(pack[langs[i].name]);
return;
}
}
// 沒有預設語言
res.setHeader('Content-Language', 'en')
res.end(pack[defaultLanguage]);// 預設語言;
}else{
res.setHeader('Content-Language', 'en')
res.end(pack[defaultLanguage]);// 預設語言;
}
}).listen(3000);
//accept-language: zh-CN,zh;q=0.7,en;q=0.8,fr;q=0.1
複製程式碼
測試:curl -v --header "Accept-Language:zh;n;q=0.8,fr;q=1" http://localhost:3000
這個包後臺一般是我們自己寫的,前端是有包比如i8n
- 參考 :白偉業