Linux系统之----UDP、TCP详解
1.端口号
1)定义
端口号是一个16位的数字,用于标识主机上的特定进程或服务。每个端口号对应一个特定的应用程序或服务,使得数据能够正确地分发到目标应用程序。
2)端口号的范围
端口号的范围是从0到65535(2^16 - 1),因为端口号是16位的。通常,端口号被分为以下几类:
知名端口(Well-Known Ports):0到1023,这些端口号被IANA(互联网号码分配局)分配给特定的服务。例如,HTTP服务通常使用端口80,HTTPS服务使用端口443,TELNET端口23,SSH端口22,FTP端口21
注册端口(Registered Ports):1024到49151,这些端口号可以由用户或组织注册使用,但需要遵循一定的注册流程。
动态或私有端口(Dynamic or Private Ports):49152到65535,这些端口号通常不分配给固定服务,而是由应用程序在运行时动态选择。
查看知名端口号:
cat /etc/services2.UDP
我们来看一下UDP的结构:

16位源端口号:标识发送方的端口号。如果发送方是客户端,这个端口号可以是临时分配的;如果是服务器,通常是已知的端口号。
16位目的端口号:标识接收方的端口号,用于将数据报分发到正确的应用程序。
16位UDP长度:表示整个UDP数据报的长度,包括UDP头部和数据部分。这个字段确保接收方知道需要读取多少字节来完整地接收一个UDP数据报。
16位UDP校验和:用于错误检测,确保数据在传输过程中没有被损坏。校验和是可选的,但在大多数情况下都会使用。
数据(如果有):这是UDP数据报的实际数据部分,长度可变,取决于应用的需求。
3.UDP的特点
1)基本特点
1)无连接:无连接意味着UDP在发送数据之前不需要建立一个稳定的连接。发送方可以直接向接收方发送数据包,而不需要先进行握手或连接建立过程。
不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。即不提供数据传输的可靠性保证,包括数据包的确认、重传和顺序保证。
面向数据报:不能够灵活的控制读写数据的次数和数量;它将应用程序发送的数据视为一系列独立的数据报进行传输。每个数据报都是单独封装和传输的,它们之间没有关联,每个数据报都包含完整的信息,如源端口号、目的端口号等。说人话就是,应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并;用 UDP 传输 100 个字节的数据:如果发送端调用一次 sendto, 发送 100 个字节, 那么接收端也必须调用对应的一次 recvfrom, 接收 100 个字节; 而不能循环调用 10 次 recvfrom, 每次接收 10 个字节;
注意:1)UDP具有接收缓冲区。但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
2)UDP的socket既能读,也能写,这个概念叫做全双工
2)协议报头和有效载荷分离
报头和有效载荷分离:在网络协议中,报头(Header)包含了必要的控制信息,如源端口、目的端口、长度和校验和等。有效载荷(Payload)则是实际传输的数据。
UDP报头和有效载荷如何分离:UDP报头长度是固定的8字节,这使得UDP报头和有效载荷的分离变得简单。报头之后的所有内容都是有效载荷。
UDP长度是发送方udp层填写的报文的总长度:UDP报头中的“长度”字段包括了UDP报头和数据部分的总长度。这个字段确保接收方知道需要读取多少字节来完整地接收一个UDP数据报。
4.TCP
4.1 概述
TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
4.2 协议段格式及标志位

源端口和目的端口就是表示数据从哪里来,到哪里去
之后是4位首部长度,这里的长度是指的整个报文的长度,所以可以计算一下TCP的选项,即[0000-1111],也就是[0,15],单位为4字节,所以总字节就是【0,60】,但是由于固定报头长度为20字节,所以TCP选项最多60-20=40字节
之后是6个标志位,我们要先了解到什么是标志位,标志位就是结构体位段中的比特位!0(无效)or 1(有效)----把标志位具象化!那为什么要有标志位呢?就是区分报文类型!!!
下面我们来看一看这些标志位,首先是ACK标志位,用于确认收到的数据,这是一个确认报文,一般是以捎带应答的方式出现,所谓的捎带应答,可以理解为现实生活中的“捎句话”,在大部分情况下,ACK的值为1,表示成功接受!
1)三次握手
之后是SYN标志位,其用于建立连接。在TCP三次握手过程中,SYN标志位被用来同步序列号。SYN为1时称为同步报文段。我们知道,TCP是面向连接的,所以要在连接前进行三次握手,具体步骤就是客户端向服务端发送ACK,之后服务端对其回应SYN+ACK,最后客户端再回应ACK,实际上,三次握手也是四次握手,因为第二次第三次往往被捎带应答了!!!

