【Linux从青铜到王者】关于tcp协议你不知道的事

前言

首先我们应该知道协议=统一规定的的结构化字段

序列化就是将结构化字段里的数据转换成字节流

反序列化就是将字节流里的数据放进结构化字段

只要协议定好了,序列化和反序列化也就很相对轻松了,不同设备之间就可以通过字节流传输数据了,不再使用传结构体这种受平台和语言限制的方法了

报文=报头+有效载荷

很多教材画图的时候都是只标个数据或者ACK,其实每次传递的都是一个完整的报文(只是可能没有有效载荷罢了)

tcp报头如下所示

tcp的特性(关于加粗部分都是需要我们重点去体会 的)

tcp是面向字节流

tcp是具有可靠性传输控制协议

我们将从外到内一层一层剥开tcp协议

16位端口号

16位源端口号和目的端口号就不多赘述了,通过ip+端口号可以标识网络世界里的唯一进程

因为一台主机的ip地址是唯一的

一台主机的端口号也是唯一的,一个端口号不能被多个进程绑定,但是一个进程可以绑定多个port

我们知道网络分为应用层,传输层,网络层,数据链路层,数据是从上往下不断根据每一层的协议添加不同协议的报头的

所以有两个问题是每个协议都要解决的

  • 1.报头和有效载荷的分离
  • 2.有效载荷向上交付的问题

我们先来解决第一个问题——数据粘包

除选项以外的报头数据占20个字节,这是规定好的?那么报头就只有20个字节吗?当然不是,下面还有个选项

从上图tcp报文图片中可以看到4位首都长度——0000-1111,只能表示0-15,还没有20个字节大呢,所以首都长度的基本单位大小其实是4字节,这样包括的范围即【0,15*4】,也就是60个字节

所以tcp报头的长度是动态的,最多可以为60个字节,最少就是20个字节

将报头里的首都长度读取出来,就知道报头有多大,读取对应字节大小的内存即可,然后通过指针的类型转换就可以读取报头的数据,也就实现了报头和有效载荷的分离

有效载荷向上交付的问题

也就是有效载荷要交给谁的问题,通过报头里的16位目的端口号即可知道有效载荷要传给哪个进程

接下来我们要进行标志位的学习,但是之前要先穿插一点tcp的基本知识

tcp具有发送缓冲区和接收缓冲区(我们将缓冲区认定为字节数组!!!)

因为tcp是全双工的,可以同时支持读写,如果只有一个缓冲区,那就乱套了,数据很可能不一致

确认应答机制

tcp每发送一个数据,如果对方收到了,都要进行一次应答

应答就是接收方再发送一个报文给回发送方,报文的ACK字段被设为1

受到应答就表示应答之前的数据已经送到了

如果有应答,对于发送方,就能保证上一条信息对方已经收到!!!

只要收到应答,就能100%保证历史最近一条数据发送是100%被对方收到

注意:

最新一条消息不保证一定能被接收到,因为没有应答

确认应答机制体现了tcp的可靠性

A------>B

A向B发送了一个数据,B如果收到了,收到后要给A一个应答——也是一个完整的报文,只是里面的ACK标志位被置1,不携带有效数据

16位窗口大小

A应用层数据向下交付给tcp的时候是拷贝到A的tcp的发送缓冲区,发给B的接受缓冲区,B的应用层从B的tcp接收缓冲区提取数据

但是这里有个问题,如果B的应用层一直不拿数据,B的接收缓冲区一直被写入数据,那么迟早会满

如果B的接收缓冲区满了A还继续发,A的数据到B的接收缓冲区后才知道,A的数据要怎么处理呢?直接废弃吗??两个主机的进程之间通信要通过很多机器和协议,这样不是很浪费时间吗,效率一点都不高吗?

所以发送方得知道接收方的存储能力,也就是要知道接收方的接收缓冲区中,剩余空间的大小

通过报头里的16位窗口大小字段(2^16),所以B接收数据后,在应答的报文里将自己剩余空间大小写入报文中,再发回给A,这样A就知道了B的剩余大小,下次发送数据就不会超过这个范围了

但是这里还存在问题?B满的时候,A不再发了,那到时候B有空间了,A要怎么知道呢

当B满的时候,A虽然不会再发带有有效载荷的报文,但是会发一些试探报文,检测B的空间是否仍是满的并且督促B快点向上交付(与PSH标记位有关)

32位序号和32位确认序号

通过上文的理解,大家心中的tcp通信应该是这张图所示

该图所示是单行发送的,一来一回也就是串行,感觉效率很低啊

所以将发送时间进行重叠,也就是一次可以发多个报文,又有确认应答机制,这样就同时保证了效率性和可靠性

保证数据的按序到达(tcp可靠性)——(按序-队列)

