# HTTP精选面试题

# 简要概括一下 HTTP 的特点?HTTP 有哪些缺点?

HTTP 特点

  1. 灵活可扩展,主要体现在两个方面。一个是语义上的自由,只规定了基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任意数据,非常方便。

  2. 可靠传输。HTTP 基于 TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。

  3. 请求-应答。也就是一发一收、有来有回, 当然这个请求方和应答方不单单指客户端和服务器之间,如果某台服务器作为代理来连接后端的服务端,那么这台服务器也会扮演请求方的角色。

  4. 无状态。这里的状态是指通信过程的上下文信息,而每次 http 请求都是独立、无关的,默认不需要保留状态信息。

HTTP 缺点

  1. 无状态

    • 所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态
    • 在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了
    • 但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点
  2. 明文传输

    • 即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式
    • 这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息
  3. 队头阻塞问题

    • 当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。接下来会有一小节讨论这个问题。

# HTTP 和 HTTPS 的区别?

  1. 传输信息安全性不同;
    • http 协议是超文本传输协议,信息是明文传输,不安全
    • https 协议是具有安全性的 ssl 加密传输协议,是安全的协议
  2. 连接方式不同
    • http 的连接很简单,是无状态的
    • https 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议
  3. 端口不同
    • http 协议使用的端口是 80
    • https 协议使用的端口是 443
  4. 证书申请方式不同
    • http 协议免费申请
    • https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用

# HTTP Get 和 Post 的区别?

  1. 从 REST 服务角度上说,Get 是幂等的,即读取同一个资源,总是得到相同的数据,而 Post 不是幂等的,因为每次请求对资源的改变并不是相同的;进一步地,Get 不会改变服务器上的资源,而 Post 会对服务器资源进行改变
  2. Get 请求能缓存,Post 不能
  3. Post 相对 Get 安全一点,因为Get 请求都包含在 URL 里(当然你想写到 body 里也是可以的),且会被浏览器保存历史纪录。Post 不会,但是在抓包的情况下都是一样的
  4. URL 有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的, Post 默认不受限制
  5. Post 支持更多的编码类型且不对数据类型限制

# 什么是无状态协议?HTTP 是无状态协议么?怎么解决?

无状态协议是指浏览器对于事务没有记忆能力

HTTP 就是一种无状态的协议,它对用户的操作没有记忆能力,自动登录等功能大都是通过 Session 实现的。

可以通过 cookie 和 token 等方式解决

# UDP 协议与 TCP 协议的区别?


UDP 与 TCP 两者都属于传输层协议。

UDP 协议是面向无连接的,也就是说不需要在正式传递数据之前先连接起双方。然后 UDP 协议只是数据报文的搬运工,不能保证有序且不丢失的传递到对端,并且 UDP 协议也没有任何控制流量的算法, UDP 相较于 TCP 更加的轻便。

TCP 基本是和 UDP 反着来,建立连接断开连接都需要先需要进行握手。在传输数据的过程中,通过各种算法保证数据的可靠性,当然带来的问题就是相比 UDP 来说不那么的高效。

两者对比:

TCP UDP
连接性 面向连接 面向非连接
传输可靠性 可靠 不可靠
报文 面向字节流 面向报文
效率 传输效率低 传输效率高
流量控制 滑动窗口
拥塞控制 慢开始、拥塞避免、快重传、快恢复
传输速度
错误处理 错误校验,能够进行错误修复 错误检查,但会丢弃错误的数据包
应用场合 对效率要求低,对准确性要求高或要求有连接的场景 对效率要求高,对准确性要求低(直播、王者荣耀)

根据上面对比可以总结:

  1. TCP 面向连接,UDP 是无连接的,即发送数据之前不需要建立连接
  2. CP 支持拥塞控制和流量控制,以提供可靠的服务,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付
  3. TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的
  4. TCP 的传输效率相对较低,UDP 的传输效率高
  5. 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
  6. TCP 首部开销 20 字节;UDP 的首部开销小,只有 8 个字节

TCP 与 UDP 的应用场景:

TCP与UDP的应用场景

# HTTPS 的工作原理是什么?

HTTPS 的工作原理即 TCL 握手过程:

1、客户端先生成一个随机数,中间会带上客户端这边支持的加密套件(加密协议和加密方式等)

2、服务端收到客户端的随机值,存储并且自己也产生一个随机值,并根据客户端支持的协议和加密方式来选择最合适的加密套件,并且发送自己的证书(也就是我们之前说到的公钥)

3、客户端拿到服务端的随机数,也先存储,并且通过服务端给的证书(公钥),生成一个预主密钥,生成的过程当中也会有一个新的随机数(所以总共有3个随机数),这时候生成的随机数通过公钥加密后传输给服务器(这个过程无法被中间人解析,因为只有服务端有私钥解密)

4、加密的预主密钥被传输到服务器后,服务器通过私钥解密拿到预主密钥。这样客户端和服务端都有三个随机数了,这时候客户端和服务端同时对这三个随机数进行算法处理(用前面说的加密套件处理)生成主密钥

5、最后双方通过这个主密钥进行对称加密和解密进行数据传输,确保数据的安全传输

