基於tcp的http應用,斷點續傳,範圍請求

渣渣的生存之道發表於2018-08-03

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()函式執行錯誤
  1. 我們今天要說的是網路層的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);
複製程式碼

基於tcp的http應用,斷點續傳,範圍請求

如上請求報文包括,請求行,請求首部,請求實體(帶content的都是實體),,然後請求還會返回響應頭,請求頭和響應頭都有的叫通用首部欄位,請求頭獨有的叫請求首部欄位。

基於tcp的http應用,斷點續傳,範圍請求
這個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: 如果當前請求不允許訪問,返回裂圖

檔案結構

基於tcp的http應用,斷點續傳,範圍請求

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

相關文章