0%

tcp之三次握手

[TOC]

过程

1
2
3
4
5
6
7
# 以下的分别使用 tcp 报文帧中的 标志位, 序列号位, 确认号位

Client --> 置SYN标志 序列号 = J,确认号 = 0 ----> Server

Client <-- 置SYN标志 置ACK标志 序列号 = K, 确认号 = J + 1 <-- Server

Clinet --> 置ACK标志 序列号 = J + 1,确认号 = K + 1 --> Server

链接建立后,接下来Client端发送的数据包将从J + 1开始,Server端发送的数据包将从K + 1开始,这里要说明的是:建立链接时,Client端宣称自己的初始序列号是J,Server端宣称自己的初始序列号是K,但是建立连接后的数据包却各自中初始序列号+1开始,这是因为SYN请求本身需要占用一个序列号

TCP 的定义

很多人尝试回答或者思考这个问题的时候其实关注点都放在了三次握手中的三次上面,这确实很重要,但是如果重新审视这个问题,我们对于『什么是连接』真的清楚?只有知道连接的定义,我们才能去尝试回答为什么 TCP 建立连接需要三次握手。

The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.

RFC 793 - Transmission Control Protocol 文档中非常清楚地定义了 TCP 中的连接是什么,我们简单总结一下:用于保证可靠性和流控制机制的信息,包括 Socket、序列号以及窗口大小叫做连接

所以,建立 TCP 连接就是通信的双方需要对上述的三种信息达成共识,连接中的一对 Socket 是由互联网地址标志符和端口组成的,窗口大小主要用来做流控制,最后的序列号是用来追踪通信发起方发送的数据包序号,接收方可以通过序列号向发送方确认某个数据包的成功接收。

为什么需要三次握手

这篇文章主要会从以下几个方面介绍为什么我们需要通过三次握手才可以初始化 Sockets、窗口大小、初始序列号并建立 TCP 连接:

  • 通过三次握手才能阻止重复历史连接的初始化;
  • 通过三次握手才能对通信双方的初始序列号进行初始化;
  • 讨论其他次数握手建立连接的可能性;

历史连接

最大的问题是发起创建连接的一方A在发出syn之后没有收到ack(网络原因等),它就会再发出一个syn,那么接受方B其实收到了两个 syn并对两个syn 发出了确认,那么实际上其中一个syn已经是历史连接了。TCP 选择使用三次握手来建立连接并在连接引入了 RST 这一控制消息,接收方当收到请求时会将发送方发来的 SEQ+1 发送给对方,这时由发送方来判断当前连接是否是历史连接

  • 如果当前连接是历史连接,即 SEQ 过期或者超时,那么发送方就会直接发送 RST 控制消息中止这一次连接;
  • 如果当前连接不是历史连接,那么发送方就会发送 ACK 控制消息,通信双方就会成功建立连接;

初始化序列号

另一个使用三次握手的重要的原因就是通信双方都需要获得一个用于发送信息的初始化序列号,作为一个可靠的传输层协议,TCP 需要在不稳定的网络环境中构建一个可靠的传输层,网络的不确定性可能会导致数据包的缺失和顺序颠倒等问题,常见的问题可能包括:

  • 数据包被发送方多次发送造成数据的重复;
  • 数据包在传输的过程中被路由或者其他节点丢失;
  • 数据包到达接收方可能无法按照发送顺序;

为了解决上述这些可能存在的问题,TCP 协议要求发送方在数据包中加入『序列号』字段,有了数据包对应的序列号,我们就可以:

  • 接收方可以通过序列号对重复的数据包进行去重;
  • 发送方会在对应数据包未被 ACK 时进行重复发送;
  • 接收方可以根据数据包的序列号对它们进行重新排序;

除此之外,网络作为一个分布式的系统,其中并不存在一个用于计数的全局时钟,而 TCP 可以通过不同的机制来初始化序列号,作为 TCP 连接的接收方我们无法判断对方传来的初始化序列号是否过期,所以我们需要交由对方来判断,TCP 连接的发起方可以通过保存发出的序列号判断连接是否过期,如果让接收方来保存并判断序列号却是不现实的,这也再一次强化了我们在上一节中提出的观点 —— 避免历史错连接的初始化。

TCP 控制位

控制位

ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。

PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。

RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。

SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。

FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。

数据传输中的序列号及确认号

  • 序号(Sequence Number)
    • 也称为序列号,长度为32位,序号用来标识从TCP发送端向接入端发送的数据字节流进行编号,可以理解成对字节流的计数。例如一个报文段的序号为 55555,此报文段数据部分共有 11 字节,则下一个报文段的序号为 55566。
  • 确认号(ack Number)
    • 指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。比如 client 发送的确认号是 22222,那么 server回复的 序号就是 22222。
    • 确认号还有一个更重要的功能就是对消息的确认,它等于请求的 序列号 + 数据包的长度(表示收到了这个序列号的这么多数据)
1
2
3
4
5
6
7
# 在 tcp 的数据传输过程中,一次数据请求,至少需要发送三个tcp报文,分别是 请求报文,响应报文,收到响应后的ack报文。
# TODO 这个还没有抓包确认
Client --> 置PSH标志,置ACK标志 序列号 = 55555, 确认号 = 22222,数据包长度 = 11 ---> Server

Client <-- 置ACK标志,序列号 = 22222, 确认号 = 55566 (=55555 + 11),数据包长度 = 22 <--- Server

Client --> 置ACK标志,序列号 = 55566, 确认号 = 22244(=22222+22),数据包长度 = 0 ---> Server

让我们来抓包看看

注意wireshark 入口不要选 wlan, 要选 loopback traffic capture

