参考:
1 前言
由于在网络层中的IP协议,它是不稳定的,该层与硬件联系紧密。所以,当路由器宕机、电路不稳定时,会对网络层造成影响。
而在传输层针对上面的问题,有两种处理方式:
- 完全不弥补:即该方式不会去处理网络层出现的问题,网络层传来是什么样,我就按照什么样传输。这种方式使用的协议是UDP协议——无连接不可靠报文传输。
- 完全弥补:该方式会处理网络层出现的错误,比如会有重发的机制。使用TCP协议——面向连接的可靠数据报传递。
下面就再讲解一些关于TCP协议的知识:
1.1 TCP协议
TCP 协议是一个安全的、面向连接的、流式传输协议,所谓的面向连接就是三次握手,对于程序猿来说只需要在客户端调用 connect()
函数,三次握手就自动进行了。先通过下图看一下 TCP 协议的格式,然后再介绍三次握手的具体流程。
在 Tcp 协议中,比较重要的字段有:
源端口:表示发送端端口号,字段长 16 位,2 个字节
目的端口:表示接收端端口号,字段长 16 位,2 个字节
序号(sequence number):字段长 32 位,占 4 个字节,序号的范围为 [0,4284967296]。
- 由于 TCP 是面向字节流的,在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号
- 首部中的序号字段则是指本报文段所发送的数据的第一个字节的序号,这是随机生成的。
- 序号是循环使用的,当序号增加到最大值时,下一个序号就又回到了 0
确认序号(acknowledgement number):占 32 位(4 字节),表示收到的下一个报文段的第一个数据字节的序号,如果确认序号为 N,序号为 S,则表明到序号 N-S 为止的所有数据字节都已经被正确地接收到了。
8 个标志位(Flag):
- CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小;
- ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设为 1.;
- URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关;
- ACK:该位设为 1,确认应答的字段有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设为 1;
- PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议,若设为 0,则先将数据进行缓存;
RST:该位设为 1,表示 TCP 连接出现异常必须强制断开连接; - SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定;
- FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。
窗口大小:该字段长 16 位,表示从确认序号所指位置开始能够接收的数据大小,TCP 不允许发送超过该窗口大小的数据。
2 TCP通信时序
下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。
在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。
两条竖线表示通讯的两端,从上到下表示时间的先后顺序;
注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。
双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出
例如段2的箭头上标着
SYN, 8000(0), ACK 1001, <mss 1024>
,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024。
下面就来看看 三次握手、四次挥手:
2.1 三次握手
三次握手的目的:确定客户端与服务端之间通信的通路是否畅通。也就是建立连接。
Tcp 连接是双向连接,客户端和服务器需要分别向对方发送连接请求,并且建立连接,三次握手成功之后,二者之间的双向连接也就成功建立了。如果要保证三次握手顺利完成,必须要满足以下条件:
- 服务器端:已经启动,并且启动了监听(被动接受连接的一端)
- 客户端:基于服务器端监听的 IP 和端口,向服务器端发起连接请求(主动发起连接的一端)
三次握手具体过程:
第一次握手:
- 客户端:客户端向服务器端发起连接请求将报文中的 SYN 字段置为 1,生成随机序号 x,seq=x
- 服务器端:接收客户端发送的请求数据,解析 tcp 协议,校验 SYN 标志位是否为 1,并得到序号 x
第二次握手:
- 服务器端:给客户端回复数据
- 回复 ACK, 将 tcp 协议 ACK 对应的标志位设置为 1,表示同意了客户端建立连接的请求
- 回复了 ack=x+1, 这是确认序号
- x: 客户端生成的随机序号
- 1: 客户端给服务器发送的数据的量,SYN 标志位存储到某一个字节中,因此按照一个字节计算,表示客户端给服务器发送的 1 个字节服务器收到了。
- 将 tcp 协议中的 SYN 对应的标志位设置为 1, 服务器向客户端发起了连接请求
- 服务器端生成了一个随机序号 y, 发送给了客户端
- 客户端:接收回复的数据,并解析 tcp 协议
- 校验 ACK 标志位,为 1 表示服务器接收了客户端的连接请求
- 数据校验,确认发送给服务器的数据服务器收到了没有,计算公式如下:发送的数据的量 = 使用服务器回复的确认序号 - 客户端生成的随机序号 ===>
1=x+1-x
- 校验 SYN 标志位,为 1 表示服务器请求和客户端建立连接
- 得到服务器生成的随机序号: y
- 服务器端:给客户端回复数据
第三次握手
客户端:发送数据给服务器
将 tcp 协议中 ACK 标志位设置为 1,表示同意了服务器的连接请求
给服务器回复了一个确认序号 ack = y+1
- y:服务器端生成的随机序号
- 1:服务器给客户端发送的数据量,服务器给客户端发送了 ACK 和 SYN, 都存储在这一个字节中
发送给服务器的序号就是上一次从服务器端收的确认序号因此 seq = x+1
服务器端:接收数据,并解析 tcp 协议
- 查看 ACK 对应的标志位是否为 1, 如果是 1 代表,客户端同意了服务器的连接请求
- 数据校验,确认发送给客户端的数据客户端收到了没有,计算公式如下:给客户端发送的数据量 = 确认序号 - 服务器生成的随机序号 ===> 1=y+1-y
- 得到客户端发送的序号:x+1
2.2 四次挥手
为什么要四次挥手:在Linux操作系统中,允许建立连接的双方中,有一方关闭连接 而 另一端不关闭——半关闭状态。
四次挥手是断开连接的过程,需要双向断开,关于由哪一端先断开连接是没有要求的。通信的两端如果想要断开连接就需要调用 close()
函数,当两端都调用了该函数,四次挥手也就完成了。
- 客户端和服务器断开连接 -> 单向断开
- 服务器和客户端断开连接 -> 单向断开
进行了两次单向断开,双向断开就完成了,每进行一次单向断开,就会完成两次挥手的动作。
基于上图的例子对四次挥手的具体过程进行阐述(实际上哪端先断开连接都是允许的):
第一次挥手:
- 主动断开连接的一方:发送断开连接的请求
- 将 tcp 协议中 FIN 标志位设置为 1,表示请求断开连接
- 发送序号 x 给对端,seq=x,基于这个序号用于客户端数据校验的计算
- 被动断开连接的一方:接收请求数据,并解析 TCP 协议
- 校验 FIN 标志位是否为 1
- 收到了序号 x,基于这个数据计算回复的确认序号 ack 的值
- 主动断开连接的一方:发送断开连接的请求
第二次回收:
- 被动断开连接的一方:回复数据
- 同意了对方断开连接的请求,将 ACK 标志位设置为 1
- 回复 ack=x+1,表示成功接受了客户端发送的一个字节数据
- 向客户端发送序号 seq=y,基于这个序号用于服务器端数据校验的计算
- 主动断开连接的一方:接收回复数据,并解析 TCP 协议
- 校验 ACK 标志位,如果为 1 表示断开连接的请求对方已经同意了
- 校验 ack 确认发送的数据服务器是否收到了,发送的数据 = ack - x = x + 1 -x = 1
- 被动断开连接的一方:回复数据
第三次挥手:
- 被动断开连接的一方:将 tcp 协议中 FIN 标志位设置为 1,表示请求断开连接
- 主动断开连接的一方:接收请求数据,并解析 TCP 协议,校验 FIN 标志位是否为 1
第四次挥手:
- 主动断开连接的一方:回复数据
- 将 tcp 协议中 ACK 对应的标志位设置为 1,表示同意了断开连接的请求
- ack=y+1,表示服务器发送给客户端的一个字节客户端接收到了
- 序号 seq=h,此时的 h 应该等于 x+1,也就是第三次挥手时服务器回复的确认序号 ack 的值
- 被动断开连接的一方:收到回复的 ACK, 此时双向连接双向断开,通信的两端没有任何关系了
- 主动断开连接的一方:回复数据