通過HTTP的HEADER完成各種騷操作

小美娜娜發表於2018-08-19

作為一名專業的切圖工程師,我從來不care網頁的header,最多關心Status Code是不是200。但是HEADER真的很重要啊,客戶端從伺服器端獲取內容,首先就是通過HEADER進行各種溝通!HEADER可以幫助我們完成許多騷操作,提高網站的效能,使用者的體驗。好了讓我們來feel一下。

初級騷操作

  • 多語言(Accept-Language
  • 防盜鏈(RefererReferered
  • gzip,簡單地說就是省流量(Accept-EncodingContent-Encoding

多語言

多語言就是一個網站可以實現多種語言的切換,這裡不討論建N個網站,一個語言對應一個網站。這裡討論如何智慧返回使用者所需的語言。

server client
向server扔過去了Accept-Language
接收對方的Accept-Language
欄位大概這樣子zh,en-US;q=0.9,en;q=0.8
開始處理,將欄位變成帶權重q的陣列
排序好大概長這樣[{"name":"zh","q":1},{"name":"en-US","q":0.9},{"name":"en","q":0.8}]
根據權重返回擁有的語言,有zh返回zh,沒有zh就返回en-US
萬一我沒有對方需要的語言包,怎麼辦?急,線上等!
沒辦法了,只能給對方我們的官方(預設)語言
傳送,請接收
您的ACCEPT語言已匹配 這個網站挺上道的,雖然是國外網站,但知道我是中文
我們沒有你所在地區的語言包 emmmm,這是火星文嗎?

附贈多語言的簡易實現版:

let languages = {
    zh:{
        title:"你好",
        content:"同學"
    },
    en:{
        title:"Hey",
        content:"guy"
    },
}
//設定預設語言,萬一使用者的語言我們不支援呢?
let defaultLanguage="zh"
let http = require('http');
function getLanguage(client_langs){
    let finalLanguage=defaultLanguage
    try{
        if(client_langs){
            //排序獲取語言順序
            client_langs=client_langs.split(',').map(l=>{
                let [name,q] = l.split(';');
                q = q?Number(q.split('=')[1]):1 
                return {name,q}
            }).sort((a,b)=>b.q-a.q);
            //匹配伺服器有的語言,並返回
            for(let i = 0 ;i <languages.length;i++){
                let name= languages[i].name;
                if(languages[name]){
                    finalLanguage=name;
                    break;
                }
            }
        }
    }catch(e){}
    return languages[finalLanguage]
}
http.createServer(function (req,res) {
    //獲取客戶端的語言
    let client_langs = req.headers['Accept-Language'];
    let lan=getLanguage(client_langs)
    //將語言列印到客戶端
    res.end(`<p>${lan.title}</p><p>${lan.content}</p>`)
}).listen(3000);
複製程式碼

防盜鏈

這個技術用的最多的應該就是對於圖片的限制,只有本域名可以獲取到,其他域名想都不要想。

server client
在某網站上請求了一張圖片
通過RefererReferered發現此網站域名不在我方白名單內
此圖片不提供給某網站
此時po上了一張萬用土
支援正版請上我們網站

實現原理,此處我用iframe來做例子,其實原理很簡單就是對比來源,要麼和請求資源一致要麼在白名單內,不然就拒絕。當然如果沒有來源的情況下就直接放行,萬一人家是單獨開啟的呢,不是盜鏈:

let http =  require('http');
let fs = require('fs');
let url = require('url');
let path = require('path');
// 設定白名單
let whiteList = ['localhost:3000'];
http.createServer(function (req,res) {
    //獲取請求地址
    let { pathname } = url.parse(req.url);
    // 獲取實體地址
    let realPath = path.join(__dirname,pathname);
    // 獲取檔案狀態
    fs.stat(realPath,function(err,statObj) {
        if(err){
            res.statusCode = 404;
            res.end();
        }else{
             // 重點來了
            let Referer = req.headers['Referer'] || req.headers['referred'];
            //如果有來源
            if(Referer){
                //獲取雙方域名
                let current = req.headers['host'] 
                Referer = url.parse(Referer).host
                console.log(current,Referer)
                //如果域名相同活在白名單中,放行!
                if (current === Referer || whiteList.includes(Referer)){
                    fs.createReadStream(realPath).pipe(res);
                }else{
                    //不放行,此乃盜鏈!給你個眼神自行體會
                    fs.createReadStream(path.join(__dirname,'files/2.html')).pipe(res);
                }
            }else{
                //沒有來源,也放行。萬一是單獨開啟的呢~
                fs.createReadStream(realPath).pipe(res);
            }
        }
    })
}).listen(3000);
複製程式碼

gzip

現代瀏覽器很高階,已經可以接受壓縮包了。佩服佩服。那麼該如何傳輸壓縮的網頁呢?

server client
向server扔過去了Accept-Encoding
大概結構是這樣的gzip, deflate, br
get到了對方的用意,開始配置壓縮
如果支援壓縮,先設定個頭部Content-Encoding
有很多種壓縮方式,按照server優先支援的匹配
線上壓縮網頁,成功後返回client
歡歡喜喜省了流量,而且不影響體驗

附贈建議程式碼,大家測試的時候,別忘了建立測試的html檔案

let http = require('http');
//用於壓縮檔案所需的庫
let fs = require('fs');
let path = require('path');
//壓縮的庫
let zlib = require('zlib');
http.createServer(function (req,res) {
    //獲取客戶端接受的壓縮方式
    let rule = req.headers['Accept-Encoding'];
    // 建立原檔案可讀流
    let originStream=fs.createReadStream(path.join(__dirname, '1.html'));
    if(rule){
        // 啊啊啊!正則是個坎,我怕我是跨不過去了。
        if(rule.match(/\bgzip\b/)){
            //如果支援壓縮!一定要設定頭部!
            res.setHeader('Content-Encoding','gzip');
            originStream=originStream.pipe(zlib.createGzip())
        } else if (rule.match(/\bdeflate\b/)){
            res.setHeader('Content-Encoding', 'deflate');
            originStream=originStream.pipe(zlib.createDeflate())
        }
    }
    // 輸出處理後的可讀流
    originStream.pipe(res)
}).listen(3000);
複製程式碼

中級操作

初級操作大多隻需要靠***配置HEADER即可以實現***,中級我們當然要難一點,大多需要client和server打配合。

  • client給server傳送內容(Content-TypeContent-Length)
  • client從server獲取內容(RangeContent-Range)
  • client爬蟲,抓取網頁

client給server傳送內容

server client
給你了一串資料,你給處理下
沒頭沒腦,誰知道你要做什麼,請設定好HEADER
好吧,告訴你Content-TypeContent-Length
可以可以,資料的內容型別是長度是很必要的
把資料傳給你了,你看一下
收到~監聽收到的資料是一組Buffer
接受完畢,合併Buffer
根據Content-Type對資料進行處理
格式化資料,end

Server程式碼

let http = require('http');
let server = http.createServer();
let arr=[]
server.on('request', (req, res)=>{
  req.on('data',function (data) {
    //把獲取到的Buffer資料都放入熟組
    arr.push(data);
  });
  req.on('end',function() {
    // 請求結束了,好了可以開始處理斷斷續續收到的Buffer了
    // 合併buffer
    let r = Buffer.concat(arr).toString();
    if (req.headers['content-type'] === 'x-www-form-urlencoded'){
        let querystring = require('querystring');
        r = querystring.parse(r); // a=1&b=2然後格式化
        console.log("querystring",r);
      } else if (req.headers['content-type'] === 'application/json'){
        //聽說是JSON格式的
        console.log("json",JSON.parse(r));
      } else{
        //沒有格式?那原來是啥就是啥吧。
        console.log("no type",r);
      }
      arr=[]
      res.end('結束了!');
  });
})
server.listen(3000,()=>{
  console.log(`server start`);
});
複製程式碼

Client程式碼

// 設定請求地址的配置
let opts = {
  host:'localhost',
  port:3000,
  path:'/',
  // 頭部設定很重要,頭部設定很重要,頭部設定很重要
  headers:{
    'Content-Type':'x-www-form-urlencoded',
    //長度超過3就沒有人理你了
    "Content-Length":7
  }
}
let http = require('http');
let client = http.request(opts,function (res) {
  res.on('data',function (data) {
      console.log(data);
  })
});
client.end("a=1&b=2");
複製程式碼

client從server獲取部分內容

server client
我想要資源的部分內容
可以啊,告訴我範圍
我放在HEADER中的Range了,bytes=0-3
Content-Range:bytes 0-3/7,請接受,此檔案一共8位元組,前3位元組已經給你了 好的,那麼把接下來的給我吧,bytes=4-7
給你給你都給你 end

大家都發現了吧,這樣的range獲取資料,完全是斷點續傳的簡陋版啊!不過這邊有一個點容易犯錯就是檔案大小的計算,因為檔案位元組的位置是按照0開始算,所以range的全範圍都是0~size-1/size-1,大家注意下。

server 端

let http = require('http');
let fs = require('fs');
let path = require('path');
// 當前要下載的檔案的大小
let size = fs.statSync(path.join(__dirname, 'my.txt')).size;
let server = http.createServer(function (req, res) {
  let range = req.headers['range']; //獲取client請求訪問的部分內容
  if (range) {
    let [, start, end] = range.match(/(\d*)-(\d*)/);
    start = start ? Number(start) : 0;
    end = end ? Number(end) : size - 1; // 10個位元組 size 10  (0-9)
    console.log(`bytes ${start}-${end}/${size - 1}`)
    res.setHeader('Content-Range', `bytes ${start}-${end}/${size - 1}`);
    fs.createReadStream(path.join(__dirname, 'my.txt'), { start, end }).pipe(res);
  } else {
    // 會把檔案的內容寫給客戶端
    fs.createReadStream(path.join(__dirname, 'my.txt')).pipe(res);
  }
});
server.listen(3000);

複製程式碼

client端

let opts = {
    host:'localhost',
    port:3000,
    headers:{}
  }
let http = require('http');
let start = 0;
let fs = require('fs');
function download() {
    //分流下載,部分下載
    opts.headers.Range = `bytes=${start}-${start+3}`;
    start+=4;
    let client = http.request(opts,function (res) {
        let total = res.headers['content-range'].split('/')[1];
        res.on('data',function (data) {
          fs.appendFileSync('./download.txt',data);
        });
        res.on('end',function () {
            //結束之後,1s之後再下載
          setTimeout(() => {
              console.log(start,total)
            if (start <= total)
              download();
          }, 1000);
        })
    });
    client.end();
}
download()
複製程式碼

client抓取網頁內容,簡易爬蟲

這一塊的操作其實很簡單,只要建一個請求獲取到網頁就可以了。 難點在於:如何將有效的資訊剝離網頁,過濾掉無用資訊。 我這裡抓去了百度的娛樂版,百度還算良心,是utf8的,不然就要亂碼了。

let http = require('http');
let opts = {
  host:'news.baidu.com',
  path:'/ent'
}
//建立一個請求,獲取網站內容
let client = http.request(opts,function (r) {
    let arr= [];
    //資源不可能一次下載完成,因此每次獲取到資料都要push到arr中
    r.on('data',function (data) {
        arr.push(data);
    });
    r.on('end',function() {
        //合併資源
        let result = Buffer.concat(arr).toString();
        //對資源進行處理,可以是變成我這樣的物件,之後不管做什麼處理都很方便
        let content = result.match(/<ul class="ulist mix-ulist">(?:[\s\S]*?)<\/ul>/img).toString().match(/<li>(?:[\s\S]*?)<\/li>/img);
        content=content.map((c)=>{
            let href=/<a href="(?:[\S]*?)"/img.exec(c)
            let title=/">(?:[\s\S]*?)<\/a>/img.exec(c)
            return {
                href:href[0].replace(/"/img,"").replace("<a href=",""),
                title:title[0].replace(/">/img,"").replace("</a>","")
            }
        })
        console.log(JSON.stringify(content))
        arr= [];
    })
});
client.end();
複製程式碼

相關文章