HTTP от входа до отказа

Node.js внешний интерфейс сервер алгоритм

базовые знания

  • Запрашивающая сторона называется клиентской, а отвечающая сторона называется серверной.
  • Общение через запросы и ответы
  • HTTP — это протокол, который не сохраняет состояние

сообщение запроса

alt

Общие методы:

  • ПОЛУЧИТЬ, чтобы получить ресурс
  • Сообщение отправляет данные в тело транспортного объекта сервера
  • ПОСТАВИТЬ файл передачи
  • HEAD получает заголовок сообщения
  • УДАЛИТЬ удалить файл
  • ВАРИАНТЫ Запросить поддерживаемые методы
  • TRACE путь трассировки

ответное сообщение

alt

код состояния

категория иллюстрировать
1XX Информационная (информационный код статуса
2XX Успех (код состояния успеха)
3XX перенаправление
4XX Ошибка клиента (код состояния ошибки клиента)
5XX Ошибка сервера (статус ошибки сервера)

2ХХ успехов

  • 200 (ОК. Данные, отправленные клиентом, обрабатываются нормально
  • 204 (Нормальный ответ Not Content, нет объекта
  • 206 (запрос диапазона частичного содержимого, возвращает частичные данные, содержимое объекта, указанное Content-Range в ответном сообщении

3XX редирект

  • 301 (перемещено навсегда) постоянное перенаправление
  • 302 (найдено) временное перенаправление, спецификация требует, чтобы имя метода осталось неизменным, но это изменится
  • 303 (см. Другое) 302 и т.п., но необходимо использовать метод GET
  • 304 (Не изменено) Состояние не изменилось с помощью (If-Match, If-- Modified-Since, If-None_Match, If-Range, If-Unmodified-Since)
  • 307 (Temporary Redirect) Временное перенаправление, метод запроса менять не следует.

4XX Ошибка клиента

  • 400 (Bad Request) Ошибка синтаксиса сообщения запроса
  • 401 (неавторизованный) Требуется аутентификация
  • 403 (Запрещено) Сервер отказывается обращаться к соответствующему ресурсу
  • 404 (Not Found) не удается найти ресурсы на сервере

5XX ошибка на стороне сервера

  • 500 (внутренняя ошибка сервера) сбой сервера
  • 503 (Служба недоступна) Сервер перегружен или отключен для обслуживания.

headers

Общие поля заголовка

имя поля заголовка иллюстрировать
Cache-Control Управление поведением кэша
Connection управление ссылками
Date дата сообщения
Pragma команда сообщения
Trailer заголовок в конце сообщения
Trasfer-Encoding Указывает метод кодирования передачи тела сообщения.
Upgrade Обновление до других протоколов
Via информация о прокси-сервере
Warning уведомление об ошибке

поля заголовка запроса

имя поля заголовка иллюстрировать
Accept типы мультимедиа, которые может обрабатывать пользовательский агент
Accept-Charset Предпочтительный набор символов
Accept-Encoding приоритетное кодирование
Accept-Langulage предпочтительный язык
Authorization Информация о веб-аутентификации
Expect Ожидайте определенного поведения от сервера
From адрес электронной почты пользователя
Host Сервер, на котором находится запрошенный ресурс
If-Match Сравните теги сущностей
If-Modified-Since Сравните время обновления ресурсов
If-None-Match Сравните теги сущностей
If-Range Отправить запрос диапазона для байта сущности, когда ресурс не обновлен
If-Unmodified-Since Сравните время обновления ресурсов (в отличие от If-Modified-Since)
Max-Forwards Максимальное количество переходов передачи
Proxy-Authorization Прокси-сервер требует аутентификации клиента
Range Запрос диапазона байтов сущности
Referer Оригинальный геттер URI в запросе
TE Приоритет кодирования передачи
User-Agent Информация о клиентской программе HTTP

поля заголовка ответа

имя поля заголовка иллюстрировать
Accept-Ranges принимать ли диапазоны байтов
Age когда ресурс был создан
ETag информация о сопоставлении ресурсов
Location Клиент перенаправляет на указанный URI
Proxy-Authenticate Информация об аутентификации прокси-сервера для клиента
Retry-After Когда отправлять запрос повторно
Server информация о сервере
Vary Информация управления, кэшированная прокси-сервером
www-Authenticate Аутентификация между сервером и клиентом

поле заголовка сущности

имя поля заголовка иллюстрировать
Allow Методы HTTP, поддерживаемые ресурсом
Content-Encoding как закодирована сущность
Content-Language Естественный язык сущности
Content-Length Размер содержимого объекта (в байтах)
Content-Location Подставьте URI соответствующего ресурса
Content-MD5 Дайджест сообщения сущности
Content-Range диапазон местонахождения сущности
Content-Type Тип носителя тела объекта
Expires Срок действия сущности
Last-Modified Время последнего изменения ресурса

Лично я думаю, что вам нужно понимать только заголовок запроса и заголовок ответа. Среди них диапазон в заголовке ответа (его можно возобновить с помощью точки останова, о которой будет сказано ниже) и кеш (ETag) — это те знания, которые необходимо освоить.

создать http

  • метод первый:
const http = require('http');
const server = http.createServer(function(req,res){
    res.end(123);
});
server.listen(8080);
  • Способ второй:
const http = require('http'); 
const server = http.createServer();
// req是请求 是一个可读流 = socket
// res是响应 是一个可写流
server.on('request',function(req,res){
    let method = req.method;
    let httpVersion  = req.httpVersion;
    let url = req.url;
    let headers = req.headers; // 请求头的名字都是小写的
    console.log(method,httpVersion,url,headers);
    // 如果数据 大于64k data事件可能会触发多次
    let buffers = [];
    // 如果没有请求体 不会走on('data'),没有请求体也会触发end事件
    req.on('data',function(data){
        console.log(1)
        buffers.push(data);
    });

    req.on('end',function(){
        console.log(Buffer.concat(buffers).toString());
        // socket.write socket.end
        res.write('hello');
        res.end('world');
    });
});
// 监听请求的到来
server.on('connection',function(socket){
    console.log('建立连接');
});
server.on('close',function(){
    console.log('服务端关闭')
})
server.on('error',function(err){
    console.log(err);
});
server.listen(8080);

Просмотрев исходный код, мы обнаружим согласованность двух приведенных выше способов написания:

alt

alt

URL-модуль

Вы можете использовать его для обработки запрошенного URL.Простая демонстрация выглядит следующим образом:

let url = require('url');
let u = 'http://www.baidu.com:80/abc/index.html?a=1&b=2#hash';

// 可以将查询字符串转化成对象
let urlObj = url.parse(u,true);
console.log(urlObj.query); // 查询字符串
console.log(urlObj.pathname); //  路径

res

В последнем содержании предыдущей статьи упоминались некоторые свойства req, так что давайте взглянем на методы res.

write && end

Вы можете использовать метод записи для отправки содержимого ответа

response.write(chunk,[encoding]);
response.end([chunk],[encoding]);

writeHead

res.writeHead(statusCode, [headers]);

setHeader

res.setHeader(key, value);

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

То есть после writeHeader выполнение setHeader сообщит об ошибке.

заявление

Подключение клиента

// server端
const http = require('http');
const server = http.createServer(function(req,res){
    let contentType = req.headers['content-type'];
    let buffers = [];
    req.on('data',function(chunk){
        buffers.push(chunk);
    });
    req.on('end',function(){
        let content = Buffer.concat(buffers).toString();
        if(contentType === 'application/json'){
            console.log(JSON.parse(content).name)
        }else if(contentType === 'application/x-www-form-urlencoded'){
            let queryString = require('querystring');
            console.log(queryString.parse(content).age)
        }
        res.end('hello');
    });
});
server.listen(4000);
// client
const http = require('http');
const options = {
    hostname:'localhost',
    port:4000,
    path: '/',
    method:'get',
    // 告诉服务端我当前要给你发什么样的数据
    headers:{
        'Content-Type':'application/x-www-form-urlencoded',
        'Content-Length':5
    }
}
const req = http.request(options, function(res) {
    res.on('data',function(chunk){
        console.log(chunk.toString());
    });
});
req.end('age=1');

Следует отметить, что клиентский запрос должен передавать Content-Length, иначе будут проблемы.

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

alt

Многоязычный переключатель

Язык браузера можно определить с помощью Accept-Language.

  • Формат заголовка запроса Accept-Language: Accept-Language:zh-CN,zh;q=0.9
  • Формат заголовка ответа Content-Language:zh-CN
const pack = {
    'en': { title: 'english' },
    'zh-CN': { title: '中文' }
}
const http = require('http');
const server = http.createServer(function (req, res) {
    let lan = 'en';
    let language = req.headers['accept-language'];
    if (language) {
        lan = language.split(',').map(function (item) {
            let values = item.split(';');
            return {
                name: values[0],
                q: values[1] ? parseInt(values[1]) : 1
            }
        }).sort((lang1, lang2) => lang2.q - lang1.q).shift().name;
        console.log(lan)
    }
    res.end(pack[lan] ? pack[lan].title : pack['en'].title);
}).listen(4000);

Картинка противоугонная цепочка

Это космическое изображение QQ относительно распространено, и оно станет разделенным изображением после того, как его цитировали в прошлом.

Принцип реализации:

  • При переходе с веб-сайта или ссылке на веб-страницу в файл ресурсов HTTP-запрос содержит Referer для указания URL-адреса исходной веб-страницы.
  • Определите доменное имя исходной страницы, проверив Referer в заголовке запроса.
  • Если исходного доменного имени нет в белом списке, будет возвращено сообщение об ошибке. Нет реферера для прямого доступа к адресу изображения с помощью браузера.
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const root = path.join(__dirname, 'public');

function removePort(host) {
    return host.split(':')[0]
}

function getHostName(urlAddr) {
    const { host } = url.parse(urlAddr);
    return removePort(host);
}

function request(req, res) {
    let refer = req.headers['referer'] || req.headers['referrer'];
    if (refer) {
        let referHost = getHostName(refer);
        let host = removePort(req.headers['host']);
        if (referHost != host) {
            sendForbidden(req, res);
        } else {
            serve(req, res);
        }
    } else {
        serve(req, res);
    }
}

function serve(req, res) {
    let {
        pathname
    } = url.parse(req.url);
    let filepath = path.join(root, pathname);
    console.log(req.url, filepath);

    fs.stat(filepath, (err, stat) => {
        if (err) {
            res.end('Not Found');
        } else {
            fs.createReadStream(filepath).pipe(res);
        }
    });
}

function sendForbidden(req, res) {
    res.end('防盗链');
}
const server = http.createServer();
server.on('request', request);
server.listen(8080);

прокси-сервер

Прямой прокси и обратный прокси [Обзор]

let httpProxy = require('http-proxy');
let http = require('http');
let proxy = httpProxy.createProxyServer();

http.createServer(function (req, res) {
    proxy.web(req, res, {
        target: 'http://localhost:8000'
    });
    proxy.on('error', function (err) {
        console.log('出错了');
        res.end(err.toString());
    });
}).listen(8080);

Приведенный выше код представляет собой запросlocalhost:8080при пересылкеhttp://localhost:8000. Модули запроса пересылки, такие как webpack-dev-server:http-proxy-middleware, используетсяhttp-proxy.

веб хостинг

Через Host несколько веб-сайтов используют один порт, а несколько веб-сайтов используют один сервер.

const http = require('http');
const httpProxy = require('http-proxy');
const proxy = httpProxy.createProxyServer();

let hosts = {
    'www.test1.com': 'http://localhost:8000',
    'www.test2.com': 'http://localhost:9000'
}
http.createServer(function (req, res) {
    let host = req.headers['host'];
    host = host.split(':')[0];
    let target = hosts[host];
    proxy.web(req, res, {
        target
    });
}).listen(80);

Range

Когда пользователь слушает песню, если слышна половина песни (сеть загружена наполовину), сеть отключена, и пользователю необходимо продолжить прослушивание, если файловый сервер не поддерживает точки останова, пользователю необходимо скачайте файл еще раз. Если Range поддерживает его, клиент должен записать диапазон файлов, который был прочитан ранее.После восстановления сети он отправит запрос на сервер для чтения оставшегося Range.Серверу нужно отправить только часть запрошенного контента клиентом, а не весь файл, отправленный обратно клиенту, что экономит пропускную способность сети.

Запрос диапазона для получения частичного контента

Чтобы реализовать требование прерванной и возобновленной загрузки, необходимо иметь возможность загружать диапазон объектов указанной загрузки.

  • Диапазон в заголовке запроса для указания диапазона байтов ресурса
  • Ответ вернет ответ ответ с кодом состояния 206
  • Для запроса диапазона с несколькими диапазонами в ответе будут указаны multipart/byteranges в поле заголовка Content-Type.

alt

alt

Как подать заявку

// 服务端
const http = require('http');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const stat = promisify(fs.stat);
// 客户端要发一个头Range:bytes=0-10
// 服务端返回一个头
// Accept-Ranges:bytes
// Content-Range:0-10/总大小
const server = http.createServer(async function (req, res) {

    let p = path.join(__dirname, 'content.txt');
    // 判断当前文件的大小
    let statObj = await stat(p);
    let start = 0;
    let end = statObj.size - 1; // 读流是包前又包后的
    let total = end
    let range = req.headers['range'];
    if (range) {
        // 告诉它支持范围请求
        res.setHeader('Accept-Ranges','bytes');
        // ['匹配的字符串','第一个分组']
        let result = range.match(/bytes=(\d*)-(\d*)/);
        start = result[1]?parseInt(result[1]):start;
        end = result[2]?parseInt(result[2])-1:end;
        // 获取成功并且文件总大小是多少
        res.setHeader('Content-Range',`${start}-${end}/${total}`)
    }
    res.setHeader('Content-Type', 'text/plain;charset=utf8');
    fs.createReadStream(p, { start, end }).pipe(res);
});
server.listen(3000);
// 客户端
const options = {
    hostname: 'localhost',
    port: 3000,
    path: '/',
    method: 'GET'
}
const fs = require('fs');
const path = require('path');
const http = require('http');
const ws = fs.createWriteStream('./download.txt');
let pause = false;
let start = 0;
// 监听键盘事件,如果有输入p,则暂停
process.stdin.on('data', function (chunk) {
    chunk = chunk.toString();
    if (chunk.includes('p')) {
        pause = true
    } else {
        pause = false;
        download();
    }
});
function download() {
    options.headers = {
        Range: `bytes=${start}-${start + 10}`
    }
    start += 10;
    // 发请求
    // 0-10
    http.get(options, function (res) {
        let range = res.headers['content-range'];
        let total = range.split('/')[1];
        let buffers = [];
        res.on('data', function (chunk) {
            buffers.push(chunk);
        });
        res.on('end', function () {
            //将获取的数据写入到文件中
            ws.write(Buffer.concat(buffers));
            setTimeout(function () {
                if (pause === false && start < total) {
                    download();
                }
            }, 1000)
        })
    })
}
download();

компрессия

узлапочтовый файл

const fs = require('fs');
const path = require('path')
const zlib = require('zlib');

// 压缩
function zip(src){
    // 压缩流 转化流
    const gzip = zlib.createGzip();
    fs.createReadStream(src).pipe(gzip).pipe(fs.createWriteStream(src+'.gz'))
}
// zip(path.join(__dirname,'./1.txt'));

// 解压
function unzip(src){
    const gunzip = zlib.createGunzip();
    fs.createReadStream(src)
    .pipe(gunzip)
    .pipe(fs.createWriteStream(path.basename(src,'.gz')));
}
// unzip(path.join(__dirname,'./1.txt.gz'));

В HTTP мы можем судить о том, следует ли сжимать передаваемый контент, по заголовку запроса.

const http = require('http');
const path = require('path');
const fs = require('fs');
const zlib = require('zlib');
http.createServer(function (req, res) {
    const p = path.join(__dirname, '1.txt');
    // Accept-Encoding: gzip, deflate, br 客户端
    const header = req.headers['accept-encoding'];
    res.setHeader('Content-Type','text/html;charset=utf8');
    if (header) {
        if (header.match(/\bgzip\b/)) {
            const gzip = zlib.createGzip();
            res.setHeader('Content-Encoding','gzip');
            fs.createReadStream(p).pipe(gzip).pipe(res);
        } else if (header.match(/\bdeflate\b/)) {
            const deflate = zlib.createDeflate();
            res.setHeader('Content-Encoding','deflate')
            fs.createReadStream(p).pipe(deflate).pipe(res);
        }else{
            fs.createReadStream(p).pipe(res);
        }
    }else{
        fs.createReadStream(p).pipe(res);
    }
}).listen(8080);

шифрование

crypto

crypto — это модуль, который реализует шифрование и дешифрование в node.js.В node.js библиотека классов OpenSSL используется в качестве внутреннего средства шифрования и дешифрования.OpenSSL — это тщательно протестированный и надежный инструмент реализации алгоритмов шифрования и дешифрования.

Хэш (хэш) алгоритм

Алгоритм хэширования, также называемый хеш-алгоритмом, используется для преобразования ввода любой длины в вывод фиксированной длины.Обычными являются md5, sha1 и т. д. Он имеет следующие характеристики:

  • Тот же вход дает тот же результат
  • Разные результаты дают разные результаты
  • Произвольная длина ввода длина вывода одинакова
  • Значение ввода не может быть выведено из вывода

Пример

var crypto = require('crypto');
var md5 = crypto.createHash('md5');//返回哈希算法
var md5Sum = md5.update('hello');//指定要摘要的原始内容,可以在摘要被输出之前使用多次update方法来添加摘要内容
var result = md5Sum.digest('hex');//摘要输出,在使用digest方法之后不能再向hash对象追加摘要内容。
console.log(result);

несколько обновлений

const fs = require('fs');
const shasum = crypto.createHash('sha1');//返回sha1哈希算法
const rs = fs.createReadStream('./readme.txt');
rs.on('data', function (data) {
    shasum.update(data);//指定要摘要的原始内容,可以在摘要被输出之前使用多次update方法来添加摘要内容
});
rs.on('end', function () {
    const result = shasum.digest('hex');//摘要输出,在使用digest方法之后不能再向hash对象追加摘要内容。
    console.log(result);
})

Алгоритм HMAC

Алгоритм HMAC сочетает в себе алгоритм хэширования с секретным ключом для предотвращения нарушения целостности подписи.

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const m = crypto.createHmac('sha1', fs.readFileSync(path.join(__dirname,'./content.txt'))); //将content文件中的内容作为一个密钥
m.update('ok'); 
console.log(m.digest('hex'));

Соответствующее шифрование

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

alt

Метод генерации закрытого ключа:

openssl genrsa -out rsa_private.key 1024
const crypto = require('crypto');
const fs = require('fs');
let str = 'hello';
const cipher = crypto.createCipher('blowfish', fs.readFileSync(path.join(__dirname, 'rsa_private.key')));
let encry = cipher.update(str, 'utf8','hex');
encry += cipher.final('hex');
console.log(encry);

// 对称解密
const deciper = crypto.createDecipher('blowfish', fs.readFileSync(path.join(__dirname, 'rsa_private.key')));
let deEncry = deciper.update(encry, 'hex','utf8');
deEncry += deciper.final('utf8');
console.log(deEncry);

Асимметричное шифрование

  • Алгоритм асимметричного шифрования требует два ключа: открытый ключ и закрытый ключ.
  • Открытый ключ и закрытый ключ представляют собой пару. Если данные зашифрованы с помощью открытого ключа, для их расшифровки можно использовать только соответствующий закрытый ключ. Если закрытый ключ зашифрован, расшифровать можно только открытый ключ.
  • Поскольку для шифрования и дешифрования используются два разных ключа, этот алгоритм называется алгоритмом асимметричного шифрования.

alt

Создать открытый ключ для закрытого ключа

openssl rsa -in rsa_private.key -pubout -out rsa_public.key
const crypto = require('crypto');
const fs = require('fs');
let key = fs.readFileSync(path.join(__dirname, 'rsa_private.key'));
let cert = fs.readFileSync(path.join(__dirname, 'rsa_public.key'));
let secret = crypto.publicEncrypt(cert, buffer);//公钥加密
let result = crypto.privateDecrypt(key, secret);//私钥解密
console.log(result.toString());

подписать

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

alt

let private = fs.readFileSync(path.join(__dirname, 'rsa_private.key'), 'ascii');
let public = fs.readFileSync(path.join(__dirname, 'rsa_public.key'), 'ascii');
let str = 'hello';
let sign = crypto.createSign('RSA-SHA256');
sign.update(str);
let signed = sign.sign(private, 'hex');
let verify = crypto.createVerify('RSA-SHA256');
verify.update(str);
let verifyResult = verify.verify(public,signed,'hex'); //true

тайник

Классификация кэша

Принудительно кэшировать

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

// 当访问 localhost:8080/a.js
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const server = http.createServer(function(req,res){
   let {pathname} =  url.parse(req.url);
   console.log(pathname);
   let p = path.join(__dirname,'public','.'+pathname);
   // 这里可能会存在问题,p有可能是个目录
   fs.stat(p,function(err,stat){
        if(!err){
            sendFile(req,res,p);
        }else{
            sendError(res);
        }
   })
});
function sendError(res){
    res.statusCode = 404;
    res.end();
}
function sendFile(req,res,p){
    let date = new Date(Date.now()+10*1000);
   // res.setHeader('Expires',date.toUTCString());
    res.setHeader('Cache-Control','max-age=10');
    res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
    fs.createReadStream(p).pipe(res);
}
server.listen(8080);

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

Проведя некоторые исследования, я пришел к выводу, что:

Simple Test: Open Chrome Developper Tools / Network. Reload a page multiple times. The table column "Size" will tell you that some files are loaded "from memory cache". Now close the browser, open Developper Tools / Network again and load that page again. All cached files are loaded "from disk cache" now, because your memory cache is empty.

такой жеэта статьяВывод: В случае сильного попадания в кеш первая отрисовка процесса будет считывать ресурсы кеша с диска. Chrome сохранит некоторые ресурсы в памяти

Сравнить кеш

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

Сравните по времени последнего изменения:

const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const server = http.createServer(function (req, res) {
    let { pathname } = url.parse(req.url);
    let p = path.join(__dirname, 'public', '.' + pathname);
    fs.stat(p, function (err, stat) {
        // 根据修改时间判断
        // if-modified-since  Last-Modified
        if (!err) {
            let since = req.headers['if-modified-since'];
            if(since){
                if(since === stat.ctime.toUTCString()){
                    res.statusCode = 304;
                    res.end();
                }else{
                    sendFile(req,res,p,stat);
                }
            }else{
                sendFile(req,res,p,stat);
            }
        } else {
            sendError(res);
        }
    })
});
function sendError(res) {
    res.statusCode = 404;
    res.end();
}
function sendFile(req, res, p,stat) {
    res.setHeader('Cache-Control','no-cache')
    res.setHeader('Last-Modified',stat.ctime.toUTCString());
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
    fs.createReadStream(p).pipe(res);
}
server.listen(8080);

Последнее решение времени модификации не очень надежно, оно имеет следующие проблемы:

  • Некоторые серверы не могут точно получить время последнего изменения файла, поэтому невозможно определить, был ли файл обновлен по времени последнего изменения.
  • Некоторые файлы изменяются очень часто и изменяются за доли секунды. Last-Modified может быть точным только до секунды.
  • Изменилось время последней модификации некоторых файлов, но содержимое не изменилось. Мы не хотим, чтобы клиенты думали, что этот файл был изменен.
  • Если один и тот же файл находится на нескольких CDN-серверах, несмотря на то, что содержимое одинаковое, время модификации разное.

В связи с этим мы можем использовать схему ETag. Это хэш-строка, сгенерированная в соответствии с содержимым объекта, которая может идентифицировать состояние ресурса. При изменении ресурса меняется и ETag. ETag генерируется веб-сервером и затем отправляется клиенту браузера.

const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const crypto = require('crypto');
// 根据的是最新修改时间  这回根据的是文件的内容 
// ETag:md5加密 / if-none-match
const server = http.createServer(function (req, res) {
    let { pathname } = url.parse(req.url);
    let p = path.join(__dirname, 'public', '.' + pathname);
    fs.stat(p, function (err, stat) {
        let md5 = crypto.createHash('md5');
        let rs = fs.createReadStream(p);
        rs.on('data',function(data){
            md5.update(data);
        });
        rs.on('end',function(){
            let r = md5.digest('hex'); // 当前文件的唯一标识
            // 下次再拿最新文件的加密值 和客户端请求来比较
            let ifNoneMatch = req.headers['if-none-match'];
            if(ifNoneMatch){
                if(ifNoneMatch === r){
                    res.statusCode = 304;
                    res.end();
                }else{
                    sendFile(req,res,p,r);
                }
            }else{
                sendFile(req,res,p,r);
            }
        });
    })
});
function sendError(res) {
    res.statusCode = 404;
    res.end();
}
function sendFile(req, res, p,r) {
    res.setHeader('Cache-Control','no-cache')
    res.setHeader('Etag',r);
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
    fs.createReadStream(p).pipe(res);
}
server.listen(8080);

Конечно, если файл относительно большой, например 1G, производительность будет относительно низкой при каждом выполнении этой операции, поэтому вы также можете рассмотреть для сравнения stat.ctime+stat.size или сложить две предыдущие схемы вместе.

процесс запроса

Первый запрос:

alt

Второй запрос:

alt

Cache-Control

  • частные клиенты могут кэшировать
  • И общедоступные клиенты, и прокси-серверы могут кэшировать
  • max-age=60 содержимое кеша истечет через 60 секунд
  • no-cache должен использовать кеш сравнения для проверки данных, снова принудительно выполняя проверку на исходном сервере.
  • no-store Весь контент не будет кэшироваться, и ни принудительный кеш, ни кеш сравнения не будут активированы

Скаффолдинг для реализации статического сервера

В соответствии с приведенным выше кодом можно создать простую версию. Вероятно, это следующие части:

  • Определите, является ли URL-адрес папкой или файлом. Если это файл, перейдите к следующему шагу. Если это папка, отобразите список.
  • Определить, был ли файл закэширован, если есть кеш, прочитать кэшированное содержимое напрямую, если нет (необходимо установить заголовок, например, etag и т. д.), то перейти к следующему шагу
  • сжатый файл gzip
  • Функции, поддерживающие диапазон

В процессе написания лесов, есть два инструмента, которые необходимо упомянуть.

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

alt

последний из последних

Чтобы рекламировать мой блог, пожалуйста, посетите:передняя часть винглета