如果感觉难理解的话我举个例子,就是假设我跟我喜欢的女生说,我们在一起吧,你做我女朋友吧,她回复好呀,什么时候,之后我回复就现在!这短短三句话之后,我们便确认了关系,因为通过这三句话,我们确认了共识,
那么也许会有人问为什么是三次握手,而不是两次,或者四五六次呢?原因就在于如果使用两次握手,服务器可能无法区分一个旧的失效连接请求突然又传送到了服务器,还是一个新的连接请求。三次握手通过客户端发送一个确认报文来确认服务器的SYN-ACK报文,从而避免了这种情况。而且四五六次握手就会浪费通信资源,这是不必要的浪费,可能有点难理解,我们在结合上述例子想一下,假设现在是两次握手,假设我跟她说我们在一起吧,她也回了好啊,但是我们首先要承认一个人类通信的事实,就是我们永远不知道我发送的最后一条消息对方收到了没有,那么对于这个例子也同理,假设她说了好啊,但是由于网络问题,我没有收到他的消息,于是以为她对我没兴趣,随后我们的链接就建立失败了,因为我们没有达成共识!!!她跟我达成共识了,但是我没有收到她跟我达成共识的这一消息,所以也不会对她进行回复,而通过三次握手,就既能保证我收到了她的共识,也能保证她收到了我的共识,这是最少的建立连接的次数,所以为三次握手!!!
这里我们还要理解一下什么是握手!只要是发出和收到,就是握手,不必等到对方收到,你也等不到!!
随后我们在讲解一下RST标志位,其全称为RESET,用于重置由于某种原因出现错误的连接。当RST标志位被设置时,连接会被立即终止。RST置1,重置,这个有什么用呢,就是其实我们TCP的三次握手也不一定能100%保证建立连接成功,此时就要RST重置一下在连接了~
之后是PSH标志位,全称为PUSH,推,PSH标志位被设置时,接收方的TCP层会立即将收到的数据推送给应用程序,而不是在缓冲区中等待更多的数据。
之后是URG标志位,这是个紧急指针,当URG标志位被设置时,表示该TCP报文中包含紧急数据,这些数据需要被接收方优先处理。其由16位的紧急指针来指向,当前报文有效载荷部分的一个偏移量!!那么紧急数据一共只有一个字节,那就是任务码!!!这个不怎么常用。就是我们的数据是要按顺序进行发送和接受的,但是假设我前面还有100个G的数据,但是我不想传输了,于是输入停止指令,但是正常情况下我这个停止指令要在100G数据传输完成之后才能被收到进行解读,那我发了个寂寞啊!所以此时URG就有用了,发完之后,URG置为1,数据传输就立即停止了,相当于插队了~
2)四次挥手
最后就是FIN标志位,FIN标志位用于终止连接。当FIN标志位被设置时,表示发送方已经发送完所有的数据,并希望关闭连接,我们称其为结束报文段。在结束连接时,要通信四次,我们称之为四次挥手,具体看下图:

说直白点就是,client ->httprequest->可以关闭自己的写端了,server->httpresponse->fin
还是拿恋爱的例子来说,假设我女朋友觉得我天天打游戏没出息,要跟我分手,于是她说我要跟你分手(FIN),我回复好(ACK),我也要和你分手(FIN),她又回复说好(ACK),于是我们建立了分手的共识,之后便分开了,对于网络TCP也是这样,这样来来回回通信四次,所以称为四次挥手,
下一个问题就是为什么是四次而不是两次三次或者五次呢?原因跟三次握手是可以类比过来的~
低于四次就分开属于渣男(女)行为,是单方面分手+拉黑,不可取,而超过四次又没有必要了,具体的不说了,直接类比吧!
这里最后在说一下细节吧!
1)消息 != 应答, 前者包含了用户信息的内容,后者就是一个单纯的应答
2)我们不是要保证应答的可靠性,而是要保证消息的可靠性
3)应答方,不确定应答对方是否收到,但是发送方,有没有收到对方的应答是能确认的。
4)收到应答,本质是为了保证历史消息的可靠性
3)TIME_WAIT
我们先来了解一个函数,shutdown函数,这个函数有三种关闭方式,大家自己看一下吧

