摘要:报文主体并不是一定要有的。缓存缓存作用减少了冗余的数据传输,节省了网费。当资源发生改变时,也随之发生变化。本人水平有限,有不足之处,望大家指出改正。
前言
或许你在面试时遇到过这样的问题:从输入URL到浏览器显示页面发生了什么?
简单的回答就是:
DNS解析
TCP建立连接
发送HTTP请求
服务器处理请求
如果有缓存直接读缓存
没有缓存返回响应内容
TCP断开连接
浏览器解析渲染页面
如果你觉得这样回答过于简单,不如来深入了解一下吧。
网络基础在此之前,先了解一下TCP/IP基础知识。
TCP/IP参考模型
早期的TCP/IP模型是一个四层结构,从下往上依次是网络接口层、互联网层、传输层和应用层,后来将网络接口层划分为了物理层和数据链路层
应用层(Application)提供网络与用户应用软件之间的接口服务
传输层(Transimission)提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
传输层有两个性质不同的协议:TCP(传输控制协议)和UDP(用户数据报协议)
网络层(Network)处理网络间路由,确保数据及时传送(路由器)
数据链路层(DataLink)负责无错传输数据,确认帧、发错重传等(交换机)
物理层(Physics)提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)
各层常用协议这里可以看到HTTP协议是构建于TCP之上,属于应用层协议。
具体过程 1. DNS解析DNS服务是和HTTP协议一样位于应用层的协议,提供域名到IP地址的解析服务。
得到IP地址后就可以建立连接了,这里还有两个知识需要了解:
持久连接
持久连接(也称为HTTP keep-alive)的特点是,只要任意一段没有提出断开连接,就保持TCP连接状态。
管线化
持久连接建立后就可以使用管线化发送了,可以同时并发多个请求,不用等待一个接一个的响应。(在这里我想到了流的pipe方法。)
大致说一下:
计算机通过端口号识别访问哪个服务,比如http;源端口号进行随机端口,目的端口决定哪个程序进行接收
数据序号和确认序号用于保障传输数据的完整性和顺序
需要注意的是TCP的连接、传输和断开都受六个控制位的指挥(比如三次握手和四次挥手)
PSH(push急迫位)缓存区将满,立刻速度传输
RST(reset重置位)连接断了重新连接
URG(urgent紧急位)紧急信号
ACK(acknowlegement确认)为1就表示确认号
SYN(synchronous建立联机)同步序号位 TCP建立连接时将这个值设为1
用户数据存储了应用层生成的HTTP报文
了解了这些,那么开始讲重点
2.2 TCP三次握手和四次挥手三次握手
客户端先发送一个带SYN标志的数据包给服务器端
服务器收到后,回传一个带有SYN/ACK标志的数据包表示确认收到
客户端再发送一个带SYN/ACK标志的数据包,代表握手结束
四次挥手
客户端向服务器发出了FIN报文段
服务器收到后,回复一个ACK应答
服务器也向客户端发送一个FIN报文段,随后关闭了服务器端的连接
客户端收到之后,又向服务器回复一个ACK应答,过了一段计时等待,客户端也关闭了连接(计时等待是为了确认服务器端已正常关闭)
四次挥手并不是必然的,当服务器已经没有内容发给客户端了,就直接发送FIN报文段,这样就变成了三次挥手。3. HTTP请求/响应 3.1 HTTP报文
HTTP报文大致可分为报文首部和报文主体两块,两者由空行(就相当于用了两个换行符rnrn)来划分。报文主体并不是一定要有的。
3.1.1 请求报文常用请求行方法:
GET 获取资源
POST 向服务器端发送数据,传输实体主体
PUT 传输文件
HEAD 获取报文首部
DELETE 删除文件
OPTIONS 询问支持的方法
TRACE 追踪路径
3.1.2 响应报文说到响应报文,就必要谈到状态码:
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) (通常缓存会返回304状态码)
4XX 客户端错误
400(Bad Request) 请求报文语法错误
401 (unauthorized) 需要认证
403(Forbidden) 服务器拒绝访问对应的资源
404(Not Found) 服务器上无法找到资源
5XX 服务器端错误
500(Internal Server Error) 服务器故障
503(Service Unavailable) 服务器处于超负载或正在停机维护
3.1.3 首部通用首部
首部字段名 | 说明 |
---|---|
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 | 资源的最后修改时间 |
创建HTTP服务端
let http = require("http"); let app = http.createServer((req, res) => {// req是可读流/res是可写流 // 获取请求报文信息 let method = req.method;// 方法 let httpVersion = req.httpVersion;// HTTP版本 let url = req.url; let headers = req.headers; console.log(method, httpVersion, url, headers); // 获取请求体(如果请求体的数据大于64k,data事件会被触发多次) let buffers = []; req.on("data", data => { buffers.push(data); }) req.on("end", () => { console.log(Buffer.concat(buffers).toString()); res.write("hello"); res.end("world"); }) }) // 监听服务器事件 app.on("connection", socket => { console.log("建立连接"); }); app.on("close", () => { console.log("服务器关闭") }); app.on("error", err => { console.log(err); }); app.listen(3000, () => { console.log("server is starting on port 3000"); });
创建客户端
let http = require("http"); let options = { hostname: "localhost", port: 3000, path: "/", method: "GET", // 设置实体首部 告诉服务端我当前要给你发什么样的数据 headers: { "content-Type": "application/x-www-form-urlencoded", "Content-Length": 15 } } let req = http.request(options); req.on("response", res => { res.on("data", chunk => { console.log(chunk.toString()); }); }); req.end("name=js&&age=22")
然后使用node运行我们的客户端
说了这么多,你可能已经大致了解了
从输入URL到浏览器显示页面发生了什么,不用多说,我们再来看一下缓存。
减少了冗余的数据传输,节省了网费。
减少了服务器的负担, 大大提高了网站的性能
加快了客户端加载网页的速度
4.2 缓存分类强制缓存:说白了就是第一次请求数据时,服务端将数据和缓存规则一并返回,下一次请求时浏览器直接根据缓存规则进行判断,有就直接读缓存数据库,不用连接服务器;没有,再去找服务器。
对比缓存,顾名思义,需要进行比较判断是否可以使用缓存。
浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知* 客户端比较成功,可以使用缓存数据。
4.3 请求流程从上张图我们可以看到,判断缓存是否可用,有两种方式
ETag是实体标签的缩写,根据实体内容生成的一段hash字符串,可以标识资源的状态。当资源发生改变时,ETag也随之发生变化。ETag是Web服务端产生的,然后发给浏览器客户端。
Last-Modified是此资源的最后修改时间,
如果客户端在请求到的资源中发现实体首部里有Last-Modified声明,再次请求就会在头里带上if-Modified-Since字段
服务端收到请求后发现if-Modified-Since字段则与被请求资源的最后修改时间进行对比
说了这么多,不如直接来实现一下缓存
通过最后修改时间来判断缓存是否可用
let http = require("http"); let url = require("url"); let path = require("path"); let fs = require("fs"); let mime = require("mime"); let app = http.createServer((req, res) => { // 根据url获取客户端要请求的文件路径 let { parsename } = url.parse(req.url); let p = path.join(__dirname, "public", "." + pathname); // fs.stat()用来读取文件信息,文件最后修改时间就是stat.ctime fs.stat(p, (err, stat) => { 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 { 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); } app.listen(3000, () => { console.log("server is starting on port 3000"); });
最后修改时间存在问题:
1. 某些服务器不能精确得到文件的最后修改时间, 这样就无法通过最后修改时间来判断文件是否更新了。
2. 某些文件的修改非常频繁,在秒以下的时间内进行修改. Last-Modified只能精确到秒。
3. 一些文件的最后修改时间改变了,但是内容并未改变。 我们不希望客户端认为这个文件修改了。
4. 如果同样的一个文件位于多个CDN服务器上的时候内容虽然一样,修改时间不一样。
通过ETag来判断缓存是否可用
ETag就是根据文件内容来判断,说白了就是采用MD5(md5并不叫加密算法,它不可逆,应该叫摘要算法)产生信息摘要,用摘要来进行比对。
let http = require("http"); let url = require("url"); let path = require("path"); let fs = require("fs"); let mime = require("mime"); // crypto是node.js中实现加密和解密的模块 具体详解请自行了解 let crypto = require("crypto"); let app = http.createServer((req, res) => { // 根据url获取客户端要请求的文件路径 let { parsename } = url.parse(req.url); let p = path.join(__dirname, "public", "." + pathname); // fs.stat()用来读取文件信息,文件最后修改时间就是stat.ctime fs.stat(p, (err, stat) => { let md5 = crypto.createHash("md5");//创建md5对象 let rs = fs.createReadStream(p); rs.on("data", function (data) { md5.update(data); }); rs.on("end", () => { let r = md5.digest("hex"); // 对文件进行md5加密 // 下次就拿最新文件的加密值 和客户端请求来比较 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, stat) { res.setHeader("Cache-Control", "no-cache");// 设置通用首部字段 控制缓存行为 res.setHeader("Etag", r);// 响应首部字段 资源的匹配信息 res.setHeader("Content-Type", mime.getType(p) + ";charset=utf8") fs.createReadStream(p).pipe(res); } app.listen(3000, () => { console.log("server is starting on port 3000"); });最后
想深入学习http的同学,我推荐一本书《图解HTTP》。
本人水平有限,有不足之处,望大家指出改正。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/94579.html
摘要:先介绍一下本人应届前端开发一枚,非科班出身,专业是化学,大学期间开始自学前端开发,在今年春招实习和秋招的时候投了一些公司,拿到一些京东拼多多虎牙等,总体来说还算满意,特地写一篇文章来总结一下面试的那些套路。 showImg(https://segmentfault.com/img/remote/1460000011897700); 先介绍一下本人应届前端开发一枚,非科班出身,专业是化学...
摘要:先介绍一下本人应届前端开发一枚,非科班出身,专业是化学,大学期间开始自学前端开发,在今年春招实习和秋招的时候投了一些公司,拿到一些京东拼多多虎牙等,总体来说还算满意,特地写一篇文章来总结一下面试的那些套路。 showImg(https://segmentfault.com/img/remote/1460000011897700); 先介绍一下本人应届前端开发一枚,非科班出身,专业是化学...
摘要:先介绍一下本人应届前端开发一枚,非科班出身,专业是化学,大学期间开始自学前端开发,在今年春招实习和秋招的时候投了一些公司,拿到一些京东拼多多虎牙等,总体来说还算满意,特地写一篇文章来总结一下面试的那些套路。 showImg(https://segmentfault.com/img/remote/1460000011897700); 先介绍一下本人应届前端开发一枚,非科班出身,专业是化学...
摘要:扩展阅读收集的前端面试题和答案前端开发面试题史上最全的前端面试题汇总及答案前端工程师手册协议工作原理协议运行机制的概述 本书的 GitHub 地址:https://github.com/todayqq/PH... 对于大公司,很少会有全栈工程师这个岗位,全栈是个花哨的词,对于现在比较热门的技术,不论是 Vue 还是 Laravel,只要智商不差,看着文档,都能写出一个 CURD 来,...
摘要:前端篇收集的前端面试题和答案前端开发面试题史上最全的前端面试题汇总及答案前端工程师手册协议工作原理协议运行机制的概述协议篇原理原理解析的工作原理与的区别理解后端篇年的面试总结垃圾回收机制面向对象设计浅谈说清楚是什么和的区别索引原理及慢查 前端篇 收集的前端面试题和答案 前端开发面试题 史上最全的web前端面试题汇总及答案 前端工程师手册 HTTP协议:工作原理 SSL/TLS协议运行...
摘要:地址每次面试多多少少都会被问到等等之类协议,协议相关的问题也可以说是面试必备,所以我把这些知识单独收集成了一篇文章。即标志位和标志位均为。发送完毕后,服务器端进入状态。认证服务器对客户端进行认证以后,确认无误,同意发放令牌。 Git 地址:https://github.com/todayqq/PH... 每次面试多多少少都会被问到 HTTP、HTTPS、TCP、Socket、 OAu...
阅读 3436·2021-09-02 09:53
阅读 1754·2021-08-26 14:13
阅读 2688·2019-08-30 15:44
阅读 1275·2019-08-30 14:03
阅读 1918·2019-08-26 13:42
阅读 2981·2019-08-26 12:21
阅读 1277·2019-08-26 11:54
阅读 1879·2019-08-26 10:46