当前位置: 首页 > news >正文

计算机网络【UDP与TCP协议(三次握手、四次挥手)】

计算机网络【UDP与TCP协议(三次握手、四次挥手)】

  • 🍎一.UDP与TCP区别
    • 🍒1.1UDP与TCP性质区别
    • 🍒1.2UDP与TCP编译区别
  • 🍎二.UDP协议
    • 🍒2.1什么是UDP协议
    • 🍒2.2UDP协议格式
  • 🍎三.TCP协议
    • 🍒3.1TCP协议格式
    • 🍒3.2确定应答
    • 🍒3.3超时重传
    • 🍒3.4连接管理(三次握手,四次挥手)
      • 🍉3.4.1三次握手
      • 🍉3.4.2四次挥手
    • 🍒3.5滑动窗口
    • 🍒3.6流量控制
    • 🍒3.7拥堵控制
    • 🍒3.8延时应答
    • 🍒3.9捎带应答
    • 🍒3.10粘包问题
    • 🍒3.11 TCP异常

🍎一.UDP与TCP区别

🍒1.1UDP与TCP性质区别

对于网络编程,操作系统提供了一组API,叫做socket,可以视为应用层与传输层之间的通信桥梁,而传输层中有两个非常重要的协议,那就是TCP与UDP

●TCP:
TCP是一种面向有连接的传输层协议,它可以保证两端通信主机之间的通信可达能够正确处理在传输过程中丢包、传输顺序乱掉等异常情况。此外,TCP还能够有效利用带宽,缓解网络拥堵。然而,为了建立与断开连接,有时它需要至少7次的发包收包(三次握手,四次挥手),导致网络流量的浪费。此外,为了提高网络的利用率,TCP协议中定义了各种各样复杂的规范,因此不利于视频会议(音频、视频的数据量既定)等场合使用

●UDP:
UDP是一种面向无连接的传输层协议,不会关注对端是否真的收到了传送过去的数据,如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。UDP常用于分组数据较少或多播、广播通信以及视频通信等多媒体领域

TCP与UDP区别:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务,也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保 证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是全双工不可靠信道

TCP是:有连接,可靠传输,面向字节流,全双工
UDP是:无连接,不可靠传输,面向数据报,全双工

拓展:UDP与TCP之间的比较,UDP的传输效率高于TCP,但传输的文件较小和对可靠性要求不高时优先使用UDP。TCP是在保证可靠性的前提下,尽可能地去提升效率,但是还是有效率牺牲的,所以TCP的传输效率不如UDP,但是可靠性优于UDP。那么如何基于UDP实现可靠性?这个问题实际上在问你TCP,将TCP可靠性实现的思路在应用层复刻就可以了。当然传输层的协议不只有UDP与TCP,其他的如QUIC,游戏中经常使用

🍒1.2UDP与TCP编译区别

基于上述不同,UDP和TCP编程步骤也有些不同,如下:
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();  
2、设置socket属性,用函数setsockopt(); (可选)
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;

TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();(可选)
3、绑定IP地址、端口等信息到socket上,用函数bind();(可选)
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
                          
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();(可选)
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;

UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();(可选)
3、绑定IP地址、端口等信息到socket上,用函数bind();(可选)
4、设置对方的IP地址和端口等属性;  
5、发送数据,用函数sendto();
6、关闭网络连接;

🍎二.UDP协议

🍒2.1什么是UDP协议

UDP是User Datagram Protocol的缩写,该协议不需要连接,不稳定传输,面向数据报,全双工,简单且高效,但是它的数据载荷较小,一般适用于以下场景:

●包总量较少的通信(DNS、SNMP等)
●视频、音频等多媒体通信(即时通信)
●限定于LAN等特定网络中的应用通信
●广播通信(广播、多播)

🍒2.2UDP协议格式

UDP协议的格式:
在这里插入图片描述
源端口号(16位):就是操作系统自动给客户端分配的端口号

目的端口号(16位):服务器端口号

报文长度(16位):因为报文长度是2字节,所只可以传输0~65535大小的(64k)文件,当我们需要传输比较大的文件的时候UDP协议就会规定将文件拆分分为多个数据报进行传输,接收方就会将这些拆分的文件进行组装成完整的数据