# 简单说说 HTTP1.0/1.1/2.0 的区别?

  • HTTP/0.9

    • HTTP 协议的最初版本,仅支持 GET 方法,并且仅能请求访问 HTML 格式的资源
  • HTTP/1.0

    • 增加了请求方式 POST 和 HEAD
    • 根据 Content-Type 可以支持多种数据格式,例如:text/html、image/jpeg 等
    • 同时也开始支持 cache,当客户端在规定时间内访问统一网站,直接访问缓存即可
    • HTTP 请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等
    • 每次 TCP 连接只能发送一个请求,当服务器响应后就会关闭这次连接,下一个请求需要再次建立 TCP 连接,即不支持 keep-alive
  • HTTP/1.1

    • 引入了持久连接,或叫长连接( persistent connection),即 TCP 连接默认不关闭,可以被多个请求复用,不用声明 Connection: keep-alive
    • 引入了管道机制( pipelining),即在同一个 TCP 连接里,客户端可以同时发送多个请求,进一步改进了 HTTP 协议的效率
    • 新增方法:PUT、 PATCH、 OPTIONS、 DELETE。
    • http 协议不带有状态,每次请求都必须附上所有信息。请求的很多字段都是重复的,浪费带宽,影响速度
  • HTTP/2.0

    • 在之前的 HTTP 版本中,我们是通过文本的方式传输数据。在 HTTP/2 中引入了新的编码机制,采用二进制分帧。所有传输的数据都会被分割,并采用二进制格式编码,称为帧(frame),帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流(stream),流也就是多个帧组成的数据流。
    • 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序一一对应,避免了队头堵塞的问题),即多路复用
    • 在 HTTP/2 中,服务端可以在客户端进行某个请求后,主动推送其他资源给客户端,即服务端推送
    • 引入头信息压缩机制( header compression),头信息使用 gzip 或 compress 压缩后再发送。
    • 还可以进行请求优先级控制以及更精细的流量控制

# TCP 三次握手和四次挥手分别是怎么样的?为什么要三次握手,两次不行么?为什么挥手却又是四次呢?

TCP三次握手

TCP三次握手

  • 第一次握手(SYN=1, seq=x)

    • 建立连接。客户端发送连接请求报文段,这是报文首部中的同步位SYN=1,同时选择一个初始序列号 seq=x ,此时,客户端进程进入了 SYN-SENT(同步已发送状态)状态。(TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号)
  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1)

    • 服务器收到客户端的 SYN 报文段,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号 ACKnum=x+1,同时,自己还要发送 SYN 请求信息,SYN=1,为自己初始化一个序列号 seq=y,服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。(这个报文也不能携带数据,但是同样要消耗一个序号)
  • 第三次握手(ACK=1,ACKnum=y+1)

    • 客户端收到服务器的 SYN+ACK 报文段,再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,确认号 ACKnum = y+1,这个报文段发送完毕以后,客户端和服务器端都进入 ESTABLISHED(已建立连接)状态,完成TCP三次握手

为什么需要三次握手呢?两次不行吗?

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

具体例子:

“已失效的连接请求报文段”的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server 。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。

假设不采用“三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。主要目的防止 server 端一直等待,浪费资源。

TCP四次挥手

TCP三次握手

客户端或服务器均可主动发起挥手动作

  • 第一次挥手(FIN=1,seq=x)

    • 主机1(可以使客户端,也可以是服务器端),设置 seq=x,向主机2发送一个 FIN 报文段;此时,主机1进入 FIN_WAIT_1 状态;这表示主机1没有数据要发送给主机2了;
  • 第二次挥手(ACK=1,ACKnum=x+1)

    • 主机2收到了主机1发送的 FIN 报文段,向主机1回一个 ACK 报文段,Acknnum=x+1,主机1进入 FIN_WAIT_2 状态;主机2告诉主机1,我“同意”你的关闭请求;
  • 第三次挥手(FIN=1,seq=y)

    • 主机2向主机1发送 FIN 报文段,请求关闭连接,同时主机2进入LAST_ACK 状态
  • 第四次挥手(ACK=1,ACKnum=y+1)

    • 主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入 TIME_WAIT 状态;主机2收到主机1的 ACK 报文段以后,就关闭连接;此时,主机1等待 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,主机1也可以关闭连接了,进入 CLOSED 状态。(主机1等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。)

为什么连接的时候是三次握手,关闭的时候却是四次握手?

这是因为服务端在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,可以把 ACK 和 SYN 放在一个报文里发送给客户端。其中ACK报文是用来应答的,SYN报文是用来同步的。而关闭连接时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即 close,也可以发送一些数据给对方后,再发送 FIN 报文给对方来表示同意现在关闭连接,因此,己方 ACK 和 FIN 一般都会分开发送,故需要四步握手。

由于 TCP 协议是全双工的,也就是说客户端和服务端都可以发起断开连接。两边各发起一次断开连接的申请,加上各自的两次确认,看起来就像执行了四次挥手。

为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到CLOSE状态?

虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

还有一个原因,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

# 说说 TCP 快速打开的原理(TFO)

前面讲了 TCP 三次握手,可能有人会说,每次都三次握手好麻烦呀!能不能优化一点?

可以啊。今天来说说这个优化后的 TCP 握手流程,也就是 TCP 快速打开(TCP Fast Open, 即TFO)的原理。

优化的过程是这样的,还记得我们说 SYN Flood 攻击时提到的 SYN Cookie 吗?这个 Cookie 可不是浏览器的Cookie, 用它同样可以实现 TFO。

TFO 流程

首轮三次握手:

首先客户端发送SYN给服务端,服务端接收到。

注意哦!现在服务端不是立刻回复 SYN + ACK,而是通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,然后才给客户端返回。

客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。

首轮三次握手就是这样的流程。而后面的三次握手就不一样啦!

后面的三次握手:

在后面的三次握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK。

重点来了,现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。

当然,客户端的 ACK 还得正常传过来,不然怎么叫三次握手嘛。

流程如下:

注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。

TFO 的优势

TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了1 个 RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。

# 介绍一下 TCP 报文头部的字段

报文头部结构如下(单位为字节):

请大家牢记这张图!

# 能不能说说TCP报文中时间戳的作用?

timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节,格式如下:

kind(1 字节) + length(1 字节) + info(8 个字节)
1

其中 kind = 8, length = 10, info 有两部分构成: timestamp和timestamp echo,各占 4 个字节。

那么这些字段都是干嘛的呢?它们用来解决那些问题?

接下来我们就来一一梳理,TCP 的时间戳主要解决两大问题:

  • 计算往返时延 RTT(Round-Trip Time)

  • 防止序列号的回绕问题

# TCP 的超时重传时间是如何计算的?

TCP 具有超时重传机制,即间隔一段时间没有等到数据包的回复时,重传这个数据包。 那么这个重传间隔是如何来计算的呢?

