Применение заголовков HTTP-запросов и заголовков ответов

Node.js задняя часть сервер

В этой статье речь пойдет о милых головках браузеров....

Глава 1 найдены заголовки

Когда мы открываем веб-сайт наугад (например, Baidu, который все часто используют для тестирования сети), открываемNetwork, вы увидите следующие заголовки запросов и заголовки ответов:

Для чего вообще нужны эти заголовки? Давайте узнаем один за другим.

Назначение заголовков главы 2

2.1 Content-Type

Content-TypeУказывает тип содержимого заголовка запроса или заголовка ответа. При использовании в качестве заголовка запроса его можно использовать дляbody-parser. Тааак~What is body-parser?

body-parser — это промежуточное ПО, обычно используемое узлом, и его функции:

Parse incoming request bodies in a middleware before your handlers, available under the req.body property.

То есть используйте промежуточное ПО для анализа тела почтового запроса перед обработкой данных.body-parserПримером может быть:

В следующем примере показано, как добавитьbody parser. Обычно этоexpressНаиболее рекомендуемое использованиеbody-parserМетоды.

var express = require('express')
var bodyParser = require('body-parser')
var app = express()
// create application/json parser
var jsonParser = bodyParser.json()
// create application/x-www-form-urlencoded parser
var urlencodedParser = bodyParser.urlencoded({ extended: false })
// POST /login gets urlencoded bodies
app.post('/login', urlencodedParser, function (req, res) {
  if (!req.body) return res.sendStatus(400)
  res.send('welcome, ' + req.body.username)
})
// POST /api/users gets JSON bodies
app.post('/api/users', jsonParser, function (req, res) {
  if (!req.body) return res.sendStatus(400)
  // create user in req.body
})

body-parserОсновной исходный код:

  // this uses a switch for static require analysis
  switch (parserName) {
    case 'json':
      parser = require('./lib/types/json')
      break
    case 'raw':
      parser = require('./lib/types/raw')
      break
    case 'text':
      parser = require('./lib/types/text')
      break
    case 'urlencoded':
      parser = require('./lib/types/urlencoded')
      break
  }

отjsonНапример:

var contentType = require('content-type')
//...
/**
 * Get the charset of a request.
 *
 * @param {object} req
 * @api private
 */
function getCharset (req) {
  try {
    return (contentType.parse(req).parameters.charset || '').toLowerCase()
  } catch (e) {
    return undefined
  }
}
//...
// assert charset per RFC 7159 sec 8.1
var charset = getCharset(req) || 'utf-8'
if (charset.substr(0, 4) !== 'utf-') {
  debug('invalid charset')
  next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
    charset: charset,
    type: 'charset.unsupported'
  }))
  return
}

Видно, что принцип работы заключается в анализе заголовка запроса.Content-TypeТип обработки данных выполняется по разным типам, смоделируем сами:

Шаг 1: построить сначалаserver.js:

 req.on('end',function (params) {
    let r = Buffer.concat(arr).toString();
    // body-parser  解析请求,根据不同的格式进行不同的解析
    if (req.headers['content-type'] === www.js){
      let querystring = require('querystring');
      r = querystring.parse(r); // a=1&b=2
      console.log(r,1);
    } else if (req.headers['content-type'] === 'application/json'){
      console.log(JSON.parse(r),2);
    } else{
      console.log(r,3);
    }
    res.end('end'); 
  })

Шаг 2: Клиент имитирует запрос:

let opts = {
  host:'localhost',
  port:3000,
  path:'/hello',
  headers:{
    'a':1,
    'Content-Type':'application/json',
    "Content-Length":7 //模拟的时候需要带上长度,不然客户端会当成没有传递数据
  }
}
let http = require('http');
let client = http.request(opts,function (res) {
  res.on('data',function (data) {
      console.log(data.toString());
  })
});
client.end("{\"a\":1}"); // 表示把请求发出去