校验和(16位):是来判断网络传输的这个数据是否是正确的,因为网络传输的数据本质就是光信号和电信号,但是在外界作用的干扰下,如磁场,电磁场之类的情况下会导致数据会发生改变.
校验和的校验方法可以计算传输数据的个数与接收数据的个数是否一致,还可以根据数据的内容进行校验

数据部分:完整的应用层的数据

●传输层的数据通常叫做:段–“segment”
●网络层的数据通常叫做:包/报–“packet”
●数据链路层数据通常叫做:帧–"frame"

🍎三.TCP协议

🍒3.1TCP协议格式

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。

在这里插入图片描述

CWR(Congestion Window Reduced)
CWR标志, 与后面的ECE标志都用于IP首部的ECN字段。ECE标志为1时,则通知对方已将拥塞窗口缩小

ECE(ECN-Echo)
ECE标志, 表示ECN-Echo。置为1会通知通信对方,从对方到这边的网络有拥塞。在收到数据包的IP首部中ECN为1时将TCP首部中的ECE设置为1

URG(Urgent Flag)
该位为1时,表示包中有需要紧急处理的数据。对于需要紧急处理的数据,会在后面的紧急指针中再进行解释

ACK(Acknowledgement Flag)
该位为1时,确认应答的字段变为有效。TCP规定除了最初建立连接时的SYN包之外该位必须设置为1

PSH(Push Flag)
该位为1时,表示需要将受到的数据立刻传给上层应用协议。PSH为0时,则不需要立即传而是先进行缓存

RST(Reset Flag)
该位为1时表示TCP连接中出现异常必须强制断开连接。例如,一个没有被使用的端口即使发来连接请求,也无法进行通信。此时就可以返回一个RST设置为1的包。此外,程序宕掉或切断电源等原因导致主机重启的情况下,由于所有的连接信息将全部被初始化,所以原有的TCP通信也将不能继续进行。这种情况下,如果通信对方发送一个设置为1的RST包,就会使通信强制断开连接

SYN(Synchronize Flag)
用于建立连接。SYN为1表示希望建立连接,并在其序列号的字段进行序列号初始值的设定(Synchronize本身有同步的意思。也就意味着建立连接的双方,序列号和确认应答号要保持同步。)

FIN(Fin Flag)
该位为1时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位置为1的TCP段。每个主机又对对方的FIN包进行确认应答以后就可以断开连接。不过,主机收到FIN设置为1的TCP段以后不必马上回复一个FIN包,而是可以等到缓冲区中的所有数据都因已成功发送而被自动删除之后再发。

TCP数据段是可变的,可以简单的理解数据载荷没有限制

🍒3.2确定应答

保证数据可靠传输的第一关就是确认应答,我们知道可靠性的核心就是发送方知道发送数据有没有被接收方收到,确认应答机制是实现可靠性的核心机制。

确认应答的关键就是发送方发送数据给接收方后,接收方会自动返回一个响应表示收到数据了

比如,你和你的女神聊天说要请她吃麻辣烫,当你看到她的回复的时候,你就知道消息她收到了

在这里插入图片描述
当我们进行多次发送请求时,会遇到发送信息和应答信息不对应,容易产生乌龙
如下:
在这里插入图片描述
为了解决这个问题,我们就引入了序号和确认序号,这样就避免了乌龙
在这里插入图片描述
在这里插入图片描述
实际中的TCP传输中,是针对每一个字节进行编号,比如传输数据的序号是1,发送了1000个字节的数据,那么接收方收到数据后会返回一个1001,表示1001之前的数据以及全部收到了
在这里插入图片描述
在这里插入图片描述
其中B给A回复的确认应答报文也被称为ACK,但是现实毕竟是现实,理想是理想,总会有意外,比如网络原因可能会导致发送数据或者ACK丢包了,丢包了怎么办?那只能重新传一个新的数据包呗,所以就有了下面的超时重传机制。

🍒3.3超时重传

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

