TCP / IP体系结构
从输入URL到页面展示过程
- 浏览器地址栏输入 URL 并回车
- 浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期
- DNS 解析 URL 对应的 IP
- 根据 IP 建立 TCP 连接(三次握手)
- 发送 http 请求
- 服务器处理请求,浏览器接受 HTTP 响应
- 浏览器解析并渲染页面
- 关闭 TCP 连接(四次握手)
DNS
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:
- 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。
- 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。
DNS如何工作的
当你在浏览器中想访问 www.google.com
时,会通过进行以下操作:
- 本地客户端向服务器发起请求查询 IP 地址
- 查看浏览器有没有该域名的 IP 缓存
- 查看操作系统有没有该域名的 IP 缓存
- 查看 Host 文件有没有该域名的解析配置
- 如果这时候还没得话,会通过直接去 DNS 根服务器查询,这一步查询会找出负责
com
这个一级域名的服务器 - 然后去该服务器查询
google.com
这个二级域名 - 接下来查询
www.google.com
这个三级域名的地址 - 返回给 DNS 客户端并缓存起来
我们通过一张图来看看它的查询过程吧👇
这张图很生动的展示了DNS在本地DNS服务器是如何查询的,一般向本地DNS服务器发送请求是递归查询的
本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程👇
递归查询和迭代查询
- 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。
- 迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。
所以一般而言,本地服务器查询是递归查询,而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程
DNS缓存
缓存也很好理解,在一个请求中,当某个DNS服务器收到一个DNS回答后,它能够回答中的信息缓存在本地存储器中。返回的资源记录中的 TTL 代表了该条记录的缓存的时间。
DNS实现负载平衡
它是如何实现负载均衡的呢?首先我们得清楚DNS 是可以用于在冗余的服务器上实现负载平衡。
原因: 这是因为一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应 多个服务器地址。
举个例子来说👇
- 当用户发起网站域名的 DNS 请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合
- 在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。
- 以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。
DNS 为什么使用 UDP 协议作为传输层协议?
DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议时造成的连接时延
- 为了得到一个域名的 IP 地址,往往会向多个域名服务器查询,如果使用 TCP 协议,那么每次请求都会存在连接时延,这样使 DNS 服务变得很慢。
- 大多数的地址查询请求,都是浏览器请求页面时发出的,这样会造成网页的等待时间过长。
总结
- DNS域名系统,是应用层协议,运行UDP协议之上,使用端口43。
- 查询过程,本地查询是递归查询,依次通过
浏览器缓存
—>>本地hosts文件
—>>本地DNS解析器
—>>本地DNS服务器
—>>其他域名服务器
请求。 接下来的过程就是迭代过程。 - 递归查询一般而言,发送一次请求就够,迭代过程需要用户发送多次请求。
在查找过程中,有以下优化点:
- DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种:
浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存
。 - 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。
数据传输
互联网中的数据是通过数据包来传输的。如果发送的数据很大,那么该数据就会被拆分为很多小数据包来传输。比如你现在听的音频数据,是拆分成一个个小的数据包来传输的,并不是一个大的文件一次传输过来的
IP:把数据包送达目的主机
数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称IP)标准。互联网上不同的在线设备都有唯一的地址,地址只是一个数字,这和大部分家庭收件地址类似,你只需要知道一个家庭的具体地址,就可以往这个地址发送包裹,这样物流系统就能把物品送到目的地
计算机的地址就称为IP地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
如果要想把一个数据包从主机A发送给主机B,那么在传输之前,数据包上会被附加上主机B的IP地址信息,这样在传输过程中才能正确寻址。额外地,数据包上还会附加上主机A本身的IP地址,有了这些信息主机B才可以回复信息给主机A。这些附加的信息会被装进一个叫IP头的数据结构里。IP头是IP数据包开头的信息,包含IP版本、源IP地址、目标IP地址、生存时间等信息
为了方便理解,我先把网络简单分为三层结构,如下图
下面我们一起来看下一个数据包从主机A到主机B的旅程:
- 上层将含有“极客时间”的数据包交给网络层;
- 网络层再将IP头附加到数据包上,组成新的 IP数据包,并交给底层;
- 底层通过物理网络将数据包传输给主机B;
- 数据包被传输到主机B的网络层,在这里主机B拆开数据包的IP头信息,并将拆开来的数据部分交给上层;
- 最终,含有“极客时间”信息的数据包就到达了主机B的上层了
地址解析协议 ARP
网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。 ARP 实现由 IP 地址得到 MAC 地址。
UDP:把数据包送达应用程序
IP是非常底层的协议,只负责把数据包传送到对方电脑,但是对方电脑并不知道把数据包交给哪个程序,是交给浏览器还是交给王者荣耀?因此,需要基于IP之上开发能和应用打交道的协议,最常见的是“用户数据包协议(User Datagram Protocol)”,简称UDP。
UDP中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号UDP就能把指定的数据包发送给指定的程序了,所以IP通过IP地址信息把数据包发送给指定的电脑,而UDP通过端口号把数据包分发给正确的程序。和IP头一样,端口号会被装进UDP头里面,UDP头再和原始数据包合并组成新的UDP数据包。UDP头中除了目的端口,还有源端口号等信息
为了支持UDP协议,我把前面的三层结构扩充为四层结构,在网络层和上层之间增加了传输层,如下图所示:
下面我们一起来看下一个数据包从主机A旅行到主机B的路线:
- 上层将含有“极客时间”的数据包交给传输层;
- 传输层会在数据包前面附加上UDP头,组成新的UDP数据包,再将新的UDP数据包交给网络层;
- 网络层再将IP头附加到数据包上,组成新的IP数据包,并交给底层;
- 数据包被传输到主机B的网络层,在这里主机B拆开IP头信息,并将拆开来的数据部分交给传输层;
- 在传输层,数据包中的UDP头会被拆开,并根据UDP中所提供的端口号,把数据部分交给上层的应用程序;
- 最终,含有“极客时间”信息的数据包就旅行到了主机B上层应用程序这里
在使用UDP发送数据时,有各种因素会导致数据包出错,虽然UDP可以校验数据是否正确,但是对于错误的数据包,UDP并不提供重发机制,只是丢弃当前的包,而且UDP在发送之后也无法知道是否能达到目的地。
虽说UDP不能保证数据可靠性,但是传输速度却非常快,所以UDP会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等
UDP详解
1.1 面向报文
UDP
是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是UDP
只是报文的搬运工,不会对报文进行任何拆分和拼接操作
具体来说
- 在发送端,应用层将数据传递给传输层的
UDP
协议,UDP
只会给数据增加一个UDP
头标识下是UDP
协议,然后就传递给网络层了 - 在接收端,网络层将数据传递给传输层,
UDP
只去除IP
报文头就传递给应用层,不会任何拼接操作
1.2 不可靠性
UDP
是无连接的,也就是说通信不需要建立和断开连接。UDP
也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的UDP
没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是TCP
1.3 高效
- 因为
UDP
没有TCP
那么复杂,需要保证数据不丢失且有序到达。所以UDP
的头部开销小,只有八字节,相比TCP
的至少二十字节要少得多,在传输数据报文时是很高效的
头部包含了以下几个数据
- 两个十六位的端口号,分别为源端口(可选字段)和目标端口 整个数据报文的长度
- 整个数据报文的检验和(
IPv4
可选 字段),该字段用于发现头部信息和数据中的错误
1.4 传输方式
UDP
不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能
TCP:把数据完整地送达应用程序
对于浏览器请求,或者邮件这类要求数据传输可靠性(reliability)的应用,如果使用UDP来传输会存在两个问题:
- 数据包在传输过程中容易丢失;
- 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而UDP协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件
基于这两个问题,我们引入TCP了。TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于UDP,TCP有下面两个特点:
- 对于数据包丢失的情况,TCP提供重传机制;
- TCP引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
和UDP头一样,TCP头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。
下面看看TCP下的单个数据包的传输流程
通过上图你应该可以了解一个数据包是如何通过TCP来传输的。TCP单个数据包的传输流程和UDP流程差不多,不同的地方在于,通过TCP头的信息保证了一块大的数据传输的完整性。
下面我们再看下完整的TCP连接过程,通过这个过程你可以明白TCP是如何保证重传机制和数据包的排序功能的。
从下图可以看出,一个完整的TCP连接的生命周期包括了“建立连接”“传输数据”和“断开连接”三个阶段。
- 首先,建立连接阶段。这个阶段是通过“三次握手”来建立客户端和服务器之间的连接。TCP 提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好两端之间的准备工作。所谓三次握手,是指在建立一个TCP连接时,客户端和服务器总共要发送三个数据包以确认连接的建立。
- 其次,传输数据阶段。在该阶段,接收端需要对每个数据包进行确认操作,也就是接收端在接收到数据包之后,需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照TCP头中的序号为其排序,从而保证组成完整的数据。
- 最后,断开连接阶段。数据传输完毕之后,就要终止连接了,涉及到最后一个阶段“四次挥手”来保证双方都能断开连接
到这里你应该就明白了,TCP为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。
TCP详解
2.1 头部
TCP
头部比UDP
头部复杂的多
对于
TCP
头部来说,以下几个字段是很重要的
Sequence number
,这个序号保证了TCP
传输的报文都是有序的,对端可以通过序号顺序的拼接报文Acknowledgement Number
,这个序号表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到Window Size
,窗口大小,表示还能接收多少字节的数据,用于流量控制
标识符
URG=1
:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。ACK=1
:该字段为一表示确认号字段有效。此外,TCP
还规定在连接建立后传送的所有报文段都必须把ACK
置为一PSH=1
:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。RST=1
:该字段为一表示当前TCP
连接出现严重问题,可能需要重新建立TCP
连接,也可以用于拒绝非法的报文段和拒绝连接请求。SYN=1
:当SYN=1
,ACK=0
时,表示当前报文段是一个连接请求报文。当SYN=1
,ACK=1
时,表示当前报文段是一个同意建立连接的应答报文。FIN=1
:该字段为一表示此报文段是一个释放连接的请求报文
2.2 状态机
HTTP
是无连接的,所以作为下层的TCP
协议也是无连接的,虽然看似TCP
将两端连接了起来,但是其实只是两端共同维护了一个状态
TCP
的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。- 在这之前需要了解一个重要的性能指标 RTT。该指标表示发送端发送数据到接收到对端数据所需的往返时间
建立连接三次握手
- 在
TCP
协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP
连接建立完后都能发送和接收数据,所以TCP
也是一个全双工的协议。 - 起初,两端都为
CLOSED
状态。在通信开始前,双方都会创建TCB
。 服务器创建完TCB
后遍进入LISTEN
状态,此时开始等待客户端发送数据
第一次握手
客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。
第二次握手
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入
SYN-RECEIVED
状态。
第三次握手
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入
ESTABLISHED
状态,服务端收到这个应答后也进入ESTABLISHED
状态,此时连接建立成功。
- PS:第三次握手可以包含数据,通过
TCP
快速打开(TFO
)技术。其实只要涉及到握手的协议,都可以使用类似TFO
的方式,客户端和服务端存储相同cookie
,下次握手时发出cookie
达到减少RTT
的目的
你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?
- 因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误
可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入
ESTABLISHED
状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费
PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求
断开链接四次握手
TCP
是全双工的,在断开连接时两端都需要发送FIN
和ACK
。
第一次握手
若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
第二次握手
B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。
第三次握手
B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。
第四次握手
- A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。
为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?
- 为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭
总结
区别 | UDP | TCP |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输 |
URI
URI 编码
URI 只能使用ASCII
, ASCII 之外的字符是不支持显示的,而且还有一部分符号是界定符,如果不加以处理就会导致解析出错。
因此,URI 引入了编码
机制,将所有非 ASCII 码字符和界定符转为十六进制字节值,然后在前面加个%
。
如,空格被转义成了%20
,三元被转义成了%E4%B8%89%E5%85%83
HTTP
HTTP报文的组成部分
HTTP 首部
通用字段 | 作用 |
---|---|
Cache-Control | 控制缓存的行为 |
Connection | 浏览器想要优先使用的连接类型,比如 keep-alive |
Date | 创建报文时间 |
Pragma | 报文指令 |
Via | 代理服务器相关信息 |
Transfer-Encoding | 传输编码方式 |
Upgrade | 要求客户端升级协议 |
Warning | 在内容中可能存在错误 |
请求字段 | 作用 |
---|---|
Accept | 能正确接收的媒体类型 |
Accept-Charset | 能正确接收的字符集 |
Accept-Encoding | 能正确接收的编码格式列表 |
Accept-Language | 能正确接收的语言列表 |
Expect | 期待服务端的指定行为 |
From | 请求方邮箱地址 |
Host | 服务器的域名 |
If-Match | 两端资源标记比较 |
If-Modified-Since | 本地资源未修改返回 304(比较时间) |
If-None-Match | 本地资源未修改返回 304(比较标记) |
User-Agent | 客户端信息 |
Max-Forwards | 限制可被代理及网关转发的次数 |
Proxy-Authorization | 向代理服务器发送验证信息 |
Range | 请求某个内容的一部分 |
Referer | 表示浏览器所访问的前一个页面 |
TE | 传输编码方式 |
响应字段 | 作用 |
---|---|
Accept-Ranges | 是否支持某些种类的范围 |
Age | 资源在代理缓存中存在的时间 |
ETag | 资源标识 |
Location | 客户端重定向到某个 URL |
Proxy-Authenticate | 向代理服务器发送验证信息 |
Server | 服务器名字 |
WWW-Authenticate | 获取资源需要的验证信息 |
实体字段 | 作用 |
---|---|
Allow | 资源的正确请求方式 |
Content-Encoding | 内容的编码格式 |
Content-Language | 内容使用的语言 |
Content-Length | request body 长度 |
Content-Location | 返回数据的备用地址 |
Content-MD5 | Base64 加密格式的内容MD5 检验值 |
Content-Range | 内容的位置范围 |
Content-Type | 内容的媒体类型 |
Expires | 内容的过期时间 |
Last_modified | 内容的最后修改时间 |
数据格式
HTTP 从MIME type取了一部分来标记报文 body 部分的数据类型
- text:即文本格式的可读数据,我们最熟悉的应该就是
text/html
了,表示超文本文档,此外还有纯文本text/plain
、样式表text/css
等。 image
:即图像文件,有image/gif
、image/jpeg
、image/png
等。audio/video
:音频和视频数据,例如audio/mpeg
、video/mp4
等。application
:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有application/json
,application/javascript
、application/pdf
等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是application/octet-stream
,即不透明的二进制数据
请求
Accept: text/html,application/xml,image/webp,image/png
响应
Content-Type: text/html
仅有
MIME type
还不够,因为HTTP
在传输时为了节约带宽,有时候还会压缩数据,为了不要让浏览器继续“猜”,还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。
ncoding type
就少了很多,常用的只有下面三种
gzip
:GNU zip
压缩格式,也是互联网上最流行的压缩格式;deflate
:zlib
(deflate
)压缩格式,流行程度仅次于gzip
;br
:一种专门为HTTP
优化的新压缩算法(Brotli
)
请求
Accept-Encoding: gzip, deflate, br
响应
Content-Encoding: gzip
HTTP数据传输
定长数据
对于定长的数据包而言,发送端在发送数据的过程中,需要设置Content-Length
,来指明发送数据的长度。
当然了如果采用了Gzip压缩的话,Content-Length设置的就是压缩后的传输长度。
我们还需要知道的是👇
Content-Length
如果存在并且有效的话,则必须和消息内容的传输长度完全一致,也就是说,如果过短就会截断,过长的话,就会导致超时。- 如果采用短链接的话,直接可以通过服务器关闭连接来确定消息的传输长度。
- 那么在HTTP/1.0之前的版本中,Content-Length字段可有可无,因为一旦服务器关闭连接,我们就可以获取到传输数据的长度了。
- 在HTTP/1.1版本中,如果是Keep-alive的话,chunked优先级高于
Content-Length
,若是非Keep-alive,跟前面情况一样,Content-Length可有可无。
那怎么来设置Content-Length
举个例子来看看👇
const server = require('http').createServer();
server.on('request', (req, res) => {
if(req.url === '/index') {
// 设置数据类型
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10);
res.write("你好,使用的是Content-Length设置传输数据形式");
}
})
server.listen(3000, () => {
console.log("成功启动--TinaTian");
})
不定长数据
现在采用最多的就是HTTP/1.1版本,来完成传输数据,在保存Keep-alive状态下,当数据是不定长的时候,我们需要设置新的头部字段👇
Transfer-Encoding: chunked
通过chunked机制,可以完成对不定长数据的处理,当然了,你需要知道的是
- 如果头部信息中有
Transfer-Encoding
,优先采用Transfer-Encoding里面的方法来找到对应的长度。 - 如果设置了Transfer-Encoding,那么Content-Length将被忽视。
- 使用长连接的话,会持续的推送动态内容。
那我们来模拟一下吧👇
const server = require('http').createServer();
server.on('request', (req, res) => {
if(req.url === '/index') {
// 设置数据类型
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');
res.write("你好,使用的是Transfer-Encoding设置传输数据形式");
setTimeout(() => {
res.write("第一次传输数据给您<br/>");
}, 1000);
res.write("骚等一下");
setTimeout(() => {
res.write("第一次传输数据给您");
res.end()
}, 3000);
}
})
server.listen(3000, () => {
console.log("成功启动--TinaTian");
})
HTTP 的请求方法
- HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法
- HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT
http/1.1
规定了以下请求方法(注意,都是大写):
- GET: 请求获取Request-URI所标识的资源
- POST: 在Request-URI所标识的资源后附加新的数据
- HEAD: 请求获取由Request-URI所标识的资源的响应消息报头
- PUT: 请求服务器存储一个资源,并用Request-URI作为其标识(修改数据)
- DELETE: 请求服务器删除对应所标识的资源
- TRACE: 请求服务器回送收到的请求信息,主要用于测试或诊断
- CONNECT: 建立连接隧道,用于代理服务器
- OPTIONS: 列出可对资源实行的请求方法,用来跨域请求
从应用场景角度来看,Get 多用于无副作用,幂等的场景,例如搜索关键字。Post 多用于副作用,不幂等的场景,例如注册。
options 方法有什么用
- OPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能。
- 这个方法会请求服务器返回该资源所支持的所有 HTTP 请求方法,该方法会用'*'来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常。
- JS 的 XMLHttpRequest对象进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。
get 和 post的区别
- 浏览器在回退时,
get
不会重新请求,但是post
会重新请求。【重要】 get
请求会被浏览器主动缓存,而post
不会。【重要】get
请求的参数,会报保留在浏览器的历史记录里,而post
不会。做业务时要注意。为了防止CSRF
攻击,很多公司把get
统一改成了post
。get
请求在url
中传递的参数有大小限制,基本是
2kb`,不同的浏览器略有不同。而post没有注意。get
的参数是直接暴露在url
上的,相对不安全。而post
是放在请求体中的。
Get
请求能缓存,Post
不能Post
相对Get
安全一点点,因为Get
请求都包含在URL
里,且会被浏览器保存历史纪录,Post
不会,但是在抓包的情况下都是一样的。Post
可以通过request body
来传输比Get
更多的数据,Get
没有这个技术URL
有长度限制,会影响Get
请求,但是这个长度限制是浏览器规定的,不是RFC
规定的Post
支持更多的编码类型且不对数据类型限制
HTTP状态码
HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等。
2XX | 成功(这系列表明请求被正常处理了) |
---|---|
200 | OK,表示从客户端发来的请求在服务器端被正确处理 |
204 | No content,表示请求成功,但响应报文不含实体的主体部分 |
206 | Partial Content,进行范围请求成功 |
3XX | 重定向(表明浏览器要执行特殊处理) |
---|---|
301 | moved permanently,永久性重定向,表示资源已被分配了新的 URL |
302 | found,临时性重定向,表示资源临时被分配了新的 URL |
303 | see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源(对于301/302/303响应,几乎所有浏览器都会删除报文主体并自动用GET重新请求) |
304 | not modified,表示服务器允许访问资源,但请求未满足条件的情况(与重定向无关) |
307 | temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求 |
4XX | 客户端错误 |
---|---|
400 | bad request,请求报文存在语法错误 |
401 | unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 |
403 | forbidden,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述 |
404 | not found,表示在服务器上没有找到请求的资源 |
5XX | 服务器错误 |
---|---|
500 | internal sever error,表示服务器端在执行请求时发生了错误 |
501 | Not Implemented,表示服务器不支持当前请求所需要的某个功能 |
503 | service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求 |
HTTP 缺点
无状态
所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态。
在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。
但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。
http协议无状态中的 状态 到底指的是什么?!
- 【状态】的含义就是:客户端和服务器在某次会话中产生的数据
- 那么对应的【无状态】就意味着:这些数据不会被保留
- 通过增加
cookie
和session
机制,现在的网络请求其实是有状态的 - 没有状态的
http
协议下,服务器也一定会保留你每次网络请求对数据的修改,但这跟保留每次访问的数据是不一样的,保留的只是会话产生的结果,而没有保留会话
明文传输
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱
就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。
队头阻塞问题
当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。
有什么解决办法吗👇
并发连接
我们知道对于一个域名而言,是允许分配多个长连接的,那么可以理解成增加了任务队列,也就是说不会导致一个任务阻塞了该任务队列的其他任务,在RFC规范
中规定客户端最多并发2个连接,不过实际情况就是要比这个还要多,举个例子,Chrome中是6个。
域名分片
顾名思义,我们可以在一个域名下分出多个二级域名出来,而它们最终指向的还是同一个服务器,这样子的话就可以并发处理的任务队列更多,也更好的解决了队头阻塞的问题。
举个例子,比如TianTian.com
,可以分出很多二级域名,比如Day1.TianTian.com
,Day2.TianTian.com
,Day3.TianTian.com
,这样子就可以有效解决队头阻塞问题。
HTTP1.0 / HTTP1.1 / HTTP2.0
-
HTTP 1.0
- 任何格式的内容都可以发送,这使得互联网不仅可以传输文字,还能传输图像、视频、二进制等文件。
- 除了GET命令,还引入了POST命令和HEAD命令。
- http请求和回应的格式改变,除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。
- 只使用 header 中的 If-Modified-Since 和 Expires 作为缓存失效的标准。
- 不支持断点续传,也就是说,每次都会传送全部的页面和数据。
- 通常每台计算机只能绑定一个 IP,所以请求消息中的 URL 并没有传递主机名(hostname)
-
HTTP 1.1
- 引入了持久连接( persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。长连接的连接时长可以通过请求头中的
keep-alive
来设置 - 引入了管道机制( pipelining),即在同一个TCP连接里,客户端可以同时发送多个 请求,进一步改进了HTTP协议的效率。
- HTTP 1.1 中新增加了 E-tag,If-Unmodified-Since, If-Match, If-None-Match 等缓存控制标头来控制缓存失效。
- 支持断点续传,通过使用请求头中的
Range
来实现。 - 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。
- 新增方法:PUT、 PATCH、 OPTIONS、 DELETE。
- 引入了持久连接( persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。长连接的连接时长可以通过请求头中的
http1.x版本问题
-
在传输数据过程中,所有内容都是明文,客户端和服务器端都无法验证对方的身份,无法保证数据的安全性。
-
HTTP/1.1 版本默认允许复用TCP连接,但是在同一个TCP连接里,所有数据通信是按次序进行的,服务器通常在处理完一个回应后,才会继续去处理下一个,这样子就会造成队头阻塞。
-
http/1.x 版本支持Keep-alive,用此方案来弥补创建多次连接产生的延迟,但是同样会给服务器带来压力,并且的话,对于单文件被不断请求的服务,Keep-alive会极大影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。
-
HTTP 2.0
二进制分帧
这是一次彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧":头信息帧和数据帧。头部压缩
HTTP 1.1版本会出现 User-Agent、Cookie、Accept、Server、Range 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP 2.0 使用HPACK
算法进行压缩。多路复用
复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序一一对应,这样子解决了队头阻塞的问题。服务器推送
允许服务器未经请求,主动向客户端发送资源,即服务器推送。请求优先级
可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。
cookie
cookie和localSrorage、sessionSrorage、indexDB 的区别
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
数据存储大小 | 4K | 5M | 5M | 无限 |
与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
从上表可以看到,cookie
已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStorage
和 sessionStorage
。对于不怎么改变的数据尽量使用 localStorage
存储,否则可以用 sessionStorage
存储。
对于 cookie
,我们还需要注意安全性
属性 | 作用 |
---|---|
value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
http-only | 不能通过 JS 访问 Cookie ,减少 XSS 攻击 |
secure | 只能在协议为 HTTPS 的请求中携带 |
same-site | 规定浏览器不能在跨域请求中携带 Cookie ,减少 CSRF 攻击 |
Name
,即该Cookie
的名称。Cookie
一旦创建,名称便不可更改。Value
,即该Cookie
的值。如果值为Unicode
字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64
编码。Max Age
,即该Cookie
失效的时间,单位秒,也常和Expires
一起使用,通过它可以计算出其有效时间。Max Age
如果为正数,则该Cookie
在Max Age
秒之后失效。如果为负数,则关闭浏览器时Cookie
即失效,浏览器也不会以任何形式保存该Cookie
。Path
,即该Cookie
的使用路径。如果设置为/path/
,则只有路径为/path/
的页面可以访问该Cookie
。如果设置为/
,则本域名下的所有页面都可以访问该Cookie
。Domain
,即可以访问该Cookie
的域名。例如如果设置为.zhihu.com
,则所有以zhihu.com
,结尾的域名都可以访问该Cookie
。Size
字段,即此Cookie
的大小。Http
字段,即Cookie
的httponly
属性。若此属性为true
,则只有在HTTP Headers
中会带有此 Cookie 的信息,而不能通过document.cookie
来访问此 Cookie。Secure
,即该Cookie
是否仅被使用安全协议传输。安全协议。安全协议有HTTPS、SSL
等,在网络上传输数据之前先将数据加密。默认为false
。
session
Session 是一种记录客户状态的机制,Session 不能跨域
session 认证流程:
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
- 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
- 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
- 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
Session 与 Cookie 的区别
- 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
- 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
- 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
- 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。