⭐️前面的话⭐️
本文介绍计算机网络中有关传输层协议的知识——UDP与TCP协议,在TCP协议中,为了保证数据的可靠传输,引入了十大保证可靠性的机制,即确认应答,超时重传,连接管理(三次握手,四次挥手),滑动窗口,流量控制,拥塞控制,延时应答,捎带应答,粘包问题,TCP异常处理,这些都是UDP所不具备的,因为UDP不能保证数据的可靠性。
博客主页:未见花闻的博客主页
欢迎关注点赞收藏⭐️留言
本文由未见花闻原创,CSDN首发!
首发时间:2022年5月2日
✉️坚持和努力一定能换来诗与远方!
参考书籍:《图解TCP/IP》,《计算机网络》
参考在线编程网站:牛客网力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
UDP是User Datagram Protocol的缩写,该协议不需要连接,不稳定传输,面向数据报,全双工,简单且高效,但是它的数据载荷较小,一般适用于以下场景:
UDP协议的格式:
端口号即一种属于传输层的“地址”,就像数据链路和IP中的地址,分别指的是MAC地址和IP地址。MAC地址和IP地址是用来定位那一台主机,而端口号用来识别同一台计算机中进行通信的不同应用程序。
端口号本质上是一个16比特的无符号整数,即2字节,范围是0-65535,其中0-1023范围的端口号被称为知名端口号,用于非常常用的通信服务。
源端口号(2字节):发送方的端口号。
目的端口(2字节):接收方的端口号。
包长度,也叫报文长度:表示一个UDP数据报的大小,单位为字节,也就是说一个UDP数据报最大不超过64k。
校验和:用来验证网络传输的数据是否正确。
TCP协议相比于UDP协议要复杂一些,TCP需要连接,传输是可靠的,面向字节流,全双工。
TCP协议格式:
TCP这里的源端口号与目的端口号的意思与UDP完全一样,表示发送方与接收方的端口号。校验和的作用与UDP也是一样的,但是TCP的校验和功能不能关闭,而UDP可以关闭。
数据偏移,表示TCP数据起始处与TCP报文起始处之间的距离,其实就是TCP首部报头长度了,一共4比特,能表示0-15,单位为4字节,也就是说能表示TCP报头长度为0-60字节,基本上完全够用了,就算有一天不够用了,数据偏移后面还有6比特备用。
选项可有可无也可以有多个,可能包括“窗口扩大因子”、“时间戳”等选项。长度可变,最长可达 40 字节,当没有使用选项时,TCP 首部长度是 20 字节。填充是为了保证选项为32比特的整数倍。
控制位,字段长为8比特,每一位从左至右分别为CWR、ECE、URG、ACK、PSH、RST、SYN、FIN。
具体含义(来源《图解TCP/IP》)
TCP数据段是可变的,可以简单的理解数据载荷没有限制。
其他的我们慢慢细说,我们知道TCP是稳定传输,具体说是在保证稳定的情况下尽可能地去提高传输效率,可以从以下十个方面来解释TCP是如何保证稳定传输以及尽可能提高效率的。
保证数据可靠传输的第一关就是确认应答,我们知道可靠性的核心就是发送方知道发送数据有没有被接收方收到,确认应答机制是实现可靠性的核心机制。
确认应答的关键就是发送方发送数据给接收方后,接收方会自动返回一个响应表示收到数据了。
比如,你和你的女神聊天说要请她吃火锅,当你看到她的回复的时候,你就知道消息她收到了。
但是如果多条消息同时发送,可能会发生后发先至的情况,比如你约那你的女神是否吃火锅后来一句:“女神,女神,做我女朋友好吗?”。可能会是下面的情况:
这样就导致消息错位了。
为了解决这个问题,我们就引入了序号和确认序号。
上面发送方的序号就是序号,接收方的序号就是确认序号,这样就没有问题了,你也知道女神是针对哪一条消息做出应答。
这里的序号与确认序号就是对应与TCP报头中的序号与确认序号,它们都是32位大小。
实际中的TCP传输中,是针对每一个字节进行编号,比如传输数据的序号是1,发送了1000个字节的数据,那么接收方收到数据后会返回一个1001,表示1001之前的数据以及全部收到了。
其中B给A回复的确认应答报文也被称为ACK,但是现实毕竟是现实,理想是理想,总会有意外,比如网络原因可能会导致发送数据或者ACK丢包了,丢包了怎么办?那只能重新传一个新的数据包呗,所以就有了下面的超时重传机制。
超时重传是确认应答机制的一个补充,当出现丢包等情况时,超时重传就要“上阵”了。
当一个数据发送后,一定时间段内,没有收到确认应答,就会进行重发,当然重发不是无限制的,重发一定次数后就会降频重发或者就不会再重发了,因为在网络正常的情况下丢包的概率是很小的,两次以及多次丢包的概率那就更加小了。
触发重传有两种情况,一是数据丢了,那么发送方等待一段时间没有收到回应就会重新发送。
第二种情况,ACK丢包,对于发送方是不知道是自己这边发送的消息出问题还是对方发送的消息出问题了,就会按照最坏的情况,重发数据,这时候接收方会收到两份一样的数据,TCP发明者当然注意到这个问题了,于是就实现了一个去重机制,就是相同的数据不会被放入接收方缓冲区(有阻塞队列功能,但不限于阻塞队列)。由于数据相同,接收发会丢弃该数据,并发送一个与之前相同的ACK。
对于TCP是需要建立连接的,所以就有了连接管理机制,其实也是保证可靠性的一种机制。对于TCP的连接管理就是三次握手,四次挥手。
客户端与服务器之间进行三次交互建立连接的过程被形象地称为“三次握手”。
我们知道客户端是主动发出请求的一端,所以客户端与服务器建立连接时,首先客户端会向服务器发出一个连接请求,即SYN,然后服务器收到请求后会给客户端响应一个ACK和SYN,客户端收到服务器的SYN后会立即发送一个ACK给服务器,服务器收到客户端的确认应答后,客户端与服务器就连接成功了。其实严格来说应该有四步,只不过中间服务器响应的ACK与SYN合并为一条发送了而已,
有没有觉得这里的SYN和ACK在哪里见过呢?这不就是TCP首部里面的控制位嘛。
其实发送SYN本质就是将SYN置为1
,发送ACK的本质就是将ACK置为1
,SYN与ACK同时发就是将这两位同时置为1
,同理其他的也是如此。
在建立三次握手的过程中,服务器与客户端的状态不断在改变,三次握手更加详细的图如下:
三次握手的作用不限于建立连接,除此之外三次握手还能检测发送能力与接收能力是否正常。
当发送方发出SYN后,接收方都到发送方的SYN后,此时接收方就能够确定发送方的发送能力和接收方的接收能力是正常的,然后接收方回应ACK和SYN,当接收方收到ACK和SYN后,就知道了发送方以及接收方的发送能力和接收能力都是正常的,最后发送方回应ACK给接收方,此时接收方也确定了接收方以及发送方的接收能力和发送能力都是正常的。当然三次握手的过程中客户端与服务器还会“协商”配置一些重要的信息,这里就不展开了。
既然有连接,那么自然会有断开连接的时候,客户端与服务器通过四次交互而断开连接的过程被称为“四次挥手”。
其中断开连接的请求也被称为FIN,FIN也是属于控制位家族的一员,此外四次挥手可以是客户端发起断开连接请求,也可以是服务器首先发起断开连接请求,我们以客户端主动断开连接为例。
首先,客户端发出断开连接FIN请求,然后服务器收到请求后立即响应ACK(内核返回ACK),服务器调用socket.close()
方法后才会给客户端发送FIN(应用层返回FIN),所以服务器这里ACK与FIN发送的时机并不是在一起的,不像三次握手(时机相同,能够合并)能够合并ACK与SYN。当然如果ACK与FIN相差的时间较小的话,还是可以合并发送的(延时应答机制),但是间隔时间长了不行,总体上还是不能合并的。
同理,四次挥手过程中客户端与服务器的状态也在发生改变,我们来了解一下这些状态。
上图的主动方是客户端,被动方是服务器。
由于最后一次主动方发送ACK后可能存在丢包,如果丢包了,被动方就会以最坏情况认为自己的FIN丢了,会重发FIN,所以主动方需要等待被动方重发FIN,因此预留了一段时间用来等待被动方FIN重传,等待时间是2MSL,MSL表示报文最大生存时间,也就是在网络传输过程中的最大传输时间。如果等待过程中没有接收到FIN,服务器与客户端就断开连接了。
虽然可靠性是TCP最高机制,但是TCP会在可靠性保证的情况下尽量提升传输速度,所以为了在稳定性的基础上提升性能,引出了滑动窗口等机制。
滑动窗口在保证传输的可靠性的前提下,尽量地提高传输效率。
我们不难发现,由于确认应答机制的存在,导致每执行一次发送操作,都需要等待ACK的到达,大部分的时间都用在等ACK上了。
滑动窗口的本质就是发送多组数据,然后等多组数据的ACK。
比如,客户端一次发送了4组数据,然后等ACK的到达,但并不是等4组数据的ACK全部到达后才继续发送数据,而是每收到一次ACK就发一组数据,比如如上图所示,客户端发出1-1000,1001-2000,2001-3000,3001-4000,四组数据后,客户端收到1001,就发送4001-5000,收到2001,就发送5001-6000以此类推。
这就相当于一个大小为4的窗口滑动,原来数据的范围是1-4000,收到一个1001确认应答响应后,数据的范围就变成了1001-5000,相当于窗口向右滑动了一格。
但是上面的是正常的情况,也就是没有考虑后发先制和丢包的情况,下面我们来讨论一下这几种情况,
情况1:后发先制,当出现1001-2000比1-1000先到达这种情况时,由于确认应答机制,收到1001-2000后,服务器会认为2001之前的数据已经全部到达了,因此会返回确认应答2001,客户端收到2001后,窗口会向右移动两格,传输的数据范围为3001-7000,只要1-1000的数据没有丢,没有任何影响,所以后发先制的情况,不用处理,数据仍然能够正常传输。
情况2:ACK丢了,这种情况其实与后发先制相似,比如1001丢了,当客户端收到2001时发送窗口就会右移两格,数据还是能够正常传输的,只要大部分的ACK没有丢,客户端可以通过下一次或者后面的确认应答序号来进行确认,所以ACK丢了不要紧,该情况也不用处理。
情况3:数据丢了,这种情况不用想,肯定有问题,必须得处理的,比如1-3000的数据中,其中1001-2000的数据丢了,那服务器每收到一个数据,都会返回1001,表示让客户端重传1001-2000这个数据,当客户端收到若干个个相同的确认应答序号时,就明白了,数据丢了,就会对丢失的数据进行重传,直到服务器收到1001-2000的数据,就会返回最新的确认应答序号,这种机制也被称为高速重发控制。
那这么说,只要窗口越大,那么传输速度不就快了吗?你的窗口大了,发送方的发送速度确实提高了,但是接收方能接受得过来吗?如果发送速度过快,接收方的接收缓冲区满了之后,传来的数据就放不下了,就会造成数据丢失,那数据丢了不也还需要重传嘛。所以并不是窗口大小越大,传输效率就越高。只有保证发送方与接收方发送与接收的速率最大并保持一致时,传输效率才是最高的,因此为了做到这一点,就有了流量控制的机制。
流量控制的关键就是得到处理方的处理速度,然后根据处理方的处理速度来动态调节发送的速度。
而此处是通过接收方缓冲区的剩余容量来衡量接收方处理速度的,发送方发送数据后会放到一个缓冲区,然后接收方通过这个缓冲区来读取数据,这样的一个过程也可以理解为生产者消费者模型,即发送方是生产者,接收方缓冲区是“交易场所”,发送方是消费者,当生产者的生产速度与消费者的消费速度到达平衡时,传输的数据既快又稳定。
不妨把这个接收方的缓冲区看作成一个水池,那么发送方的工作就是注水,接收方的工作就是使用水池中的水,当水位比较低(剩余空间大)那就注水的时候就快一点,水位比较高(剩余空间小)那就注水的时候就慢一点。
当发送方的数据到达接收方的时候,接收方都会返回一个ACK,这个ACK除了确认能够确认应答,还能告知接收方缓冲区的空间还剩余多少,然后发送方根据接收方缓冲区剩余的容量来控制发送速度(窗口大小),当接送方得知接收方缓冲区空间满了的时候,就不会去发送 数据,而是会去发送一个探测窗口报文,获取接收方缓冲区剩余空间的大小。
而上面的获取接收方缓冲区剩余空间大小,是通过TCP报头中的窗口大小来进行获取,占据16位(即64k),实际上这里的可描述的窗口大小不止64k,因为TCP报头选项中还包含选项,选项里面有一个窗口扩大因子M,实际窗口大小是将窗口大小字段左移M位,也就是扩大 2 M 2^M 2M倍。
其中的窗口探测报文不包含任何数据,只是触发接收方响应ACK。
拥塞控制是滑动窗口的延伸,也就是限制滑动窗口中数据的发送速度,拥塞控制描述的是从接收方到发送方之间链路的拥堵情况。
发送方发送的多快不仅取决于接收方的处理能力,还取决于中间链路的处理能力。而发送方与接收方中间的结点的个数我们是不得而知的,因此拥塞控制采取“测试实验”的方式来逐渐调整发送的速度。
一开始的时候接收方会以较小的窗口进行发送,通过逐渐提高窗口的大小,当窗口达到一定的大小,就会出现丢包的情况,这就意味着链路就出现了“拥堵”,这时候就会减小窗口的大小,是快速的减小窗口大小,因为如果出现丢包减小窗口大小的速度不够大,可能会出现持续性的丢包,对网络通信的质量会造成很大的影响。
延时应答,相当于流量控制的延伸,流量控制的目的是为了使发送方不要发送太快,而延时应答在此基础上,想让窗口的大小尽量大一点。
就是发送方询问接收方窗口大小时,不立即做出回答,而是稍等一下在回答,比如一个水池,如果立即回答,回应的剩余空间是20吨,但如果等一下再应答,可能回应的剩余空间是28吨(因为接收方是一直在处理数据的),这样就能使窗口的大小尽量大一点。
捎带应答是延迟应答的延伸,由于延时应答的存在,ACK并不是立即就发送响应的,当应用层代码需要响应时机与ACK响应时机重合时,就会将这两个数据合二为一,这就是捎带应答。
粘包问题,就是应用层去取缓冲区的数据时,会出现分不清哪些数据是哪些TCP数据包里面的应用层数据,那也很可能就不知道从哪里到哪里是一个完整的应用层数据报·,就造成了粘包问题,其实不止TCP传输存在这种问题,所有面向字节流读文件都会有这种问题。
那么要怎么解决呢?由于是找不到应用层的数据始末,所以去TCP上面做功夫是不可行的,问题出在哪里,就要在哪里解决,毕竟解铃还须系铃人。所以解决办法就是在应用层协议中加上包与包之间的边界,比如在应用层数据报结尾加上一个;
,这样在读取的时候,就能区分出一个完整的应用层数据报了。
进行TCP协议传输时会出现以下几种情况:
进程终止: 在进程毫无准备的情况下,突然结束进程,偷袭它,其实它有闪,会偷袭失败。
我们知道TCP连接是通过socket来进行连接的,socket本质上是进程打开的一个文件,文件就存在与PCBZ中的文件描述符表之中,每次打开一个socket文件都会在文件描述表中添加一项,删除会减少一项。
当你强制结束进程时,PCB没了,里面的文件描述符表也没有了,就相当于文件自动关闭了,这个过程和手动调用socket.close方法没有区别,依然会执行四次挥手过程。
机器关机:
机器关机和进程终止是一样的,首先会将PCB全部杀死,然后再进行关机,四次挥手依然会进行。
、
机器断网/断电:
当电源或网络直接断开时,端电时没有任何时间留给操作系统去反应,所以根本来不及去四次挥手,断网数据都发不出去,那四次挥手也无法成功。
当客户端断电或断网时,服务器就会尝试重新建立连接,,重连失败一定次数,就会放弃连接。
当服务器断电或断网时,客户端会发送一个探测报文,触发服务器的ACK,如果没有反应,客户端就认为服务器出现了问题。
总结一下,UDP与TCP之间的比较,UDP的传输效率高于TCP,但传输的文件较小和对可靠性要求不高时优先使用UDP。TCP是在保证可靠性的前提下,尽可能地去提升效率,但是还是有效率牺牲的,所以TCP的传输效率不如UDP,但是可靠性优于UDP。那么如何基于UDP实现可靠性?这个问题实际上在问你TCP,将TCP可靠性实现的思路在应用层复刻就可以了。当然传输层的协议不只有UDP与TCP,其他的如QUIC,游戏中经常使用。