在这里插入图片描述
还有另一种情况,当我A已经所发送的信息已经成功了,但是B回复我我们的ACK却丢失了,而A不知道A所发送的信息是否发送成功了,就会触发超时重传,所以A会一直发送信息等待B的回答(例如A重复发送向B借钱的信息就会产生一些不必要的麻烦),所以TCP内部会有一个去重操作

**去重操作:**接收方会先把发送过来的数据放到操作系统内核的"接收缓冲区"中,接收缓冲区可以视为一个内存空间,这个内存空间也可以视为一个阻塞队列,当接收到的数据,TCP会现根据序号来检查内存缓冲区是否已经存在了,如果存在就直接丢弃,如果不存在就放进去,用来保证调用的socket.api拿到的这个数据一定不是重复的
在这里插入图片描述
超时重传一定会成功码?当然不会!
如果我们遇到网络抖动一下之后,这个时候重传还是可以解决的,但是我们遇到网路断开,想要恢复就没有那么容易了,所以在这种情况下我们重传不会一直进行下去,当TCP连续重传都没有接收到回馈的ACK后,TCP的传输就只能放弃了,重传的时间并不是固定的,传输的间隔时间会根据每次传输的数次逐渐变大

操作系统具体传输多少次失败放弃?
操作系统具体每次重新传输时间增加多少?
一些一般我们是可以根据机制来进行配置的

基于上述这两个机制,TCP的可靠徐就得到了有效的保证

🍒3.4连接管理(三次握手,四次挥手)

🍉3.4.1三次握手

三次握手就是类似于有两个人进行打电话环节:
●A:你好,我是A,你能听到我说话吗?
●B:你好A能听到你说话;你能听到我说话吗?
●A:能听到!

其实发送SYN本质就是将SYN置为1,发送ACK的本质就是将ACK置为1,SYN与ACK同时发就是将这两位同时置为1,同理其他的也是如此
在这里插入图片描述
在这里插入图片描述

上面的状态我们了解一下即可:

CLOSED 表示客户端或服务器处于关闭状态
●LISTEN 表示服务器就绪,等待服务器连接状态
●SYN_RCVD 表示服务器已经收到客户端的连接请求,发送ACK和SYN并进入阻塞等待客户端连接状态,一般此状态存在时间很短
●SYN_SENT 表示客户端连接请求已发送,此时客户端进入阻塞等待服务器确认应答状态,一般此状态的存在时间很短
●ESTABLISHED 表示客户端或服务器已经建立连接成功,随时可以进行通信

三次握手的作用不限于建立连接,除此之外三次握手还能检测发送能力与接收能力是否正常
当发送方发出SYN后,接收方都到发送方的SYN后,此时接收方就能够确定发送方的发送能力和接收方的接收能力是正常的,然后接收方回应ACK和SYN,当接收方收到ACK和SYN后,就知道了发送方以及接收方的发送能力和接收能力都是正常的,最后发送方回应ACK给接收方,此时接收方也确定了接收方以及发送方的接收能力和发送能力都是正常的。当然三次握手的过程中客户端与服务器还会“协商”配置一些重要的信息,这里就不展开了

🍉3.4.2四次挥手

既然有连接,那么自然会有断开连接的时候,客户端与服务器通过四次交互而断开连接的过程被称为“四次挥手”
在这里插入图片描述
其中断开连接的请求也被称为FIN,FIN也是属于控制位家族的一员,此外四次挥手可以是客户端发起断开连接请求,也可以是服务器首先发起断开连接请求,我们以客户端主动断开连接为例。

首先,客户端发出断开连接FIN请求,然后服务器收到请求后立即响应ACK(内核返回ACK),服务器调用socket.close()方法后才会给客户端发送FIN(应用层返回FIN),所以服务器这里ACK与FIN发送的时机并不是在一起的,不像三次握手(时机相同,能够合并)能够合并ACK与SYN。当然如果ACK与FIN相差的时间较小的话,还是可以合并发送的(延时应答机制),但是间隔时间长了不行,总体上还是不能合并的。

同理,四次挥手过程中客户端与服务器的状态也在发生改变,我们来了解一下这些状态

在这里插入图片描述
上图的主动方是客户端,被动方是服务器

