超文本传输协议(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);
压缩之前