1

理清 HTTP 之下的 TCP 流程,让你的 HTTP 水平更上一层

 2 years ago
source link: https://developer.51cto.com/article/706959.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
理清 HTTP 之下的 TCP 流程,让你的 HTTP 水平更上一层-51CTO.COM
理清 HTTP 之下的 TCP 流程,让你的 HTTP 水平更上一层
作者:神说要有光 2022-04-20 07:52:01
我们平时都是分析 HTTP 请求响应,TCP 对我们来说看不见摸不着的,理解的模模糊糊。所以今天我们用 WireShark 抓了下 TCP 的包,来理清了 TCP 和 HTTP 的关系。

57b9d37054efbd19beb889ccd85976658c3e43.png

大家都知道 HTTP 的底层是 TCP,但是可能仅限于知道,并不是真正理解它们的关系。

平时我们用 chrome devtools 的 Network 工具也只是能分析 HTTP 请求:

f69ec5938a05ca027353833889e26586bc1820.png

TCP 层的东西看不见摸不着的,所以对它的理解也模模糊糊。

那怎么能看到 TCP 层的数据包来理清 TCP 和 HTTP 的关系呢?

这里推荐一个抓包工具 WireShark,它能抓取 TCP 层的包:

632f0e149e52d0f07a8819de62c17f1bc07578.png

今天我们就用它来抓包分析下 TCP 和 HTTP 吧!

首先,我们准备这样一段服务端代码:

const express = require('express')
const app = express()
app.get('/', function (req, res) {
  res.setHeader('Connection', 'close')
  res.end('hello world');
})
app.listen(4000)

用 express 起了一个服务,监听 4000 端口,处理路径为 / 的 get 请求,返回 hello world 的响应体,并设置 Connection: close 的 header。

浏览器访问下:

b8ec129410bc232c4cc331271ba99a337de341.png

header 和 body 都符合预期。

那 TCP 层都做了什么呢?

我们用 WireShark 抓包分析下:

打开 WireShark 后会看到有个设置按钮:

b524a530023b156b62e3459182e1c3672fcb14.png

因为我们访问的是 localhost: 4000,所以这里选择本地回环地址那个虚拟网卡,并输入抓包过滤条件为 port 4000:

41f8cde09c956f9963d37189d71d0a0cc415eb.png

点击 start 开始录制,然后刷新一下浏览器:

这样就能看到抓到的 TCP 数据包:

52cfe4406218d6e23513159ae3535fdb761011.png

我们一一分析下。

在分析之前需要了解一些 TCP 基础知识:

TCP 的头部是这样的:

c5118803072495d44bb805ed0df2cdb254393e.png

TCP 是从端口到端口的传输协议,所以开始是源端口和目的端口。

接下来是序列号(sequence number),表示当前包的序号,后面是确认的序列号(acknowledgment number),表示我收到了序号为 xxx 的包。

然后红框标出的部分是 flags 标识位,通过 0、1 表示有没有:

这里我们只会用到其中的 SYN、ACK、FIN:

  • SYN:请求建立一个连接(说明这是链接的开始)。
  • ACK:表示 ack number 是否是有效的。
  • FIN:表示本端要断开链接了(说明这是链接的结束)。

有了这些,我们就知道怎么区分 TCP 链接的开始和结束了。

再看一下抓到的包:

077b77f0189b38d9fd967564fe64a8a3c61a1f.png

有 SYN 标志位的是连接的开始,有 FIN 标志位的是连接的结束,所以我们分为 3 段来看:

首先是连接开始的部分:

83a84409720f50216bb52851242bfb13f0aaef.giff4804ec269a06299bc9466ec454bf14d544fcb.png

大家听过 TCP 的三次握手么?说的就是这个。

其中有一个端口是 4000,这个是服务的端口,那另一个端口 57454 明显就是浏览器的端口。

首先是浏览器向服务器发送了一个 SYN 的 TCP 请求,表示希望建立连接,序列号 Seq 是 0。

严格来说,序列号的相对值是 0,绝对值是 2454579144。

76e7bd42892a320545315641bfcd3232071830.gife64891e67b1ae363ce365606485cb64274475c.png

然后服务器向浏览器发送了一个 SYN 的 TCP 请求,表示希望建立连接,ACK 是 1,代表现在的 ack number 是有效的:

43a32b081b50b172472497fa7a7ce34adeeae2.png

这里 ack number 的相对值是 1,绝对值是 2454579145,不就是上个 TCP 数据包的 seq 加 1 么?

TCP 连接中就是通过返回 seq number + 1 作为 ack number 来确认收到的。

然后又返回了一个 seq number 给浏览器,相对值是 0, 绝对值是 2765691269。

浏览器收到后返回了一个 TCP 数据包给服务器,ack number 自然是 2765691270,代表收到了连接请求。

39a1b7b54d1760cc4cd6987bead037d237acff.png

这样浏览器和服务器各自向对方发送了 SYN 的建立连接请求,并且都收到了对方的确认,那么 TCP 连接就建立成功了。

这就是 TCP 三次握手的原理!

a4e73a10096b98b41c1807ba41dbf6b377edc4.png

趁热打铁来看下四次挥手的部分:

a86dd446358ebafbcde479d92447846bc2e983.gif587fd676064403bab8f578fd1d431456bddd4d.png

浏览器向服务器发送了有 FIN 标志位的数据包,表示要断开连接,然后服务端返回了 ACK 的包表示确认。