●FIN_WAIT_1 主动方发送FIN后,进入等待被动方确认断开响应状态
●FIN_WAIT_2 收到被动方的ACK,进入等待主动方FIN状态
●CLOSE_WAIT 当被动方收到主动方发送的FIN请求后,被动方响应ACK并进入准备关闭状态
●LAST_ACK 被动方FIN发送后,进入等待最后一个ACK状态
●TIME_WAIT 收到被动方FIN,发送最后一次ACK并进入最后等待状态

🍒3.5滑动窗口

滑动窗口在保证传输的可靠性的前提下,尽量地提高传输效率
在这里插入图片描述
滑动窗口的本质就是发送多组数据,然后等多组数据的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的数据,就会返回最新的确认应答序号,这种机制也被称为高速重发控制
在这里插入图片描述
当遇到A传输数据过程中失败后,主机B一直返回上一层的序号A就明白自己在哪里传输数据的时候传输失败了,并且会在失败的位置重新输入
在这里插入图片描述
那这么说,只要窗口越大,那么传输速度不就快了吗?你的窗口大了,发送方的发送速度确实提高了,但是接收方能接受得过来吗?如果发送速度过快,接收方的接收缓冲区满了之后,传来的数据就放不下了,就会造成数据丢失,那数据丢了不也还需要重传嘛。所以并不是窗口大小越大,传输效率就越高。只有保证发送方与接收方发送与接收的速率最大并保持一致时,传输效率才是最高的,因此为了做到这一点,就有了流量控制的机制

🍒3.6流量控制

流量控制的关键就是得到处理方的处理速度,然后根据处理方的处理速度来动态调节发送的速度
在这里插入图片描述

而此处是通过接收方缓冲区的剩余容量来衡量接收方处理速度的,发送方发送数据后会放到一个缓冲区,然后接收方通过这个缓冲区来读取数据,这样的一个过程也可以理解为生产者消费者模型,即发送方是生产者,接收方缓冲区是“交易场所”,发送方是消费者,当生产者的生产速度与消费者的消费速度到达平衡时,传输的数据既快又稳定。

不妨把这个接收方的缓冲区看作成一个水池,那么发送方的工作就是注水,接收方的工作就是使用水池中的水,当水位比较低(剩余空间大)那就注水的时候就快一点,水位比较高(剩余空间小)那就注水的时候就慢一点。

当发送方的数据到达接收方的时候,接收方都会返回一个ACK,这个ACK除了确认能够确认应答,还能告知接收方缓冲区的空间还剩余多少,然后发送方根据接收方缓冲区剩余的容量来控制发送速度(窗口大小),当接送方得知接收方缓冲区空间满了的时候,就不会去发送 数据,而是会去发送一个探测窗口报文,获取接收方缓冲区剩余空间的大小。

而上面的获取接收方缓冲区剩余空间大小,是通过TCP报头中的窗口大小来进行获取,占据16位(即64k),实际上这里的可描述的窗口大小不止64k,因为TCP报头选项中还包含选项,选项里面有一个窗口扩大因子M,实际窗口大小是将窗口大小字段左移M位,也就是扩大2 M 2^M2倍
在这里插入图片描述
其中的窗口探测报文不包含任何数据,只是触发接收方响应ACK

🍒3.7拥堵控制

拥塞控制是滑动窗口的延伸,也就是限制滑动窗口中数据的发送速度,拥塞控制描述的是从接收方到发送方之间链路的拥堵情况。

发送方发送的多快不仅取决于接收方的处理能力,还取决于中间链路的处理能力。而发送方与接收方中间的结点的个数我们是不得而知的,因此拥塞控制采取“测试实验”的方式来逐渐调整发送的速度。

一开始的时候接收方会以较小的窗口进行发送,通过逐渐提高窗口的大小,当窗口达到一定的大小,就会出现丢包的情况,这就意味着链路就出现了“拥堵”,这时候就会减小窗口的大小,是快速的减小窗口大小,因为如果出现丢包减小窗口大小的速度不够大,可能会出现持续性的丢包,对网络通信的质量会造成很大的影响