解决抓不到包的问题

今天将自己的电脑既作为客户端又作为服务端进行一个程序的测试,想着用WireShark来抓包分析一下问题,但由于WireShark只能抓取经过电脑网卡的包,由于我是使用localhost或者127.0.0.1进行测试的,流量是不经过电脑网卡的,所以WireShark无法抓包,一番查找之下找到了解决方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
route print

# 1. 以管理员身份打开命令提示符

# 2. 输入 route add 本机ip mask 255.255.255.255 网关ip
PS C:\WINDOWS\system32> route add 10.23.101.135 mask 255.255.255.255 10.23.101.1
操作完成!

# 2.2 如果不知道本机ip和网关ip,可以在命令行输入ipconfig查看
无线局域网适配器 WLAN:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::3d96:449b:3c6f:322%12
IPv4 地址 . . . . . . . . . . . . : 10.23.101.135
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 10.23.101.1

# 3. 将我们程序里面的localhost或者127.0.0.1替换成本机ip

# 4. 使用WireShark即可抓到本地包
# tcp and ip.addr==10.23.101.135 and tcp.port==6666

# 5. 在测试完之后,使用route delete 本机ip mask 255.255.255.255 网关ip来删除我们上面的更改,不然我们本机的所有报文都会先经过网卡再回到本机,会比较消耗性能。
route delete 10.23.101.135 mask 255.255.255.255 10.23.101.1


route add 10.248.0.0 mask 255.255.0.0 10.248.108.1
route delete 10.248.0.0 mask 255.255.0.0 10.248.108.1

mac

注意wireshark入口不要选wifi,要选择 loopback:lo0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 查看本机ip
percy@GauguindeMacBook-Pro go-mod % ifconfig | grep inet
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
inet6 fe80::aede:48ff:fe00:1122%en3 prefixlen 64 scopeid 0x4
inet6 fe80::105d:dc8c:9235:2d45%en0 prefixlen 64 secured scopeid 0x6
inet 192.168.0.103 netmask 0xffffff00 broadcast 192.168.0.255
inet6 fe80::3001:5bff:fe8f:87d2%awdl0 prefixlen 64 scopeid 0xb
inet6 fe80::3001:5bff:fe8f:87d2%llw0 prefixlen 64 scopeid 0xc
inet6 fe80::246c:eb89:6f5d:7bdc%utun0 prefixlen 64 scopeid 0xd
inet6 fe80::3d0c:86ef:9ea0:acc9%utun1 prefixlen 64 scopeid 0xe
inet6 fe80::7d55:b5c7:301c:8a04%utun2 prefixlen 64 scopeid 0xf
inet6 fe80::a6cd:6877:6f19:b84d%utun3 prefixlen 64 scopeid 0x10

# 查看默认网关
percy@GauguindeMacBook-Pro go-mod % route get default | grep gateway
gateway: 192.168.0.1



# 添加路由

$ sudo route -v add ip gateway [-netmask net-mask-ip]
# -net 参数可以指定添加一个网段
$ sudo route -v add -net 128.192 gateway
# -host 参数可以指定添加一个具体的地址
$ sudo route -v add -host 128.192.214.29 gateway

sudo route -v add -host 192.168.0.103 192.168.0.1
# 删除路由
sudo route -v delete ip
sudo route -v delete 192.168.0.103


# 查看路由信息
percy@GauguindeMacBook-Pro go-mod % netstat -r
Routing tables

Internet:
Destination Gateway Flags Netif Expire
default 192.168.0.1 UGSc en0
127 127.0.0.1 UCS lo0
127.0.0.1 127.0.0.1 UH lo0
169.254 link#6 UCS en0 !
192.168.0 link#6 UCS en0 !
192.168.0.1/32 link#6 UCS en0 !
192.168.0.1 50:f:f5:88:95:70 UHLWIir en0 1172
192.168.0.103/32 link#6 UCS en0 !
224.0.0/4 link#6 UmCS en0 !
224.0.0.251 1:0:5e:0:0:fb UHmLWI en0
239.255.255.250 1:0:5e:7f:ff:fa UHmLWI en0
255.255.255.255/32 link#6 UCS en0 !

wireshark 简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 过滤指定ip和端口, 这里的10.23.101.135 是我机器的端口
tcp and ip.addr==10.23.101.135 and tcp.port==6666


# 一些语法
# ip
ip.src eq 192.168.1.107 or ip.dst eq 192.168.1.107
# port
tcp.port == 80
tcp.port eq 2722
tcp.port eq 80 or udp.port eq 80
tcp.dstport == 80 // 只显tcp协议的目标端口80
tcp.srcport == 80 // 只显tcp协议的来源端口80
udp.port eq 15000

tcp 包分析

tcp 握手和挥手包分析

测试代码 go-mod/netx/tcp_demo/client.go, testConn

BaAtlq.md.png

tcp 连接后立即发送一个请求包的分析

测试代码 go-mod/netx/tcp_demo/client.go, testSendOne

这里可以看到, 三次握手建立连接造成的后果就是,HTTP 请求最少必须在一个 RTT(从客户端到服务器一个往返的时间)后才能发送。最少的请求就是握手的最后一个 ack 包和请求包合并。

BaAw0U.md.png

tcp 连接后睡眠一秒,然后两次请求的分析

测试代码 go-mod/netx/tcp_demo/client.go, testSendTwo

BaA2X6.md.png

参考

最重要的参考: 为什么 TCP建立连接需要三次握手

TCP序列号和确认号详解

一次http完整的请求tcp报文分析

windows 下 WireShark如何抓取本地localhost的包

wireshark filter