刚刚我们了解到,cient发送FIN的信息后,服务端也要发送FIN,但是如果我们的服务器端没有调用close(socket)来关闭连接,那么服务器端的连接就会处于CLOSE_WAIT状态,可能会导致资源泄漏。在这种状态下,服务器端仍然可以发送数据给客户端,但是不能接收数据。当主动断开连接的一方发送了FIN包并收到了对方的ACK后,它会进入TIME_WAIT状态。这个状态的持续时间通常是2倍的MSL(Maximum Segment Lifetime,最大报文段生存时间,通常设置为2分钟),以确保网络中可能存在的旧的重复报文能够被丢弃,防止旧的重复报文干扰新的连接,以及确保四次挥手能够正确结束。
4.3 几种常见的应答方式
第一种就是一问一答,client发一句,server回一句,说的专业一点就是对对方给我发的有效消息做应答,保证两个朝向上的可靠性!如下图:

这种方式的TCP通信,通信双方的地位是对等的,但是问题也很明显,就是效率低下,而且不要考虑特殊情况,即丢包问题,
所以我们来看一下第二种通信模式,也就是一般模式,就是client一股脑给server发了一堆消息,server收到后在一股脑的回消息,如此一来,发送消息在时间上就产生了重叠,效率就高了,但是现在有一个问题,我发的消息按顺序发的,那收到的消息一定是按照我发的顺序来的吗?
实际上不是,这就好比几个人同时骑车从东北到广东,路线自选,那先走的不一定先到,我们的消息也是这样,可能有的消息很倒霉,第一个走的,但是走了个远路,还发生了拥塞,最后到也不是不可能的!!
那有人问了,那接收到的消息岂不是乱序的了吗?实际上我们的TCP在设计的时候就考虑到了这一点,所以发送的报文中都是带有序号的,如果收到的是乱序,那收端自己按照序号排一下就好了~
4.4 发送机制

比如我要将消息从A发到B,实际上是经过了这些步骤:主机A的应用层创建了一个消息对象,并将消息对象序列化成字符串格式。之后主机A的应用层通过系统调用(如write函数)将序列化后的消息发送到操作系统内核,之后将消息放入发送缓冲区。随后消息从发送缓冲区被复制到TCP协议的发送缓冲区,之后传输到主机B,主机B的应用层通过系统调用(如read函数)从接收缓冲区读取消息。系统调用将消息从接收缓冲区复制到应用层,在应用层对消息进行反序列化,恢复成原始的消息对象。
4.5 确认序号
确认序号(ack_seq)是TCP协议中用于确保数据可靠传输的一个重要机制。它表示接收方已经成功接收到发送方的数据,并希望接收方从某个特定的序列号开始发送新的数据。
确认序号(ACK Seq)是接收方期望接收的下一个字节的序列号。例如,如果接收方已经成功接收到序列号为100到200的数据,那么它会发送一个确认序号为201的ACK报文,表示它已经准备好接收序列号为201及之后的数据。实际上,这是一种确认应答机制。
确认序号的计算方法是:接收方收到的数据的最后一个字节的序列号加1。例如,如果接收方收到的数据报文的序列号范围是100到200,那么确认序号就是201。
通过这种方式,TCP协议确保了数据的可靠传输。如果发送方没有收到确认报文,它会重新发送未被确认的数据,直到收到确认为止。

上图是正常没有丢包的情况
这也保证了在丢包之后,我们还能又补救措施:如下图

会一直反馈1001-2000的包丢了,反馈超过3次就会重发了。
这也引出了我们下一个问题:快重传问题和丢包问题!
但是在此之前,我们先来了解一下超时重传~
4.6 超时重传
首先我们来谈论一个问题,如何去看待丢包问题,第一种情况是我发的报文,你有没有收到,我其实是可以知道的,因为如果我收到了应答,那就100%知道对方收到了;第二种情况就是我发的报文,丢失了,那我不会知道它丢失的事情,所以说我们判定是否丢包,是被约定出来的,实际上我们的丢包只有两种情况,要么是数据丢,要么是应答丢,二者是不可能同时存在的,但是不论是哪种,我们的结果都是你收不到对方的应答!!
所以这里我们就可以约定一个特定的时间,在这个时间以内往返应答,确认应答了,我们就说收到了数据,这里的时间我们称之为往返时间RTT(全称Round-Trip Time),如果超过这个时间没有应答,我们就判定超时(不叫判定丢包),此时进行我们的数据的重发!那么我们这个时间,是一成不变的吗?实际上,这个RTT也是根据网络的浮动,进行动态调整的!RTT要是太长,会导致我们的发送效率降低,但是要是太短的话,又会导致重传机制被高频触发,会频繁发送重复的包。
所以TCP为了保证有效高性能通信,,会动态计算这个最大超时时间~,Linux 中,超时以 500ms 为一个单位进行控 制, 每次判定超时重发的超时时间都是 500ms 的整数倍,如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增。累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接
也许有人会说那不万一传重了一份那不就变成不可靠了吗?但是我们的系统会根据序号去重!!!多了总比少了好!!!
4.7 快重传
我们已经了解到,在TCP协议中,发送方发送数据后,需要等待接收方的确认(ACK)。如果发送方在预定的时间内没有收到确认,它会认为数据包可能已经丢失,并触发重传机制。传统的重传机制依赖于超时(Timeout)来检测丢包,但这种方法可能不够及时,因为它需要等待一个固定的超时时间,所以我们引入了快重传机制。
快重传机制通过检测接收方发送的重复确认来快速识别丢包。重复确认是指接收方收到一个或多个失序的报文段后,发送的确认号相同的ACK,就像上面那个图一样。如果发送方连续收到三个或更多的重复确认,它将立即重传最后一个被确认的报文段之后的第一个未被确认的报文段,而不需要等待重传计时器超时。
也许就会有人说,那这两个重传机制会不会重复呢?实际上,快重传机制和超时重传机制通常是同时使用的。快重传机制用于快速响应局部丢包,而超时重传机制用于处理更严重的网络问题,如长时间的网络延迟或大规模丢包。说白了超时重传用来兜底,快重传用来提高传递速率!
4.8 滑动窗口