tcp一次收到一个连接里的多个报文,而每个报文的传输路径可能不一样,所以接收方收到文件的顺序可能是乱序的,tcp又是字节流式的,乱序是不可靠的一种体现!!!

通过32位序号解决,32位序号也可以称为起始序号,标志该报文的数据从发送缓冲区的哪个下标开始,接收方通过32位序号排序就可以保证接收的有序性了!!!

32位确认应答序号

为了解决历史发送的哪些报文已经被对方接收到的问题

假如一方接受到ACK报文,并且确认应答序号为2001,证明2001之前的数据已经被接收到了

确认序号之前的所有报文已经被对方全部收到(超级重点!!!)

为什么要这么规定??允许少量应答进行丢失

例子:

A给B发送的序号为100,B收到后,返回确认序号为101的报文(假设只发送一个字节)

表示101之前的数据全部被读取

tcp在保证可靠性的同时,还会进行各种提高效率的设定

为什么要有两个序号,只用一个序号行不行?

不行,因为tcp是全双工的,可以同时读写,序号和确认序号可能会被同时使用

序号是发给接收方用的,确认序号是发给发送方使用的,所以一定要分开(给别人发的时候用32位序号,收到信息给别人应答的时候用确认序号)

关于序号和确认序号,在后面的滑动窗口讲解里又会给大家新的理解!

报头标志位

我们知道一个服务器肯定会同时收到各种各样不同类型的tcp报文,报文要有类型,所以就有了各种标志位

ACK

最常见的,表示应答,这里就不多赘述了

SYN

在tcp正常通信前要进行三次握手,由发送方调用connect操作后,本质就是向接收方发送了一个SYN置1的报文,此后由双方操作系统自动完成三次握手,关于三次握手的问题后面也会详解的,先将标志位讲完

FIN

tcp结束通信的时候要进行四次挥手,主动断开的一方先发送FIN,接收方应答ACK,然后也发送FIN,

URG

和16位紧急指针一起使用,数值表示紧急数据在有效载荷中的偏移量

因为tcp保证有序,但是如果紧急数据也只能干等的话就不太合理了,所以通过URG和紧急指针尽快处理紧急任务——终止或者暂停上传的行为;服务检测——紧急指针

PSH

还记得在上面遗留的接收缓冲区满了以后,发送方怎么知道接收方缓冲区的情况呢?

发送方定时发送询问报文+PSH,PSH告知对方,尽快进行把数据向上交付,所以数据需要被尽快交付的时候,都可以使用,例如指令输入的时候

RST

重置连接标志位

有时候在三次握手中,A-->B

A发送SYN,B回应SYN+ACK,A收到SYN+ACK的时候,建立连接,然后A再发ACK给B,B收到ACK后,建立连接

前面两个报文不怕丢失,因为丢了就不会被应答,也就知道前两个报文是否收到

但是最后一个ACK因为是最新的报文,所以保证不了一定到达(上文呼应),但是可以通过RST处理

当三次握手中最后一个报文丢失的时候,A建立连接,B没建立连接,A以为B建立好了,向B发数据的时候发现B没建立好,这时B回发一个报文+RST,此时A收到后进行销毁已建立连接,重新进行三次握手

所以tcp的可靠性不是保证100%发送到对方,而是体现在发送的数据到了我知道,数据的发送出现问题了我也知道

三次握手不是一定成功的,也有可能失败!!所以tcp就是在赌,赌最后一个报文对方收到了!!!如果没收到,就采取RST策略

补充其他知识:

每对tcp连接,都要有接收和发送缓冲区(环形队列)

超时重传

分为丢包(B没收到数据)和应答丢失(B收到数据)

但是两者本质都是A没收到应答,不知道B是否收到了数据,所以每隔一定的时间,A就会重发一次数据

如果间隔时间太短,就会过于频繁的进行重传

如果间隔时间太长,效率就会很低

间隔时间一般是500ms*2^n(n表示次数)

所以网络状态是变化的!!

如果是第二种情况,A再发送报文给B,但是此时B已经有相同的报文了,所以就要进行去重

如何去重??通过序号!!!所以序号有去重和保证数据按序到达两个作用

但是发送方发送数据后,一段时间内(收到应答之前)发送的数据是不能被移除的!!!应该被保存起来——与后序的滑动窗口有关

连接管理机制

建立连接的本质

本质就是os在内存开辟一段空间存放对应的描述结构体

一个机器内肯定存在大量的连接,所以os肯定要对连接进行管理——先描述在组织

描述就是将其用一个结构体struct links描述,管理就是将这些链表连起来进行管理

所以对连接的管理就变成了对链表的增删查改!!!

三次握手

tcp在正式传输数据之前,要进行三次握手

为什么要进行三次握手呢?为什么不能进行一次握手,两次握手,或则四次握手呢?

首先三次握手是以最小成本验证全双工

A->B

A发送SYN给B,B收到后发送SYN+ACK给A,A收到后,就完成了A发送和接收能力的检测