今天我们就来讨论一下这个问题。

这个重传间隔也叫做超时重传时间(Retransmission TimeOut, 简称RTO),它的计算跟上一节提到的 RTT 密切相关。这里我们将介绍两种主要的方法,一个是经典方法,一个是标准方法。

经典方法

经典方法引入了一个新的概念——SRTT(Smoothed round trip time,即平滑往返时间),没产生一次新的 RTT. 就根据一定的算法对 SRTT 进行更新,具体而言,计算方式如下(SRTT 初始值为0):

SRTT =  (α * SRTT) + ((1 - α) * RTT)
1

其中,α 是平滑因子,建议值是0.8,范围是0.8 ~ 0.9。

拿到 SRTT,我们就可以计算 RTO 的值了:

RTO = min(ubound, max(lbound, β * SRTT))
1

β 是加权因子,一般为1.3 ~ 2.0, lbound 是下界,ubound 是上界。

其实这个算法过程还是很简单的,但是也存在一定的局限,就是在 RTT 稳定的地方表现还可以,而在 RTT 变化较大的地方就不行了,因为平滑因子 α 的范围是0.8 ~ 0.9, RTT 对于 RTO 的影响太小。

标准方法

为了解决经典方法对于 RTT 变化不敏感的问题,后面又引出了标准方法,也叫Jacobson / Karels 算法。一共有三步。

第一步: 计算SRTT,公式如下:

SRTT = (1 - α) * SRTT + α * RTT
1

注意这个时候的 α跟经典方法中的α取值不一样了,建议值是1/8,也就是0.125。

第二步: 计算RTTVAR(round-trip time variation)这个中间变量。

RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|)
1

β 建议值为 0.25。这个值是这个算法中出彩的地方,也就是说,它记录了最新的 RTT 与当前 SRTT 之间的差值,给我们在后续感知到 RTT 的变化提供了抓手。

第三步: 计算最终的RTO:

RTO = µ * SRTT + ∂ * RTTVAR 
1

µ建议值取1, ∂建议值取4。

这个公式在 SRTT 的基础上加上了最新 RTT 与它的偏移,从而很好的感知了 RTT 的变化,这种算法下,RTO 与 RTT 变化的差值关系更加密切。

# 能不能说一说 TCP 的流量控制?

对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。

而流量控制索要做的事情,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。

要具体理解流量控制,首先需要了解滑动窗口的概念。

在 TCP 中,两端其实都维护着窗口:分别为发送端窗口和接收端窗口。

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

当发送端接收到应答报文后,会随之将窗口进行滑动

滑动窗口是一个很重要的概念,它帮助 TCP 实现了流量控制的功能。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据,防止出现接收方带宽已满,但是发送方还一直发送数据的情况。

流量控制过程

这里我们不用太复杂的例子,以一个最简单的来回来模拟一下流量控制的过程,方便大家理解。

首先双方三次握手,初始化各自的窗口大小,均为 200 个字节。

假如当前发送端给接收端发送 100 个字节,那么此时对于发送端而言,SND.NXT 当然要右移 100 个字节,也就是说当前的可用窗口减少了 100 个字节,这很好理解。

现在这 100 个到达了接收端,被放到接收端的缓冲队列中。不过此时由于大量负载的原因,接收端处理不了这么多字节,只能处理 40 个字节,剩下的 60 个字节被留在了缓冲队列中。

注意了,此时接收端的情况是处理能力不够用啦,你发送端给我少发点,所以此时接收端的接收窗口应该缩小,具体来说,缩小 60 个字节,由 200 个字节变成了 140 字节,因为缓冲队列还有 60 个字节没被应用拿走。

因此,接收端会在 ACK 的报文首部带上缩小后的滑动窗口 140 字节,发送端对应地调整发送窗口的大小为 140 个字节。

此时对于发送端而言,已经发送且确认的部分增加 40 字节,也就是 SND.UNA 右移 40 个字节,同时发送窗口缩小为 140 个字节。

这也就是流量控制的过程。尽管回合再多,整个控制的过程和原理是一样的。

# 能不能说说 TCP 的拥塞控制?

上一节所说的流量控制发生在发送端跟接收端之间,并没有考虑到整个网络环境的影响,如果说当前网络特别差,特别容易丢包,那么发送端就应该注意一些了。而这,也正是拥塞控制需要处理的问题。

对于拥塞控制来说,TCP 每条连接都需要维护两个核心状态:

  • 拥塞窗口(Congestion Window,cwnd)

  • 慢启动阈值(Slow Start Threshold,ssthresh)

涉及到的算法有这几个:

  • 慢启动
  • 拥塞避免
  • 快速重传和快速恢复

接下来,我们就来一一拆解这些状态和算法。首先,从拥塞窗口说起。

拥塞窗口

拥塞窗口(Congestion Window,cwnd)是指目前自己还能传输的数据量大小。

那么之前介绍了接收窗口的概念,两者有什么区别呢?

  • 接收窗口(rwnd)是接收端给的限制

  • 拥塞窗口(cwnd)是发送端的限制

限制谁呢?

限制的是发送窗口的大小。

有了这两个窗口,如何来计算发送窗口?

发送窗口大小 = min(rwnd, cwnd)
1

取两者的较小值。而拥塞控制,就是来控制 cwnd 的变化。

慢启动

刚开始进入传输数据的时候,你是不知道现在的网路到底是稳定还是拥堵的,如果做的太激进,发包太急,那么疯狂丢包,造成雪崩式的网络灾难。

因此,拥塞控制首先就是要采用一种保守的算法来慢慢地适应整个网路,这种算法叫慢启动。运作过程如下:

  • 首先,三次握手,双方宣告自己的接收窗口大小

  • 双方初始化自己的拥塞窗口(cwnd)大小

  • 在开始传输的一段时间,发送端每收到一个 ACK,拥塞窗口大小加 1,也就是说,每经过一个 RTT,cwnd 翻倍。如果说初始窗口为 10,那么第一轮 10 个报文传完且发送端收到 ACK 后,cwnd 变为 20,第二轮变为 40,第三轮变为 80,依次类推。