由此,我们引出滑动窗口的概念:
实际上我的算法篇也提到过这个滑动窗口的问题,二者道理是相同的~
听起来可能有点抽象,我们画图理解一下,假设我现在有一个数组,我们用数组下标来模仿指针的行为,在start的左边,是已经发送的数据,在end的右边是未发送的数据,所以我们还可以得出一个公式:start=确认序号(ack),end+start+win(窗口大小),所以我们滑动窗口的的大小是由win窗口大小来确定的,而win窗口大小又取决于流量控制和网络拥塞程度,这个我们后面会谈的~

所以我们先总结出三个结论:
1)滑动窗口的存在,他把发送缓冲区切分成为了三部分:
a. 直接发,暂时不要应答 (红色框框)
b. 已发送,已确认            (start左侧)
c. 待发送(待发送和空的位置)(end右侧)
2)窗口滑动的方向:从左向右滑动的!因为我们win>0,所以不可能向左滑动!!!
3)滑动的本质是:下标增多,就是滑动!
下面我们来讨论滑动窗口的范围:滑动窗口的大小由接收方的接收能力决定,接收方通过ACK报文中的窗口大小字段告诉发送方其当前的接收窗口大小。
1)当接收方的处理能力增加时,它会增大窗口大小,允许发送方发送更多的数据。
2)当接收方的处理能力减少时,即接收缓冲区大小减小,它就会减小窗口大小,甚至可能减小到 0,表示暂时无法接收更多数据。
3)当窗口大小减小到0时,发送方会进入零窗口探测状态,定期发送探测报文段,询问接收方是否可以开始接收数据。

