http & http应用

1,726 阅读9分钟

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。

  • 请求的一方叫客户端,响应的一方叫服务器端
  • 通过请求和响应达成通信
  • HTTP是一种不保存状态的协议

http

URI和URL

URI

URI(Uniform Resource Identifier)是统一资源标识符,在某个规则下能把这个资源独一无二标示出来,比如人的身份证号

  • Uniform 不用根据上下文来识别资源指定的访问方式
  • Resource 可以标识的任何东西
  • Identifier 表示可标识的对象

URL

统一资源定位符,表示资源的地点,URL时使用浏览器访问WEB页面时需要输入的网页地址

  • Uniform 不用根据上下文来识别资源指定的访问方式
  • Resource 可以标识的任何东西
  • Location 定位

URL的格式

报文

请求报文

  • 请求行 方法 协议 url
  • 请求头
  • 请求体

响应报文

  • 响应行 协议 状态 状态码短语
  • 响应头
  • 响应体

首部

通用首部字段

首部字段名 说明
Cache-Control 控制缓存行为
Connection 链接的管理
Date 报文日期
Pragma 报文指令
Trailer 报文尾部的首部
Trasfer-Encoding 指定报文主体的传输编码方式
Upgrade 升级为其他协议
Via 代理服务器信息
Warning 错误通知

请求首部字段

首部字段名 说明
Accept 用户代理可处理的媒体类型
Accept-Charset 优先的字符集
Accept-Encoding 优先的编码
Accept-Langulage 优先的语言
Authorization Web认证信息
Expect 期待服务器的特定行为
From 用户的电子邮箱地址
Host 请求资源所在的服务器
If-Match 比较实体标记
If-Modified-Since 比较资源的更新时间
If-None-Match 比较实体标记
If-Range 资源未更新时发送实体Byte的范围请求
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 资源的最后修改时间

状态码

状态码负责表示客户端请求的返回结果、标记服务器端是否正常、通知出现的错误

类别 原因短语
1XX Informational(信息性状态码)
2XX Success(成功状态码)
3XX Redirection(重定向)
4XX Client Error(客户端错误状态码)
5XX Server Error(服务器错误状态吗)

2XX 成功

  • 200(OK) 客户端发过来的数据被正常处理
  • 204(Not Content) 正常响应,没有实体
  • 206(Partial Content) 范围请求,返回部分数据,响应报文中由Content-Range指定实体内容

3XX 重定向

  • 301(Moved Permanently) 永久重定向
  • 302(Found) ) 临时重定向,规范要求,方法名不变,但是都会改变
  • 303(See Other) 和302类似,但必须用GET方法
  • 304(Not Modified) 状态未改变,配合(If-Match、If-Modified-Since、If-None_Match、If-Range、If-Unmodified-Since)
  • 307(Temporary Redirect)) 临时重定向,不该改变请求方法-

5XX 服务器端错误

  • 500(Internal Server Error) 服务器故障
  • 503(Service Unavailable) 服务器处于超负载或正在停机维护

http应用

范围请求

  • client Range:bytes=0-5
  • server
    Accept-Ranges: bytes
    Content-Range: bytes 0-5/705
    Status Code 206
服务端(1.server.js)
let http = require('http');
let path = require('path');
let util = require('util');
let p = path.resolve(__dirname, '1.range.txt');
// let fs = require('fs');
// let stat = utils.promisify(fs.stat);
// let readFile = utils.promisify(fs.readFile);

//mz
let fs = require('mz/fs');

async function listener(req, res) {
    let range = req.headers['range'];
    if (range) {
        let [, start, end] = range.match(/(\d*)-(\d*)/);
        let statObj = await fs.stat(p);
        let total = statObj.size;
        start = start ? Number(start) : 0;
        end = end ? Number(end) : total - 1;
        
        res.statusCode = 206;
        res.setHeader('Accept-Ranges', 'bytes');
        res.setHeader('Content-Range', `bytes ${start}-${end}/${total}`);
        fs.createReadStream(p, {start, end}).pipe(res);
    }else {
        //读取文件 把它响应给客户端
        fs.createReadStream(p).pipe(res);
    }
}
let server = http.createServer(listener);
server.listen(3000, () => {
    console.log('server start ' + 3000)
})
客户端(1.client.js)
let http = require('http');
let path = require('path');
let config = {
    host: 'localhost',
    port: 3000,
}
let fs = require('fs');
let ws = fs.createWriteStream(path.join(__dirname, '/download.txt'))
let start = 0;
function download () {
    config.headers = {
        'Range': `bytes=${start}-${start + 4}`
    }
    start += 5;
    let client = http.request(config, (res) => {
        let total = res.headers['content-range'].split('/')[1];
        let buffers = [];
        res.on('data', (data) => {
            buffers.push(data);
        });
        res.on('end', () => {
            let buf = Buffer.concat(buffers);
            ws.write(buf);
            setTimeout(function() {
                if (start < total) {
                    download();
                }
            }, 1000);
        })
    }); 
    //必须调用 end,否则请求不会发送
    client.end();
}

