目录
一. 再谈端口号
端口号(Port)标识了一个主机上进行通信的不同应用程序。 在TCP/IP协议中,用“源IP”,“源端口号”,“目的IP”,“目的端口号”,“协议号”这样一个五元组来标识一个通信。
端口号是通过2个字节的无符号整数表示的,取值范围0~65535,实际上,0端口比较特殊,是一个随机设定空闲端口,不会使用,一般使用1~65535。其中,1~1023 属于已经被预定好了的(有一些知名的服务器,已经提前预定了这个端口),这样的端口称为 “知名端口号”。在日常开发中,也会避开这些端口。说是“知名”,其实这里大部分的服务器,现在已经都不使用了。
但有些端口对应的服务器还是比较知名的,比如:
- 80 --> http
- 443 --> https
- 22 --> ssh
现在一些知名的服务器,像mysql,redis,rabbitmq,tomcat.....使用的端口,反而是1024以上的了。
什么时候会涉及到一个进程(服务器)绑定多个端口呢?
- 编写的服务器,肯定需要首先绑定至少一个端口,和客户端进行交互 --> “业务端口”
- 服务器运行过程中,希望能够对这个服务器的行为,进行一些“控制”,比如让服务器重新加载某个数据 / 某个配置 / 修改服务器的某个功能,也可以通过网络通信完成上述功能。此时,就可以让服务器绑定另一个端口,通过这个端口,编写一个客户端,给服务器发送一些“控制类”请求 --> “管理端口”
- 当需要针对服务器运行状态进行检测和调试,或查看服务器运行中某个关键变量的数值......千万不能用调试器来调试,使用调试器一旦调试这个服务,就会使服务器的一些线程被阻塞住,无法正常为客户端提供服务了(可以通过日志进行打印调试,但是不方便,还需要修改代码并且重启服务器)。此时,可以让服务器绑定另一个端口,然后实现一些相关的打印关键变量的逻辑,客户端发送对应的调试请求 --> “调试端口”
二. UDP协议(用户数据报协议)
2.1 UDP协议端格式
网络通信中,UDP报文内容涉及到四个关键信息:1)源端口:发送数据包的端口号 2)目的端口:接收数据包的端口 3)UDP报文长度 4)UDP检验和
UDP报头总共8字节,固定长度。报头中的四个字段,没有固定分隔符,而是通过长度进行区分。 端口号是在传输层的概念,使用2个字节长度表示端口号,这也是端口号范围在0~65535的原因,如果使用了一个更大端口,就会在系统底层被截断。
2.2 UDP报文长度
UDP报文长度,就是 “报头长度+载荷长度”,报文长度单位是“字节”,比如 报文长度是1024,指的是整个UDP数据包大小是1024 字节,由于是2个字节来表示长度(0~65535),因此数据包的最大值约为64KB。如果使用UDP协议,传输一个很大的数据,就会在底层传输时进行数据截断,既不会拆分数据,也不会合并数据。UDP协议本身并没有限制数据包的大小,但是底层网络协议(如IPv4)会限制数据包的大小。
- 不拆分数据:UDP不会像TCP那样自动拆分大数据块为多个数据包。如果应用程序发送的数据超过了网络层的最大传输单元(MTU),那么这个数据包可能会在网络上被分片。这是由网络层(如IPv4)处理的,而不是UDP层。UDP只是简单地发送整个数据包,不管它的大小。
- 不合并数据:UDP也不会合并多个数据包。每个UDP数据包都是独立的,包含自己的源端口、目标端口、长度和校验和信息。接收端接收到的是一个个独立的UDP数据包,不会将它们合并。
针对上述情况,有如下两个方案:
- 把一个大的数据包拆成多个,分别进行传输(很快被否定)。因为实现分包与组包的过程充满不确定性,非常复杂。(网络传输本身存在不确定性,包括包的接收顺序、包是否在传输过程中丢失等问题,就需要花费更大的成本解决这也问题)
- 直接使用TCP。因为 1)TCP对于长度,本身没有限制 2)TCP自身带有可靠传输机制,对于整体的通信质量也是有利的 3)代码修改成本比较低
因此,以后在使用UDP协议传输数据的时候,一定要留意UDP报文长度大小是否在64KB以下。
2.3 UDP校验和
校验和/检验和(checksum):网络传输过程中,非常容易出现错误,电信号/光信号/电磁波等信号易受到环境的干扰,使里面的传输信号发生改变(比特翻转 1 -> 0 0 -> 1),由于现在通信技术不断发展,在地面上发生比特翻转的概率比较低,在太空中,比特翻转的现象会更加严重。
校验和存在的目的,是为了能够发现或者纠正传输过程中出现的错误。就可以给传输的数据中引入 “额外的信息 -> checksum” 用来发现/纠正传输数据的错误。 如果只是发现错误,携带的额外信息就少;如果想要纠正错误,携带的额外信息就多,要消耗更多的带宽。UDP的校验和是用来发现错误,如果有错误,就会将数据包丢弃,不会让对方重发。
校验和的工作原理:
结合内容 / 内容片段生成校验和,如果出错的数据没有被校验和检查出来,这个情况也是可能发生的,但是一个良好的校验和算法,可以让上述问题的可能性非常低。
UDP使用简单有效的方案 —— LRC校验和(纵向冗余校验),把UDP数据报整个数据都进行遍历,分别取出每个字节,进行累加。由于整个数据可能很多,加着加着就可能结果溢出,溢出也没关系,重点不关心最终加和是多少,而是关心校验和的结果是否在传输中发生改变。
例如,传输一个UDP数据报
如果传输过程中,数据变了,校验和变成了checksum3,此时接收方计算得到校验和checksum4,此时checksum3大概率不等于checksum4。
在计算校验和的过程中,是否可能出现两个不同的数据,生成的校验和相同呢?—— 可能会存在但是概率很低。除了LRC之外,还可能会用到一些其他的算法计算校验和。
另外两个典型的算法,MD5 和 SHA-1,二者都是基于MD结构的哈希函数,通过一系列的压缩函数来处理消息。此处简单讲解一下MD5算法(背后的实现过程是一个“数学过程”,仅需了解特点即可)
MD5算法特点:
- 定长:无论输入的字符串,长度多长,算出的结果都是固定长度 --> 适合做“校验和算法”
- 分散:输入的内容,哪怕只有一点点发生改变,得到的md5值都会相差很大 --> 适合做“hash算法”
- 不可逆:MD5是一个单向散列函数,这意味着从哈希值无法推导出原始消息内容 --> 适合作为“加密算法”
三. TCP协议(传输控制协议)
3.1 TCP协议段格式
1)源端口和目的端口
- 大小:各16位,共32位
- 作用:标识发送和接收TCP段的端口号。这些端口号与IP头部中的源IP地址和目标IP地址结合,唯一地标识了网络中的每个TCP连接。
2)序列号(seq --> Sequence Number)
- 大小:32位
- 作用:在TCP连接中,每个字节都按顺序编号。序列号描述了载荷部分,第一个字节的序号是多少。
3) 确认号(ack --> Acknowledgment Number)
- 大小:32位
- 作用:期望收到对方下一个报文段的第一个数据字节的序列号。确认号用于确认已成功接收的数据。(在“确认应答”核心机制中,发挥重要作用)
4)首部长度 / 数据偏移
- 大小:4位
- 作用:TCP报头的大小,不包括载荷,以32位字(4字节)为单位。TCP来说,报头长度是可变长的,4个比特位,表示的数据范围0~15,因此,数据偏移的最大值为15,表示TCP报头长度最大为60字节。这个字段之所以重要,是因为它帮助接收方确定TCP头部在哪里结束,以及载荷数据从哪里开始。这样,接收方可以正确地解析TCP头部中的控制信息,并提取出载荷数据交给上层应用程序处理。
1. 首部长度/数据偏移不会表示载荷的长度,是否意味着TCP携带的载荷部分是没有限制的呢?—— 不是
TCP载荷的大小具体取决于网络环境和TCP连接的协商结果。限制TCP载荷大小的因素跨越了链路层、网络层和传输层。每个层都有其自己的限制,这些限制共同作用于最终的数据包大小。通常,这些限制是由链路层的MTU和网络层的总长度字段所设定的,而传输层的TCP协议,则最终通过MSS选项来适配这些底层限制,决定TCP的载荷长度。
MSS(MaximumSegmentSize,最大段大小),是指在不进行分片的情况下,网络层可以传输的最大数据量。MSS的值通常由路径MTU(最大传输单元)决定,并减去TCP和IP头部长度。
2. 如果一次发送的数据超过限制怎么办?—— 分包发送
TCP是面向字节流的,一个TCP数据包和下一个TCP数据包携带的数据,天然就是“可拼装的”。
比如要传输一个特别大的数据,传输过程中,本身就会通过多个TCP数据包携带。这些TCP数据包彼此之间携带的载荷都是可以在接收方自动被拼起来的,不像UDP存在传输的上限。
使用UDP传输大数据,就需要考虑调用一次send操作,参数是否超过64KB,超过64KB就不行;使用TCP的话,就没关系,可以调用一次write,也可以调用多次write(无论如何进行write,在网络传输和对端接受角度来看是没差别的,因为TCP是面向字节流的)
5)保留位(Reserved)
- 大小:6位
- 作用:保留给未来使用,目前必须设置为0。如果未来某一天,TCP需要新增属性/或者某个属性的长度不够用,就可以把保留位拿出来,用作对应的作用,TCP的结构不需要发生大的改变的,这样的升级就会容易很多了,充分吸取了UDP的教训(UDP报文长度字段无法扩展了)
6)选项(Options)
- 大小:可变长度,以4字节为单位
- 作用:用于提供额外的通信控制信息,如最大报文段长度(MSS)、窗口缩放、选择性确认(SACK)等,可以有,也可以没有,这是TCP报头变长的主要原因。
7)标志位(与TCP的核心机制有关)
3.2 核心机制
TCP最重要的就是“可靠传输”,保证数据尽可能到达对方:1)发送方发送数据之后,能感知到对方是否收到 2)发送的数据如果丢失了,发送方能够进行重试
3.2.1 确认应答 —— “感知对方是否收到”
在讲解“确认应答”机制之前,先详细说说TCP的序号:
由于TCP是面向字节流的,实际上编号,并非是按照“第一条、第二条....”这样的方式编排,而是按照“第一个字节,第2个字节....”的方式。每个字节都有一个独立的编号,字节和字节之间,编号是“连续递增”的。
按照字节编号这样的机制,就称为“TCP的序号”,每一个TCP数据包的序号代表TCP数据包载荷中的第一个字节的序号。序列号用于标识传输的每个字节,以便接收方可以正确地重新组装数据流,并处理乱序或重复的数据包(序号只是针对TCP数据包携带的载荷进行编号的,TCP报头不参与)
序列号特点:
- 唯一性:每个TCP连接的序列号都是唯一的,即使在不同的TCP连接中,序列号也不会重复。
- 随机化:为了提高安全性,现代TCP实现通常会使用一个初始序列号(ISN),这个序列号是随机生成的,而不是从0开始,有助于防止序列号预测攻击。
- 单调递增:一旦确定了ISN,后续发送的每个数据包的序列号都会基于ISN递增。每个数据包的序列号是相对于ISN的偏移量。例如,如果ISN是1000,并且发送了一个包含100字节的数据包,那么该数据包的序列号将是1000,下一个数据包的序列号将是1100,依此类推。
在TCP协议中,“确认应答”机制主要是为了应对不可靠的网络传输问题,如数据包丢失、后发先至、乱序到达、网络拥塞等。
比如,在网络通信没那么发达的时候,一条短信是非常容易发丢的。当A给B发一条短信时,如果B收到短信,就会给A发送一条回复的短信,于是根据这条回应,就知道A成功发送了短信。
像B发送给A的这条回复短信,在TCP协议中,就称作是“应答报文”(acknowledge,缩写为“ACK”)。在应答报文中,“确认序号”表示接收方期望收到的下一个TCP数据段的序列号。
数据传输中,接收方在处理完数据段后,会发送一个确认应答报文(ACK)给发送方。确认应答中包含确认序号,这个序号是接收方期望从发送方收到的下一个数据段的第一个字节的序列号,表示的含义是小于这个确认列号的数据都收到了。TCP引入序列号和确认序号 ,目的是使应答报文和传输的数据对应上。
如果是普通报文,序号是有效的,确认序号是无效的,设置ACK=0;如果是应答报文,序号和确认序号都是有效的,设置ACK=1(这是另一套编号的体系,和传输的数据的序号不是一套的)
除此之外,序列号还能够解决网络中数据包“乱序”的问题。在网络传输中,很有可能发生“后发先至”的问题。比如
网络传输中的“后发先至”问题,也称为“乱序交付”,是指在网络中,先发送的数据包(称为“先发”数据包)在到达目的地时,可能会比后发送的数据包(称为“后发”数据包)晚到达。这种现象在IP网络中是可能发生的,以下是一些导致后发先至问题的常见原因:
- 不同的路由路径:数据包可能会通过不同的路由路径到达目的地。如果后发的数据包选择了更快的路径或者路径上的网络拥塞较少,它可能会先于先发的数据包到达。
- 网络拥塞:网络中的某些部分可能会出现拥塞,导致数据包在传输过程中被延迟。先发的数据包可能会遇到更严重的拥塞,而后发的数据包则在拥塞减轻后发送,因此先到达。
后发先至是客观的情况,无法改变,但可以给传输的数据添加“编号”,通过编号区分出数据的先后顺序。
TCP解决乱序到达的机制:
1. 分配序列号:TCP为每个数据包分配一个唯一的序列号。序列号标识了数据包中第一个字节的编号,这样接收方就可以根据序列号来正确地重新排序数据。
2. 接收数据包:当TCP接收到数据包时,它会检查数据包的序列号。如果序列号是期望的下一个序列号,数据包就会被接受并传递给应用层。如果数据包的序列号不是期望的下一个序列号,这意味着数据包可能是乱序到达的。
3. 缓冲乱序数据包:TCP接收方维护一个“接收缓冲区”,用于存储乱序到达的数据包。这些数据包不会被立即传递给应用层,而是保存在缓冲区中,直到所有缺失的数据包都到达并可以按顺序处理。
4. 确认机制:TCP使用累积确认机制,这意味着接收方只会确认最后一个按顺序接收到的数据包。即使后续的数据包已经到达,只要它们不是按顺序的,就不会被确认。
3.2.2 超时重传
实际上,网络传输不会一帆风顺,而是可能出现“丢包”的情况。那么为什么会丢包呢?
产生丢包的原因有很多种,比如:1)数据传输过程中,发生了比特翻转,收到这个数据的接收方/中间的路由器等,计算的校验和对不上,就会把这个数据包丢弃掉,不继续往后转发/不交给应用层使用。 2)数据传输到某个节点(路由器/交换机),这个节点负载太高了,某个路由器单位时间只能转发N个包,此时是网络高峰期,这个路由器单位时间需要转发的数据包超过N了(发不过来了,类似堵车),后续传输过来的数据就可能被这个路由器直接丢弃掉。
发生丢包,是完全随机的,不可预测。
- 那么,TCP是如何对抗丢包的呢?—— 重新发一次。
假设网络丢包率10%,数据包到达对方的概率是90%(10%的丢包率是相当高的数字,出现这个情况,一般都是网络发生严重故障)。此时,进行一次重传,两次传输至少一次到达对方的概率
1 - 10%*10% = 99%。由此可见,传输次数越多,数据到达对方的概率就越大。
是否丢包,需要通过应答报文来区分:收到应答报文,说明数据没丢包;没收到应答报文,就说明数据丢包了。
数据在网络传输是需要消耗时间的,“没收到应答报文” 可能是:1)暂时没收到,一会儿就收到了 2)永远都收不到
因此,为了区分上述情况,发送方发送数据之后,会给出一个“时间限制”(超时时间)。如果在这个时间限制之内,没有收到ACK,就视为数据丢包了。
发送方无法区分是上面的哪种情况,因此发送方都会在达到超时时间之后,重传数据。
针对确认应答丢包的情况,此时主机B就受到了 两份一样的数据。如果传输的数据是“扣款”这样的请求,不处理这种情况就会出问题。
TCP针对上述情况做了处理:接收方有一个接收缓冲区,收到的数据先进入到缓冲区中,后续再收到数据,就会根据序列号,在缓冲区中找对应的位置(排序)。如果发现,当前1~1000的数据已经在缓冲区中存在了,就直接把新收到数据丢弃掉。确保应用程序,调用read读出来的数据是唯一的,不重复的。
超时重传的时间设定
这里的时间,不是固定值,而是动态变化的。发送方第一次重传,超时时间是t1。如果重传之后,仍然没有收到 ACK,还会继续重传。第二次重传的超时时间是 t2,其中 t2 > t1。
每多重传一次,超时时间的间隔就会变大,重传的频次就会降低。因为,经过一次重传之后,就能让数据到达对方的概率提升很多;再重传一次,又会提升很多。反之,如果重传几次,都没有顺利到达,说明网络的丢包率,已经达到了一个非常高的程度,说明网络发生了严重故障,大概率无法继续使用了,重传再快也没意义了。
重传也不会无休止的进行,当重传达到一定次数之后,TCP不会尝试重传,就认为这个连接已经断开了。此时TCP会先尝试进行 “重置/复位连接”,发送一个特殊的数据包—— “复位报文RST”。RST报文不需要接收方确认,发送方发送RST后,TCP就会立即单方面放弃连接(连接就是通信双方各自保存对方的信息,发送方释放掉之前保存的接收方的相关信息,这个连接就没有了)
再谈TCP可以自动拼装数据包的关键点:
- 有序性:由于序列号的存在,接收方可以知道每个段在数据流中的位置,从而可以按照正确的顺序重组数据。
- 可靠性:确认应答和重传机制确保了所有数据最终都会到达接收方,即使某些段在传输过程中丢失或损坏。
- 缓冲区管理:接收方维护一个缓冲区,用于存储乱序到达的段,直到所有缺失的段都到达并可以按顺序拼装成完整的数据。
- 端到端通信:TCP提供端到端的通信服务,这意味着它负责在整个网络路径上维护数据的完整性和顺序,而不是依赖每个中间节点。
3.3.3 连接管理 —— 三次握手与四次挥手
A. 三次握手 —— 建立连接
- 次数:网络通信交互的次数
- 握手/挥手:形象的比喻,指传输一个不携带任何业务数据(只有报头,没有载荷)的数据包
建立连接是一个“双向的操作”:A需要给B说,我想和你建立连接(A想保存B的信息);B也需要给A说,我也想和你建立连接(B想保存A的信息)。三次握手确保了双方都准备好了数据传输,从而提高了连接的可靠性。
流程简图:
SYN --> synchronization,此处是同步的意思。此处的同步,可以延伸成客户端希望与服务器统一步调,来完成后续的传输。
三次握手的意义何在?
1)投石问路,初步验证通信的链路是否畅通,这是进行可靠传输的“前提条件”,排除一些可能存在的故障。
比如 地铁也会进行“链路畅通的验证”,在地铁开始的第一班车之前,会先空跑一趟,排除在停止运营阶段可能出现的一些路线上的故障。
2)确认通信双方各自的发送能力和接收能力是否都正常,这也是可靠传输的“前提条件”。
比如,在联机打游戏的时候,双方需要确认一下,彼此的耳机与麦克风是否都正常。
3)让通信双方在进行通信之前,对通信过程中需要用到的一些关键参数,进行协商。
比如,TCP通信时,起始数据的序号,就是通过三次握手,协商确定的。换而言之,TCP序号并不是从1开始的。每次建立连接,TCP的起始序号都不同,而且故意差别很大。
对于B来说,需要区分当前收到的数据是“本朝”还是“前朝”的 —— 给每个连接,协商不同的起始序列号。如果发现收到的数据,和起始序列号以及和最近收到的数据序列号,都差别很大的话,就视为这个数据就是“前朝”的数据。
总结
进行TCP三次握手的原因
- 投石问路,验证通信路径上是否畅通
- 验证通信双方的发送能力和接收能力是否正常,确保双方都准备好数据传输,并且能够互相确认
- 协商重要的参数,比如同步TCP连接中的起始序列号,以便后续的数据传输能够正确地被识别和处理,防止旧的连接请求(可能因为网络延迟而延迟到达)被错误地接受
B. 四次挥手 —— (优雅的)断开连接
前面说到“超时重传”的时候,谈到了发送RST报文“单方面释放连接”;而“四次挥手”是双方都释放连接的过程,即双方各自把对端的信息删除掉。断开连接,不一定是“客户端主动”,“服务器”也可以主动断开。
四次挥手的过程:
第一次挥手
1.客户端发送FIN:客户端完成发送数据后,会发送一个FIN(Finish,完成)报文段给服务器。这个FIN报文段中,FIN标志位被设置为1,序列号字段包含客户端发送的最后一个字节的序列号加1。
第二次挥手
2.服务器响应ACK:服务器收到这个FIN报文段后,会发送一个ACK报文段作为响应。在ACK报文段中,确认序号字段设置为客户端发送的FIN报文段的序列号加1。此时,客户端到服务器方向的连接被关闭,但服务器到客户端方向的连接仍然开放,服务器可以继续发送数据。
第三次挥手
3.服务器发送FIN:当服务器也完成了数据的发送,它会发送一个FIN报文段给客户端,请求关闭服务器到客户端方向的连接。在这个FIN报文段中,FIN标志位被设置为1,序列号字段包含服务器发送的最后一个字节的序列号加1。
第四次挥手
4.客户端响应ACK:客户端收到服务器的FIN报文段后,会发送一个ACK报文段作为响应。在ACK报文段中,确认序号字段设置为服务器发送的FIN报文段的序列号加1。在发送完这个ACK报文段后,客户端会进入TIME_WAIT状态,等待足够长的时间(通常是2倍的最大段生命周期MSL),以确保服务器收到了确认。* 注意:当客户端发送FIN包后,虽然客户端的进程可以选择关闭,但TCP协议栈仍然会保持活跃,并不代表TCP连接已经完全断开。客户端的TCP协议栈仍然可以在需要时发送ACK包,直到TCP连接的挥手过程完全结束。
流程简图:
通信双方,各自给对方发送“FIN”,各自给对方返回“ACK”。
- 为什么是三次握手,四次挥手呢?
三次握手,三次是因为中间两次的交互合并在一起了。对于四次挥手来说,中间两次不一定能合并的(大概率是不能)
对于三次握手来说,中间的两次 ACK+SYN,都是在内核中,由操作系统负责进行的,时机都是在收到SYN之后,可以合并;
对于四次挥手来说,ACK是内核控制的,但是FIN的触发,则是通过应用程序,调用 close / 进程退出 来触发的。代码中,socket.close --> 系统内部,就是发送FIN。客户端先 close / 客户端退出,都会触发客户端给服务器发送FIN报文段,服务器收到FIN就会立即返回ACK(内核控制的),并且结束读取数据的循环。
如果close之前要执行很多逻辑,消耗很多时间,就会使服务器发送FIN的时间更晚。
下面是整个过程的详细图解,包括了TCP的状态转换:
上图中的几个关键状态需要掌握:
- LISTEN:当服务器上的一个应用程序准备好接收进入的连接请求时,它会告诉内核它想要在某个端口上监听,把端口绑定好。此时,该端口的TCP状态变为LISTEN。此时服务器就已经初始化完毕,等待客户端发起连接请求(手机开机,信号良好,随时可以有人来打电话了)
- ESTABLISHED:表示一个TCP连接已经成功建立。这是在客户端和服务器完成三次握手之后,都会进入的状态。
- CLOSE_WAIT:被动断开连接的一方,会进入这个状态,先收到FIN的一方,“等待代码执行close方法”。一般而言,对于服务器上出现大量的CLOSE_WAIT状态,原因就是服务器没有正确的关闭socket,导致四次挥手没有正确完成。这是一个bug,只需要加上对应的 close 即可。
- TIME_WAIT:主动断开连接的一方,会进入这个状态。此处的TIME_WAIT按照时间来等待,达到一定时间之后,连接就释放了。TIME_WAIT的时间为2MSL(MSL是TCP报文的最大生存时间)
为什么不直接释放,而是要等待一定时间呢?—— 防止最后的ACK丢包。
保留一段时间之后,也没有收到重传的FIN,说明刚才的ACK应该就是到了,就可以释放这里的连接了。
为什么 TIME_WAIT 的时间是2MSL?
- MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN。这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK);
3.3.4 滑动窗口
可靠传输,会降低传输的效率,TCP希望能在可靠传输的基础上,有一个不错的效率,于是引入了滑动窗口。这里的提高效率,只是“亡羊补牢”,使传输效率的损失尽可能降低,但不能使传输效率比UDP还高。
在TCP中,滑动窗口是一种流量控制机制,它允许发送方在不等待确认的情况下发送多个数据包,同时也允许接收方控制发送方的发送速率,以避免网络拥塞和数据丢失。
改进方案,就是把“发送一个等待一个”改成“发送一批等待一批”。把多次等待ack的时间,合并成一份时间。批量发送的数据越多,此时效率就可以认为越高。批量发送数据的量,称为“窗口大小”,单位是“字节,不是“条”。
批量发送了1001~5000的数据后,会批量等待这组数据的ACK。当收到第一个ACK后,滑动窗口会移动到ACK的确认序号处,并根据ACK返回的窗口大小,调整右边窗口的位置。
当收到 2001的ACK,说明1~2000数据得到了应答,然后立即发送5001~6000这个数据。此时,等待的ACK的范围就是2001~6000(四份数据),窗口大小还是4000。窗口大小不变,窗口处在的位置改变了。
滑动窗口移动过程
1. 初始化窗口
- 在TCP连接建立时,双方协商初始窗口大小
- 发送方维护一个发送窗口,它表示发送方可以发送但尚未得到确认的数据的最大范围
2.发送数据
- 发送方开始发送数据,直到达到发送窗口的边界
- 发送的数据被标记为已发送但未确认
3. 接收确认
- 接收方收到数据后,发送一个ACK给发送方,ACK中包含下一个期望接收的数据序列号
- 接收方根据自身的缓冲区情况维护接收窗口,并通过ACK中的窗口字段告知发送方自己的接收窗口大小
4.窗口移动
- 当发送方收到一个ACK时,它会根据ACK中的序列号和窗口字段来移动发送窗口,将发送窗口的左沿移动到ACK的序列号处,这表示 1 ~(ACK序列号-1)的数据已经得到确认,可以从发送方的缓冲区中移除。
- 窗口的左边界向右移动,以反映已经确认的数据,这个边界称为左沿
- 窗口的右边界可能根据接收方报告的窗口大小向右扩展或向左收缩,这个边界称为右沿
滑动窗口的前提是可靠性,如果在滑动窗口传输中,出现丢包怎么办?
情况一:数据包已经抵达,ACK包丢了
- 解决办法:不需要任何处理
批量发送数据,就需要批量接收ACK。多个ACK只是丢其中一部分,不可能全丢。注意,确认序号表示的是收到数据最后一个字节的下一个序号,进一步理解成,确认序号之前的数据都已经收到了,接下来发送方要发的数据就从确认序号开始往后发。虽然1001 ACK丢了,但是2001ACK到达,发送方收到2001ACK之后,意味着2001之前的数据都已经收到了,后一个ACK能够涵盖前一个ACK。
情况二:数据包直接丢了
- 解决办法:快送重传机制
如果1001~2000数据包丢了,此时,当B收到2001~3000的时候,返回的ACK确认序号不是3001,而是1001,意味着B在向A索要1001的数据。
接下来,B收到3001~4000,4001~5000,5001~6000....数据,但对应的ACK确认序号都是1001。主机A连续收到多个1001的ACK之后,主机A便意识到1001数据丢包了,于是会重传1001~2000的数据。
当1001~2000重传过来了之后,由于之前2001~7000数据都已经收到了,1001~2000相当于补全了之前的空缺,意味着1~7000的数据都收到了,此时从7001开始发数据即可。
上述过程就是“快速重传”机制。
快速重传
- 当接收方收到一个数据包,但期望的下一个数据包尚未到达时,它会发送一个重复的ACK,这个ACK的编号是接收方期望收到的下一个数据包的序列号。
- 如果发送方连续收到三个(这个数字可能会根据不同的TCP实现而有所不同)重复的ACK,它就会认为网络中有一个数据包丢失了。
上述过程,能快速识别出是哪个数据丢包,并且针对性的重传,其他顺利到达的数据都无需重传。快速重传可以视为是“滑动窗口”下搭配的“超时重传”
- 滑动窗口 <——> 快速重传
- 确认应答 <——> 超时重传
彼此之间并不冲突的。如果通信双方,单位时间发送的数据量比较少,就是按照确认应答 / 超时重传的机制;如果单位时间发送的数据比较多,就会按照滑动窗口 / 快速重传的机制
3.3.5 流量控制
类似于生产者消费者模型,如果发送速度特别快,消费数据比较慢,就会使接收缓冲区满了。此时,如果发送方强行发数据,就会产生丢包(被接收方丢弃了)。因此,需要让接收方的处理能力,反向制约发送方的发送速度——“流量控制”
- 如果接收缓冲区的空闲空间越大,说明应用程序处理速度比较快,就可以让发送方发的快一点,设置一个更大的窗口大小;
- 如果接收缓冲区的空闲空间越小,说明应用程序处理速度比较慢,就可以让发送方发的慢一点,设置一个更小的窗口大小。
TCP中,接收方收到数据的时候,就会把接收缓冲区剩余空间大小,通过ACK数据报,反馈给发送方,发送方就可以依据这个数据来设置发送的窗口大小了。
通常情况下,窗口大小是在ACK报文中被发送方用来告知接收方其接收缓冲区剩余的大小,即当前能够接收的数据量。接收方可以根据自己的缓冲区状况动态调整窗口大小,并通过发送ACK报文将新的窗口大小通知给发送方。
窗口大小
在TCP协议中,窗口大小的单位是字节。TCP头中的窗口字段用于指示发送方可以在没有收到ACK的情况下发送的数据量,这个字段的值表示的是窗口的大小,单位是字节。
具体来说,TCP头中的窗口字段是一个16位的字段,这意味着它可以表示的最大数值是2^16-1,即 65535字节。然而,通过使用TCP窗口缩放选项,可以扩展这个窗口大小,允许更大的窗口,从而支持更高的传输速率和网络吞吐量。窗口缩放选项是在TCP选项中定义的,它允许将窗口大小乘以一个缩放因子,这个因子也是通过TCP选项来协商。这样,即使16位的窗口字段本身的值不变,实际允许的窗口大小可以远远超过65535字节。例如,如果缩放因子是7,那么窗口大小可以达到最大值65535*2^7 = 4,294,967,295字节。
窗口探测包不携带业务数据(载荷部分是空着的),只是为了触发ACK,通过这个查询接收方接收缓冲区剩余多少。
流量控制,也不是TCP独有的机制,其他的协议也可能会涉及到流量控制(比如,数据链路层中有的协议,也支持流量控制)
3.3.6 拥塞控制
- 流量控制,是站在接收方的视角来限制发送方的速度的
- 拥塞控制,是站在传输链路的视角来限制发送方的速度的
假设,主机B处理速度非常快,此时A就可以无限速度的发送数据吗?—— 当然不行! 因为中间链路上的设备可能顶不住!
针对传输数据速度的问题,流量控制,可以使用接收方接收缓冲区剩余空间来进行衡量。
考量中间节点的情况,就麻烦了
- 中间的节点非常多
- 每次传输数据,走的路线还都不一样
- 中间哪个节点遇到瓶颈了都不好说
- 中间节点传输数据不止有主机A的数据,还有很多其他设备的数据
拥塞控制中,把中间传输的节点,视为一个整体,并不关心内部的细节。
拥塞控制是通过“做实验”的方式,找到一个合适的发送速度(面多,加水;水多,加面)
- 先按照一个比较小的速度,发送数据
- 数据非常畅通,没有丢包,说明网络上传输数据整体是比较畅通的,就可以加快传输数据的速度
- 增大到一定速度之后,发现出现丢包了,说明网络上可能存在拥堵了,就减慢传输数据的速度
- 减速之后,发现又不丢包了,继续再加速
- 加速之后发现又丢包了,继续减速
一直持续动态变化(毕竟网络环境也是一直变化的),整个过程如下图所示:
cwnd(拥塞窗口)
在TCP协议中,cwnd的单位是段(Segment),可以理解成“份”,而一个段通常指的是一个TCP数据包的大小。
具体来说,cwnd的单位可以MSS,在讨论TCP拥塞控制时,经常使用MSS作为cwnd的单位,因为它更直观地表示了可以发送的完整数据包的数量。
例如,如果MSS是1460字节(假设路径MTU是1500字节,减去20字节的TCP头和20字节的IP头),那么cwnd为10MSS,意味着当前可以发送10个这样的数据包,即14600字节的数据。
拥塞控制过程描述
1.链路建立(三次握手)
- 在开始数据传输之前,TCP通过三次握手建立连接,并初始化拥塞窗口(cwnd)和慢启动阈值(ssthresh)
2.慢启动
- 初始化:连接建立后,cwnd通常被设置为1个MSS(由于当前网络是否拥堵,未知,刚启动的时候,发数据的速度很慢),ssthresh被设置为较大的值(例如64KB)。
- 窗口增长:每收到一个ACK,cwnd增加1个MSS。由于每个RTT(往返时间)可以发送多个段,cwnd的增长是指数级的。
- 转换条件:当cwnd达到ssthresh时,停止慢启动,进入拥塞避免阶段。
3.拥塞避免
- 窗口调整:在拥塞避免阶段,每个RTT只将cwnd增加1个MSS,cwnd的增长变为线性的。
- 维持状态:只要网络没有出现拥塞迹象(如丢包),TCP将继续以这种方式增加cwnd。
4.拥塞检测
- 丢包事件:当网络出现拥塞,表现为数据包丢失(通过超时或收到三个重复ACK)。
- 慢启动闯值调整:发生拥塞时,ssthresh被设置为当前cwnd的一半(对于超时)或减少到当前cwnd和接收到的重复ACK指示的窗口大小的最小值(对于快速重传)。
5.快速重传和快速恢复
- 快速重传:收到三个重复ACK时,立即重传丢失的数据包,而不是等待超时。
- 恢复过程:当收到丢失数据包的ACK时,将cwnd设置为ssthresh的值,然后进入拥塞避免阶段。
6.拥塞控制循环
- 重复过程:如果再次发生拥塞(丢包),TCP将重复上述过程,调整cwnd和ssthresh,以适应网络状况。
- 自适应调整:TCP会根据网络状况动态调整cwnd和ssthresh,以找到最佳的传输速率。
流量控制,会限制发送窗口;拥塞控制,也会限制发送窗口。这两个机制,会同时起作用,最终实际的发送窗口大小,取决于上述两个机制得到的发送窗口较小值。流量控制和拥塞控制,都是对“可靠传输”进行补充。
3.3.7 延时应答
TCP的延时应答机制是一种旨在提高网络效率的优化手段,尽可能降低可靠传输带来的性能的影响。它的基本思想是,不是对每个接收到的数据包都立即发送一个确认(ACK),而是延迟发送ACK,以减少在网络中传输的ACK数量。
此时,延时返回的ACK窗口大小,大概率就要比立即返回的ACK窗口大小更大(在这个延时的时间里,会有一个消费数据的过程)。
注意,并不是所有的包都能延迟应答
- 数量限制:每隔N个包就应答一次;
- 时间限制:超过最大延迟时间就应答一次;
具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。
3.3.8 捎带应答
捎带应答是TCP中提升性能的机制,它允许在发送数据的同时附带ACK确认。这意味着当发送方有数据要发送时,它可以利用这个机会来确认之前收到的数据包。这样做可以减少网络中的包数量,提高网络带宽的利用率。
3.3.9 面向字节流
创建一个TCP的socket套接字,内核中就会自动创建一个发送缓冲区和一个接收缓冲区:
- 调用write时,数据会先写入发送缓冲区中;如果发送的字节数太长,会被拆分成多个TCP的数据包发出;如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据。
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据,这个概念叫做全双工。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,就像水流一样,例如:写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。
以接收缓冲区为例,总结一下
定义
接收缓冲区类似于一个“阻塞队列BlockingQueue”,是网络协议栈中的一个内存区域,用于存储从网络中接收到的数据,直到这些数据被上层应用程序读取或者进一步处理(B的应用程序调用read等相关方法)
功能和作用
- 暂存数据:当数据通过网络接口到达时,它们首先被存储在接收缓冲区中。这样可以确保数据不会因为应用程序未能即时处理而丢失。
- 流量控制: 接收缓冲区可以作为一种流量控制机制。当缓冲区满了时,接收方可以通知发送方减少发送速率或者暂时停止发送,直到缓冲区有足够的空间。
- 处理数据包乱序:网络传输中数据包可能会乱序到达。接收缓冲区可以重新排序这些数据包,确保应用程序按照正确的顺序接收数据。
- 数据完整性:接收缓冲区可以确保数据的完整性,即使是在网络条件不佳,数据包丢失或重复的情况下,通过TCP等协议的机制,可以恢复完整的数据流。
- 解耦发送方和接收方的处理速度:接收缓冲区允许发送方和接收方的处理速度不匹配。即使接收方的应用程序处理速度较慢,接收缓冲区也可以暂存数据,直到应用程序准备好处理。
工作过程
- 数据到达:数据包通过网络接口到达接收方的网络协议栈。
- 存储数据:数据包被解封并存储在接收缓冲区中。
- 通知应用程序:当接收缓冲区中有数据时,操作系统会通知相应的应用程序读取数据。
- 应用程序读取:应用程序从接收缓冲区中读取数据,进行处理。
- 缓冲区管理:随着数据的读取,接收缓冲区的空间被释放,可以用于存储新的数据。
3.3.10 粘包问题
首先要明确,粘包问题中的“包”,是指应用层的数据包,在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段,但是有一个序号这样的字段。
- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
- 站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
针对“粘包问题”,如何进行解决呢? —— 明确两个包之间的边界
- 方案一:在每个应用层数据包前,约定一个包总长度的字段,从而就知道了包的结束位置。如果是传输二进制数据,这个方案就很有用了。
- 方案二:在包和包之间使用明确的分隔符。比如 编写 TCP的 echo server时,我们的做法是,约定“请求”“响应” 都以“\n”结尾 —— 发送请求响应的时候,专门使用println进行写数据;读取请求响应的时候,专门使用scanner.next按照 “\n” 进行解析。但需要确认,数据内容的正文中,不能包含分隔符。如果传输的数据,是纯文本数据的话,可使用“\n”或者“ ; ”,其他的数据格式可能不适合,但是可以使用ASCII码中靠前的“控制字符”
对于UDP协议来说,是否也存在“粘包问题”呢?—— 不存在
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层,就有很明确的数据边界。
- 站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个"的情况。
3.3.11 异常情况处理
情况一:进程崩溃
看起来崩溃挺严重的,实际上操作系统会进行善后。当进程崩溃的时候,进程中的PCB就要被回收 --> PCB中的文件描述符表里对应的所有文件,也都会被系统自动关闭,其中针对socket文件,也会触发正常的关闭流程(TCP四次挥手)
情况二:主机关机
正常流程点击关机按钮,此时操作系统就会先关闭所有的进程,与情况一相同,会进行四次挥手。
a)四次挥手非常快,四次挥手已经完成了,关机动作才真正完成
b)四次挥手没来得及完成,关机就完毕了
情况三:主机掉电(拔掉电源)
- TCP正常断开指双方各自删除对方;TCP异常断开指单方面删除自己的,不管对面了
a)接收方掉电
A给B发送的数据,不会再有ACK了,此时A触发超时重传。重传的数据,没有响应,反复多次之后,A会发送一个没有载荷的TCP段,RST标志位置为1,接收到RST段的TCP端点会立即终止连接,无须四次挥手,A就能单方面释放连接了。
b)发送方掉电
主机A发着发着就不发了,在主机B看来,不知道A是掉电了,还是晚点再发。此时主机B就会给主机A发送一个不携带业务数据的探测包,想要触发主机A的ACK。
如果发了探测报文之后,A返回了ACK,说明A没有掉电;如果发了探测报文,A没有返回ACK,甚至连续多个探测报文,A都没有返回ACK,就可以与A单方面断开链接了。
这样的探测报文,是周期性的,同时这个报文是用来探测对方“生死”的,把这样的报文也称为“心跳包”。“心跳包”是一种定期发送的小型数据包,用于确认网络连接的两端是否仍然活跃,以及检查连接的稳定性。
TCP内置了心跳包,由于TCP内置的心跳包周期比较长(秒级~分钟级),应用层通常也会自行实现一些心跳包,达到更快速的“保活机制”。例如在TCP连接上,发送特定的应用数据作为心跳包。
情况四:网线断开
与主机掉电的情况是一样的。
3.3 其它标志位
1. URG(Urgent):紧急标志位,和紧急指针配合使用。当URG=1时,紧急指针能够生效,紧急指针里保存的是一个偏移量,指示紧急数据结束的位置。正常情况来说,TCP都是按照顺序来传输数据的,紧急指针就是让后面的数据插队,根据紧急指针的偏移量,把指定位置的数据,优先发送出去。特殊场景的特殊方案,不是一个通用的方案,日常开发中很少直接涉及。
2. PSH(Push):推送标志位。当PSH=1时,告诉接收方应该尽快将数据推送给接收应用程序,而不是等待缓冲区满(也是特殊场景下的特殊方案)
3.4 TCP / UDP的对比
我们说了TCP是可靠连接,那么是不是TCP一定就优于UDP呢?TCP和UDP之间的优点和缺点,不能简单,绝对的进行比较
- TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景;
- UDP对于可靠性要求不高,用于对高速传输和实时性要求较高的通信领域。例如,早期的QQ,视频传输等,另外UDP可以用于广播,分布式系统中。
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。