如上图,窗口起始位置(start)为2000,结束位置(end)为6000,窗口大小(win)为4000。当接收方确认了从2000到3000的数据后,窗口向右滑动1000字节。新的起始位置(start)为3000,结束位置(end)为6000,窗口大小(win)仍为3000。以此类推,窗口大小减减减,直到0为止。
下面我们来讨论几种丢包的问题~
4.9 三种常见丢包问题
1)最左侧报文丢失(滑动窗口左侧丢包)
当滑动窗口中最左侧的报文段丢失时,发送方无法收到该报文段的确认(ACK),因此滑动窗口左侧不会移动,也就不能发送新的数据,该数据也不能被删除,因为已经发送但是没有被确认收到的数据会被保存在窗口内部,这样会方便我们在发生丢包问题时,可以将丢的数据重传!
处理方式是发送方会重传丢失的报文段,并等待接收方的确认。一旦收到确认,滑动窗口左侧就会相应地向右移动。
2)中间报文丢失
这种情况解决起来很简单,中间丢包了,窗口左侧的又没有丢,直接左侧向右移动到丢的这里就完事了,然后就转化为情况一了~
3)最右侧报文丢失(滑动窗口右侧丢包)
这个处理方法跟情况二是一样的,直接窗口左侧++移动,转化为情况一就好了~
所以这三种情况是一种问题,我们可以都转化为左侧丢包的问题~
滑动窗口可以完美的处理这三种丢包问题,实际上是得益于确认序号机制,滑动窗口的滑动,是不准跳过没有确认的报文的,这也保证了滑动窗口滑动的连续性!
还有最后一个小问题,就是万一它滑出去了怎么办?会不会发生越界啊?这一点,我们目前逻辑上可以将发送缓冲区想成一个环形队列~
总结:
滑动窗口是输出缓冲区中的一段,可以暂时不用应答,直接发送的数据区域,是流量控制和重传机制的底层实现,其中滑动窗口的大小由接收方控制,发送方根据接收方的窗口大小来调整发送速率。
4.10 流量控制
TCP流量控制是一个动态过程,首先接收端根据自身缓冲区容量设定一个窗口大小值,并通过TCP首部中的"窗口大小"字段以及ACK报文段通知发送端其可接收的数据量。发送方根据接收方通告的窗口大小来控制发送速率,确保不超过接收方的处理能力。随着接收方处理数据,窗口大小会动态调整,如果接收方缓冲区接近满载,它会减小窗口大小并通知发送方,导致发送方减慢发送速度。在极端情况下,接收方缓冲区满时,会将窗口大小设置为0,通知发送方暂停发送数据,但发送方会定期发送窗口探测报文段来询问接收方是否恢复接收能力,从而维持一个稳定的数据传输流,避免数据丢失和网络拥塞。这个过程确保了发送方的数据发送速率与接收方的处理能力相匹配,实现了有效的流量控制。
但是现在还有一个问题:
可是第一次的时候,主机A,如何得知主机B的窗口大小呢???
答案就是:在通信之前,双方是经历过三次握手的!!!!前两次握手,一定不携带任何数据(因为连接还没有建立成功,不能发数据)但是,双方在前两次是交换过报文的!!!!,所以,通信以前,就已经交换过双方的窗口大小了!!!!!!!
回忆我们的TCP结构,有个16位窗口大小,那么就是2^16=65536个字节吗?实际上,这65536个字节指的是接收和发送缓冲区的最大字节数,实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位;
4.11 拥塞控制
在网络传输中,如果发生少量丢包,那重传问题就好,但是要是大量丢包,OS就要判断是不是网络拥塞了~当TCP发生拥塞时,通过拥塞避免算法不发或者少发,缓解拥堵问题
这里我们在引入一个概念叫拥塞窗口,这可以理解为一个整型变量,是用来衡量网络的拥塞程度的!在这里我们还要纠正一个概念,即滑动窗口=min(对端ack win大小(接收缓冲区剩余空间大小),拥塞窗口)。如下图:

这里再介绍一下慢启动机制,慢启动算法的目的是逐渐增加发送方的发送速率,以避免在网络状况未知的情况下发送过多数据导致拥塞。
在连接建立后,拥塞窗口(cwnd)初始化为1个MSS(最大报文段大小)。之后每收到一个ACK,cwnd增加1个MSS,直到达到慢启动阈值(ssthresh)。当cwnd达到ssthresh时,慢启动结束,进入拥塞避免阶段。
之后慢启动是以2^n速率增长的,也就是前期慢,后期快,这样设计就是一旦拥堵情况,经过探测,发现并不怎么拥堵,那么主要矛盾变成了,尽快恢复正常通信!
最后总结陈述一下吧,就是
TCP拥塞控制中的慢启动机制通过指数增长的方式来逐步增加拥塞窗口(cwnd)的大小,以探测网络的容量和避免拥塞。在慢启动阶段,拥塞窗口从1个MSS(最大报文段大小)开始,每次收到一个ACK,cwnd就加倍,这种指数增长的方式可以快速利用可用的网络带宽。然而,这种增长并不是无限制的,当cwnd达到一个预设的慢启动阈值(ssthresh)时,慢启动阶段结束,进入拥塞避免阶段,此时cwnd的增长速度变为线性增长,即每个RTT(往返时间)周期内cwnd增加一个MSS。
ssthresh的设置是基于网络的动态变化和探测结果,它反映了当前网络拥塞的临界值,作为拥塞窗口增长的参考值。理论上,TCP应该持续探测网络状况,但实际中不会让拥塞窗口无限制增长,因为网络状况是动态变化的,持续探测可以帮助TCP适应这些变化,避免拥塞。
总结与升华:
好了,关于UDP和TCP的相关知识我们就分享到这里,其实二者没有谁好谁坏,也没有所谓的优缺点,所谓优缺点都是特点,只不过在不同的情况下给人的感觉不一样,但都是程序员很伟大的工具,二者不分伯仲,不要有TCP就是比UDP好这一错误观点,就像我们看人一样,要从多方面考虑,而不是仅仅通过某一个缺点而去否定这一个人,或许你所否定的这一点,在别人眼里正是他的优点,这两个协议在不同的领域都有各自的作用与光辉!!!
我们下一篇文章讲解IP相关知识,进入网络层~~~