难道就这么无止境地翻倍下去?当然不可能。它的阈值叫做慢启动阈值,当 cwnd 到达这个阈值之后,好比踩了下刹车,别涨了那么快了,老铁,先 hold 住!

在到达阈值后,如何来控制 cwnd 的大小呢?

这就是拥塞避免做的事情了。

拥塞避免

原来每收到一个 ACK,cwnd 加1,现在到达阈值了,cwnd 只能加这么一点: 1 / cwnd。那你仔细算算,一轮 RTT 下来,收到 cwnd 个 ACK, 那最后拥塞窗口的大小 cwnd 总共才增加 1。

也就是说,以前一个 RTT 下来,cwnd 翻倍,现在 cwnd 只是增加 1 而已。

当然,慢启动和拥塞避免是一起作用的,是一体的。

快速重传和快速恢复

快速重传:

在 TCP 传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的 ACK。

比如第 5 个包丢了,即使第 6、7 个包到达的接收端,接收端也一律返回第 4 个包的 ACK。当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到一个 RTO 的时间到了才重传。

这就是快速重传,它解决的是是否需要重传的问题。

选择性重传:

那你可能会问了,既然要重传,那么只重传第 5 个包还是第5、6、7 个包都重传呢?

当然第 6、7 个都已经到达了,TCP 的设计者也不傻,已经传过去干嘛还要传?干脆记录一下哪些包到了,哪些没到,针对性地重传。

在收到发送端的报文后,接收端回复一个 ACK 报文,那么在这个报文首部的可选项中,就可以加上SACK这个属性,通过 left edge 和 right edge 告知发送端已经收到了哪些区间的数据报。因此,即使第 5 个包丢包了,当收到第 6、7 个包之后,接收端依然会告诉发送端,这两个包到了。剩下第 5 个包没到,就重传这个包。这个过程也叫做选择性重传(SACK,Selective Acknowledgment),它解决的是如何重传的问题。

快速恢复:

当然,发送端收到三次重复 ACK 之后,发现丢包,觉得现在的网络已经有些拥塞了,自己会进入快速恢复阶段。

在这个阶段,发送端如下改变:

  • 拥塞阈值降低为 cwnd 的一半

  • cwnd 的大小变为拥塞阈值

  • cwnd 线性增加

以上就是 TCP 拥塞控制的经典算法: 慢启动、拥塞避免、快速重传和快速恢复。

# 能不能说说 Nagle 算法和延迟确认?

Nagle 算法

试想一个场景,发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。

而避免小包的频繁发送,这就是 Nagle 算法要做的事情。

具体来说,Nagle 算法的规则如下:

  • 当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送

  • 后面发送满足下面条件之一就可以发了:

    • 数据包大小达到最大段大小(Max Segment Size, 即 MSS)
    • 之前所有包的 ACK 都已接收到

延迟确认

试想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那我是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?

延迟确认(delayed ack)所做的事情,就是后者,稍稍延迟,然后合并 ACK,最后才回复给发送端。TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。

不过需要主要的是,有一些场景是不能延迟确认的,收到了就要马上回复:

  • 接收到了大于一个 frame 的报文,且需要调整窗口大小

  • TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)

  • 发现了乱序包

两者一起使用会怎样?

前者意味着延迟发,后者意味着延迟接收,会造成更大的延迟,产生性能问题。

# 在浏览器中输入一个 URL 至页面呈现,网络上都发生了什么事?

  1. 输入地址
  2. 浏览器查找域名的 IP 地址(DNS 查询:具体过程包括浏览器搜索自身的 DNS 缓存、搜索操作系统的 DNS 缓存、读取本地的 Host 文件和向本地 DNS 服务器进行查询等)
  3. 浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起三次握手
  4. TCP/IP 链接建立起来后,浏览器向服务器发送 HTTP 请求
  5. 服务器的永久重定向响应
  6. 浏览器跟踪重定向地址
  7. 服务器处理请求
  8. 服务器返回一个 HTTP 响应
  9. 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面

更加详细的过程,大家可以看看:从输入url到页面展示到底发生了什么 (opens new window)

# TCP/IP 与 HTTP 有什么关系吗?

TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP 协议不仅仅指的是 TCP 和 IP 两个协议,而是指一个由 FTP、SMTP、TCP、UDP、IP 等协议构成的协议簇, 只是因为在 TCP/IP 协议中 TCP 协议和 IP 协议最具代表性,所以被称为 TCP/IP 协议。

而HTTP是应用层协议,主要解决如何包装数据。

“IP” 代表网际协议,TCP 和 UDP 使用该协议从一个网络传送数据包到另一个网络。把 IP 想像成一种高速公路,它允许其它协议在上面行驶并找到到其它电脑的出口。TCP 和 UDP 是高速公路上的“卡车”,它们携带的货物就是像 HTTP,文件传输协议 FTP 这样的协议等。

# 知道流量控制和拥塞控制吗?详细讲一下 TCP 的滑动窗口?

流量控制

如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。

滑动窗口

利用滑动窗口机制可以很方便地在 TCP 连接上实现对发送方的流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

TCP三次握手

从上面的图可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。

可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。

拥塞控制

拥塞控制是指发送方先设置一个小的窗口值作为发送速率,当成功发包并接收到ACK时,便以指数速率增大发送窗口的大小,直到遇到丢包(超时/三个冗余ACK),才停止并调整窗口的大小。这么做能最大限度地利用带宽,又不至于让网络环境变得太过拥挤。

最终滑动窗口的值将设置为流量控制窗口和拥塞控制窗口中的较小值。

TCP的拥塞处理