download();
运行
node 1.client.js

图片防盗链

  • 从一个网站跳转,或者网页引用到某个资源文件时,HTTP请求中带有Referer表示来源网页的URL
  • 检查请求头中的Referer来判断来源网页的域名
  • 如果来源域名不在白名单内,则返回错误提示
  • 用浏览器直接访问图片地址是没有referer的
服务端
/*
- 请求头 host referer
- 映射 (C:\Windows\System32\drivers\etc\host)
- 目录结构 public/1.jpg、2.jpg、index.html
*/

let http = require('http');
let path = require('path');
let url = require('url');
let {exists} = require('mz/fs');
let fs = require('fs');
let static = path.resolve(__dirname, 'public');
let server = http.createServer(async (req, res) => {
    let { pathname } = url.parse(req.url, true);
    let p = path.join(static, pathname);
    let flag = await exists(p); //如果路径存在返回true
    console.log(flag);
    if (flag) {
        let refer = req.headers['referer'] || req.headers['referered'];
        if (refer) {
            refer = url.parse(refer).hostname;
            let host = req.headers['host'].split(':')[0];
            console.log(refer, host);
            if (refer != host) {
                fs.createReadStream(path.join(static, '2.jpg')).pipe(res);
            }else {
                fs.createReadStream(path.join(static, '1.jpg')).pipe(res); 
            }
        }else {
             fs.createReadStream(p).pipe(res);
        }
    }else {
        res.statusCode = 404;
        res.end('Not Found');
    }
});
server.listen(3000)
index.html
<!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>
    <img src="http://zf1.cn:3000/1.jpg" alt="">
    <br/>
    <img src="http://zf2.cn:3000/1.jpg" alt="">
</body>
</html>

正向代理

  • 访问原来无法访问的资源,如google
  • 可以做缓存,加速访问资源-
  • 对客户端访问授权,上网进行认证
  • 代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息

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

http.createServer((req, res) => {
    proxy.web(req, res, {
        target: 'http://localhost:4000'
    })
    proxy.on('error', (err) => {
        console.log(err);
    })
}).listen(5000);
4000.js
let http = require('http');
http.createServer((req, res) => {
    res.end('hello world')
}).listen(4000)

反向代理

客户端是无感知代理的存在的,反向代理对外都是透明的,访问者者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。

  • 保证内网的安全,可以使用反向代理提供WAF功能,阻止web攻击
  • 负载均衡,通过反向代理服务器来优化网站的负载
server.js
let http = require('http');
let httpProxy = require('http-proxy');

let proxy = httpProxy.createProxyServer();
let ops = {
    'myword1.com': 'http://localhost:4000',
    'myword2.com': 'http://localhost:4001'
}

http.createServer((req, res) => {
    //res.end('hello')
    let host = req.headers['host'];
    proxy.web(req, res, {
        target: ops[host]
    })
    proxy.on('error', (err) => {
        console.log(err);
    })
}).listen(80);
4000.js
let http = require('http');
http.createServer((req, res) => {
    res.end('myword1')
}).listen(4000)
4001.js
let http = require('http');
http.createServer((req, res) => {
    res.end('myword2')
}).listen(4001)

多语言

通过Accept-Language检测浏览器的语言

请求头格式 Accept-Language: Accept-Language:zh-CN,zh;q=0.9
响应头格式 Content-Language:zh-CN

let languageConfig = {
    'zh-CN': '你好',
    'en': 'hello',
    'Fr': 'Bonjour'
}
let defaultLanguage = 'en';
//获取accept-language 根据语言进行权重排序 
//zh-CN,zh;q=0.8 => [{name: 'zh-cn', q: 0.8}]
let http = require('http');
http.createServer((req, res) => {
    let language = req.headers['accept-language'];
    if (language) {
        let lans = language.split(',');
        lans = lans.map((lan) => {
            let [name, q] = lan.split(';')
            q = q ? parseFloat(q.split("=")[1]) : 1;
            return {
                name: name,
                q
            }
        });
        for (let i = 0; i < lans.length; i++) {
            let content = languageConfig[lans[i].name];
            if (content){
                return res.end(content);
            }
        }
        res.end(languageConfig[defaultLanguage]);
    }else {
        res.end(languageConfig[defaultLanguage]);
    }

}).listen(4000)

缓存

  • 减少了冗余的数据传输
  • 减少了服务器的负担,提高了网站的性能
  • 加快了客户端加载网页的速度

缓存分类

  • 强制缓存,不需要再和服务器发生交互,
  • 对比缓存,都需要与服务端发生交互