B发送SYN+ACK后,如果A接收到,A发送ACK给B,B收到后,也完成了B的发送和接收能力的检测

其次是使两端建立连接的时候具有相等的资源付出

如果只有一次挥手,A发送SYN给B,B就建立连接,因为只有一次挥手,那么A如果大量发送SYN就会让B建立很多连接,很容易被直接攻击

两次挥手也是同样的道理

三次挥手A发送SYN给B,B收到后发送SYN+ACK给A,A收到后建立连接,然后再发送ACK给B,B才建立连接

所以奇数次握手,客户端优先把连接建立好,然后服务器才建立连接

也可以从另一个角度理解:四次握手+捎带应答

因为A发送SYN给B且B收到后,一定要发ACK给回A,因为确认应答机制

此时B也一定要发回SYN给A,因为A和B双方要进行通信,所以B也要建立连接,所以发送SYN

因为B一定要发送ACK和SYN,所以可以将这两个报文合二为一,所以可以捎带应答

100%互相都已经保证对方收到了我们的连接建立的请求,也能百分百确定对方没收到——确定性

在三次握手的时候,两端还协商起始序号(随机序号+真实序号)和滑动窗口的大小!!后文会解释

四次挥手

双方断开通信的时候,要进行四次挥手,如图所示

可能大家会有个疑惑,为什么这里不能捎带应答合并成三次挥手呢?

因为这里FIN和ACK可能并不是一起发的,可能一方关闭了连接,但是另一方还没关闭,还要继续发送信息

如何理解断开连接?

C——> S断开连接:Server,我给你的数据发完了,我不再给你发送了

S——> C断开连接:Client,我给你的数据发完了,我不再给你发送了

数据传输角度就是存在两个方向,那么是不是存在一端不给另一端发信息,但是另一端发信息给一端的情况,所以不能捎带应答

也就是第二次和第三次挥手之间不是一定一起存在的,所以一定要四次挥手

如何理解TIME_WAIT状态

可以观察到,主动断开连接的一方会进行TIME_WAIT状态

为什么要进入到TIME_WAIT状态?

让两个朝向上的历史报文进行消散,防止历史报文对新连接造成影响

本质就是为了让主动断开连接方能够接收到被动断开连接方重传的FIN报文

当B发送报文给A且发送的报文在某个路由器上卡着的时候,A主动断开了连接,如果此时没有TIME_WAIT状态而是A直接关闭了连接,此时卡着的历史报文姗姗来迟,而A又建立了一个新的连接,历史报文有序号,这样新的连接就会被历史报文影响

所以要进行TIME_WAIT状态的等待,如果此时历史报文到来,就会被该连接接受并丢弃

所以当服务器断开又马上重启的时候,是会失败的

因为服务器此时处于TIME_WAIT状态,连接还没有被释放,对应的port还在被使用,bind也就失败了,也就不能重新启动了,此时可以换个端口或者用setsocketopt

TIME_WAIT状态持续多久?

持续2MSL。一个MSL时间是TCP报文的最长存在时间

所以2MSL可以保证双方朝向上的历史报文都消散

2MSL = 丢失的ACK的MSL + 重传的FIN的MSL

即使如此可能还是存在影响,所以这里双方就需要一个起始序号(随机序号+真实序号)

双方在三次握手期间都知道随机序号,所以发的时候发真实+随机,收的时候减去随机

如此一来还能造成干扰的可能就无限接近于0了

理解CLOSE_WAIT状态

假设客户端主动断开连接,服务端被动断开连接。

1)客户端进程发出连接释放报文FIN=1,并且停止发送数据。此时,客户端进入FIN-WAIT1(终止等待1)状态。这时候客户端处于一个半关闭的状态,即客户端已经没有数据需要发送了,但是服务器若要发送数据,客户端依然需要接受。

2)服务器收到连接释放报文后,发送确认报文ACK=1。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。进入CLOSE_WAIT后说明服务器准备关闭连接。

3)客户端收到服务器的确认请求之后,此时客户端就进入了FIN-WAIT2(终止等待2)状态,等待服务器发送释放链接报文。(在这个之前还需要接受服务器发送的最后的数据)。

4)当服务器真正调用close关闭连接时, 会向客户端发送FIN=1, 此时服务器进入LAST_ACK(最后确认)状态, 等待客户端的最后一次ACK回复。

5)客户端收到服务器的链接释放报文之后,必须发出确认报文ACK=1。此时,客户端就进入了TIME-WAIT(时间等待)状态,等待用户关闭套接字。注意此时TCP链接还没有释放,必须经过2*MSL(报文最大生命周期)的时间后,当客户端撤销相应的TCP后,才进入CLOSED状态。

6)服务器只要收到客户端发出的确认,彻底关闭连接,立即就进行CLOSED状态,于是就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

小结: 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题。

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值