шаг 3: тест. Сначала запустите сервер, затем запустите клиент, сервер получитapplication/jsonФорматировать проанализированные данные:{ a: 1 } 2. Content-Typeиbody-parserОтношения между ними впервые анализируются здесь. Мы рассмотрим заголовки запросов позже.

2.2 Range:bytes

Заголовок запроса может запрашивать определенную часть ресурса через Range:bytes. Используйте это поле для имитации частичного чтения. следующее:

 http.createServer(function (req, res) {
    let range = req.headers['range'];
 })

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']; // 0-3
  if (range) {
    // 模拟请求 curl -v --header "Range:bytes=0-3" http://localhost:3000
    let [, start, end] = range.match(/(\d*)-(\d*)/);
    start = start ? Number(start) : 0;
    end = end ? Number(end) : size - 1; // 10个字节 size 10  (0-9)
    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);
    //可读流可以通过pipe导到可写流
  }
});
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;
  console.log(`start is ${start}`)
  let client = http.request(opts,function (res) {
      let total = res.headers['content-range'].split('/')[1];
      // console.log(half)
      res.on('data',function (data) {
        fs.appendFileSync('./download1.txt',data);
      });
      res.on('end',function () {
        setTimeout(() => {
          if ((!pause)&&(start < total))
            download();
        }, 1000);
      })
  });
  client.end();
}
download()

Добавьте функцию паузы для сегментированного чтения, чтобы отслеживать ввод пользователя.

let pause = false;
process.stdin.on('data',function (data) {
  if (data.toString().includes('p')){
    pause = true
  }else{
    pause = false;
    download()
  }
})

Результаты теста:

Сегментированные чтения имеют следующие преимущества:

  • Повышение скорости чтения, многопоточный параллелизм, блочное чтение
  • http

Имитация параллельных загрузок:

let halfFlag = 20
function download() {
  opts.headers.Range = `bytes=${start}-${start+3}`;
  start+=4;
  console.log(`start is ${start}`)
  let client = http.request(opts,function (res) {
      let total = res.headers['content-range'].split('/')[1];
	  let halfFlag = Math.floor(total/2)
      // console.log(half)
      res.on('data',function (data) {
        fs.appendFileSync('./download1.txt',data);
      });
      res.on('end',function () {
        setTimeout(() => {
          if ((!pause)&&(start < halfFlag))
            download();
        }, 1000);
      })
  });
  client.end();
}
let half = halfFlag

function downloadTwo() {
	opts.headers.Range = `bytes=${half}-${half+3}`;
	half+=4;
	console.log(`half is ${half}`)
	let client = http.request(opts,function (res) {
		let total = res.headers['content-range'].split('/')[1];
		res.on('data',function (data) {
			fs.appendFileSync('./download2.txt',data);
		});
		res.on('end',function () {
			setTimeout(() => {
				if (!pause&&half < total)
					downloadTwo();
			}, 1000);
		})
	});
	client.end();
}
download();
downloadTwo();

Результат выполнения загрузит исходный файл на две части: download1.txt и download2.txt. контрольная работа:

Теоретически этот метод загрузки сэкономит вдвое меньше времени, чем первый метод. Тем не менее, как добиться ускорения и параллельной загрузки в реальных загрузках файлов, еще предстоит изучить.

2.3 Принудительное кэширование Cache-Control и Expires

Заголовок ответа в заголовке ответаCache-Control: max-age=1233Вы можете установить принудительный кеш относительно текущего времени, что связано сExpiresВы можете установить абсолютный момент времени, чтобы ограничить время чтения кэша. Макетная реализация:

let url = require('url'); // 专门用来处理url路径的核心模块
// http://username:password@hostname:port/pathname?query
let server = http.createServer(async function (req,res) {
	console.log(req.url)
  let { pathname,query} = url.parse(req.url,true); 
  // true就是将query转化成对象
  let readPath = path.join(__dirname, 'public', pathname);
  try {
  let statObj = await stat(readPath);
  // 根客户端说 10s 内走缓存
  res.setHeader('Cache-Control','max-age=10');
  res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString());
    // 10s之内的请求都会走cache 返回200, (from disk cache)不发生请求
    if (statObj.isDirectory()) {
      let p = path.join(readPath, 'index.html');
      await stat(p);
      // 如果当前目录下有html那么就返回这个文件
      fs.createReadStream(p).pipe(res);
    } else {
      fs.createReadStream(readPath).pipe(res);
    }
  }catch(e){
    res.statusCode = 404;
    res.end(`Not found`);
  }
}).listen(3000);

контрольная работа:

Обновить в течение 10 секунд:

2.4 Сравнение Last-Modified и If-Modified-Since of Cache

Сравнив заголовок ответа Last-Modified и с заголовком запроса If-Modified-Since, можно проверить, модифицирован ли файл по времени модификации файла, чтобы принять решение о повторном запросе или переходе к кэшу. Моделирование выглядит следующим образом: step1 не устанавливает обязательный кеш

 res.setHeader('Cache-Control','no-cache');

Шаг 2: Сравните время модификации файла приложения, чтобы увидеть, был ли он изменен.

   res.setHeader('Last-Modified', statObj.ctime.toGMTString());
      if (req.headers['if-modified-since'] === statObj.ctime.toGMTString()) {
        res.statusCode = 304;
        res.end();
        return; // 走缓存
      }
      fs.createReadStream(readPath).pipe(res);

контрольная работа:

2.5 Сравнение кэшированного Etag и If-None-Match

Сравните заголовок ответа: Etag с заголовком запроса: If-None-Match, если Etag и If-None-Match равны, вернуть 304. Как добавить этаг?

В соответствии с содержимым файла создайте сводку md5 и добавьте метку к объекту.

Хотя этот метод потребляет больше производительности, он может более точно сравнить, был ли файл изменен. Полагаться на время модификации файла для сравнения недостаточно точно. Потому что иногда файл изменился Last-Modified, но содержимое файла могло вообще не измениться. Так что эта схема лучше 2.4.

Выполнение:

let rs = fs.createReadStream(p);
let md5 = crypto.createHash('md5'); // 不能写完响应体再写头
let arr = [];
rs.on('data',function (data) {
    md5.update(data);
    arr.push(data);
});

Установить Etags

rs.on('end',function () {
let r = md5.digest('base64');
res.setHeader('Etag', r);
if (req.headers['if-none-match'] === r ){
    res.statusCode = 304;
    res.end();
    return;
}
res.end(Buffer.concat(arr));
})

контрольная работа:

2.6 Accept-Encoding

Опираясь на заголовок запроса: Accept-Encoding: gzip, deflate, br сообщает серверу допустимый формат данных. После возврата сервера формат данных будет помечен Content-Encoding через формат ответа. В формате, который принимает клиент gzip, серверная часть может пройти обработку сжатия файлов для повышения производительности. предоставляется в API узлаzlibМодуль:

Модуль zlib обеспечивает сжатие через Gzip и Deflate/Inflate.

Давайте применим zlib и заголовок запроса Accept-Encoding для достижения функции сжатия.

let zlib = require('zlib');
let fs = require('fs');
let path = require('path');
function gzip(filePath) {
  let transform = zlib.createGzip();//转化流通过transform压缩,然后再写
  fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(filePath+'.gz'));
}
 gzip('2.txt')

Распаковать:

function gunzip(filePath) {
  let transform = zlib.createGunzip();
  fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(path.basename(filePath,'.gz')));
}

path.basename(filePath,'.gz')Используется для удаления суффикса из имени файла filePath..gz.

Конкретная работа бэкенда по типу, принятому заголовком запроса:

if(req.url === '/download'){
    res.setHeader('Content-Disposition', 'attachment' )
    return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
  }
let http = require('http');
let fs = require('fs');
let path = require('path');
let zlib = require('zlib');
http.createServer(function (req,res) {
  if(req.url === '/download'){
    res.setHeader('Content-Disposition', 'attachment' )
    return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
  }
  let rule = req.headers['accept-encoding'];
  if(rule){
    if(rule.match(/\bgzip\b/)){
      res.setHeader('Content-Encoding','gzip');
      fs.createReadStream(path.join(__dirname, '1.html'))
      .pipe(zlib.createGzip())
      .pipe(res);
    } else if (rule.match(/\bdeflate\b/)){
      res.setHeader('Content-Encoding', 'deflate');
      fs.createReadStream(path.join(__dirname, '1.html'))
        .pipe(zlib.createDeflate())
        .pipe(res);
    }else{
      fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
    }
  }else{
    fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
  }
}).listen(3000);

test deflate:

curl -v --header "Accept-Encoding:deflate" http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Accept-Encoding:deflate
> 
< HTTP/1.1 200 OK
< Content-Encoding: deflate
< Date: Thu, 23 Aug 2018 03:01:13 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked

test others:

curl -v --header "Accept-Encoding:nn" http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Accept-Encoding:nn
> 
< HTTP/1.1 200 OK
< Date: Thu, 23 Aug 2018 03:02:51 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
< 
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  你好
</body>

* Connection #0 to host localhost left intact
</html>%   

2.7 referer

referer указывает URL запрошенного файла, который будет передан при запросе. Чтобы предотвратить прямые ссылки на файлы вашего собственного веб-сайта из внешней сети, вы можете настроить анти-личные ссылки, сравнив реферер, то есть запрашиваемый адрес, с локальным адресом.

let http =  require('http');
let fs = require('fs');
let url = require('url');
let path = require('path');
// 这是百度的服务器
let server = 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 // 引用图片的网址
        if (current === referer){
          fs.createReadStream(realPath).pipe(res);
        }else{
          fs.createReadStream(path.join(__dirname,'images/2.jpg')).pipe(res);
        }
      }else{
        fs.createReadStream(realPath).pipe(res);
      }
    }
  })
}).listen(3000);

2.8 Accept-Language

Заголовок запроса: Accept-Language: zh-CN,zh;q=0.9 Несколько языков разделены символом ',', вес представлен знаком '=', вес по умолчанию не равен 1.

Бэкенд принимает вес языка в соответствии с запросом на поиск, возвращает, когда он найден, и использует язык по умолчанию, когда он не найден.

let langs = {
  en:  'hello world',
  'zh-CN':'你好世界',
  zh:'你好',
  ja: 'こんにちは、世界'
}
let defualtLanguage = 'en'
// 多语言之服务端方案:来做 (浏览器会发一个头) 前端来做
// 通过url实现多语言
let http = require('http');
http.createServer(function (req,res) {
    let lan = req.headers['accept-language'];
    //[[zh,q=0.9],[zh-CN]] =>[{name:'zh-CN',q=1},{name:'zh',q:0.9}]
    if(lan){
      lan = lan.split(',');
      lan = lan.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 <lan.length;i++){
        // 将每个人的名字 取出来
        let name= lan[i].name;
        if(langs[name]){ //去语言包查找 查找到就返回
          res.end(langs[name]);
          return;
        }
      }
      res.end(langs[defualtLanguage]); // 默认语言
    }else{
      res.end(langs[defualtLanguage]); // 默认语言
    }
}).listen(3000);

контрольная работа:

Суммировать

Заголовки запросов и заголовки ответов часто используются при совместной отладке внешнего и внутреннего интерфейса. После понимания их волшебства взаимодействие между интерфейсом и сервером станет более гармоничным и гладким~

Author: Yanni Jia
Прозвище: Очень кролик
Email: 385067638@qq.com