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

TCP


要說http就繞不開tcp,TCP協議對應於傳輸層,而HTTP協議對應於應用層,從本質上來講,兩者沒有可比性。可是,http是基於tcp協議的。javascript

TCP/IP 協議七層模型


物理層 鏈路層
將二進制的0和1和電壓高低,光的閃滅和電波的強弱信號進行轉換 驅動

網絡層css

- 使用 IP 協議,IP 協議基於 IP 轉發分包數據
- IP 協議是個不可靠協議,不會重發
- IP 協議發送失敗會使用ICMP 協議通知失敗
- ARP 解析 IP 中的 MAC 地址,MAC 地址由網卡出廠提供
- IP 還隱含鏈路層的功能,無論雙方底層的鏈路層是啥,都能通訊
複製代碼

傳輸層 通用的 TCP 和 UDP 協議(tcp比udp安全)html

TCP 協議面向有鏈接,能正確處理丟包,傳輸順序錯亂的問題,可是爲了創建與斷開鏈接,須要至少7次的發包收包,資源浪費
UDP 面向無鏈接,無論對方有沒有收到,若是要獲得通知,須要經過應用層
複製代碼

會話層以上分層 TCP/IP 分層中,會話層,表示層,應用層集中在一塊兒 網絡管理經過 SNMP 協議前端

HTTP

Http協議是創建在TCP協議基礎之上的,當瀏覽器須要從服務器獲取網頁數據的時候,會發出一次Http請求。Http會經過TCP創建起一個到服務器的鏈接通道,當本次請求須要的數據完畢後,Http會當即將TCP鏈接斷開,這個過程是很短的。因此Http鏈接是一種短鏈接,是一種無狀態的鏈接。java

keep-alive http雖然沒有狀態,可是能夠經過會話例如session保持鏈接 連接可複用,節約拆橋時間。node

狀態碼


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}、圖片的防盜鏈 、加密問題{對稱,非對稱}、服務器代理(正向代理,反向代理)

寫一個靜態服務(命令行工具)web

輸入url到頁面加載都發生了什麼事情?


3次握手


客戶端–發送帶有SYN標誌的數據包–一次握手–服務端 服務端–發送帶有SYN/ACK標誌的數據包–二次握手–客戶端 客戶端–發送帶有帶有ACK標誌的數據包–三次握手–服務端ajax

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等等)
> . 瀏覽器發送異步請求**
複製代碼

管線化

能夠處理併發promise

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

相關文章
相關標籤/搜索