网络协议学习笔记 - Shiina's Blog

网络协议学习笔记

2022 年 11 月 12 日 - 19:53:06 发布
5.9K 字 20分钟
本作品采用 CC BY-NC-SA 4.0 进行许可。
本文距离首次发布已经过去了 545 天,请注意文章的时效性!

网络协议学习笔记

HTTP 1.0

这是一个最基本的网络协议,基本流程是:

  • 客户端建立一个 tcp 链接
  • 在链接上发起一个请求到服务器
  • 服务器返回一个 response
  • 关闭连接

每次来一个请求都要进行一次连接的开启与关闭,这样子有俩问题

  • 性能很差,因为连接开启与关闭都是耗时操作
  • 服务器推送问题,服务器无法再客户端没有请求的情况下主动向客户端推送消息。但很多情况往往都需要服务器在某些事件完成后主动通知客户端。

为了解决这些问题,开发者们引入了 Keep-AliveContent-Length 的属性

Keep-Alive 与 Content-Length

为了解决第一个问题,http 1.0 设计了一个 keep-alive 机制来实现 tcp 链接复用。具体实现是在请求头上加了一个 Connection: Keep-Alive 字段进行处理。服务器收到带有这种字段的请求时,在处理完请求后不会立刻关闭连接,并在其 response 里的头部也加上该字段,然后在该连接上等待客户端的下一个请求。

当然,服务端能开启的连接数有限,如果连接一直不关闭就会把服务器的可开启连接个数消耗掉,所以服务器还有一个 Keep-Alive timeout 参数,当一个连接上在一定时间内没有新的请求进来时,连接就会关闭。

为了解决第二个问题,开发者们又在请求头上添加了这样一个字段: Content-Length 这个字段告诉客户端 http response 响应的内容有多少字节,客户端收到这么多字节后,就知道数据已经发送完毕了。

HTTP 1.1

从前面的 Keep Alive 的机制中可以看出,连接复用是非常重要的一个机制。所以在 http 1.1 之后,就把连接复用变成了一个默认属性,即使不加上 Keep Alive 字段,服务器也会在处理完请求后不关闭连接,除非请求头显式地设置 Connection: Close 属性,服务器才会显式关闭连接。

在 HTTP 1.0 中可以使用 Content-Length 字段,让客户端判断一个请求的响应是否完整接收,但是这个字段计算对于服务器来说比较困难。

因此,在 HTTP 1.1 中采用了 Chunk 机制。具体来说,就是在响应头上加上: Transfer-Encoding: chunked 字段。目的是告诉客户端,响应的 body 是分成了一块块的数据,块与块之间有间隔符,。所有块的结尾都有个特殊标记,这样即使没有 Content-Length 字段,客户端也能判断出来响应的末尾。

下面展示了一个简单的具有 Chunk 机制的 HTTP 响应:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
25
This is the data in the first chunk
1C
and this is the second one
3
con
8
sequence
0

Pipeline 与 Head-of-line Blocking 问题

通过连接复用机制,成功减少了建立连接与关闭连接的开销。但还有个问题,在同个连接上请求是串行的,每次都是客户端发送一个请求,收到响应后再继续下一个请求。这样会导致并发度不够。

因此 HTTP 1.1 引入了 Pipeline 机制,在同一个 tcp 请求上可以在一个请求发出去,响应没有回来之前就发送下一个请求,这样提升了在同一个 tcp 连接上面的处理请求的效率。

不过这种机制有一个致命问题,即 Head-of-line Blocking。原因是:服务器处理请求是并发的,但是客户端接受响应是以队列的形式接受响应,先进先出。如果队列头部的请求发生了延迟,客户端迟迟收不到第一个请求的响应,则会直接堵塞同个 tcp 连接上的所有响应。

为了避免这种问题,大部分浏览器直接把这种机制关闭了。

HTTP 2 出现之前的性能提升方法

有以下四种:

  • Spriting
  • 内联 (Inlining)
  • JS 拼接
  • 请求分片技术

一来多回