强制缓存

  • Cache-Control: max-age=10 相对时间
  • Exprires: GMTString 绝对时间

Cache-Control
private 客户端可以缓存
public 客户端和代理服务器都可以缓存
max-age=60 缓存内容将在60秒后失效
no-cache 需要使用对比缓存验证数据,强制向源服务器再次验证
no-store 所有内容都不会缓存,强制缓存和对比缓存都不会触发

let util = require('util');
let fs = require('fs');
let stat = util.promisify(fs.stat); 
let path = require('path');
let url = require('url');
let mime = require('mime');
let p = path.resolve(__dirname);
http.createServer(async (req, res) => {
    let {pathname} = url.parse(req.url, true);
    let realPath = path.join(p, pathname);
    console.log(realPath);
    try {
        await stat(realPath);   
        res.setHeader('Content-Type', mime.getType(realPath) + ';chartset=utf8');
        //console.log(realPath);
        res.setHeader('Cache-Control', 'max-age=10');
        res.setHeader('Expries', new Date(Date.now() + 10 * 1000).toGMTString());
        fs.createReadStream(realPath).pipe(res);
    }catch(e) {
        res.statusCode = 404;
        res.end('Not Found')
    }
}).listen(80);

对比缓存

1、响应头 last-modified 请求头 if-modified-since

let http = require('http');
let util = require('util');
let fs = require('fs');
let stat = util.promisify(fs.stat); 
let path = require('path');
let url = require('url');
let mime = require('mime');
let p = path.resolve(__dirname);
//第一次访问,发送respones头Last-Modified
//第二次访问时,带个request头if-modified-since
http.createServer(async (req, res) => {
    let {pathname} = url.parse(req.url, true);
    let realPath = path.join(p, pathname);
    console.log(realPath);
    try {
        let statObj = await stat(realPath);   
        res.setHeader('Content-Type', 'no-cache');
        let since = req.headers['if-modified-since'];
        if (since === statObj.ctime.toGMTString()) {
            res.statusCode = 304;
            res.end();
        }else {
            res.setHeader('Last-Modified', statObj.ctime.toGMTString())
            fs.createReadStream(realPath).pipe(res);   
        }
    }catch(e) {
        res.statusCode = 404;
        res.end('Not Found');
    }
}).listen(80);

存在的问题:

  • 某些服务器不能精确得到文件的最后修改时间,无法通过最后修改时间来判断文件是否更新了。
  • 某些文件的修改非常频繁,在秒以下的时间内进行修改. Last-Modified只能精确到秒。
  • 一些文件的最后修改时间改变了,但是内容并未改变。 我们不希望客户端认为这个文件修改了。
  • 如果同样的一个文件位于多个CDN服务器上的时候内容虽然一样,修改时间不一样。

2、 响应头 Etag 请求头 if-none-match

let http = require('http');
let util = require('util');
let fs = require('fs');
let stat = util.promisify(fs.stat); 
let path = require('path');
let url = require('url');
let mime = require('mime');
let p = path.resolve(__dirname);
//第一次访问,发送respones头etag
//第二次访问时,带个request头if-none-match
http.createServer(async (req, res) => {
    let {pathname} = url.parse(req.url, true);
    let realPath = path.join(p, pathname);
    console.log(realPath);
    try {
        let statObj = await stat(realPath);   
        res.setHeader('Content-Type', 'no-cache');
        let match = req.headers['if-none-match'];
        if (match ===  statObj.size.toString()) {
            res.statusCode = 304;
            res.end();
        }else {
            res.setHeader('Etag', statObj.size.toString())
            fs.createReadStream(realPath).pipe(res);   
        }
    }catch(e) {
        res.statusCode = 404;
        res.end('Not Found');
    }
}).listen(80);

压缩

  • 请求头 Accept-Encodeing
  • 响应头 Content-Encoding
let http = require('http');
let path = require('path');
let zlib = require('zlib');
let url = require('url');
let fs = require('fs');
let mime = require('mime');

let p = path.resolve(__dirname);
http.createServer((req, res) => {
    let {pathname} = url.parse(req.url, true);
    let realPath = path.join(p, pathname);
    let encodeing = req.headers['accept-encoding'];
     if (encodeing) {
        res.setHeader('Content-Type', mime.getType(realPath));
        if (encodeing.match(/\bgzip\b/)) {
            res.setHeader('Content-Encoding', 'gzip')
            return fs.createReadStream(realPath).pipe(zlib.createGzip()).pipe(res);
        }else if (encodeing.match(/\bdeflate\b/)) {
            res.setHeader('Content-Encoding', 'deflate')
            return fs.createReadStream(realPath).pipe(zlib.createDeflate()).pipe(res);
        }else {
            fs.createReadStream(realPath).pipe(res);
        }
    }else {
        fs.createReadStream(realPath).pipe(res);
    }
}).listen(4000);

压缩之前

压缩之后