计算机网络中的带宽、交换结点中的缓存及处理机等都是网络的资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏,这种情况就叫做拥塞。拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。注意,拥塞控制和流量控制不同,前者是一个全局性的过程,而后者指点对点通信量的控制。拥塞控制的方法主要有以下四种:

  1. 慢启动:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小;
  2. 拥塞避免:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长。
  3. 快重传:快重传要求接收方在收到一个 失序的报文段 后就立即发出 重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
  4. 快恢复:快重传配合使用的还有快恢复算法,当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半,但是接下去并不执行慢开始算法:因为如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。

# 说一下对称加密与非对称加密?

对称加密(共享密钥加密)

加密与解密使用同一个密钥,常见的对称加密算法:DES,AES,3DES等。

也就是说在加密的同时,也会把密钥发送给对方。在发送密钥过程中可能会造成密钥被窃取,那么如何解决这一问题呢?

非对称密钥加密(公开密钥加密)

公开密钥使用一对非对称密钥。一把叫私有密钥,另一把叫公开密钥。私有密钥不让任何人知道,公有密钥随意发送。公钥加密的信息,只有私钥才能解密。常见的非对称加密算法:RSA,ECC等。

也就是说,发送密文方使用对方的公开密钥进行加密,对方接受到信息后,使用私有密钥进行解密。

对称加密加密与解密使用的是同样的密钥,所以速度快,但由于需要将密钥在网络传输,所以安全性不高。

非对称加密使用了一对密钥,公钥与私钥,所以安全性高,但加密与解密速度慢。

为了解决这一问题,HTTPS 采用对称加密与非对称加密的混合加密方式。采用非对称加密传递密钥,双方确认密钥后,用对称加密进行数据传输。

# 能说说你知道你知道的状态码,以及其含义么?

  • 1XX 指示信息-表示请求已被接收,继续处理

  • 2XX 成功-表示请求已被成功接收并成功进行了处理

    • 200 OK,表示从客户端发来的请求在服务器端被正确处理
    • 204 No content,表示请求成功,但响应报文不含实体的主体部分
    • 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
    • 206 Partial Content,进行范围请求
  • 3XX 重定向-要完成请求必须进行更进一步的操作

    • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
    • 302 found,临时性重定向,表示资源临时被分配了新的 URL
    • 303 see other,表示资源存在着另一个 URL,应使用 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 header怎么判断协议是不是websocket

出于兼容性的考虑,WS的握手使用HTTP来实现(此文档中提到未来有可能会使用专用的端口和方法来实现握手),客户端的握手消息就是一个「普通的,带有Upgrade头的,HTTP Request消息」。所以这一个小节到内容大部分都来自于RFC2616,这里只是它的一种应用形式,下面是RFC6455文档中给出的一个客户端握手消息示例:

GET /chat HTTP/1.1            //1
Host: server.example.com   //2
Upgrade: websocket            //3
Connection: Upgrade            //4
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==            //5
Origin: http://example.com            //6
Sec-WebSocket-Protocol: chat, superchat            //7
Sec-WebSocket-Version: 13            //8
1
2
3
4
5
6
7
8

可以看到,前两行跟HTTP的Request的起始行一模一样,而真正在WS的握手过程中起到作用的是下面几个header域。

  • Upgrade:upgrade 是 HTTP1.1 中用于定义转换协议的 header 域。它表示,如果服务器支持的话,客户端希望使用现有的「网络层」已经建立好的这个「连接(此处是TCP连接)」,切换到另外一个「应用层」(此处是WebSocket)协议。

  • Connection:HTTP1.1 中规定 Upgrade 只能应用在「直接连接」中,所以带有 Upgrade 头的 HTTP1.1消息必须含有 Connection 头,因为Connection头的意义就是,任何接收到此消息的人(往往是代理服务器)都要在转发此消息之前处理掉 Connection 中指定的域(不转发 Upgrade 域)。 如果客户端和服务器之间是通过代理连接的,那么在发送这个握手消息之前首先要发送 CONNECT 消息来建立直接连接。

  • Sec-WebSocket-*:第 7 行标识了客户端支持的子协议的列表(关于子协议会在下面介绍),第 8 行标识了客户端支持的 WS 协议的版本列表,第5行用来发送给服务器使用(服务器会使用此字段组装成另一个 key 值放在握手返回信息里发送客户端)。

  • Origin:作安全使用,防止跨站攻击,浏览器一般会使用这个来标识原始域。

如果服务器接受了这个请求,可能会发送如下这样的返回信息,这是一个标准的 HTTP 的 Response 消息。101 表示服务器收到了客户端切换协议的请求,并且同意切换到此协议。RFC2616 规定只有切换到的协议「比 HTTP1.1 更好」的时候才能同意切换:

HTTP/1.1 101 Switching Protocols //1
Upgrade: websocket. //2
Connection: Upgrade. //3
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  //4
Sec-WebSocket-Protocol: chat. //5
1
2
3
4
5

更多内容请看刨根问底HTTP和WebSocket协议 (opens new window)

# https证书被串改怎么办

为了解决证书潜在的问题,谷歌提出了一个解决方案,这就是证书透明度(CT)。CT 是一组技术解决方案,它能够审计、监控证书的签发、使用,从而让更透明,它不是证书的替代解决方案,而是证书的有效补充。通过 CT,能够达成以下的几个目标:

  • CA 机构能够知晓其签发了那些证书,并快速检测到是否签发恶意证书了。

  • 网站拥有者能够知晓域名对应证书签发的全过程,一旦发现有攻击者伪造了域名对应的证书,可以快速联系 CA 机构,吊销该证书。

  • 浏览器厂商能够审计证书的使用情况,如果发现有恶意证书,可以快速关闭HTTPS连接,保障用户的安全。

**Expect-CT **

为了确保浏览器能在访问到缺少 CT 监督的证书(例如 CA 意外发出的证书)时采取措施,Google 提案增加了一个新的 Expect-CT HTTP Header,该 HTTP Header 用来告诉浏览器期望证使用书透明度服务。Expect-CT CT 头部允许站点选择报告或强制执行证书透明度要求,这可以防止站点证书错误被忽视的情况。当站点启用 Expect-CT CT Header 时,浏览器会检查该站点使用的证书是否出现在公共 C T日志中,这能有效的避免中间人攻击等 HTTPS 威胁,让站点更加安全。

