🔥个人主页: 中草药
🔥专栏:【Java】登神长阶 史诗般的Java成神之路
🎤一.报头格式
TCP (Transmission Control Protocol) 是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 被设计用来提供端到端的数据传输服务,确保数据的完整性和顺序性。它是互联网协议族(Internet Protocol Suite)中的重要组成部分之一,在OSI七层模型中属于第四层,在TCP/IP四层模型中属于传输层。
TCP 报头主要包括以下几个部分:
- 源端口号:占用2字节,标识发送方的端口号。
- 目的端口号:占用2字节,标识接收方的端口号。
- 序列号:占用4字节,标识该TCP段中的第一个字节的序列号。
- 确认号:占用4字节,标识期望接收的下一个字节的序列号。
- 数据偏移:占用4位,指示TCP报头的长度(以32位即4字节为单位)。
- 保留:占用6位,目前未使用。
- 标志位:占用6位,包括URG、ACK、PSH、RST、SYN、FIN等标志。
- 窗口大小:占用2字节,表示接收方的可用缓冲区大小。
- 校验和:占用2字节,用于检测数据包中的错误。
- 紧急指针:占用2字节,仅在URG标志置位时有效,表示紧急数据的末尾位置相对于序列号的位置。TCP在正常情况下都是按顺序来传输数据,紧急指针里面存的是一个偏移量,让后面的数据根据偏移量,把指定位置数据优先发送出去
- 选项:可变长度,用于提供额外的功能,如窗口扩大因子、时间戳等。
- 6位标志位:
- URG: 与紧急指针配合使用
- ACK: 确认号是否有效
- PSH: 催促标志位,提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
- 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
- 16位紧急指针: 标识哪部分数据是紧急数据;
- 40字节头部选项: 暂时忽略;
🎧二.主要特点
- 面向连接:在数据传输前必须先建立连接,之后才能进行数据传输。连接的建立使用三次握手的过程,连接的终止使用四次挥手的过程。
- 可靠传输:通过序列号、确认应答、超时重传等机制来保证数据的可靠传输。
- 流量控制:使用滑动窗口机制来控制发送速率,避免发送方过快发送数据导致接收方来不及处理。
- 拥塞控制:通过慢启动、拥塞避免、快速重传和快速恢复等机制来避免网络拥塞。
- 差错控制:使用校验和来检测数据包中的错误,并通过重传来纠正错误。
🎼三.TCP状态
TCP状态机包括以下几种状态,这些状态按照一定的顺序变化,反映了TCP连接的不同阶段:
- CLOSED
- LISTEN
- SYN_SENT
- SYN_RECEIVED
- ESTABLISHED
- FIN_WAIT_1
- FIN_WAIT_2
- CLOSE_WAIT
- LAST_ACK
- CLOSING
- TIME_WAIT
详解
1. CLOSED
- 描述:这是TCP连接的初始状态,表示没有任何连接活动。
- 操作:当一个连接完全关闭后,进入此状态。
2. LISTEN
- 描述:服务器处于监听状态,等待客户端的连接请求。
- 操作:服务器调用
listen()
函数后,进入此状态。
3. SYN_SENT
- 描述:客户端发送了一个SYN(同步)报文,请求建立连接。
- 操作:客户端调用
connect()
函数后,发送SYN报文,并进入此状态。
4. SYN_RECEIVED
- 描述:服务器收到了客户端的SYN报文,并发送了一个SYN+ACK(确认)报文作为响应。
- 操作:服务器接收到客户端的SYN报文后,发送SYN+ACK报文,并进入此状态。
5. ESTABLISHED
- 描述:连接已经建立,双方可以开始传输数据。
- 操作:当客户端收到服务器的SYN+ACK报文后,发送一个ACK报文,连接建立完成,进入此状态。
6. FIN_WAIT_1
- 描述:客户端发送了FIN(终止)报文,请求关闭连接。
- 操作:客户端调用
close()
函数后,发送FIN报文,并进入此状态。 - 备注:如果此时客户端还有未发送的数据,会先发送数据,然后再发送FIN报文。
7. FIN_WAIT_2
- 描述:客户端收到了服务器的ACK报文,等待服务器发送FIN报文。
- 操作:客户端发送FIN报文后,收到服务器的ACK报文,进入此状态。
8. CLOSE_WAIT
- 描述:服务器收到了客户端的FIN报文,并发送了ACK报文,等待服务器自身的FIN报文。
- 操作:服务器接收到客户端的FIN报文后,发送ACK报文,并进入此状态。
- 备注:通常表示服务器还在处理一些数据或等待客户端发送更多的数据。
9. LAST_ACK
- 描述:服务器发送了FIN报文,并等待客户端的ACK报文。
- 操作:服务器在CLOSE_WAIT状态下发送FIN报文后,进入此状态。
10. CLOSING
- 描述:客户端和服务器几乎同时发送FIN报文,进入此状态。
- 操作:客户端发送FIN报文后,服务器也在同一时间发送FIN报文,双方都进入此状态。
- 备注:这是一种很少见的状态,通常表示双方几乎同时发送了FIN报文。
11. TIME_WAIT
- 描述:客户端收到了服务器的FIN报文,并发送了ACK报文,等待2MSL时间后关闭连接。
- 操作:客户端接收到服务器的FIN报文后,发送ACK报文,并进入此状态。
- 备注:2MSL(Maximum Segment Lifetime)是为了确保最后一个ACK报文能够被服务器收到,通常为2倍的平均往返时间(RTT)。
🎛️四.核心机制
核心机制 一:确认应答
确认应答(ACK)是一种反馈机制,接收方通过发送 ACK 消息来告知发送方数据包是否成功接收。每个 TCP 数据包都有一个序列号(Sequence Number),表示该数据包的第一个字节的编号。接收方通过 ACK 字段发送一个确认号(Acknowledge Number),表示希望接收的下一个字节的序列号。
确认应答的工作原理
-
序列号与确认号:
- 序列号:每个 TCP 数据包都包含一个序列号,表示该数据包的第一个字节的编号。
- 确认号:接收方通过 ACK 字段发送一个确认号,表示期望接收的下一个字节的序列号。
-
发送数据:
- 发送方向接收方发送一个数据包,其中包含数据和一个序列号。
- 如果接收方成功接收到数据包,它会发送一个 ACK 消息,其中包含确认号,表示期望接收的下一个字节的序列号。
-
确认应答的发送时机:
- 每次接收到数据包时,接收方都会发送一个 ACK 消息。
- 对于小的数据包,接收方可能采用累积确认(Cumulative Acknowledgment)的方式,即等到接收到多个数据包后再统一发送一个 ACK 消息。
- 对于较大的数据包,接收方可能采用逐个确认的方式,即每接收到一个数据包就发送一个 ACK 消息。
优点
- 可靠性:确认应答机制确保了数据的可靠传输,避免了数据丢失。
- 顺序性:通过序列号和确认号机制,确保数据按顺序到达。
- 拥塞控制:确认应答机制配合其他拥塞控制机制,可以有效地避免网络拥塞。
局限性
- 延迟:确认应答机制增加了网络通信的延迟,尤其是在长距离或高丢包率的网络环境中。
- 资源消耗:频繁的确认应答会增加网络带宽和处理资源的消耗。
核心机制 二:超时重传
重传机制是 TCP 用来处理数据包丢失的一种方法。当发送方发送数据后,如果没有在预期的时间内收到接收方的确认应答(ACK),就会认为数据包可能已经丢失,并重新发送该数据包。
工作原理(了解)
-
超时重传:
- 当发送方发送一个数据包后,会设置一个重传超时时间(Retransmission Timeout, RTO)。
- 如果在 RTO 内没有收到接收方的 ACK,发送方就会重新发送该数据包。
- RTO 的初始值通常是根据经验设定的,后续会根据实际网络条件动态调整。
-
RTO 的计算:
- 平均往返时间(RTT):发送方通过测量从发送数据包到接收到 ACK 的时间来估计 RTT。
- RTO 的计算公式:RTO = α × SRTT + (1 - α) × RTO
- 其中,α 是平滑因子(通常为 0.125),SRTT 是平滑后的 RTT,RTO 是重传超时时间。
- 估算 RTT:发送方维护一个样本 RTT(SampleRTT),并使用加权平均法更新 SRTT。
-
快速重传:
- 如果发送方在 RTO 到达之前收到了三个重复的 ACK(称为“三个重复 ACK 规则”),则认为中间的数据包丢失。
- 发送方会立即重传丢失的数据包,而不必等待 RTO 超时。
- 这种机制可以更快地检测到数据包丢失并及时重传,从而减少延迟。
-
选择性确认(SACK):
- 选择性确认是 TCP 的一种扩展机制,允许接收方告诉发送方哪些数据段已经被成功接收。
- 通过 SACK,发送方可以只重传那些真正丢失的数据段,而不是整个数据包。
- 这种机制提高了效率,特别是在高丢包率的情况下。
优点
- 可靠性:重传机制确保数据包能够最终到达接收方,提高了传输的可靠性。
- 灵活性:通过快速重传和选择性确认机制,可以在高丢包率的网络环境中保持高效传输。
- 动态调整:RTO 的动态调整可以根据实际网络条件优化重传策略。
局限性
- 延迟:重传机制增加了网络通信的延迟,特别是在长距离或高丢包率的网络环境中。
- 资源消耗:频繁的重传会增加网络带宽和处理资源的消耗。
- 拥塞控制:重传机制需要与拥塞控制机制协同工作,避免因重传而导致网络拥塞加剧
核心机制 三:连接管理(三次握手,四次挥手)
-
连接建立:
- 三次握手:客户端发送一个带有SYN标志的TCP段到服务器,表示请求建立连接;服务器收到后发送一个带有SYN和ACK标志的TCP段回应客户端;客户端再次发送一个带有ACK标志的TCP段确认连接已建立。
三次握手的意义:
- 初步的验证通信的链路是否畅通,这是进行可靠传输的“前提条件”
- 确认通信的双方各自的发送能力和接受能力是否都正常
- 让通信双方在通信之前,协商通信过程中的关键参数,比如TCP连接的起始序号
三次握手之所以是三次,是中间的两次的交互合并在一起(中间的ACK+SYN,都是在内核之中,由操作系统负责进行的时机都是在SYN之后,此时为同一时机就合并了)
-
数据传输:
- 可靠传输:每个TCP段都包含一个序列号,用于标识该段中的第一个字节。接收方通过确认应答(ACK)来告诉发送方哪些数据已被成功接收。
- 流量控制:使用滑动窗口机制来调整发送方的发送速率,避免发送速度过快导致接收方无法处理。
-
连接释放:
- 四次挥手:当一方发送完数据后,发送一个FIN标志的TCP段通知对方结束连接;另一方收到后发送ACK确认,并可能继续发送未发送完的数据;之后发送一个FIN标志的TCP段请求断开连接;最后,最初发送FIN的一方发送ACK确认连接断开。
“三次握手,四次挥手”详细图示(包括TCP状态转换)
核心机制 四:滑动窗口机制
滑动窗口机制允许接收方告知发送方它可以接收多少数据,从而控制发送方的数据发送速率。通过这种方式,接收方可以避免缓冲区溢出,同时保证数据的顺利接收。降低可靠性带来的性能损失
把多次等待ack的时间合并在一起
掉包情况
无需特殊处理
批量发数据,批量的ack不会全丢,只丢一部分,后续的ack会弥补缺失的信息
主机A在连续收到多个1001这样的ack之后,主机A会意识到1001的数据包丢失了,于是主机重传1001-2000
当啊1001-2000重传过来之后,由于之前的2001-7000已经发过了,再弥补了空缺之后,接下来索要7001的数据包就好了
核心机制 五: 流量控制
TCP(传输控制协议)中的流量控制机制是确保数据传输过程中不会导致接收方缓冲区溢出的重要机制。流量控制的主要目的是让发送方根据接收方的接收能力来调整数据的发送速率,从而避免数据丢失或拥塞。
流量控制是指通过某种机制限制发送方的数据发送速率,以确保接收方能够及时处理接收到的数据。在TCP中,流量控制主要是通过滑动窗口(Sliding Window)机制来实现的。
过程
- 接收端 将自己可以接受的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK端通知发送端
- 接收端一旦发现自己的缓冲区快满了,将会把窗口大小设置成一个更小的值 通知给发送端,发送端在接受这个窗口后,就会减慢自己的发送速度
- 接受端缓冲区满了,就会把窗口设置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段(不携带业务数据,只为触发ack)
- 接收方也会在接收缓冲区不为0的时候,主动触发一个 窗口更新通知 的数据报
流量控制,站在接受方的视角来限制发送方的速度
拥塞控制,站在传输链路的视角来限制发送方的速度
核心机制 六: 拥塞控制
TCP(传输控制协议)中的拥塞控制机制是确保网络资源合理分配的关键技术之一。拥塞控制的目标是在网络拥塞时 减缓数据发送速率,避免过多的数据包在网络中堆积,从而导致网络性能下降或崩溃。TCP拥塞控制机制通过多种策略来实现这一目标,以下是拥塞控制机制的详细介绍。
- 慢启动 刚开始发送数据,拥塞窗口会非常小,用一个很小的速度先发少量数据,探探路
- 如果不丢包,增大窗口大小(指数增大)
- 增长到一定程度,达到某个指定的阈值,即使,没有丢包,也会停止指数增删,变成 线性增长
- 线性增长,也会使发送速度越快,达到某个速度,就会发生丢包
- 一旦发生丢包,会回到一个新的ssthresh值,重新开始线性增长,形成动态平衡
优点
- 避免网络拥塞:通过动态调整发送速率,避免网络拥塞。
- 提高网络效率:通过快速重传和快速恢复机制,减少数据包丢失带来的延迟。
- 公平性:确保不同连接之间共享网络资源的公平性。
局限性
- 延迟:拥塞控制机制增加了网络通信的延迟,特别是在高延迟的网络环境中。
- 资源消耗:频繁的重传和调整发送速率会增加网络带宽和处理资源的消耗。
- 适应性:拥塞控制机制需要根据网络状况不断调整,对网络环境的变化适应性较强。
核心机制 七: 延迟应答
TCP(传输控制协议)中的延迟应答(Delayed Acknowledgment)机制是提高网络传输效率的一种策略。延迟应答机制允许接收方暂时不发送确认应答(ACK),而是等待一小段时间再发送,这样可以减少网络中的ACK报文数量,从而提高网络带宽的利用率。
并不是所有的包都可以延迟应答
- 数量限制:每隔N个包应答一次
- 时间限制:超过最大延迟时间就应答一次
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms
核心机制 八: 捎带应答
TCP(传输控制协议)中的捎带应答(Piggybacking Acknowledgments)是一种提高网络效率的技术,它允许接收方在发送数据时顺便携带确认应答(ACK)。这种方式减少了单独发送ACK报文的需求,从而降低了网络中的报文数量,提高了网络带宽的利用率。
核心机制 九: 面向字节流
创建一个TCP的 socket ,同时在内核中创建一个 发送缓冲区 和一个 接受缓冲区
- 调用write时,数据会先写入发送缓冲区
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出
- 如果发送的字节数太短,就会放入缓冲区中等待,等到缓冲区长度差不多了,或者其他合适的时机
- 接受数据时,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用read从接收缓冲区拿到
粘包 粘的是 应用层数据包解决方案
- 引入分隔符,对于变长的包,可以指定明确的分隔符,只要不与正文冲突就好了(应用层协议)
- 指定数据的长度,对于定长的包,保证每次按固定大小读取
- 对于变长的包,可以在包头位置,约定一个包总长度的字段,从而告知包的结束位置
核心机制 十: 异常处理
1.进程崩溃
在java中的体现是在抛出异常之后,但是没有进行catch处理,最终异常进入到JVM里面,就会让JVM崩溃,在实际运行过程中,操作系统会对进行善后,在进程崩溃以后,进程中的PCB就要被回收,PCB中的文件描述符表里对应的所有文件,也都会被系统自动关闭,其中针对 socket 文件,也会触发正常的关闭流程(TCP的四次挥手)
PCB,全称为Process Control Block(进程控制块),是操作系统用来管理和控制进程运行的重要数据结构。每个进程在创建时都会由操作系统为其分配一个PCB,并在整个生命周期中维持这个结构的存在,直到进程终止。
2.主动关机
在正常流程中执行关机操作,此时的操作系统会干掉所有的进程,同样会触发四次挥手,此时会有两种情况:
(a)四次挥手非常快,完成后,关机操作正式完成
(b)四次挥手还没有全部完成,就已经关机
3.主动掉电
(a)接收方掉电
(b)发送方掉电
如果发送探测报文后,A返回了ACK,说明A只是歇一歇,还为掉电,但是如果A没有ACK甚至说在多个探测报文后,A都没有ACK,此时的A就是掉电了
这样周期性的探测报文是用来探测对方的“生死”情况,这种的报文,称为心跳包
计算机中,非常广泛的使用 心跳包 的思想,TCP 内置了心跳包,由于 TCP 内置的心跳包周期比较长,秒级-分钟级应用程序这一层通常也会自行实现一些心跳包,达到更快速的"保活机制”
4.网线断开
🎚️六.示例代码
TCP客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;public class TcpEchoClient {private Socket socket=null;public TcpEchoClient(String serverIP,int serverPort) throws IOException {socket=new Socket(serverIP,serverPort);}public void start(){System.out.println("客户端已启动");try(InputStream inputStream=socket.getInputStream();OutputStream outputStream=socket.getOutputStream()) {Scanner scanner=new Scanner(inputStream);Scanner sc=new Scanner(System.in);PrintWriter printWriter=new PrintWriter(outputStream);while(true){//1.从控制台读取数据System.out.print("->");String request=sc.next();//2.把请求发给服务器printWriter.println(request);printWriter.flush();//3.服务器读响应if(!scanner.hasNext()){break;}String response=scanner.next();//打印日志System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpEchoClient tcpEchoClient =new TcpEchoClient("127.0.0.1",9090);tcpEchoClient.start();}
}
TCP服务器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** Tcp 有连接 可靠传输 面向字节流 全双工*/
public class TcpEchoServer {private ServerSocket serverSocket=null;public TcpEchoServer(int port) throws IOException {serverSocket=new ServerSocket(port);}public void start() throws IOException{System.out.println("启动服务器");while(true){Socket clientSocket =serverSocket.accept();/*线程池ExecutorService service= Executors.newCachedThreadPool();for (int i = 0; i < 1000; i++) {service.submit(()->{try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});}*/Thread t=new Thread(()->{try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});t.start();}}private void processConnection(Socket clientSocket) throws IOException{//打印日志,客户端信息System.out.printf("[%s,%d]客户端已上线!",clientSocket.getInetAddress(),clientSocket.getPort());//获取socket中持有的流对象try(InputStream inputStream=clientSocket.getInputStream();OutputStream outputStream=clientSocket.getOutputStream()){//为方便读取InputStream的请求,使用Scanner包装一下Scanner scanner=new Scanner(inputStream);PrintWriter printWriter=new PrintWriter(outputStream);while(true){//1.读取请求并解析if (!scanner.hasNext()){//如果用scanner也无法读出数据,则说明与客户端连接断开break;//跳出循环}String request = scanner.next();//2.根据请求计算请求String response=process(request);// 3. 把响应写回给客户端// 此处可以按照字节数组直接来写, 也可以有另外一种写法.// outputStream.write(response.getBytes());printWriter.println(response);//存在缓冲区,因此要 flushprintWriter.flush();//4.打印日志System.out.printf("[%s,%d] req=%S resp=%s",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch (IOException e){e.printStackTrace();}finally {System.out.printf("[%s,%d] 客户端已下线!",clientSocket.getInetAddress(),clientSocket.getPort());//要关闭资源clientSocket.close();}}private String process(String request){return request;}public static void main(String[] args) throws IOException {TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);tcpEchoServer.start();}
}
🧨五.总结与反思
书的真正目的在于诱导头脑自己去思考。——莫利
TCP小结
本篇文章主要讲了TCP协议的10个核心机制,或者说是常用机制,实际并不只有10个,TCP更多机制的详情请看 rcf标准文档
可靠性
- 校验和
- 序列号
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
以下是 UDP 和 TCP 的对比总结,以表格的形式呈现:
特征 | TCP (Transmission Control Protocol) | UDP (User Datagram Protocol) |
---|---|---|
类型 | 面向连接 | 无连接 |
可靠性 | 可靠的 | 不可靠的 |
顺序保证 | 保证数据按顺序到达 | 不保证数据顺序 |
流量控制 | 使用滑动窗口机制 | 没有流量控制机制 |
拥塞控制 | 动态调整发送速率以避免网络拥塞 | 没有拥塞控制 |
数据传输单元 | 流 | 数据报 (Datagram) |
重传机制 | 有,使用超时和重传 | 没有重传机制 |
确认应答 | 有,通过ACK确认收到数据 | 没有确认应答 |
头部开销 | 20字节(基本) | 8字节 |
广播/多播支持 | 不支持多播 | 支持广播和多播 |
应用场景 | Web浏览(HTTP/HTTPS),文件传输(FTP),邮件传输(SMTP)等 | 实时音频/视频(VoIP,直播),DNS查询,多人在线游戏等 |
优点 | 高可靠性,保证数据顺序,拥塞控制 | 低延迟,简单,适合实时应用 |
缺点 | 较高的开销,较慢的速度 | 数据包可能丢失或乱序 |
UDP和TCP 两个协议并没有优劣之分
TCP 对于数据需要可靠传输的场景,必然是首选
UDP 对于可靠性要求不高,对于性能要求很高的场景
解释:
- 面向连接 vs 无连接:TCP 在数据传输之前需要建立连接,而 UDP 则不需要。
- 可靠性:TCP 通过序列号、确认应答和重传来确保数据的可靠传输,而 UDP 不提供这些机制。
- 顺序保证:TCP 确保数据包按顺序到达,而 UDP 不保证数据包的顺序。
- 流量控制:TCP 使用滑动窗口机制来控制数据发送的速率,而 UDP 没有流量控制机制。
- 拥塞控制:TCP 通过慢启动、拥塞避免、快速重传和快速恢复等机制来避免网络拥塞,而 UDP 没有拥塞控制。
- 数据传输单元:TCP 是面向字节流的,而 UDP 是面向数据报的。
- 重传机制:TCP 在数据丢失时会进行重传,而 UDP 不会。
- 确认应答:TCP 使用 ACK 来确认数据包是否被接收,而 UDP 不使用确认应答。
- 头部开销:TCP 的头部至少为20字节,而 UDP 的头部为8字节。
- 广播/多播支持:UDP 支持广播和多播,而 TCP 不支持。
- 应用场景:TCP 适用于对可靠性要求较高的应用,如Web浏览、文件传输等;而 UDP 更适合实时应用,如在线游戏、视频流等。
- 优点和缺点:TCP 的优点在于高可靠性,但也有较高的开销和较慢的速度;UDP 的优点在于低延迟和简单性,但数据包可能丢失或乱序。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