有以下几种方法:

  • 客户端定期轮询 (效率低且增加服务器压力)
  • FlashSocket / WebSocket (直接基于 tcp,但是有一定局限性)
  • HTTP 长轮询 客户端发送一个 HTTP 请求,如果服务器有消息就立刻返回,如果没有则先保持这个连接,客户端一直等待该请求返回。在服务器内过了一个约定时间后,如果服务器还没有新消息,服务器就返回一个空消息。客户端收到空消息之后关闭连接。再发起一个新的连接,并重复此过程。
  • HTTP Streaming (即 Transfer-Encoding: chunked

断点续传

客户端在下载大文件时记录数据量的大小,一旦连接中断并重新建立连接后在请求头上加上 Range: first offset - last offset 字段进行数据区间的指定,就可以获得指定区间的数据,

HTTP/2

二进制分帧

HTTP/2 为了解决 HTTP 1.1 队头阻塞的问题,所设计的特性。

头部压缩

HTTP/2 有一个 HPACK 协议和对应的压缩算法进行 头部压缩。

TCP / UDP

众所周知,UDP 传输是不可靠的,而 TCP 是可靠的。什么是不可靠的?

当客户端向服务端发送数据时,有的时候会不可避免地发生以下情况:

  • 丢包(数据包要经过许多的路由节点,其中有的节点发生故障是不可避免的)
  • 时序错乱(每个包都不可能走同一条节点路线到达服务器,因此谁先到与谁后到服务器是无法确定的)

而 TCP 协议是如何做到可靠的? TCP 可靠的意思如下:

  • 数据不丢包
  • 数据包不重复
  • 时序不乱

而 TCP 就是在不稳定的网络路径上通过各种机制去尝试建立了一个 “可靠” 的网络通道。

接下来看看 TCP 是通过什么机制来做到可靠传输的:

  1. ACK + 重发 网络丢包是不可避免的会出现的,如果丢包了该怎么办?重发,服务器每次收到一个包,就对客户端进行确认,告诉客户端已经收到了包,如果客户端在一定时间内没有收到 ACK,则重发数据。
  2. 解决不重复问题 在上面的情况中,只要超过了约定的时间,客户端没收到服务端的 ACK 时就会进行重发,但有时 ACK 已经在网络上了,只是还没到达客户端,如果客户端重发,服务器就好收到重复的包,就需要判重。 服务器怎么判重?之前提到的 ACK 就是一个顺序标记。比如:服务器给客户端回复 ACK=6 ,意思是所有顺序大于或等于 6 的数据包已经收到了,之后凡是再收到这个范围的数据包时,则判定为重复的包,服务器收到后自动丢弃。
  3. 解决时序错乱问题 假设服务器收到了数据包 1 2 3,回复给客户端 (ACK=3)之后,收到了数据包 5 6 7,但是 4 迟迟没有来,那该如何处理?此时服务器会把 5 6 7 先暂时存放着,直到数据包 4 的到来,再给客户端回复 ACK=7,如果数据包不来,则服务器的 ACK 进度会一直留在 3 那里。客户端超时后会把数据包 4 5 6 7 全部重发,到了服务器会通过判重方法去掉重复的包后,回复给客户端(ACK=7)

但是 TCP 真的是 100% 可靠吗?可以看看这篇文章:https://blog.csdn.net/Edward_LF/article/details/124360441

TCP 假连接

从前面的各种操作来看,在客户端和服务端之间并不存在一个可靠的 “物理管道”。只是在逻辑层面通过一定的机制让客户端与服务器之间建立了一个 “可靠的连接”。但并不是真的可靠,只是通过各种机制去保证数据尽量的可靠。

TCP 的三次握手

图上 seq 代表的是发出去的包的编号

  1. 初始状态: 服务端监听某个端口,处于 LISTEN 状态。

  2. 客户端发送TCP连接请求 客户端会随机一个初始序列号seq=x(client_isn), 设置SYN=1 ,表示这是SYN握手报文。然后就可以把这个 SYN 报文发送给服务端了,表示向服务端发起连接,之后客户端处于 同步已发送 状态。

  3. 服务端发送针对TCP连接请求的确认 服务端收到客户端的 SYN 报文后,也随机一个初始序列号(server_isn)(seq=y) 设置ack=x+1, 表示收到了客户端的x之前的数据,希望客户端下次发送的数据从x+1开始。 设置 SYN=1 和 ACK=1。表示这是一个SYN握手和ACK确认应答报文。 最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 同步已接收 状态。

  4. 客户端发送确认的确认 客户端收到服务端报文后,还要向服务端回应最后一个应答报文, 将ACK置为 1 ,表示这是一个应答报文 ack=y+1 ,表示收到了服务器的y之前的数据,希望服务器下次发送的数据从y+1开始。 最后把报文发送给服务端,这次报文可以携带数据,之后客户端处于 连接已建立 状态。 服务器收到客户端的应答报文后,也进入连接已建立 状态

三次握手的好处:

  1. 防止历史连接 在网络拥堵的情况下,客户端第一次握手的 SYN 包迟迟没能到达服务器,那么客户端会连续发送 SYN 建立连接的报文,就有可能出现后发的报文比早发的报文先到达服务器的情况。
    • 两次握手时就不能判断当前连接是不是历史连接,导致错误发生
    • 三次握手时,客户端可以根据自身上下文判断出这个报文是一个历史的连接,如果是则客户端发送 RST 报文给服务端,终止本次连接。
  2. 避免浪费资源 如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN 。
    • 如果是三次握手,第三次握手时服务器可以得到客户端的ack,知道连接已成功建立。
    • 如果没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。第三次握手带着 ACK 回去就代表着确认成功建立连接,可以进行数据发送。

TCP 四次挥手

流程如下:

  1. 主动方向对方发送一个 FIN 结束请求报文,并设置序列号和确认号,随后主动断开方进入FIN_WAIT1状态,这表示主动断开方已经没有业务数据要发给对方了,准备关闭连接了。

  2. 被动方收到FIN断开请求后会发送一个ACK响应报文,表明同意断开请求。随后被动断开方就进入CLOSE-WAIT状态(等待关闭状态),此时若被动断开方还有数据要发送给主动方,主动方还会接受。被动方会持续一段时间。主动方收到ACK报文后,由FIN_WAIT_1转换成FIN_WAIT_2状态。

  3. 被动断开方的CLOSE-WAIT(等待关闭)结束后,被动方会向主动方发送一个FIN+ACK报文,表示被动方的数据都发完了。然后被动方进入LAST_ACK状态。

  4. 主动断开方收到FIN+ACK断开响应报文后,还需进行最后确认,向被动方发送一个ACK确认报文,然后主动方进入TIME_WAIT状态,在等待完成2MSL时间后,如果期间没有收到被动方的报文,则证明对方已正常关闭,主动断开方的连接最终关闭。被动方在收到主动方第四次挥手发来的ACK报文后,就关闭了连接。

MSL 的值默认为 120s。

一些 QA:

  1. 为什么客户端在第四次挥手后还会等待2MSL 等待2MSL是因为保证服务端接收到了ACK报文,因为ACK报文可能会丢失,如果服务端没接收到ACK报文的话,会重新发送FIN报文,只有当客户端等待了2MSL都没有收到重新发送的FIN报文时,才表示服务端是正常接收到了ACK报文,这个时候客户端就能关闭了。

参考部分文章

无意义的定型文

“本手、妙手、俗手”是围棋的三个术语。 本手是指合乎棋理的正规下法; 妙手是指出人意料的精妙下法; 俗手是指貌似合理,但从全局看通常会受损的下法。 但即便是如此精通棋术的我,看到宇佐紀小姐时,我就好像迷失了方向,感觉我的棋盘发生了天翻地覆的变化,变得难以捉摸,无从下手。 这一手棋…该怎么下,该如何下呢。 当我用了一个通宵的时间来想是什么原因的时候,我看着我自己这身经百战的双手,又想起油批那迷人的微笑,终于想明白为什么了。 在遇见宇佐紀小姐的那天,我便有了那怦然心动的感觉。

哪有什么“本手、妙手、俗手”? 只有——“宇佐紀小姐,我想牵起你的手​。”

个人信息
avatar
Shiinafan
文章
38
标签
52
归档
4
导航