之后服务端发送了 FIN 标志位的数据包给浏览器,表示要断开连接,浏览器也返回了 ACK 的包表示确认。

这样就完成了四次挥手的过程。

当然,具体确认的还是靠 ack number = seq number + 1 来实现的,和上面的一样,就不展开了:

557b23a168edc7bfbd9295d714982a0ed10619.gifb2ae82274f0ccf39693361e49cfa0dc73cceda.gif53fbc7a05beaad94d5a2457e4f43f76142d818.png

07b136b54f4a734f3473698e4aca8883b4173a.png

我们通过抓包理清了 TCP 连接建立和连接的过程。

08998784603dd355c5c68967813c63fc0bc8d3.png

那么为什么握手是三次,挥手是四次呢?

因为挥手是一个 FIN,一个 ACK,一个 FIN + ACK,一个 ACK:

41a5e3932454e05809358298be2144a715e851.gifd735444897f60a116b8753558374d98bafb789.png

而握手是一个 SYN,一个 ACK + SYN,一个 ACK:

39ad775166c8cae9f2b9998f0c730ec2ddde7c.gif8274a9e05799231e76f161df23d3c1694cdda5.png

不过是因为握手时把 ACK 和 SYN 合并到一个数据包了而已。

那挥手时能合并成三次么?

不能!因为有两个 ack number,怎么合并,冲突了,而握手时只有一个 ack number,自然可以合并。

618b6ca56ddcf6e72be20904da1045bfda9ca6.png

接下来再来看下连接建立后的 http 请求和响应吧:

d746c4e01e3fd7831831137af404bb88544d26.png

其实一次 HTTP 请求响应会有四个 TCP 数据包,其中两个数据包与滑动窗口有关,这里先不展开了。

我们就看下 HTTP 的那两个包吧:

请求的 seq 是这样的:

8790ae00399ea98fbf96326cbd754a3b62cfbf.png

而响应的 ack 是这样的:

59fabb20511abc08d2c020226ec7d849a66ef9.gifa4ccd5b74eed70da8202643a9a30cb41d0aabb.png

相对值是 ack number = seq number + 1 没错,但是绝对值不是:

绝对值 2454579855 = 2454579145 + 710,也就是 ack number = seq number + segment len。

这些细节暂时不用深究。

总之,我们知道了HTTP 的请求和响应是通过序列号关联在一起的。

就算同一个 TCP 链接并行发送多个 HTTP 的请求和响应,它们也能找到各自对应的那个。就是通过这个 seq number 和 ack number。

这里为啥链接建立了发送了一个请求就断掉了呢?

我刷新浏览器,请求了两次,发现经历了两次连接的建立、http 请求响应、连接断开:

d607441127b9d6a82859476526b72748a64a24.png

这是因为我设置了 Connection:close 的 header,它的作用就是一次 http 请求响应结束就断开 TCP 链接。

3257d34019cfbb5cb83619235cdc2c186cfa59.gifb20e0153978a4d0ef82093d76d8244d6f86a21.png

我们改成 HTTP 1.1 支持的 keep-alive 试试:

15363e0858368e7f5b220161ee01540d90252f.giff9f0935046e4c47bcc7845868660f282a607be.png

设置 Connection 为 keep-alive,然后设置 keep-alive 的细节为 timeout 10 ,也就是 10s 后断开。

重启服务器,再刷新下浏览器试试:

f945b888532b1b34ab795067deba1cd6691586.png

可以看到在一个 TCP 连接内发送了多次 http 请求响应。(通过 SYN 开始,FIN 结束)

这就是 keep-alive 的作用。

细心的同学会发现只是浏览器向服务器发送了 FIN 数据包,服务器没有发给浏览器 FIN 数据包。

这是因为 keep-alive 的 header 只是控制的浏览器的断开连接的行为,服务器的断开连接逻辑是独立的。

这样,我们就理清了 HTTP 在 TCP 层面的流程,连接的建立、断开,请求响应,还有 keep-alive。

我们平时都是分析 HTTP 请求响应,TCP 对我们来说看不见摸不着的,理解的模模糊糊。

所以今天我们用 WireShark 抓了下 TCP 的包,来理清了 TCP 和 HTTP 的关系。

TCP 是从一个端口到另一个端口的传输控制协议,TCP header 中有序列号 seq number、确认序列号 ack number,还有几个标志位:

  • SYN 标志位代表请求建立连接
  • ACK 标志位代表当前确认序列号是有效的。
  • FIN 标志位代表请求断开连接

然后我们抓了 localhost:4000 的包分析了下 HTTP 请求的 TCP 流程,理清了三次握手(SYN、SYN + ACK、ACK),四次挥手(FIN、ACK、FIN + ACK、ACK)的连接建立、断开的流程。知道了为什么不能三次挥手(因为两个 ACK 冲突了)

然后还理清了同一个 TCP 连接传输的多个 HTTP 请求响应式通过 seq number 和 ack number 来关联的。

之后我们分别测试了 Connection:close 和 Connection:keep-alive 的情况,发现确实 keep-alive 能减少频繁的连接建立和断开,能复用同一个 TCP 链接。

HTTP 是通过 TCP 完成端口到端口的数据传输的。一个 TCP 连接可以传输多个 HTTP 请求、响应。请求和响应的关联是通过 TCP 包的序列号 seq。

理清了 TCP 和 HTTP 的关系,你是否对 HTTP 的理解更深了呢?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK