网络原理——TCP

发布于:2025-07-21 ⋅ 阅读:(16) ⋅ 点赞:(0)

TCP的特点:有链接 , 可靠传输 ,面向字节流 , 全双工

(此处的可靠,不是说,A给B发一个消息,B 百分百能收到,而是A给B发了消息之后,尽可能的让B收到,但是呢A是能够知道B是否收到了,详细请看TCP的核心机制一)

TCP协议

TCP全称为 "传输控制协议(Transmission Control Protocol")。⼈如其名, 要对数据的传输进⾏⼀个详细的控制;

TCP协议段格式

  • 源/⽬的端⼝号: 表⽰数据是从哪个进程来, 到哪个进程去;(传输层的核心内容)
  • 32位序号/32位确认号: 后⾯详细讲;
  • 4位TCP报头⻓度: 表⽰该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最⼤⻓度是15 * 4 = 60(因为固定部分已经是 20 字节了,再算上选项肯定比 15 要多,所以这里不是使用字节为单位,而是使用“四字节为单位”,所以固定部分是20,选项部分最多是40)
  • 保留(6位):由于UDP的问题,长度不够,又不能扩展。TCP的设计者就考虑到这样的问题,TCP报头中就预留了一些“保留位”(现在先不用,但是占个位子)
  • 6位标志位:
    • URG: 紧急指针是否有效
    • ACK: 确认号是否有效
    • PSH: 催促标志位:提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛
    • RST: 对⽅要求重新建⽴连接; 我们把携带RST标识的称为复位报⽂段
    • SYN: 请求建⽴连接; 我们把携带SYN标识的称为同步报⽂段
    • FIN: 通知对⽅, 本端要关闭了, 我们称携带FIN标识的为结束报⽂段
  • 16位窗⼝⼤⼩: 后⾯再说
  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP⾸部, 也包含TCP数据部分.
  • 16位紧急指针: 标识哪部分数据是紧急数据;(TCP正常来说,按照序号顺序发送和接受,而紧急指针,相当于”插队“,跳过前面的数据,直接从某个指定的序号来开始read)
  • 40字节头部选项: optional ⇒ 可选的,可有可无的;

TCP的核心机制

核心机制一:确认应答

保证可靠性的一个关键前提是,发送方知道自己的数据是否被对方收到,需要对方给返回一个“应答报文”(acknowledge,ack),发送方知道应答报文,就可以确认对方是收到了。

  • 虽然应答报文可以保证可靠性,但是呢,这个中间还会穿插着一个叫后发先至的问题。举个例子:

  • 这是一个生活中简单的应答报文的例子,但这中间有明显的缺陷,如果我连续发多条,就可能出现问题了。

    此时还是能正确理解意思的。但是呢,在网络上存在一个很神奇的操作,先发后至。

  • 为什么网络上会出现后发先至呢??

    可以想象成结婚的一个流程。在接亲的时候,男方派出车队,开到女方家,把新娘一接,接到男方家里,办仪式,开始酒席。其中,这个先发后至就会发生在这个车队中。

    这个车队,一出村子,就走散了。为什么?因为十字路口有红绿灯,因此就会导致每个车,各开各的,每个司机就会按照自己收悉的路来走。网络上也是类似的情况。转发数据,每个路由器/交换机,就相当于“十字路口”。(一个车队是 N 个TCP请求,每辆车是一个TCP请求。这N个TCP请求,可能共同表示的是一个应用层数据包,也可能表示的是 M 个 应用层数据包(应用层协议怎么定的了)TCP不关心应用层咋搞,都是按照字节来传输)

TCP的处理方案,就是给传输的数据,进行编号:

TCP将每个字节的数据都进行了编号,即为序列号。(这里需要了解的是tcp报头不参与编号,序号 确认序号都是针对载荷的~)序号是保证你应用程序read数据的先后,不是数据到达对方的顺序。

                                                   (TCP的载荷部分)

注意:TCP是面向字节流的。其实在编号的时候,不是按照1条,2条这样的方式来编的,而是按照“字节”来编号的,每个字节都分配一个编号,编号,连续递增的。

了解序列号之后,我们就可以再看看TCP格式中的 32位序号以及32位确认序号了。

由于一个TCP的载荷是多个字节构成,也就意味着多个编号,那么此处序号应该写的是哪个序号呢?

答:序号字段填写的是载荷部分的第一个字节的序号。因为序号是连续递增的,知道第一个序号,也知道长度,因此最后一个序号也就知道了

那 32位确认序号 又要填什么呢?

答:填法是,把收到的数据载荷的最后一个字节序号 + 1,填写到确认序号中。因为每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发送。

因此确认序号的含义:

  1. < 1001的数据都已经确认收到了。
  2. 接下来你要从 1001 开始给我发送。

引入序号之后,接收方就可以根据序号对数据进行排序。

TCP需要处理后发先至的情况,确保应用程序通过 socket api 读到的数据顺序是正确的。即使出现后发先至,tcp 也给我处理掉,确保代码里读到的数据(InputStream,read)和发送方写入的数据顺序一致(OutputStream write)。

TCP 在接收方这里会安排“接受缓冲区”(内存,操作系统内核里),通过网卡读到的数据,先放到接受缓冲区中,后续代码里调用read,也是从接受缓冲区来读的。

根据序号来排序,序号小的在前面,序号大的在后面,确保前面的数据已经到了,然后read才能解除阻塞,如果是后面的数据先到,read就继续阻塞,不会读取到数据。

因此,基于TCP写代码的时候,完全不必担心数据顺序的问题(代码写起来就方便了),如果是基于 UDP ,实现拆包组包,就需要考虑顺序,自己实现排序逻辑。

核心机制二:超时重传

超时重传是针对丢包的情况做出处理。

在A给B传输的过程中,两种情况会发生丢包。

到达等待时间的上线,还没有收到 ACK,A就认为传输中发生丢包了:

  1. A → B 发的数据丢了。
  2. B → A 返回的ACK丢了。
  • 为啥会丢包呢?

    数据报经过某个路由器,交换机转发的时候,该路由器/交换机已经非常繁忙了,导致当前需要转发的数据量超出路由器/交换机的转发能力上限(就像是每个路口,都会有一个最大通过的车流量,超过了就会发生堵车),数据报就会消耗更多的时间才能到达对方,更糟糕的情况是,数据报太多太多了,路由器/交换机根本处理都处理不过来,接受缓冲区都满了,于是只能丢弃了(网络上的数据包都是由时效性的)

丢包是不可避免的客观现象,重传就是有效的对抗丢包的手段。

在这里引入超时时间,来判定是否丢包~

TCP 中,判定超时的时间阈值,不是固定数值,是动态改变的。

假设当前A→ B发送数据,丢包的超时时间阈值是T,当A给B传输发生超时之后,就会延长这个时间阈值,但是会继续延长这个时间不是无休止的,当超时次数达到一定程度/等待时间达到一定程度,就认为网络出现严重故障,就会放弃这一次传输。

随着进行重传,导致数据到达对方的概率越来越高的,如果重传还不成,就说明即使我们增加了概率,还是不能成功,意味着当前丢包概率是一个非常大的数值意味着网络上大概率已经出现严重故障了。此时,就算继续重传,意义也不大了,也就没有不要重传频率那么高了(破罐子破摔了)(这是摆烂的策略 ,当然还有一种相反的策略(不是网络上)频率逐渐增加,就像医院抢救病人的时候)

对于丢包的两种情况:

  1. A → B 发的数据丢了。
  2. B → A 返回的ACK丢了。

发送发 A 区分不了,当前是哪种情况,所以做法都是进行重传。

  • 主机A发送数据给B之后, 可能因为⽹络拥堵等原因, 数据⽆法到达主机B;
  • 如果主机A在⼀个特定时间间隔内没有收到B发来的确认应答, 就会进⾏重发;

但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;

  • 在这种情况,由于B已经有了 1-1000 这个数据了,在丢包的情况下,B收到了两份一样的数据,因此主机B会收到很多重复数据。如果 TCP 不处理,很可能会使应用层读到两次一样的数据~~(就比如扣款数据),那么就需要 TCP 协议需要能够识别出哪些包是重复的包,并且把重复的丢弃掉。所以,TCP 会在内部进行去重操作,在接收缓冲区中,就可以根据序号,在接受缓冲区找一下,如果存在,就直接丢弃,如果不存在,才放进去。

确认应答,超时重传是TCP协议最核心的两个机制,这两个机制保证了 TCP 能够进行可靠传输。

核心机制三:连接管理

在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接。(通信双方,各自保存对端的信息)

三次握手建立连接:

生活中的握手,打招呼~,对于握手操作,没有实际的业务,只是“打个招呼”

我发送一个不携带业务的数据,通过这个数据和对方“打个招呼”

看到上面的图可能会想,三次握手,上面这不是四次吗?**其中有两次,能够合并成一个。**在握手的过程中,我们需要知道一定是客户端主动发起syn(客户端和服务器都是“角色”,同一个程序在不同场景下可以扮演不同的角色),合并操作,是能够有效提高传输的效率的,网络传输过程,是要能够进行“封装和分用的”。

合并:

看到syn可能会想到 synchronized 同步,同步这个术语,有多种含义,在加锁这里,同步理解成“互斥”,在TCP中的同步,指的是,“数据上的同步”。

A告诉B,接下来我要和你建立连接连接,就需要你把我的关键信息,保存好,同时你也把你的信息同步发给我。

syn这一位为1表示同步报文。

双方各自让对方保存一下自己的关键信息(IP(IP报头里)和端口号(TCP报头中))(IP和端口号不是载荷数据)

注:syn和ack都是不携带载荷。

这里需要了解两个状态:

  1. LISTEN:服务器启动,随时可以有客户端连上来。
  2. ESTABLISHED:连接建立完毕,随时可以发送数据。

为啥TCP要三次握手,有什么用,解决了什么问题?

  1. 三次握手,相当于“投石问路”。

    先初步探一探网络的通信链路是否通常。(网络通畅是可靠传输的前提条件)就类似地铁早上的第一班车是不载客的,目的是为了验证路线上是通畅的。

  2. 验证通信双方的发送能力和接受能力是否也正常。

    因此,通过上述讨论,建立连接操作,如果只握手两次,是不够的。

    那握手四次是否可以呢?可以,但是没必要~(中间一次拆成两次)合并成一个就可以了

  3. 三次握手过程中,可以协商一些关键信息。

    TCP 要协商的一个非常关键的信息,通信过程中序号从几开始。初始序号,一般不是从0开始的。并且,两次连接初始序号都是不同的(往往会差别较大)。

四次挥手断开连接:

通信双方,各自给对方发 FIN,我要把你的信息删了。

  • 问:三次握手,中间两次能合并,四次挥手可不可以合并呢?

    答:有的时候能(延迟应答,后面讲),有的时候不能。不能:ack和fin这两次交互的时机是不同的。ack是内核控制返回的,内核收到FIN,第一时间返回ack,和你应用程序代码无关,第二个FIN则是代码中调用socket.close才会触发的(进程结束,也能触发)。第二个FIN的时机和ACK的时机很可能不是同一个时机。

三次握手,一定是客户端主动发起syn(第一次握手,一定是客户端开头的)。

四次挥手,客户端和服务器,都可以主动发起FIN(就看谁先调用close)。

但是,实践角度来看,还是客户端断开连接的可能性更大。

对于四次挥手,我们也需要记住两个状态——CLOSE_WAITE 和 TIME_WAITE(不要和线程的TIMED_WAITING状态搞混)。

  • 谁是主动发起FIN的一方,就会进入到TIME_WAIT(给最后一个ACK丢包行为,做一个托底)。
  • 谁是被动发起FIN的一方,就会进入到CLOSE_WAIT(这个地方在等待你的应用程序代码,来调用close方法)。

CLOSE_WAIT

  • 问:那我的服务器是如何感知到客户端发FIN呢?

    由于从hasNext返回false,到close,之间可以存在很多代码逻辑,就看你怎么写了。这就导致了不能确定具体的时间。也就确定不了状态从CLOSE_WAIT 到 LAST_ACK的时间。

不过CLOSE_WAIT正常开发中,应该是看不到的,原则上来说,感知到对方断开之后,就应该尽快的执行close。如果你发现服务器这边存在大量的CLOSE_WAIT,持续的还很久,此时就意味着你的代码大概率有bug。(就需要去检查是否执行到close了)

TIME_WAIT

网络传输中,随时会丢包。三次握手,四次挥手,也一样会丢包,丢包就需要触发超时重传。

如果要等待的话,要等多久合适呢?

2 * MSL(网络上任何两个节点传输过程中消耗的最大时间)

MSL 通常这个时间会配置成 60s。此处TIME_WAIT等待2min(不同的系统可能不一样,可以修改的。)和超时重传区分开,超时重传的时间阈值时ms级别的。

  • 想⼀想, 为什么TIME_WAIT的时间是2MSL?
    • MSL是TCP报⽂的最⼤⽣存时间, 因此TIME_WAIT持续存在2MSL的话
    • 就能保证在两个传输⽅向上的尚未被接收或迟到的报⽂段都已经消失(否则服务器⽴刻重启, 可能会收到来⾃上⼀个进程的迟到的数据, 但是这种数据很可能是错误的);
    • 同时也是在理论上保证最后⼀个报⽂可靠到达(假设最后⼀个ACK丢失, 那么服务器会再重发⼀个FIN. 这时虽然客⼾端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK)

四次挥手全都挥完,这个是属于“正常情况”,当然也有“异常情况”。比如,服务器就始终不调用close。

  • 站在A的视觉,此时A给B已经把FIN发了很久了,B也没有进行后续的挥手操作,A就会主动释放连接(也就是把B的核心信息删了)。
  • 站在B这边,由于代码逻辑都有bug,这里的连接,暂时存在(还会保存对方的信息,此时也没法进行正常的数据通信了)

核心机制四:滑动窗口

刚才我们讨论了确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个⽐较⼤的缺点, 就是性能较差. 尤其是数据往返的时间较⻓的时候。

既然这样⼀发⼀收的⽅式性能较低, 那么我们⼀次发送多条数据, 就可以⼤⼤的提⾼性能(其实是将多个段的等待时间重叠在⼀起了).

前几个数据包都是不等待直接往后发,发到一定的量之后,再等待,用一份的时间等多组ack。

那能否一直持续发?肯定是不能的,不科学,一直发就相当于没有可靠传输了。

那下一组是怎么发的呢?

  1. 等这一组的所有ack都回来,再发第二组,但这么做,花的时间会更长。
  2. 收到一个ack,就发下一条。(用这个)

窗口大小:不需要等待,能够连续发送的最大数据量。

                                    从宏观上来看,这个窗口就在快速滑动。
  • 问:B给A返回多个ack时如果发生后发先至的情况怎么办?(也就是如果2001ack比1001先到)

    如果是2001ack比1001先到,窗口直接往后走两个格子就可以了。ack内含确认序号,而确认序号的含义:该序号之前的数据,都确认收到了。因此2001ack能够涵盖1001的含义。

窗口越大,批量发的数据越多,效率就越高,但是窗口也不能无限大,太大也会影响到可靠性。

滑动窗口是在可靠传输的基础上,提高效率。(但只是亡羊补牢,引入可靠性,会使效率产生折损,引入滑动窗口,是要让折损更小,不可能效率比UDP这种还高)

滑动窗口丢包

滑动窗口过程中,当然也会丢包,如果出现了丢包,如何进行重传?这里分两种情况讨论。

  1. 数据包已经抵达了,ACK被丢了。

    丢包50% 了,相当恐怖的数字,网络已经有严重问题了。此时,不用做任何额外处理,问题不大,后一个ack能够涵盖前一个ack的含义,就可以通过后续的ack进行确认。

  2. 数据包就直接丢了

  • 当某一段报文段丢失之后,发送端A会一直收到1001这样的ACK,就像是在提醒A“我想要的是1001”一样;

  • 如果发送端A连续三次收到了同样一个“1001”这样的应答,A就意识到了1001-2000怕是丢包了,就会将对应的数据1001-2000重新发送;

  • 这个时候接收端B收到了10001之后,再次返回的ACK就是7001了,因为刚才2001-7000这些数据都收到了,被放到了接收端B操作系统内核的接收缓冲区中,就差1001-2000了,通过重传,一下就把缺失的拼图,补上了,补上之后,继续从7001往后传输就可以了。

这种机制被称为“快速重发控制”(也叫“快速重传”)。

快速重传:只是谁丢了,重传谁,其他已经收到的数据无需重传,整个重传过程速度很快的。(滑动窗口下的超时重传的变种操作)

超时重传:传输的数据量少,没有构成滑动窗口批量传输的形式。

快速重传:传输数据量多,形成滑动窗口。

这两种机制是针对不同情况下的重传机制。不矛盾!

核心机制五:流量控制

接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。

流量控制就是给发送方踩刹车,让它发的慢点。流量控制可以让接收方,根据自身处理数据的速度,反馈给发送方,限制发送方发送的速度。

看到这里,可能会有一个疑问,发送方怎么知道我还有多少才满呢?接收端如何把窗口大小告诉发送端呢?

这就不得不提在 ack中,依赖一个特殊的属性,“窗口大小”了。

16位窗口大小:接收方接收缓冲区的剩余空间大小。填到这个属性中,发送方就会按照这个数字来重新设定发送的滑动窗口大小(滑动窗口的大小,是动态变化)。

16位窗口大小是64KB,那是否滑动窗口大小,最大数值就是64KB呢?

答:不是。在UDP踩过坑之后,TCP已经长记性了,在选项里含有一个特殊的属性:窗口扩展因子。

窗口大小 << 窗口扩展因子 就是新的窗口大小了(64KB<<2 = 256KB呈指数增长)

注:

  • 接收端将⾃⼰可以接收的缓冲区⼤⼩放⼊ TCP ⾸部中的 "窗⼝⼤⼩" 字段, 通过ACK端通知发送。
  • 窗⼝⼤⼩字段越⼤, 说明⽹络的吞吐量越⾼。
  • 接收端⼀旦发现⾃⼰的缓冲区快满了, 就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端。
  • 发送端接受到这个窗⼝之后, 就会减慢⾃⼰的发送速度。
  • 如果接收端缓冲区满了, 就会将窗⼝置为0; 这时发送⽅不再发送数据, 但是需要定期发送⼀个窗⼝探测数据段, 使接收端把窗⼝⼤⼩告诉发送端。(窗口探测包,只是为了触发ack主动询问一下,接收方咋样了)

核心机制六:拥塞控制

流量控制是依据接收方处理能力,进行限制的;

阻塞控制是一句传输链路的转发能力,进行限制的。

流量控制和拥塞控制都能限制发送方的窗口大小,这两个值,哪个小,哪个说了算。

虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,任然可能引发问题。

因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。

因此,TCP引入慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

  • 此处引⼊⼀个概念程为拥塞窗⼝;
  • 发送开始的时候, 定义拥塞窗⼝⼤⼩为1;
  • 每次收到⼀个ACK应答, 拥塞窗⼝加1;
  • 每次发送数据包的时候, 将拥塞窗⼝和接收端主机反馈的窗⼝⼤⼩做⽐较, 取较⼩的值作为实际发送的窗⼝;

像上⾯这样的拥塞窗⼝增⻓速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增⻓速度⾮常快。

  • 为了不增⻓的那么快, 因此不能使拥塞窗⼝单纯的加倍.
  • 此处引⼊⼀个叫做慢启动的阈值
  • 当拥塞窗⼝超过这个阈值的时候, 不再按照指数⽅式增⻓, ⽽是按照线性⽅式增⻓
  • 当TCP开始启动的时候, 慢启动阈值等于窗⼝最⼤值;
  • 在每次超时重发的时候, 慢启动阈值会变成原来的⼀半, 同时拥塞窗⼝置回1;

少量的丢包, 我们仅仅是触发超时重传; ⼤量的丢包, 我们就认为⽹络拥塞;

拥塞窗口与传输轮次的关系图:

这个图就表示了拥塞控制的工作过程:

  • 慢启动
  • 指数增长
  • 线性增长
  • 丢包,窗口变成较小值。

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对⽅,但是⼜要避免给⽹络造成太⼤压⼒的折中⽅案。

核心机制七:延时应答

默认情况下,接收方都是在收到数据报第一瞬间,就返回ack。但是可以通过延时返回ack的方式来提高效率。

理论上不是100%提高效率,毕竟在延时期间内,接收方又收到了其他的数据,可能会导致返回的窗口更小了,但从经验上来看,还是又一定帮助的。

  • 那么所有的包都可以延时应答吗?

    肯定不是。

    • 数量限制:每隔N个包就应答一次。(不会因为ack少了影响可靠性,确认序号,后一个能够覆盖前一个)
    • 时间限制:超过最大延迟时间就应答一次。

    传输的数据密集,用第一个。传输的数据稀疏,按第二个来。

    具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。

⼀定要记得, 窗⼝越⼤, ⽹络吞吐量就越⼤, 传输效率就越⾼. 我们的⽬标是在保证⽹络不拥塞的情况下 尽量提⾼传输效率;

核心机制八:捎带应答

TCP已经有了延时应答了,基于延时应答,引入“捎带应答”,返回业务数据的时候,顺便把上次的ack给带回去。

  • 如果没有延时应答,返回ack的时机和返回响应的时机,就是不同时机。
  • 引入了延时应答,ack可以往后延时一定时间。恰好这个时候要返回响应数据,此时就可以把ack也带入到响应数据中,一起返回。
  • ack:ack设为1,窗口大小设为接收缓冲区剩余值,确认序号设为合适的值(都是报头里设置的),这些内容不影响响应数据。
  • 把两个包合成一个,就能起到提高效率的作用。

这也就是为什么说四次挥手有时候也可以三次挥手。ack延时和fin一起发送回去。

核心机制九:面向字节流

创建⼀个TCP的socket, 同时在内核中创建⼀个 发送缓冲区 和⼀个 接收缓冲区;

  • 调⽤write时, 数据会先写⼊发送缓冲区中;
  • 如果发送的字节数太⻓, 会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短, 就会先在缓冲区⾥等待, 等到缓冲区⻓度差不多了, 或者其他合适的时机发送出去;
  • 接收数据的时候, 数据也是从⽹卡驱动程序到达内核的接收缓冲区;
  • 然后应⽤程序可以调⽤read从接收缓冲区拿数据;
  • 另⼀⽅⾯, TCP的⼀个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这⼀个连接, 既可以读数据,也可以写数据. 这个概念叫做 全双⼯

由于缓冲区的存在, TCP程序的读和写不需要⼀⼀匹配, 例如:

  • 写100个字节数据时, 可以调⽤⼀次write写100个字节, 也可以调⽤100次write, 每次写⼀个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以⼀次read 100个字节, 也可以⼀次read⼀个字节, 重复100次;

粘包问题

首先要明确,粘包问题粘的是“应用层数据包”,TCP是通过字节流传输的,在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段,但是有一个序号这样的字段,站在传输层的角度,TCP是一个一个报文过来的,按照序号排号序放在缓冲区中,但站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。

(UDP来说就不存在这样的问题,以UDP的数据包为单位读写的,一个UDP数据包承载一个应用层数据包,每次receive得到的结果就是一个完整的应用层数据包)

那么如何解决粘包问题呢?站在TCP的层次上,无解。需要站咋应用层解决。定义好应用层协议,明确包之间的边界

  • 约定包和包之间的分隔符。(包的结束标记)(之前写过的echo server采用的办法,约定\n作为结束标记)
  • 约定包的长度。(比如约定每个包开头四个字节,表示数据包一共多长)
  • 对于定⻓的包, 保证每次都按固定⼤⼩读取即可; 例如上⾯的Request结构, 是固定⼤⼩的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;

HTTP中都有体现:

  • GET请求,没有body,使用空行,作为结束标记。
  • POST请求,有body的时候,通过Content-Length决定body多长。

核心机制十:异常情况处理

TCP在通信过程中存在特殊情况。

  1. 某个进程崩溃了

    进程崩溃,和主动退出,没有本质区别,进程终止会释放文件描述符,表的每个资源,调用socket的close,发送FIN,触发四次挥手。进程虽然没了,但是TCP的连接信息还存在。此时四次挥手还是可以正常进行的。(TCP连接的释放时机更晚)

  2. 主机关机了

    正常流程的关机,本质上还是会先杀死所用的用户进程(第一种情况)。关机也是需要一定的时间,如果一定时间内,四次挥手,挥完了,就和正常一样了。但是万一没挥完呢?

    最终B仍然可以把连接释放掉。

  3. 主机掉电了

    拔电源。(不要随便拔,可能会出现问题,对于台式机这样的情况,会非常伤硬盘(机械结构,盘片会高速旋转,可能导致断电一瞬间,硬盘上写入的数据就会出错))两种情况:

    • 接收方掉电

    • 发送方突然掉电了

      虽然TCP内置了心跳包,但在实际开发中,通常还是会在应用层重新实现心跳包效果,因为TCP的心跳,周期太长了,分钟级别的。现在通常希望秒级,甚至毫秒级,就能发现对端是否正常存活,从而触发一些后续的操作。

  4. 网线断开了

    • 站在A的视角,就和刚才上面“接收方掉电”是一样的情况。

    • 站在B的视角,就和刚才上面“发送方掉电”是一样的情况。

      最终都能够释放连接资源的。

TCP小结

为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能。

可靠性:校验和 - 序列号 - 确认应答 - 超时重传 - 连接管理 - 流量控制 - 拥塞控制。

提高性能: 滑动窗口 - 快速重传 - 延迟应答 - 捎带应答。

TCP/UDP 对比

  • TCP:可靠传输:大部分场景下,都会优先使用TCP(HTTP,浏览器/app访问服务器)

  • UDP:效率更高:对于性能要求高,可靠性要求不高的场景下。比如一个机房内部的主机之间通信,机房内部,网络结构比较简单,带宽通常很充裕,不太容易出现丢包的情况。(机房内部,通常对于性能要求是很高的(微服务))

如何基于UDP实现可靠传输?[经典面试题]

参考TCP的可靠性机制,在应用层实现类似的逻辑。

  • 引用序列号,保证数据顺序
  • 引入确认应答,确保对端收到了数据
  • 引入超时重传,如果隔一段时间没有应答,就重发数据
  • 等等。。。。往TCP上套。


网站公告

今日签到

点亮在社区的每一天
去签到