HTTP原理与实践
一. HTTP协议基础与发展历史
1. 五层网络模型介绍
传输层、应用层为高层,网络层、数据链路层和物理层为低层
名称 | 作用 |
---|---|
应用层 | 为应用软件提供了很多服务,建立于TCP协议之上,屏蔽网络传输细节 |
传输层 | 向用户提供可靠的端到端服务,传输层向高层屏蔽了下层数据通信的细节 |
网络层 | 为数据在结点之间传输创建逻辑链路 |
数据链路层 | 在通信的实体间建立数据链路连接 |
物理层 | 定义物理设备如何传输数据 |
2. HTTP协议的发展历史
版本 | 描述 |
---|---|
HTTP/0.9 | 只有GET请求,没有HEADER等描述数据的信息,服务器发送完毕就关闭TCP连接 |
HTTP/1.0 | 追加了很多请求方法,增加了status code和header,支持多字符集,多部分发送 |
HTTP/1.1 | 支持持久连接,支持pipeline、增加HOST和其他命令 |
HTTP/2 | 所有数据二进制传输,同连接里发送多个请求不用按顺序来,头信息压缩和推送功能 |
3. HTTP的三次握手
HTTP的传输数据的通道是TCP建立的
在http1.0中,http请求创建的TCP连接是非持久的,就是服务器响应之后就关闭了,代表没发送一个请求都要创建一次连接。
http1.1中,这个连接可以通过一种方式让它一直保持连接,直到要发送的请求都发送完毕再关闭连接
http三次握手:
- 首先客户端请求连接发送到服务端,标志位SYN=1,Seq=X
- 服务端接收到请求之后,开启TCP端口并返回给客户端,标志位SYN=1,ACK=X+1,Seq=Y
- 客户端收到响应之后,在发送具体数据ACK=Y+1,Seq=Z
- 三次握手用来规避网络传输的延迟造成的服务器开销问题
4. URI、URL和URN
-
URI(Uniform Resource Identifier / 统一资源标志符)
- URI其实是URL和URN统一的一个定义,也就是URI包含了URL和URN
- URI是为了定位某一特定的资源设计的,用来唯一标识互联网上的信息资源
-
URL(Uniform Resource Locator / 统一资源定位器)
比如链接:http://user:pass@host.com:80/path?query=string#hash
http://部分:代表用哪个协议,不同协议解析方式不一样
user:pass@部分: 代表用户认证,现在基本不用
host.com部分:用来定位服务器在网上的位置,也可以是ip
:80部分:是端口,不同的端口提供不同的服务。正式时一般不带端口,而是通过域名来访问
path部分:是路由
query=strin:搜索参数
#hash:用来锚点定位
-
URN永久统一资源定位符
在资源位置改变之后还能找到
目前没有成熟的使用方案
5. HTTP报文格式
-
请求报文:
分为两部分起始行、首部
起始行:包含一个请求方法,要请求资源的地址一个URL和HTTP版本如GET /test/hi.txt HTTP/1.0
-
响应报文:
分为三部分起始行、首部和主体
起始行: http版本、code代表请求状态和具体的含义 如HTTP/1.0 200 OK
-
HTTP方法:用来定义对资源的操作
常用的GET、POST
-
HTTP CODE
定义服务器对请求的处理结果
各个区间的CODE有各自的语义
好的HTTP服务可以通过CODE判断结果
6. 创建一个最简单的web服务
const http = require('http');
http.createServer(function(req,res){
res.end('hello world');
}).listen(8088);
console.log('服务器已开启,端口号:8088');
二. HTTP各种特性总览
1. 认识HTTP客户端
-
只要是实现了发送了一个标准的HTTP请求的工具,那就是HTTP的客户端
-
最简单的http客户端就是我们的浏览器,其他的还有curl、爬虫也是HTTP的客户端
2. CORS跨域请求的限制与解决
-
模拟跨域请求的例子:
- 第一个端口
const http = require('http'); const fs = require('fs'); http.createServer(function(req,res){ const html = fs.readFileSync('test.html','utf8') res.writeHead(200,{ 'Content-Type':'Text/html' }) res.end(html); }).listen(8088); console.log('服务器已开启,端口号:8088');
- 第二个端口
const http = require('http'); http.createServer(function (req, res) { res.end('hello world'); }).listen(8066); console.log('服务器已开启,端口号:8066');
- 页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> var xhr = new XMLHttpRequest(); xhr.open('GET','http://127.0.0.1:8066/') xhr.send() </script> </body> </html>
上面这样就实现了一个跨域问题,8088端口提供的HTML去请求8066端口的内容,会直接报错
-
解决跨域问题
- 在第二个端口设置‘Access-Control-Allow-Origin’
const http = require('http'); http.createServer(function (req, res) { res.writeHead(200,{ 'Access-Control-Allow-Origin':'*' }) res.end('hello world'); }).listen(8066); console.log('服务器已开启,端口号:8066');
- 值为‘*’ 表示允许任何域名都可以访问服务,我们也可以限制跨域的域名
const http = require('http'); http.createServer(function (req, res) { res.writeHead(200,{ 'Access-Control-Allow-Origin':'http://127.0.0.1:8088/' }) res.end('hello world'); }).listen(8066); console.log('服务器已开启,端口号:8066');
- jsonp:仅修改页面的script设置即可
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="http://127.0.0.1:8066/"></script> </body> </html>
利用script标签允许跨域的机制来实现跨域访问
3. CORS跨域限制以及预请求验证
- 修改一下页面结构,使用一个自定义的头信息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
fetch('http://127.0.0.1:8066/',{
metthod:'POST',
headers:{
'X-Test-Cors':'123'
}
})
</script>
</body>
</html>
这样会直接报错,因为跨域默认不允许使用自定义头
-
跨域的限制
- 跨域允许的方法: GET\HEAD\POST
-
Content-Type的值只能是下列值之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 只能人为设置下列首部字段: Accept、Accept-Language、Content-Language、Content-Type
- text/plain
-
XMLHttpRequestUpload对象均没有注册任何事件监听器
-
请求中没有使用ReadableStream对象
-
预请求
-
端口二设置:
const http = require('http'); http.createServer(function (req, res) { res.writeHead(200,{ 'Access-Control-Allow-Origin':'*', 'Access-Control-Allow-Headers':'X-Test-Cors', 'Access-Control-Allow-Methods': 'POST,PUT,Delete', 'Access-Control-Max-Age': '10000' }) res.end('hello world'); }).listen(8066); console.log('服务器已开启,端口号:8066');
Access-Control-Allow-Headers: 设置允许的头部字段
Access-Control-Allow-Methods:设置允许的请求方法
Access-Control-Max-Age:设置生效时长
-
设置预请求后可以发现多了一个请求,这个多出来的就是预请求
-
设置预请求后再运行就不会报错了,因为允许使用X-Test-Cors这个自定义头部字段了
-
4. 缓存头Cache-Control的含义和使用
-
特性
- 可缓存性:
public: 公有,也就是都可以对返回内容的缓存的操作
private: 私有,代表只有发起请求的浏览器才可以进行缓存
no-cache: 任何一个节点都不可以直接进行缓存, 经过验证后才可以使用- 到期
max-age = : 多少秒以后过期,再重新请求
s-maxage = : 会代替max-age, 但是只有代理服务器里面才会生效,既浏览器端还是会读取max-age的时间
max-stale=: 如果返回过期之后,但是只要在max-stale的时间内,他还是可以使用缓存,而不需要去原服务器里面请求一个新的内容,这个设置只有发起端才有用- 重新验证(不常用)
must-revalidate: 在设置max-age,重新发送请求的时候,再重新验证这个内容是否已经真的过期了
proxy-revalidate: 跟must一样的意思,但是是在代理服务器上用的- 其他
no-store: 本地和代理服务器都不可以使用缓存,彻底的不能用
no-transform: 主要用在proxy服务器那边,意思是不要随便改动返回的内容- 注意:这些头只是声明性的作用,没有强制约束力
-
例子:
const http = require('http'); const fs = require('fs'); http.createServer(function(req,res){ if(req.url === '/'){ const html = fs.readFileSync('test.html','utf8') res.writeHead(200,{ 'Content-Type':'Text/html' }) res.end(html); } if(req.url === '/script.js'){ res.writeHead(200,{ 'Content-Type':'text/javascript', 'Cache-Control':'max-age=200, public' }) res.end("console.log('script loaded')"); } }).listen(8088); console.log('服务器已开启,端口号:8088');
- 页面代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="/script.js"></script> </body> </html>
如果设置了缓存那么会有一个问题,客户端缓存之后要是服务器更新了文件。客户端显示的还会是更新前的内容
要想解决这个问题一般会使用hash码,这个码是根据文件内容生成的码,如果内容不变,那么hash码也不会变。这样就可以验证服务器端有没有更新文件
5. 缓存验证Last-Modified和Etag的使用
-
Last-Modified
配合If-Modified-Since或If-Unmodified-Since使用
过程:客户端请求资源时服务器端会返回Last-Modified头,上面指定了一个时间。客户端下次再请求时会带上这个头,通过If-Modified-Since或If-Unmodified-Since,通常为If-Modified-Since,验证资源是否更新
-
Etag
更严格的验证,使用数据签名。资源的内容会生成一个唯一的签名,如果资源的内容更改了那么签名也会变。游览器下次发请求会带上这个签名和服务器端的签名做对比,判断是否要更新缓存
配合If-Match或If-None-Match使用
-
例子:
const http = require('http'); const fs = require('fs'); http.createServer(function(req,res){ if(req.url === '/'){ const html = fs.readFileSync('test.html','utf8') res.writeHead(200,{ 'Content-Type':'Text/html' }) res.end(html); } if(req.url === '/script.js'){ const etag = req.headers['if-none-match'] if(etag === '666'){ res.writeHead(304,{ 'Content-Type':'text/javascript', 'Cache-Control':'max-age=20000000, no-cache', 'Last-Modified':'hello', 'Etag':'666' }) res.end('') }else{ res.writeHead(200,{ 'Content-Type':'text/javascript', 'Cache-Control':'max-age=20000000, no-cache', 'Last-Modified':'hello', 'Etag':'666' }) res.end("console.log('script loaded')"); } } }).listen(8088); console.log('服务器已开启,端口号:8088');
-
页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="/script.js"></script> </body> </html>
-
6. cookie和session
-
cookie
-
通过Set-Cookie设置的内容就叫cookie,游览器下次请求会自动携带cookie
-
cookie是以键值对的形式设置,可以设置多个.
-
cookie是有时效的,默认关闭就删除。可以设置max-age来设置有效时间
-
cookie的属性
- max-age和expires:设置过期时间
- secure:只在https的时候发送
- HttpOnly:无法通过document.cookie访问(防止被攻击)
-
使用案例:
const http = require('http'); const fs = require('fs'); http.createServer(function(req,res){ const html = fs.readFileSync('text.html','utf8') res.writeHead(200,{ 'Content-Type':'text/html', 'Set-Cookie':['id=123;HttpOnly','user=admin','password=6666;max-age=20'] }) res.end(html); }).listen(8088); console.log('服务器已开启,端口号:8088');
-
7. HTTP长连接
-
长连接不会马上关闭,Connection:Keep-Alive代表是长连接
- Connection如果值为close,一个请求之后连接就会关闭,不会复用
-
例子:
const http = require('http'); const fs = require('fs'); http.createServer(function(req,res){ const html = fs.readFileSync('t1.html','utf8') const img = fs.readFileSync('1.jpg') if (req.url === '/'){ res.writeHead(200, { 'Content-Type': 'text/html' }) res.end(html); } else { res.writeHead(200, { 'Content-Type': 'image/jpg' }) res.end(img); } }).listen(8088); console.log('服务器已开启,端口号:8088');
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <img src="/1.jpg" alt=""> <img src="/2.jpg" alt=""> <img src="/3.jpg" alt=""> <img src="/4.jpg" alt=""> <img src="/5.jpg" alt=""> <img src="/6.jpg" alt=""> <img src="/7.jpg" alt=""> </body> </html>
8. 数据协商
-
数据协商概念:客户端给服务端发送一个请求的时,客户端会声明我想要的数据,数据格式以及数据相关的一些限制,服务端会根据请求做出一个判断,服务端可能会有很多不同类型的数据返回,可以根据头信息进行区分
-
请求声明Accept:
Accept: 声明要怎么样的数据,声明数据类型
Accept-Encoding: 代表数据是怎么样的编码方式
Accept-Language: 判断返回的信息是什么语言
User-Agent: 表示浏览器的一些相关的信息 -
服务端Content:
Content-Type: 对应Accept,Accept可以接收很多种数据格式,Content-Type会在里面选择一种数据格式返回,在返回的时候声明返回的数据格式
Content-Encoding: 对应Accept-Encoding,告诉客户端,用了什么样的压缩数据的方式
Content-Language: 根据请求返回对应的语言 -
例子:
const http = require('http'); const fs = require('fs'); http.createServer(function(req,res){ const html = fs.readFileSync('test.html', 'utf8'); res.writeHead(200,{ 'Content-Type': 'text/html', 'X-Content-Type-Options': 'nosniff' }) res.end(html); }).listen(8088); console.log('服务器已开启,端口号:8088');
-
压缩格式
const http = require('http'); const fs = require('fs'); const zlib = require('zlib') http.createServer(function(req,res){ const html = fs.readFileSync('test.html'); res.writeHead(200,{ 'Content-Type': 'text/html', 'Content-Encoding': 'gzip' }) res.end(zlib.gzipSync(html)); }).listen(8088); console.log('服务器已开启,端口号:8088');
9. Redirect
在发送一个请求时如果请求地址的内容地址变动了,这时服务器端应该告诉客户端新的地址在那
- 例子:
const http = require('http');
const fs = require('fs');
http.createServer(function (req, res) {
if(req.url === '/'){
res.writeHead(302, {
'Location': '/new'
})
res.end('');
}
if(req.url === '/new'){
res.writeHead(200, {
'Content-Type': 'Text/html'
})
res.end('this is content');
}
}).listen(8088);
console.log('服务器已开启,端口号:8088');
这样访问http://127.0.0.1:8088/时,会自动跳转到/new
302是临时跳转,301是永久跳转
301要慎重使用,它
10. CSP
CSP(Content-Security-Policy): 内容安全策略:限制资源获取和报告资源获取越权
限制方式:
default-src限制全局 跟链接请求有关的东西,限制他的作用范围
制定资源类型
content-src img-src style-src script-src frame-src font-src media-src manifest-src
-
例子:
const http = require('http'); const fs = require('fs'); http.createServer(function (req, res) { if(req.url === '/'){ const html = fs.readFileSync('csp.html', 'utf8') res.writeHead(200, { 'Content-Type': 'Text/html', 'Content-Security-Policy': 'script-src \'self\' }) res.end(html); }else{ res.writeHead(200, { 'Content-Type': 'appliction/javascript', }) res.end('loaded script'); } }).listen(8088); console.log('服务器已开启,端口号:8088');
只能用某个网站域名下的js
'Content-Security-Policy': 'default-src \'self\' http://baidu.js'
只现在script,不限制连接
'Content-Security-Policy': 'script-src \'self\'
限制表单提交
'Content-Security-Policy': 'form-action \'self\''
可以让游览器主动发送请求汇报
'Content-Security-Policy': 'script-src \'self\'; report-uri /report';
可以通过meta标签在页面中直接设置,效果一样
<meta http-equiv='Content-Security-Policy' content='script-src "self"; form-action "self"; report-uri /report';>
三. Nginx代理以及面向未来的HTTP
1. NGINX安装和基础代理配置
-
是一个纯粹的HTTP的服务,可以一台机器配置好几个服务
-
下载地址: http://nginx.org/en/download.html
-
运行cmd进入安装目录,在控制台nginx.exe来开启
-
配置
server { listen 80; server_name test.com; location / { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; } }
2. Nginx代理配置和代理缓存的用处
-
配置缓存
proxy_cache_path cache levels=1:2 keys_zone=my_cache:10m; server{ listen 80; server_name test.com; location/{ proxy_cache my_cache; proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; } }
const http = require('http'); const fs = require('fs'); const wait = (seconds) =>{ return new Promise((resolve, reject) =>{ setTimeout(()=>{ resolve() },seconds * 1000) }) } http.createServer(function (req, res) { if (req.url === '/') { const html = fs.readFileSync('cache.html', 'utf8') res.writeHead(200, { 'Content-Type': 'Text/html' }) res.end(html); } if(req.url === '/data') { res.writeHead(200, { 'Cache-Control': 'max-age=2, s-maxage=20, private' }) wait(2).then(()=> res.end('success')) } }).listen(8088); console.log('服务器已开启,端口号:8088');
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>This is content, and data is:<span id="data"></span></h1> <script> fetch('/data').then(function(resp){ return resp.text() }).then(function(text){ document.getElementById('data').innerText = text }) </script> </body> </html>
3. HTTPS解析
- 加密
- 私钥:只放在服务器上
- 公钥:有私钥才能解密
4. 使用Nginx部署HTTPS服务
proxy_cache_path cache levels=1:2 keys_zone=my_cache:10m;
server{
listen 80 default_server;
listen [::]:80 default_server;
server_name test.com;
return 302 http://$server_name$request_uri;
}
server{
listen 443;
server_name test.com;
ssl on;
ssl_certificate_key ../certs/localhost-privkey.pem;
ssl_certificate ../certs/localhost-cert.pem;
location/{
proxy_cache my_cache;
proxy_pass http://127.0.0.1:8088;
proxy_set_header Host $host;
}
}
5. HTTP2的优势和Nginx配置HTTP2的简单使用
优势:
- 信道复用
- 分帧传输
- Server Push
proxy_cache_path cache levels=1:2 keys_zone=my_cache:10m;
server{
listen 80 default_server;
listen [::]:80 default_server;
server_name test.com;
return 302 http://$server_name$request_uri;
}
server{
listen 443 http2;
server_name test.com;
http2_push_preload on;
ssl on;
ssl_certificate_key ../certs/localhost-privkey.pem;
ssl_certificate ../certs/localhost-cert.pem;
location/{
proxy_cache my_cache;
proxy_pass http://127.0.0.1:8088;
proxy_set_header Host $host;
}
}