# 为什么是tcp而不是udp。tcp丢包怎么办,怎么知道丢包,怎么知道已经重传成功了

TCP三次握手保证可靠性,而UDP就没有了,信息发出后,不验证是否到达,不可靠。丢包就重传。有seq,是连续的,如果收到的是不连续,说明中间缺了包;或者是超时了还没收到。因为有seq吧,所以多一个少一个也是知道的

# http2为什么快,多了什么特性,头部压缩算法是怎样?

  • 二进制传输
  • 多路复用
  • Header 压缩
  • 服务端 Push

在 HTTP /2 中,使用了 HPACK 压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

# 了解http3的quic吗

UDP 快而不可靠,所以衍生 QUIC。

QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议。

QUIC 协议虽然效率很高,但是并不是那么的可靠。QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,比如 多路复用、0-RTT、使用 TLS1.3 加密、流量控制、有序交付、重传等等功能。

# quic用udp怎么保证了可靠性

用 rudp 来优化资源的占用率和响应时间,提高系统的并发能力。seq、超时重传、fec 前向纠错

# post请求和put请求的区别

  • POST方法和PUT方法请求最根本的区别是发起请求的目的不同。post请求可以根据实际请求的资源来决定到底怎么处理,而put请求的目的是用来替换整个目标资源。put 请求具有 幂等性(idempotent)。
  • post 是可以被缓存的,put 却不行

# 浏览器缓存了解吗?强缓存一般存放在哪里?计算整个文件得到 etag 会耗费性能,怎么解决?如果我不想要使用缓存了,每次都请求最新的,怎么做?no-store 和 no-cache 的区别是什么?

浏览器缓存主要分为四个阶段

  1. 强制缓存阶段:先在本地查找该资源,如果有发现该资源,而且该资源还没有过期,就使用这一个资源,完全不会发送 http 请求到服务器。

  2. 协商缓存阶段:如果在本地缓存找到对应的资源,但是不知道该资源是否过期或者已经过期,则发一个 http 请求到服务器,然后服务器判断这个请求,如果请求的资源在服务器上没有改动过,则返回 304,让浏览器使用本地找到的那个资源。

  3. 启发式缓存阶段:当缓存过期时间的字段一个都没有的时候,浏览器下次并不会直接进入协商阶段,而是先进入启发式缓存阶段,它根据响应头中 2 个时间字段 Date 和 Last-Modified 之间的时间差值,取其值的 10%作为缓存时间周期。也就是说,当存有 Last-Modified 字段的时候,即使是断网,且强缓存都失效后,也有一定时间是直接读取缓存文件的。etag 是没有这个阶段的。

  4. 缓存失败阶段:当服务器发现请求的资源已经修改过,或者这是一个新的请求(再本来没有找到资源),服务器则返回该资源的数据,并且返回 200, 当然这个是指找到资源的情况下,如果服务器上没有这个资源,则返回 404。

强缓存一般放在哪里

强缓存一般存放于 Memory Cache 或者 Disk Cache。

计算整个文件得到 etag 会耗费性能,怎么解决

etag 可以通过文件的 Last-Modified 和 content-length 计算。

Nginx官方默认的 ETag 计算方式是为"文件最后修改时间16进制-文件长度16进制"。例:ETag: “59e72c84-2404”

不管怎么样的算法,在服务器端都要进行计算,计算就有开销,会带来性能损失。因此为了榨干这一点点性能,不少网站完全把 Etag 禁用了(比如Yahoo!),这其实不符合 HTTP/1.1 的规定,因为 HTTP/1.1 总是鼓励服务器尽可能的开启 Etag。

不使用缓存的方式,让每次请求都是最新的

不使用缓存常见的方法是通过 url 拼接 random 的方式或者设置 Cache-Control 设置 no-cache。

no-stroe & no-cache 区别

  • no-store 禁止浏览器和中间服务器缓存。每次都从服务器获取。

    • 注意,no-store 才是真正的完完全全的禁止本地缓存。
  • no-cache 每次请求都会验证该缓存是否过期。可以在本地缓存,可以在代理服务器缓存,但是这个缓存要服务器验证才可以使用

    • 注意,no-cache 不是不缓存的意思。

# 如何理解 HTTP 缓存及缓存代理?

HTTP 缓存

首先通过 Cache-Control 验证强缓存是否可用

  • 如果强缓存可用,直接使用

  • 否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的 If-Modified-Since 或者 If-None-Match 这些条件请求字段检查资源是否更新

    • 若资源更新,返回资源和200状态码
    • 否则,返回304,告诉浏览器直接从缓存获取资源

代理缓存

对于源服务器来说,它也是有缓存的,比如Redis, Memcache,但对于 HTTP 缓存来说,如果每次客户端缓存失效都要到源服务器获取,那给源服务器的压力是很大的。

由此引入了缓存代理的机制。让代理服务器接管一部分的服务端 HTTP 缓存,客户端缓存过期后就近到代理缓存中获取,代理缓存过期了才请求源服务器,这样流量巨大的时候能明显降低源服务器的压力。

缓存代理的控制分为两部分,一部分是源服务器端的控制,一部分是客户端的控制

源服务器的缓存控制

private 和 public

在源服务器的响应头中,会加上 Cache-Control 这个字段进行缓存控制字段,那么它的值当中可以加入 private 或者 public 表示是否允许代理服务器缓存,前者禁止,后者为允许。

比如对于一些非常私密的数据,如果缓存到代理服务器,别人直接访问代理就可以拿到这些数据,是非常危险的,因此对于这些数据一般是不会允许代理服务器进行缓存的,将响应头部的 Cache-Control 设为private,而不是public。

proxy-revalidate

must-revalidate 的意思是客户端缓存过期就去源服务器获取,而 proxy-revalidate 则表示代理服务器的缓存过期后到源服务器获取。

