通用问题
网络分层模型
TCP/IP 四层模型:应用、传输、网络、网络接口;
TCP/IP 五层模型(主流):应用、传输、网络、数据链路、物理;
OSI 七层模型:
- 应用:http、ftp
- 表示:telnet
- 会话:dns
- 传输:udp、tcp
- 网络:ip
- 数据链路:以太网协议
- 物理;各种物理连接设备
TCP
TCP 是传输可靠、有连接、全双工的传输层协议;
三次握手:
- C to S:SYN=1,seq=x;
- S to C:SYN=1,ACK=1,seq=y,ack=x+1;
- C to S:ACK=1,seq=x+1,ack=y+1;
四次挥手:
- C to S:FIN=1,seq=u;
- S to C:ACK=1,seq=v,ack=u+1,服务器继续发送旧数据;
- S to C:FIN=1,ACK=1,seq=w,ack=u+1,服务器停止发送任何数据;
- C to S:ACK=1,seq=u+1,ack=w+1;
第四次挥手后,客户端等待 2MSL 后再断开连接,否则如果服务器没有收到第四次挥手报文,将重复发送第三次挥手报文,由于此时客户端已经断开,服务器永远无法收到第四次挥手报文,将导致后续连接和数据包的混乱;
为什么是三次握手?
确保双方同处于一个上下文中,使得信息同步,进而保证连接可靠性;
如果是一次握手,显然发起方不能确认连接是否成功建立;
如果是两次握手,接收方将无法确认发起方的第一次握手是否还有效,如果发起方第一次握手后放弃建立连接(非断开连接),客户端将无法感知到。
为什么是四次挥手?
相比三次握手,四次挥手多的是第二次挥手,这是因为第二次挥手是为了通知客户端还有旧数据要传输,传输完成后才可以说双方数据均以传输完毕,可以断开连接。
TCP 确保传输可靠的机制
- 序列号和确认号;
- 超时重传;
- 流量控制;
- 拥塞控制;
拥塞控制
滑动窗口的大小使用慢启动动态改变,一旦发生丢包或超时则启动快速重传或快速恢复;
飞包守恒原则:总是收到一个旧包的 ack 确认包才会发送新包;
慢启动:
- 维护一个拥塞窗口大小,初始为指定倍 MSS(最大报文长度,双方协商的值),发送窗口大小取拥塞窗口与接受窗口的较小者,每 1RTT 拥塞窗口翻倍(每收到一个 ack,大小加一);
- 维护一个慢启动阈值,初始为
2^16 - 1
,一旦拥塞窗口超过该值,启动拥塞避免算法; - 随着拥塞窗口指数级上升,到达阈值后变为线性上升,而一旦接收到三个相同 ack,就会启动快速重传和快速恢复;
超时重传:指定时间内没有收到 ack 确认包,重发超时的包;
冗余确认:发送的多个包,收到的却是相同的 ack 确认包(三个相同包),这时可以认定发生了丢包;
快速重传:确认丢包(冗余确认)后不必等待超时,立即重发丢失的包,之后启动快速恢复;
快速恢复:丢包后,先把阈值置为拥塞窗口的一半,继续发送数据,过程中使拥塞窗口尽量平滑地降低到阈值附近,然后退出快速恢复状态;
拥塞避免:拥塞窗口大小累加 1MSS,开始线性增长。
UDP
什么是 UDP?
UDP 是一种尽力交付但不保证交付、无连接、面向报文的传输层协议,支持一对一,一对多,多对多的通信;
不可靠是因为不具有 TCP 的拥塞控制、流量控制等机制;
面向报文是指 UDP 不会拆分、组合应用层的报文,加上 UDP 首部之后就会直接发送;
IP
IP 协议是无连接、不可靠的网络层协议,用于将数据从源地址发送到目的地址;
IP 地址是一个 32 位的二进制数,由于 MAC 地址称为物理地址,所以 IP 地址也称逻辑地址,IP 地址解决了网络中如何将数据传输到指定端的问题;
IP 协议首部举例:
- 源地址;
- 目的地址;
- TTL:每经过一个设备就减一,为零时必须丢弃,避免找不到目的地址的 IP 数据包在网络中无限传播;
- IP 协议版本;
- 数据协议;
- 片偏移:数据包切分后用于标识偏移量;
DNS
DNS 是负责域名解析的会话层协议,由于 IP 地址不好记忆,所以可以注册一个域名来代替该 IP 地址,但由于 IP 协议不能直接识别域名,所以需要 DNS 协议从域名服务器获取到域名对应的 IP 地址;
DNS 解析过程:先查询本地 DNS 服务器的缓存,本地 DNS 服务器如果没有记录,则依次查询根、顶级、二级域名服务器,直到查询到 IP 地址;
SSL/TLS
第一阶段:
- Client Hello(SSL version,加密套件,session id 等);
- Server Hello(SSL version,加密套件,session id 等);
第二阶段:
- Server Certificate;
- Server Hello Done;
第三阶段:
- Client:Client Key Exchange(Client 端的 pre-master-secret);
第四阶段:
- Client:Change Cipher Spec;
- Client:Finished;
- Server:Change Cipher Spec;
- Server:Finished;
服务器证书由权威 CA 机构签发,其真实性如何保证?如下:
- 服务器提供数据(如服务器端生成的公钥),CA 机构使用自己的私钥为数据签名,生成数字证书,颁发给服务器;
- 浏览器和操作系统内置了各大权威 CA 机构的根证书,证书内就是 CA 机构的公钥,用于验证服务器证书的签名与内容是否一致(验证 HASH 值);
以上两点加粗内容就是整个证书互信机制的核心,可见一切都建立在 CA 机构私钥绝对安全,以及浏览器、操作系统根证书不被篡改的基础上,一旦这两点不能保证,则整套机制失去意义。
注 1:SSL 建立连接过程此处为了简便,默认两点,一是使用 RSA 算法,二是由服务器单方提供证书,而非双方都提供证书(类似于银行和“U 盾”的场景);
注 2:私钥加签,公钥验签;公钥加密,私钥解密;
HTTP
常见状态码
2xx:
- 200 OK
- 206 Partial Content
3xx:
- 301 Moved Permanently
- 304 Not Modified
- 307 Temporary Redirect,临时重定向,但新请求不能改变请求方法;
- 308 Permanent Redirect,永久重定向,但新请求不能改变请求方法;
4xx:
- 400 Bad Request,请求的语法有误;
- 403 Forbidden,服务器拒绝响应;
- 404 Not Found,请求的资源不存在;
- 405 Method Not Allowed,请求方法不合法;
5xx:
- 500 Internal Server Error
- 503 Service Unavailable,服务器宕机;
浏览器缓存机制
强缓存:由 Cache-Control、Expires 控制的缓存策略;
Cache-Control 相关指令:
- 如果不希望复用 Response,使用 no-store;
- 如果希望复用 Response
- 希望每次都验证资源有效性,使用 no-cache;
- 不希望每次都验证,不用 no-store 或 no-cache
- 允许中间服务器缓存,使用 public;
- (默认行为)不允许中间服务器缓存,使用 private;
- 设置缓存有效时间,使用 max-age/s-max-age 指令,优先级高于 Expires 首部;
强缓存未命中的情况下,还有协商缓存策略:发送一条带有 if-none-match/if-modified-since 首部的条件请求到源服务器,服务器验证资源有效性,有效则返回状态码 304 Not Modified,失效则返回新资源,状态码 200 OK;
注 1:s-max-age 可覆盖 max-age 和 Expires,只能用于中间服务器缓存的时效控制;
注 2:其他时效控制指令还有 max-stale/min-fresh 等;
注 3:private 指令下,一般由浏览器本地缓存;
注 4:if-match 验证的是资源的 ETag,if-modified-since 验证的是资源的最近修改时间;
CORS
Cross-Origin Resource Sharing(跨域资源共享),包含一组扩展的 HTTP 首部,跨域的简单请求
将直接发送,非简单请求
将发送一个 method 为 options 的 preflight 请求;
请求首部:
- Access-Control-Request-Method
- Access-Control-Request-Headers
- Origin
响应首部:
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Expose-Headers
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Max-Age
简单请求:
- Method 为 Get/Post/Head;
- 仅包含以下首部:
- Other Auto-seted by user-agent headers(Connection/User-Agent);
- Content-Type 仅包含:
- application/x-www-form-urlencoded;
- text/plain;
- multipart/form-data;
- Accept;
- Accept-Language;
- Content-Language;
- Content-Type;
- DPR;
- Downlink;
- Save-Data;
- Viewport-Width;
- Width;
注:简单、非简单请求为过时的称呼,现行规范中不再使用;
流程:
- 浏览器判断请求为:
- 简单请求,直接发送该请求;
- 非简单请求,则
- 发送 preflight 请求;
- 服务器响应该 preflight 请求;
- 发送真实请求;
简单请求只需要通过 Origin 和 Access-Control-Allow-Origin 就可以控制跨域资源的访问;
非简单请求还需要通过*-Request-Method\Headers 和*-Request-Methods\Headers 来控制跨域 HTTP 报文允许哪些 Method 和 Header,*-Max-Age 指定一段时间内无需再次发送 preflight 请求;
跨域访问限制与解决方案
通信双方的协议、域名和端口都相同,则称之为同源,否则就是不同源,不同源之间的通信称为跨域访问(跨源访问),浏览器对跨域访问有限制,为了解决该问题,有以下方案:
- CORS
- Websocket:websocket 不受跨域限制;
- Jsonp:提前定义好回调函数,构建 script 元素,将跨域请求 URL 写入 src,插入 DOM,由于静态资源加载允许跨域,所以可以发送跨域请求,服务器在返回的 js 片段中编写回调函数的调用代码,并将服务器端的数据以回调函数参数的形式拼接进去。
- 正向代理、反向代理:确保客户端与代理服务器不跨域,由代理服务器与目标服务器通信,服务器之间通信自然不受浏览器的规则限制;
- postMessage & onmessage:用于不同源 tab 或 iframe 之间跨域通信,postMessage 发送数据,onmessage 监听数据接收事件;
注:
- 正向代理:向指定的客户端负责,任何这些客户端的请求,都通过该代理服务器代为发送;
- 反向代理:向指定的服务端负责,任何这些服务端的响应,都通过该代理服务器代为发送;
Cookie
HTTP 扩展首部:
- set-cookie:服务器在 http 响应报文中通过该首部通知浏览器设置 cookie 的值;
- cookie:浏览器在 http 请求报文中通过该首部将 cookie 发送到服务器;
- 形式上如 key=value,多个键值对以分号
;
分隔;
cookie 指令:
- 时效:
Expires=<date>
:过期时间,默认为会话结束后过期;Max-age=<seconds>
:有效时间,小于等于 0 将直接失效,优先级高于 expires,兼容性差于 expires;
- 目标:
Domain=<domain>
:指定访问哪些域需要携带 cookie;Path=<path>
:指定访问哪些资源路径(含所有子路径)需要携带 cookie;
- 安全:
Secure
:只允许通过 https 携带 cookie;HttpOnly
:禁止 js 读写 cookie;SameSite=<option>
:是否允许以第三方 cookie 形式发送,可选项如下:strict
:完全禁止;lax
:大部分禁止,只允许 Get 请求携带(超链接、Get 表单、资源预加载);none
:不禁止;
注:浏览器禁止 cookie 跨域读写,但允许跨域请求,即 B 不能读写 A 的 cookie,但 B 对 A 可发送请求,这个请求可以携带 A 的 cookie,由于 B 页面和用户是第一二方,A 属于第三方,A 的 cookie 也就是第三方 cookie;
HTTP 1.1
- 长连接:通过
Connection: keep-alive
维持连接状态,避免 1.0 中每次发送、接收报文都要重新建立连接; - 管道化:请求可以连续发送,而不必等待响应,解决了客户端队首阻塞问题,但服务器仍存在队首阻塞问题,因为 1.1 要求服务器必须按顺序响应请求;
HTTP 2.0
2.0 引入了介于 HTTP 与 TCP 之间的二进制分帧层,用于将 HTTP 报文分割成帧,基于该机制实现了以下特性:
- 多路复用:由于每个流的帧可以错序发送,所以一个连接上可以存在多个流,接收端根据 Stream Id 将帧组合成完整报文;
- 头部压缩:根据 HPACK 算法压缩首部;
- 服务端推送:服务端可以不必等待客户端的请求,而直接推送数据;
- 请求优先级:每个流可以标识优先级,以便具有依赖关系的请求可以正确按依赖顺序发送和接收;
- 流量控制:类似于 TCP 流量控制,将双方接收速率控制在一个合理的程度,避免资源浪费;
HPACK 算法:通过静态或动态字典将首部替换成字节更少的索引,字典中不存在的首部使用哈夫曼编码发送;
- 优先去静态字典中查找,内置了大量常用首部与取值;
- 然后去动态字典中查找,每个连接维护一个动态字典,相当于静态字典的补充;
- 都找不到则使用哈夫曼编码;