1.网络通信概述
1.1网络通信基本模式:
| CS | Client-Server | 客户端(自行开发)-服务端 |
|---|---|---|
| BS | Browser-Server | 浏览器 - 服务端 |
2.网络通信三要素
| IP地址 | 设备在网络中的地址,唯一标识 |
|---|---|
| 端口 | 应用程序在设备中唯一标识 |
| 协议 | 数据在网络中传输的规则,常见协议有UDP协议和TCP协议 |
2.1 IP地址
| IP | 全称互联网协议地址,分配给上网设备的唯一标志 |
|---|---|
| 常见 IP分类 | |
IPV4 | 32bit(4字节,每个字节0-256) ; 例子: 192.168.0.128 |
IPV6 | 128bit(16字节),号称可以为地球上每一粒沙子编号; 分成八个整数,每个整数用四个16进制位表示,之间用:分开 |
2.1.1 IP地址基本寻路
dns服务器上找不到,去运营商服务器寻找ip返回
| 服务器分类 | |
|---|---|
| dns服务器 | dns是多级服务器; 电脑中配置有dns缓存, ipconfig /displaydns查看dns缓存 ;ipconfig /flushdns清理dns缓存 |
| 运营商服务器 | 存在所有公开的ip地址,访问部分网站慢,可以将ip地址配置在自己的电脑中,方法自行百度 |
2.1.2 IP地址形式
| 地址形式 | 地址用法 | 备注 |
|---|---|---|
| 公网地址 | ||
| 私有地址(局域网使用) | 192.168开头是常见局域网地址,范围192.168.0.0~192.168.255.255 | 专门为组织机构使用,节约公网IP分配 |
| 特殊IP地址 | 本机127.0.0.1或者localhost: | 称为回送地址,也可称本地回环地址,只会寻找当前所在机器 |
2.1.3 IP常用命令
| 命令 | |
|---|---|
| ipconfig | 查看本机IP地址 |
| ping 192.168.0.0(IP地址) | 检查网络是否连接 |
2.1.4 IP地址操作类:InetAddress
/**
* IP地址操作类:InetAddress
*/
public class inetAddressDemo01 {
public static void main(String[] args) throws IOException {
//1.获取本机地址对象
InetAddress localHostIp = InetAddress.getLocalHost();
System.out.println(localHostIp);
System.out.println(localHostIp.getHostName());//DESKTOP-HIVV0B9
System.out.println(localHostIp.getHostAddress());//192.168.1.8
// 2.获取域名地址对象
InetAddress bDIp = InetAddress.getByName("www.baidu.com");
System.out.println(bDIp.getHostName());//www.baidu.com
System.out.println(bDIp.getHostAddress());//220.181.38.150 服务器地址不止一个
// 3.获取公网ip地址对象
InetAddress bDIp2 = InetAddress.getByName("220.181.38.150");
System.out.println(bDIp2.getHostName());//www.baidu.com
System.out.println(bDIp2.getHostAddress());//220.181.38.150 服务器地址不止一个
// 4.判断是否能联通:ping 5s内是否联通
System.out.println(bDIp.isReachable(5 * 1000));//true
}
}
2.2 端口
2.2.1 端口号
标识机器上运行的进程(程序),规定为一个16位二进制,范围0~65535;
2.2.2 端口类型
| 端口类型 | 范围和使用者 | 举例 |
|---|---|---|
| 周知端口 | 0~1023,被预先定义的知名应用占用 | HTTP 80,FTP 21 |
| 注册端口 | 1024~49151,分配给用户进程或某些应用程序 | Tomcat 8080,MySQL 3306 |
| 动态端口 | 49152~65535,称为动态端口,不固定分配 |
注意:开发程序选择注册端口,同设备不能运行两个端口一样的程序,会报错
2.3 协议
连接和通信数据的规则称为网络通信协议;
2.3.1 网络通信协议有两套参考模型
| 参考模型 | 概述 | 常见协议 |
|---|---|---|
| OSI参考模型 | 世界互联协议标准,全球通信规范,过于理想化,未能广泛推广; | |
| TCP/IP参考模型(TCP/IP协议) | 事实上的国际标准,传输层2个常见协议 | TCP:传输控制协议:可靠传输协议,效率低; |
| UDP:用户数据报协议:不可靠 |
2.3.2 TCP协议特点和通信场景
- TCP协议,必须双方先建立连接,面向连接的可靠通信协议;
- 传输前,**“三次握手”**建立连接;
- 1次,客户端向服务端发出连接请求(有需要回复是需要带的id),等待服务器确认
- 2次,服务器返回响应,回复收到请求(上方的id,和自己确认时需要的id)
- 3次,客户端发送确认信息,连接建立
- 连接中,可进行大数据量的传输;
- 连接、发送数据都需要确认,传输完毕后,**“四次挥手”**释放建立的连接,通信效率低
- 1次,客户端向服务端发出取消连接请求;
- 2次,服务器返回响应,表示收到请求,处理当前所有数据
- 3次,数据处理完,向客户端发出确认取消信息
- 4次,客户端再次发送确认消息,连接取消
- 通信场景:
- 信息安全要求较高,如文件下载,金融等数据通信;
2.3.3 UDP协议
- UDP是一种无连接、不可靠传输协议;
- 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接;
- 每个数据包大小限制在64KB内;
- 发送不管对方是否准备好,接收方收到也不确认,不可靠;
- 可以广播发送,结束时无需释放资源,开销小,速度快;
- 场景:
- 语音、视频通话
3.UDP通信
3.1 UDP协议通信演示
/**
* 服务端端
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动!");
//1.创建接收端对象:注册端口(可以指定端口)
DatagramSocket socket = new DatagramSocket(8888);
// 2.创建一个数据包对象封装数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
//3.等待接收数据
socket.receive(packet);
//4.取出数据;读多少取多少
//读取当前得到数据的大小
int len = packet.getLength();
String s = new String(buffer,0,len);
System.out.println(s);
//获得发送端ip和端口
String ip = packet.getSocketAddress().toString();
int port = packet.getPort();
System.out.println("IP:"+ip+" PORT:"+port);
//释放资源
socket.close();
}
}
/**
* 发送端
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动!");
//1.创建发送端对象,发送端自带默认端口号(可以指定端口)
DatagramSocket socket = new DatagramSocket(6666);
//DatagramSocket socket = new DatagramSocket();
/**
* DatagramPacket( 数据包对象
* byte buf[] 封装要发送的数据;字节数组
* , int length 数据大小
* ,InetAddress address 服务端主机IP地址
* , int port) 服务端端口
*/
// 2.创建一个数据包对象封装数据
byte[] buffer = "事了拂衣去,深藏身与名".getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getLocalHost(), 8888);
//3.发送数据
socket.send(packet);
//释放资源
socket.close();
}
}
3.2 使用UDP通信实现(弹幕)多发多收信息
/**
* 客户端(可多开)
* 在程序设置中勾选 Allow Parallel run
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动!");
//1.创建发送端对象,发送端自带默认端口号(可以指定端口)
DatagramSocket socket = new DatagramSocket();
// 2.创建一个数据包对象封装数据
String msg = "默认消息";
byte[] buffer = msg.getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getLocalHost(), 8888);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送消息:");
msg = scanner.nextLine();
if (msg.equals("exit")){
System.out.println("离线成功");
socket.close();
break;
}
//写入的消息放入信息包中
buffer = msg.getBytes();
packet.setData(buffer,0, buffer.length);
//3.发送数据
socket.send(packet);
}
}
}
/**
* 服务端端
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动!");
//1.创建接收端对象:注册端口(可以指定端口)
DatagramSocket socket = new DatagramSocket(8888);
// 2.创建一个数据包对象封装数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
//3.等待接收数据
socket.receive(packet);
//4.取出数据;读多少取多少
int len = packet.getLength();
String rs = new String(buffer,0,len);
System.out.println("来自IP:"+packet.getAddress()+"中的端口:"+packet.getPort()+"的信息----"+rs);
}
//释放资源
//socket.close();
/**
* 服务端启动!
* 来自IP:/192.168.1.8中的端口:56802的信息----你好
* 来自IP:/192.168.1.8中的端口:58954的信息----你好,我是01,请多指教!
* 来自IP:/192.168.1.8中的端口:58954的信息----你叫什么?
* 来自IP:/192.168.1.8中的端口:56802的信息----我恁爹!
*/
}
}
3.3 UDP实现广播
-
使用广播地址:255.255.255.255
-
发送端数据包目的地写广播地址,指定端口号9999;
-
本机所在网段的其他主机程序只要匹配端口(9999)成功就可以收到消息;
-
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("255.255.255.255"), 9999);
3.4 UDP组播
- 使用组播地址:224.0.0.0~239.255.255.255
- 发送端数据包目的地组播IP224.0.0.1,指定端口号9999;
- 接收端必须绑定该组播IP,端口对应
- DatagramSocket子类MulticastSocket可以在接收端绑定组播IP;
/**
* 客户端(可多开)
* 在程序设置中勾选 Allow Parallel run
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动!");
//1.创建发送端对象,发送端自带默认端口号(可以指定端口)
DatagramSocket socket = new DatagramSocket();
// 2.创建一个数据包对象封装数据
String msg = "默认消息";
byte[] buffer = msg.getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("224.0.0.1"), 9999);
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送消息:");
msg = scanner.nextLine();
if (msg.equals("exit")){
System.out.println("离线成功");
socket.close();
break;
}
//写入的消息放入信息包中
buffer = msg.getBytes();
packet.setData(buffer,0, buffer.length);
//3.发送数据
socket.send(packet);
}
}
}
/**
* 组播服务端
* 创建接收端对象MulticastSocket
* socket.joinGroup 当前接收端加入到一个组播组中,绑定对应的组播消息的组播IP
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动!");
//1.创建接收端对象MulticastSocket:注册端口(可以指定端口)
MulticastSocket socket = new MulticastSocket(9999);
//新增:当前接收端加入到一个组播组中,绑定对应的组播消息的组播IP
//socket.joinGroup(InetAddress.getByName("224.0.0.1"));
//新版本的用法,用当前本地的ip接收组播
socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.0.1"),9999)
,NetworkInterface.getByInetAddress(InetAddress.getLocalHost()));
// 2.创建一个数据包对象封装数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
//3.等待接收数据
socket.receive(packet);
//4.取出数据;读多少取多少
int len = packet.getLength();
String rs = new String(buffer,0,len);
System.out.println("来自IP:"+packet.getAddress().getHostAddress()+"中的端口:"+packet.getPort()+"的信息----"+rs);
}
}
}
4.TPC通信
注意:Java中使用java.net.Socket类实现通信,底层即是使用TCP协议;
4.1 TCP服务端用的代表类
- ServerSocket类,注册端口;
- 调用accept()方法阻塞,等待接收客户端连接,得到Socket对象;
4.2 TCP通信基本原理
- 客户端怎么发,服务端怎么接;
- 客户端没有消息,服务端阻塞等待;
- Socket一方关闭或者异常,对方Socket也会失效或者出错;
/**
* Socket 网络编程
* 客户端
*/
public class TCPClientDemo {
public static void main(String[] args) {
System.out.println("客户端启动!");
try {
//1.创建Socket通信管道请求服务端的连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级字节流包装成打印流
PrintStream ps = new PrintStream(os);
//4.发送消息
/**
* 注意:消息按行读取 ps.print没有出现换行符,会报错Connection reset
*/
//ps.print("TCP客户端,请求与你对接,是否同意。");
ps.println("TCP客户端,请求与你对接,是否同意。");
ps.flush();
//关闭资源,不推荐
//socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Socket 网络编程
* 服务端
*/
public class TCPServerDemo {
public static void main(String[] args) {
System.out.println("服务端启动!");
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(9999);
// 2.必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
//此处等待客户端新消息
Socket socket = serverSocket.accept();
// 3.从socket通信管道中得到字节输入流进行消息接收
InputStream is = socket.getInputStream();
// 4.字节输入流包装成缓冲字符输入流;用转换流将字节流转为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5.按行读取
String msg;
if ((msg = br.readLine())!= null){
System.out.println(socket.getRemoteSocketAddress()+" 说了: "+msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.3 实现服务端接收多个客户端消息
- 主线程定义循环负责接收客户端Socket管道连接;
- 每接收一个Socket通信管道后,分配一个独立子线程负责处理;
/**
* Socket 网络编程
* 服务端
* 单线程 只能处理一个客户端信息
* Socket socket = serverSocket.accept();
*/
public class TCPServerDemo {
public static void main(String[] args) {
try {
System.out.println("服务端启动!");
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
// 2.每接收一个客户端的Socket连接请求,交给一个独立子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+"上线了");
//创建子线程
new ServerReaderThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 每接收一个Socket通信管道后,分配一个独立子线程负责处理;
*/
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从socket通信管道中得到字节输入流进行消息接收
InputStream is = socket.getInputStream();
// 4.字节输入流包装成缓冲字符输入流;用转换流将字节流转为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5.按行读取
String msg;
while ((msg = br.readLine())!= null){
System.out.println(socket.getRemoteSocketAddress()+" 说了: "+msg);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress()+"下线了");
e.printStackTrace();
}
}
}
/**
* Socket 网络编程
* 客户端
*/
public class TCPClientDemo {
public static void main(String[] args) {
System.out.println("客户端启动!");
try {
//1.创建Socket通信管道请求服务端的连接
Socket socket = new Socket("127.0.0.1", 9999);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级字节流包装成打印流
PrintStream ps = new PrintStream(os);
//4.发送消息
/**
* 注意:消息按行读取 ps.print没有出现换行符,会报错Connection reset
*/
//ps.print("TCP客户端,请求与你对接,是否同意。");
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送消息:");
String msg = scanner.nextLine();
if (msg.equals("exit")){
System.out.println("离线成功");
socket.close();
break;
}
ps.println(msg);
ps.flush();
}
//关闭资源,不推荐
//socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.4 线程池优化
上述代码存在问题:客户端过多,子线程过多;
4.4.1 使用线程池优势
- 服务端可以复用线程,处理多个客户端,可以避免系统瘫痪;
- 适合客户端通信时长较短的场景;
/**
* Socket 网络编程
* 客户端同上述
*/
/**
* Socket 网络编程
* 服务端 线程池优化
*/
public class TCPServerDemo {
//使用静态变量定义一个线程池对象;最多处理5个,排队2个,第八个报错丢弃
private static ExecutorService pool = new ThreadPoolExecutor(3,5,6
, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2)
, Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
try {
System.out.println("服务端启动!");
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
// 2.每接收一个客户端的Socket连接请求,交给一个独立子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+"上线了");
//创建任务
ServerReaderRunnable runnable = new ServerReaderRunnable(socket);
//任务送进线程池
pool.execute(runnable);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 实现Runnable 将每个Socket连接请求打成任务
*/
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从socket通信管道中得到字节输入流进行消息接收
InputStream is = socket.getInputStream();
// 4.字节输入流包装成缓冲字符输入流;用转换流将字节流转为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5.按行读取
String msg;
while ((msg = br.readLine())!= null){
System.out.println(socket.getRemoteSocketAddress()+" 说了: "+msg);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress()+"下线了");
e.printStackTrace();
}
}
}
4.5即时通信
4.5.1 即时通信含义,怎么设计
- 即时通信,一个客户端消息发出去,其他客户端可以接收;
- 即时通信需要进行端口转发的设计思想;
- 服务端需要把在线的Socket管道存储起来;
- 一旦收到消息,推送其他管道;
/**
* 即时通信
* 服务端 线程池优化
*/
public class TCPServerSMSDemo {
//使用静态变量定义一个线程池对象;最多处理5个,排队2个,第八个报错丢弃
private static ExecutorService pool = new ThreadPoolExecutor(3,5,6
, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2)
, Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
//静态定义List集合存储当前全部在线管道
public static List<Socket> allOnlineSockets = new ArrayList<>();
public static void main(String[] args) {
try {
System.out.println("服务端启动!");
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
// 2.每接收一个客户端的Socket连接请求,交给一个独立子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+"上线了");
//上线管道存入allOnlineSockets
allOnlineSockets.add(socket);
//创建任务
ServerReaderRunnable runnable = new ServerReaderRunnable(socket);
//任务送进线程池
pool.execute(runnable);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从socket通信管道中得到字节输入流进行消息接收
InputStream is = socket.getInputStream();
// 4.字节输入流包装成缓冲字符输入流;用转换流将字节流转为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5.按行读取
String msg;
while ((msg = br.readLine())!= null){
System.out.println(socket.getRemoteSocketAddress()+" 说了: "+msg);
//消息转发给所有端口
sendMessageToAll(msg);
}
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress()+"下线了");
TCPServerSMSDemo.allOnlineSockets.remove(socket);
}
}
private void sendMessageToAll(String msg) {
for (Socket socket : TCPServerSMSDemo.allOnlineSockets) {
try {
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(msg);
ps.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 即时通信
* 客户端
*/
public class TCPClientSMSDemo {
public static void main(String[] args) {
System.out.println("客户端启动!");
try {
//1.创建Socket通信管道请求服务端的连接
Socket socket = new Socket("127.0.0.1", 9999);
//创建独立线程读消息
new ClientReaderThread(socket).start();
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级字节流包装成打印流
PrintStream ps = new PrintStream(os);
//4.发送消息
/**
* 注意:消息按行读取 ps.print没有出现换行符,会报错Connection reset
*/
//ps.print("TCP客户端,请求与你对接,是否同意。");
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送消息:");
String msg = scanner.nextLine();
if (msg.equals("exit")){
System.out.println("离线成功");
socket.close();
break;
}
ps.println(msg);
ps.flush();
}
//关闭资源,不推荐
//socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientReaderThread extends Thread{
private Socket socket;
public ClientReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从socket通信管道中得到字节输入流进行消息接收
InputStream is = socket.getInputStream();
// 4.字节输入流包装成缓冲字符输入流;用转换流将字节流转为字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5.按行读取
String msg;
while ((msg = br.readLine())!= null){
System.out.println("收到: "+msg);
}
} catch (IOException e) {
System.out.println("服务端把你踢出去了!!!");
e.printStackTrace();
}
}
}
5.BS系统
注意:服务器必须给浏览器响应HTTP协议格式数据,否则浏览器不识别;
5.1 TCP如何实现BS请求网页信息
- 客户使用浏览器发起请求;
- 服务端必须按照浏览器协议规则HTTP响应数据;
/**
* TCP实现BS请求网页信息
*/
public class BSServerDemo {
//使用静态变量定义一个线程池对象;最多处理5个,排队2个,第八个报错丢弃
private static ExecutorService pool = new ThreadPoolExecutor(3,5,6
, TimeUnit.SECONDS,new ArrayBlockingQueue<>(2)
, Executors.defaultThreadFactory()
,new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080);
while (true){
Socket socket = serverSocket.accept();
ServerReaderRunnable runnable = new ServerReaderRunnable(socket);
pool.execute(runnable);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("HTTP/1.1 200 ok");//协议类型和版本 状态码 状态码描述
ps.println("Content-Type:text/html;charset=UTF-8");//响应的数据类型:文本/网页
ps.println();//必须发送一个空行
ps.println("<a href=\"http://www.runoob.com\">这是一个链接使用了 href 属性</a>");
ps.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
RecordDate:2021/08/21