s-maxage

s 是 share 的意思,限定了缓存在代理服务器中可以存放多久,和限制客户端缓存时间的max-age并不冲突。

讲了这几个字段,我们不妨来举个小例子,源服务器在响应头中加入这样一个字段:

Cache-Control: public, max-age=1000, s-maxage=2000
1

复制代码相当于源服务器说: 我这个响应是允许代理服务器缓存的,客户端缓存过期了到代理中拿,并且在客户端的缓存时间为 1000 秒,在代理服务器中的缓存时间为 2000 s。

客户端的缓存控制

max-stale 和 min-fresh

在客户端的请求头中,可以加入这两个字段,来对代理服务器上的缓存进行宽容和限制操作。比如:

max-stale: 5
1

复制代码表示客户端到代理服务器上拿缓存的时候,即使代理缓存过期了也不要紧,只要过期时间在5秒之内,还是可以从代理中获取的。

又比如:

min-fresh: 5
1

复制代码表示代理缓存需要一定的新鲜度,不要等到缓存刚好到期再拿,一定要在到期前 5 秒之前的时间拿,否则拿不到。

only-if-cached

这个字段加上后表示客户端只会接受代理缓存,而不会接受源服务器的响应。如果代理缓存无效,则直接返回504(Gateway Timeout)。

以上便是缓存代理的内容,涉及的字段比较多,希望能好好回顾一下,加深理解。

# HTTP1.1 如何解决 HTTP 的队头阻塞问题?

什么是 HTTP 队头阻塞?

HTTP 传输是基于 请求-应答 的模式进行的,报文必须是一发一收,但值得注意的是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是著名的 HTTP队头阻塞 问题。

并发连接

对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。在RFC2616规定过客户端最多并发 2 个连接,不过事实上在现在的浏览器标准中,这个上限要多很多,Chrome 中是 6 个。

但其实,即使是提高了并发连接,还是不能满足人们对性能的需求。

域名分片

一个域名不是可以并发 6 个长连接吗?那我就多分几个域名。

比如 content1.sanyuan.com 、content2.sanyuan.com。

这样一个 sanyuan.com 域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了,事实上也更好地解决了队头阻塞的问题。

# 说说半连接队列和 SYN Flood 攻击的关系

三次握手前,服务端的状态从 CLOSED 变为 LISTEN, 同时在内部创建了两个队列:半连接队列和全连接队列,即 SYN 队列和 ACCEPT 队列。

半连接队列

当客户端发送 SYN 到服务端,服务端收到以后回复 ACK 和 SYN,状态由 LISTEN 变为 SYN_RCVD,此时这个连接就被推入了 SYN 队列,也就是半连接队列。

全连接队列

当客户端返回 ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)。

SYN Flood 攻击原理

SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN。对于服务端而言,会产生两个危险的后果:

  1. 处理大量的 SYN 包并返回对应 ACK, 势必有大量连接处于 SYN_RCVD 状态,从而占满整个半连接队列,无法处理正常的请求。

  2. 由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源。

如何应对 SYN Flood 攻击?

  1. 增加 SYN 连接,也就是增加半连接队列的容量。

  2. 减少 SYN + ACK 重试次数,避免大量的超时重发。

  3. 利用 SYN Cookie 技术,在服务端接收到 SYN 后不立即分配连接资源,而是根据这个 SYN 计算出一个 Cookie,连同第二次握手回复给客户端,在客户端回复 ACK 的时候带上这个 Cookie 值,服务端验证 Cookie 合法之后才分配连接资源。

# 对 Accept 系列字段了解多少?

对于Accept系列字段的介绍分为四个部分: 数据格式、压缩方式、支持语言和字符集。

  • 数据格式

    • Accept 为接收端,以下为可能的取值
      • text: text/html, text/plain, text/css 等
      • mage: image/gif, image/jpeg, image/png 等
      • audio/video: audio/mpeg, video/mp4 等
      • application: application/json, application/javascript, application/pdf, application/octet-stream
    • 发送端 Content-Type,取值和上面一样
  • 压缩方式

    • Accept-Encoding 为接收端,以下为可能的取值
      • gzip: 当今最流行的压缩格式
      • deflate: 另外一种著名的压缩格式
      • br: 一种专门为 HTTP 发明的压缩算法
    • 发送端 Content-Encoding,取值和上面一样
  • 支持语言

    • 接收端 Accept-Language: zh-CN, zh, en
    • 发送端 Content-Language: zh-CN, zh, en
  • 字符集

    • 接收端 Accept-Charset: charset=utf-8
    • 发送端 Content-Type: text/html; charset=utf-8

# 对于定长和不定长的数据,HTTP 是怎么传输的?

对于定长包体而言,发送端在传输的时候一般会带上 Content-Length, 来指明包体的长度。

对于不定长包体而言,会利用 Transfer-Encoding: chunked 这个 http 头部字段进行分块传输,设置这个字段后会自动产生两个效果:

  • Content-Length 字段会被忽略
  • 基于长连接持续推送动态内容

# HTTP 中如何处理表单数据的提交?

在 http 中,有两种主要的表单提交的方式,体现在两种不同的Content-Type取值:

  • application/x-www-form-urlencoded

    • 其中的数据会被编码成以&分隔的键值对
    • 字符以URL编码方式编码
  • multipart/form-data

    • 请求头中的 Content-Type 字段会包含 boundary,且 boundary 的值有浏览器默认指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe
    • 数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP 头部描述子包体,如Content-Type,在最后的分隔符会加上--表示结束。
    • 每一个表单元素都是独立的资源表述,在实际的场景中,对于图片等文件的上传基本采用 multipart/form-data 而不用 application/x-www-form-urlencoded,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间

Cookie 简介

HTTP 是一个无状态的协议,每次 http 请求都是独立、无关的,默认不需要保留状态信息。但有时候需要保存一些状态,怎么办呢?