阈值不是一直不变的,阈值会根据每次达到拥塞的时候进行适量提高达到接收缓冲区的满状态后,会重新在ssthresh中进行线性增长知道趋于平衡,每次丢包的时候就会将丢包的时候就会将拥塞窗口减少到一半
在这里插入图片描述
关于拥塞窗口说明如下图: 引用-未见花闻博客图画:https://weijianhuawen.blog.csdn.net/
在这里插入图片描述

🍒3.8延时应答

延时应答,相当于流量控制的延伸,流量控制的目的是为了使发送方不要发送太快,而延时应答在此基础上,想让窗口的大小尽量大一点
就是发送方询问接收方窗口大小时,不立即做出回答,而是稍等一下在回答,比如一个水池,如果立即回答,回应的剩余空间是20吨,但如果等一下再应答,可能回应的剩余空间是28吨(因为接收方是一直在处理数据的),这样就能使窗口的大小尽量大一点

🍒3.9捎带应答

捎带应答是延迟应答的延伸,由于延时应答的存在,ACK并不是立即就发送响应的,当应用层代码需要响应时机与ACK响应时机重合时,就会将这两个数据合二为一,这就是捎带应答
捎带应答我们有时可以将操作系统内核的ACK与程序代码实现的命令进行同时打包一起发送,所以可以节省传输成本,增加传输效率

客户端与服务端通信的几种模型:
一问一答: 客户端发出请求,服务器响应
●一问多答: 上传文件
●多问一答: 下载文件
●多问多答: 直播

🍒3.10粘包问题

粘包问题,就是应用层去取缓冲区的数据时,会出现分不清哪些数据是哪些TCP数据包里面的应用层数据,那也很可能就不知道从哪里到哪里是一个完整的应用层数据报·,就造成了粘包问题,其实不止TCP传输存在这种问题,所有面向字节流读文件都会有这种问题

那么要怎么解决呢?由于是找不到应用层的数据始末,所以去TCP上面做功夫是不可行的,问题出在哪里,就要在哪里解决,毕竟解铃还须系铃人。所以解决办法就是在应用层协议中加上包与包之间的边界,比如在应用层数据报结尾加上一个";"这个包分割条件,这样在读取的时候,就能区分出一个完整的应用层数据报了

🍒3.11 TCP异常

进行TCP协议传输时会出现以下几种情况:

进程终止:

在进程毫无准备的情况下,突然结束进程,偷袭它,其实它有闪,会"偷袭失败",我们知道TCP连接是通过socket来进行连接的,socket本质上是进程打开的一个文件,文件就存在与PCBZ中的文件描述符表之中,每次打开一个socket文件都会在文件描述表中添加一项,删除会减少一项,当你强制结束进程时,PCB没了,里面的文件描述符表也没有了,就相当于文件自动关闭了,这个过程和手动调用socket.close方法没有区别,依然会执行四次挥手过程

机器关机:
也是按照操作系统关闭PCB关闭线程操作流程,也是需要经过socket来进行四次挥手关闭,“偷袭失败”

停电/网络断开:
"偷袭成功"操作系统没有进行任何操作,没有反应过来就已经停止运行了

相关文章:

  • Linux进程控制
  • Unity 分享 功能 用Unity Native Share Plugin 实现链接、图片、视频等文件的分享+ 安卓 Ios 都可以,代码图文详解
  • 基于javaweb的嘟嘟二手书商城系统(java+jsp+springboot+mysql+thymeleaf+ftp)
  • 2.1.1 操作系统之进程的定义、特征、组成、组织
  • 一文了解数据结构
  • [LeetCode刷题笔记]4 - 寻找两个正序数组的中位数(归并 / 递归 / 二分查找)
  • 字符串的读入(char与string)
  • C++运算符重载函数
  • 图解操作系统-cpu cache
  • 软件测试怎么去介绍一个项目的测试流程?
  • Redis实战篇一 (短信登录)
  • SpringBoot使用在控制层切面注解配置的方式将日志存储在mysql
  • 二叉树小记
  • 使用SpringBoot整合国产数据库连接池Druid
  • Servlet的一些操作
  • 设计模式 1 - 单例模式:附全套 Git 简洁代码
  • 模板·初阶
  • 【MATLAB教程案例30】基于MATLAB的图像阴影检测和消除算法的实现
  • 字符串拼接