TCP协议
TCP全称为"传输控制协议",要对数据的传输进行一个详细的控制。
特点
- 面向连接的
- 可靠性
- 字节流
TCP的协议段格式
- 源/目的端口:表示数据从哪个进程来,到哪个进程
- 4位首部长度:表示该TCP头部有多少字节(注意它的单位是4字节),因为TCP报头的范围为[20,60],所以4位首部长度范围就是[5,15]
- 16位校验和:校验报文是否符合要求,不符合直接丢弃
6位标记位
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示服务端立刻将TCP缓冲区的数据读走
- RST:对方要求重新建立链接
- SYN:请求建立连接,我们携带SYN标识的称为同步报文段
- FIN:通知对方,本端将断开连接。
我将从下面的几个场景中来阐述一下TCP中协议段格式的每个含义。
ACK确认应答机制
当我们主机A发送数据的时候,主机B需要给主机A发送"已收到"(ACK),这时主机A开知道数据已经发送到了主机B。但如果主机B想要发数据给主机A,那么就又要需要一次write了,所以效率其实是不高的,所以有了后面的捎带应答。
捎带应答
我们发现,很多情况下,客户端服务器在应用层也是 "一发一收"的.意味着客户端给服务器说了 "How are you",服务器也会给客户端回一个 "Fine,thank you";那么这个时候 ACK就可以搭顺风车,和服务器回应的 "Fine, thank you"一起回给客户端
32位序号和32位确认序号
32位序号:在建立连接的时候,计算机会随机生成一个随机数作为初始值,当传送一次数据的时候,会累加上数据的大小。 用来解决网络中乱序的问题
32位确认序号:发送端接受到确认应答后,可以认为这个序号之前的序号全部被收到了。用来解决网络中丢包的问题
三次握手与四次挥手
TCP建立连接
- 一开始的时候,客户端和服务端都是CLOSED状态,然后服务端去监听某个端口,然后处于LISTEN状态
- 客户端就会去建立连接,然后会先初始化序号(clinet_isn),将SYN标记位置为1,然后将整个报头发送过去,之后客户端除以SYN_SENT状态
- 服务端收到了来自客户端携带的SYN报头后,也会先初始化自己的序(server_isn)号,然后将确认应答设置为clinet_isn+1,,将TCP报头中ACK和SYN设置为1,然后将报文发送给客户端。之后状态为SYN_REVD
- 最后客户端收到了来自服务端的数据,将TCP中ACK置为1,将确认序号设置为server_isn+1,然后将报文发送给服务端(这次可以携带数据),之后客户端状态为ESTABLISHED。
从上面的三次握手中可以看到,第二次服务端返回ACK和SYN的时候,其实就用到了捎带应答。 而且注意 前两次握手是不能发送数据的,而第三次是可以的。
四次挥手
- 客户端首先会先发送携带FIN的报文给服务端,表示要断开连接。之后状态设置为FIN_WAIT_1
- 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
- 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
- 这时,服务端可能需要处理数据。之后会发送FIN报文,之后状态为LAST_ACK
- 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
- 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
- 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
2MSL:刚好是客户端发送数据到服务端的时间 + 服务端发送数据回到客户端的时间(这样就确保再2MSL之后,再无数据处理了)
其实你看,三次握手因为捎带应答,合并成了一次,而四次挥手中,不合并是因为,服务端可能还会发送数据给客户端。
之所以被称为四次挥手,你看图中,是不是客户端和服务端都会发送FIN和ACK来表示自己要断开连接,一来一回刚好4次。
为什么是三次握手?1次可以吗?2次可以吗?4次可以吗?
先来回答一下为什么是三次握手
- 需要确保通信是正常的。3次刚好可以验证TCP全双工
- 确保双方OS是健康的,且愿意通信(各自发送ACK)
再聊一聊如果TCP1次和2次握手会发生什么事
- 我们要知道建立连接是需要消耗资源的,所以如果有人恶意的发送大量SYN报文呢,并且不想接受数据,这样看,1次和2次握手是不是很不合理呢?
4次握手可以吗?
- 当然可以,但是没必要,因为3次握手已经是最少可靠的连接建立了,并且保证了全双工。
超时重传机制
TCP要保证所有的数据包可以达到对方,就必须要有超时重传的机制。
- 主机A发送数据给主机B,但由于网络的原因,数据没有到达主机B
- 主机A在一个特定的时间间隔(这个时间间隔在不同的内核版本中是不同的)内没有收到主机B的数据,就会重新发送(注意重新传的SYN报文是一样的)。
但是,主机 A未收到 B发来的确认应答,也可能是因为 ACK丢失了
因此主机 B会收到很多重复数据.那么 TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉.
所以就有了序号,可以做到去重的效果。
那么最后一个问题来了??
超时的时间怎么确定呢??
- 最理想的情况下,找到一个最小的时间,保证 "确认应答一定能在这个时间内返回".
- 但是这个时间的长短,随着网络环境的不同,是有差异的
- 如果超时时间设的太长,会影响整体的重传效率
- 如果超时时间设的太短,有可能会频繁发送重复的包
所以TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.
- Linux中(BSD Unix和 Windows也是如此),超时以 500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后,仍然得不到应答,等待 2*500ms后再进行重传.
- 如果仍然得不到应答,等待 4*500ms进行重传.依次类推,以指数形式递增.
重发的次数和操作系统有关。
在ubuntu20.04下是6次
如何理解面向字节流
在创建一个TCP的socket,内核会创建一个发送缓冲区 和一个接受缓冲区
- 调用write,数据会先写入发送缓冲区中
- 如果发送的字节流太长,会被拆分成多个TCP的数据发出
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者等待时机发送出去
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用 read 从接收缓冲区拿数据
- 另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一
个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
就是由于这个缓冲区的原因,TCP的读和写不再需要一一匹配,例如
- 写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节
- 读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次