HTTP 为此引入了 Cookie。Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在 chrome 开发者面板的 Application 这一栏可以看到)。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。而服务端可以通过响应头中的 Set-Cookie 字段来对客户端写入 Cookie。

生存周期

Cookie 的有效期可以通过 Expires 和 Max-Age 两个属性来设置。

  • Expires即过期时间

  • Max-Age用的是一段时间间隔,单位是秒,从浏览器收到报文开始计算。

若 Cookie 过期,则这个 Cookie 会被删除,并不会发送给服务端。

作用域

关于作用域也有两个属性: Domain 和 path, 给 Cookie 绑定了域名和路径,在发送请求之前,发现域名或者路径和这两个属性不匹配,那么就不会带上 Cookie。值得注意的是,对于路径来说,/表示域名下的任意路径都允许使用 Cookie。

安全相关

如果带上 Secure,说明只能通过 HTTPS 传输 cookie。

如果 cookie 字段带上 HttpOnly,那么说明只能通过 HTTP 协议传输,不能通过 JS 访问,这也是预防 XSS 攻击的重要手段。

相应的,对于 CSRF 攻击的预防,也有 SameSite 属性。

SameSite可以设置为三个值,Strict、Lax和None:

  • 在Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其他网站请求都不能。

  • 在Lax模式,就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能。

  • 在None模式下,也就是默认模式,请求会自动携带上 Cookie。

Cookie 的缺点

  • 容量缺陷。Cookie 的体积上限只有4KB,只能用来存储少量的信息。

  • 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。但可以通过 Domain 和 Path 指定作用域来解决。

  • 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在HttpOnly为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。

在默认情况下,Cookie 是不允许跨域发送的。Cookie 是由浏览器存储和发送的,遵循同源策略(Same-Origin Policy),该策略限制了不同源(域、协议或端口)之间的交互。这意味着在跨域请求中,浏览器不会自动发送包含 Cookie 的请求头。

然而,可以通过一些特定的技术手段来实现跨域发送 Cookie:

跨域资源共享(CORS):使用 CORS,服务器可以在响应中设置特定的响应头,允许跨域请求携带 Cookie。服务器需要在响应头中包含 Access-Control-Allow-Credentials: true,并且在请求头中设置 withCredentials: true。

图像标签或链接:可以通过在页面中插入 标签或创建 链接来实现跨域请求。这些标签和链接可以指向其他域的资源,并且浏览器会自动发送包含 Cookie 的请求。

  1. 同一顶级域名(Top-Level Domain):如果多个域名属于同一顶级域名,例如example.com和subdomain.example.com,浏览器会默认将 Cookie 在这些域名之间共享。这是因为浏览器的同源策略是基于域名的,而不考虑子域名。

  2. 显式设置 Cookie 的域名:在设置 Cookie 时,可以通过设置 domain 属性来指定共享 Cookie 的域名。例如,将 domain 设置为 .example.com,这样所有以 example.com 结尾的域名都可以共享该 Cookie。需要注意的是,设置 domain 属性时需要遵循一些规则,例如域名必须包含至少两个点号(.)。

  3. 服务器设置响应头:服务器可以在响应中设置 Set-Cookie 头部,指定 domain 属性为共享 Cookie 的域名。例如,Set-Cookie: key=value; domain=.example.com。这样浏览器会将该 Cookie 在指定的域名及其子域名之间共享。

# 如何理解 HTTP 代理?

我们知道在 HTTP 是基于请求-响应模型的协议,一般由客户端发请求,服务器来进行响应。

当然,也有特殊情况,就是代理服务器的情况。引入代理之后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份。

那代理服务器到底是用来做什么的呢?

  1. 负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP 都是多少,客户端是不知道的。因此,这个代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台源服务器的负载尽量平均。当然,这样的算法有很多,包括随机算法、轮询、一致性hash、LRU(最近最少使用)等等,不过这些算法并不是本文的重点,大家有兴趣自己可以研究一下。

  2. 保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进行过滤,对非法 IP 限流,这些都是代理服务器的工作。

  3. 缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得而不用到源服务器那里。下一节详细拆解。

相关头部字段

  • Via

    • Via中代理的顺序即为在 HTTP 传输中报文传达的顺序。
  • X-Forwarded-For

    • 字面意思就是为谁转发, 它记录的是请求方的IP地址(注意,和Via区分开,X-Forwarded-For记录的是请求方这一个IP)。
  • X-Real-IP

    • 这个字段始终记录最初的客户端的IP

# HTTP 如何处理大文件的传输?

对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。因此,HTTP 针对这一场景,采取了范围请求的解决方案,允许客户端仅仅请求一个资源的一部分。

如何支持

当然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:

Accept-Ranges: none
1

用来告知客户端这边是支持范围请求的。

Range 字段拆解

而对于客户端而言,它需要指定请求哪一部分,通过 Range 这个请求头字段确定,格式为 bytes=x-y。接下来就来讨论一下这个 Range 的书写格式:

  • 0-499表示从开始到第 499 个字节。
  • 500- 表示从第 500 字节到文件终点。
  • -100表示文件的最后100个字节。

服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回416错误码,否则读取相应片段,返回 206 状态码。

同时,服务器需要添加 Content-Range 字段,这个字段的格式根据请求头中Range字段的不同而有所差异。

具体来说,请求单段数据和请求多段数据,响应头是不一样的。

单段数据

对于单段数据的请求,返回的响应如下:

HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100

i am xxxxx
1
2
3
4
5
6

值得注意的是 Content-Range 字段,0-9表示请求的返回,100表示资源的总大小,很好理解。

多段数据

接下来我们看看多段请求的情况。得到的响应会是下面这个形式:

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes


--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96

i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96

eex jspy e
--00000010101--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这个时候出现了一个非常关键的字段 Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:

  • 请求一定是多段数据请求
  • 响应体中的分隔符是 00000010101

因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上 -- 表示结束。

以上就是 http 针对大文件传输所采用的手段。

# 知识图谱

http

(建议精读)HTTP灵魂之问,巩固你的 HTTP 知识体系 (opens new window)