Socket

1. Socket

1.1. 知识点梳理复习

Socket01包: 单次对话  

Socket02包:持久化通信  客户端可以循环键盘端输入,服务器循环读取,再转发  client.java ,server.java

Socket03包: 实际的群中,发送,和接收是2个相互独立的线程,client客户端:只能先发送,再读取,发送和读取都在main线程里,和实际情况不符,send,receive  implements Runnable

现在的问题,启动多个客户端,仍旧只能谁先到,谁先聊 ,服务器只能支持和1个客户端通信 , Socket socket = server.accept();

While(true){

Socket socket = server.accept();

DataInputStream dis = new DataInputStream(socket.getInputStream);

DataOutputStream dos = new DataOutputStream(socket.getOutputStream);

While(true){

String msg = dis.readUTF();

dos.writeUTF(msg);

dos.flush();

}

}

这种解决方法,不可行,第一个客户端过来了,如果处在内层的死循环中不退出,即使有第2个客户端过来了,仍旧没有办法accept()到, 这种写法造成的还是谁先到,谁先聊

Socket04包: 将服务端开启多线程,ServerChannel多线程类(10086客服) ,来一个客户端,开启一个多线程类接待一下 serverChannel转发

Socket05包:群聊 ,1个客户端发送的,自身不收,其他的客户端能收到。客户端数量很多的时候,如何处理呢?如何取到这些客户端对应的客服(ServerChannel),利用这些客服发消息。

想到了解决办法,将客服类设计成内部类 ,在Server类声明的时候,声明1个集合List,专门用来存储客服(ServerChannel) ,转发给其他人实际上就是遍历集合

集合里数据何时存入呢?  接收到1个客户端,new1个多线程类对象 new1个ServerChannel对象后,这个时候就可以加入集合

Public class Server{

Private List<ServerChannel> list =new ArrayList<ServerChannel>();

Private  class ServerChannel{

}

}

Socket06包: 加入私聊,支持人名 ,屏蔽关键词,保存聊天记录

1.2. Socket使用

见包socket06下的代码

关闭流 通用的工具类

package com.net.socket06;

import java.io.Closeable;

import java.io.IOException;

/**

 * 通用的工具类,用来关闭流

 * @author Administrator

 *

 */

public class CloseUtil {

/**

 * ... 用法和数组一样,既可以表示一个参数,也可以表示多个参数,甚至0个参数

 * @param io

 */

public static void closeAll(Closeable... io){

for(Closeable temp:io){

if(null!=temp){

try {

temp.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

}

加载物理文件中的内容到集合中,写方法判断,是否包含集合中的关键词

package com.net.socket06;

import java.io.BufferedReader;

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;

/**

 * 读取文本文件中的内容,按行读取,保存在集合中

 * 只读取1次 静态代码块

 * @author Administrator

 */

public class WordsUtil {

private static List<String> wordsList = new ArrayList<String>();

static{

BufferedReader br = null;

try {

br = new BufferedReader(new FileReader("E:/others/关键词.txt"));

String str = null;

while((str = br.readLine())!=null){

wordsList.add(str);

}

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}finally{

CloseUtil.closeAll(br);

}

}

/**

 * 判断是否包含集合中的关键词,如果包含返回true,否则返回false

 * @param msg

 * @return

 */

public static  boolean isContain(String msg){

for(String s:WordsUtil.wordsList){

if(msg.contains(s)){

return true;

}

}

return false;

}

}

Server类服务端代码:

package com.net.socket06;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.ArrayList;

import java.util.Date;

import java.util.Iterator;

import java.util.List;

/**

 * 1.3个客户端

 * 客户端1:

 * 启动后   请输入姓名:jack

 * 系统消息:jack,欢迎你进入。

 * 系统消息:tom,已经进入了聊天室

 *      系统消息:helen,已经进入了聊天室

 *      @helen: hello,你在做什么

 *      今天吃了10个包子

 * 客户端2:

 * 请输入姓名  tom

 * 系统消息:tom,欢迎你进入

 * 系统消息:helen,已经进入了聊天室

 * jack对大家说:今天吃了10个包子

 * 客户端3:

 *      请输入姓名  helen

 *      系统消息:helen,欢迎你进入

 *      jack悄悄对你说: hello,你在做什么

 *      jack对大家说:今天吃了10个包子

 * 支持私聊  如果发出的格式  @XXX: 只有 XXX能收到消息,其他人收不到

 *

 * 服务端:存储聊天记录   客户端 ip地址 + “  ”+客户端姓名: + “  ”+ 年-月-日 时:分:秒:  聊天内容 ,按行存储 ,存储到

 * E:/others/聊天内容.txt中,追加

 * 服务端: E:/others/关键词.txt, 里面有多行关键词,一行一个 ,“笨蛋”,“白痴”,“傻瓜”.....

 * 如果发现要转发的内容中包含了关键词,这个时候,服务端提醒该发送者 :你发送的内容中有侮辱性词语,已被屏蔽。服务器不做转发。

 *

 * for(ServerChannel chan :clist){

 * if(chan!=this){

 * chan.sendMsg(msg);

 * }

 * }

 *

 * for(ServerChannel chan :clist){

 * if(chan==this){

 * continue;

 * }

 * chan.sendMsg(msg);

 * }

 *

 * **

 * 转发给其他人的方式2

 *

private void sendToOthers(String msg,boolean isSys){

//将读取的内容转发给其他客户端

for(ServerChannel chan:clist){

//屏蔽自身,除了自身不发送

if(chan==this){

continue;

}

//chan :表示的是其他客户端对应的客服(多线程类ServerChannel)

if(isSys){ //系统消息

chan.sendMsg(msg);

}else{ //不是系统消息,正常的转发

chan.sendMsg(this.name+"对大家说: "+msg);

}

}

}

 *

 * 1.需要解决的 ,自身不能和自身私聊 ,提示用户

 * 2.如果私聊的对象不存在,提示用户

 * @author Administrator

 *

 */

public class Server {

private List<ServerChannel> clist = new ArrayList<ServerChannel>();

public static void main(String[] args) throws IOException {

//对象名.方法名()

new Server().start();

}

public void start() throws IOException{

//创建服务端ServerSocket

ServerSocket server = new ServerSocket(8023);

while(true){

Socket socket = server.accept();

System.out.println("看, 有1个客户端过来了,开启一个客服接待一下,该客户端的ip地址:"+socket.getInetAddress().getHostAddress());

ServerChannel chan = new ServerChannel(socket);

//加入集合,方便后面的遍历

clist.add(chan);

new Thread(chan,"客服").start();

}

}

//设计成内部类,仍旧转发

public class ServerChannel implements Runnable{

private DataInputStream dis;

private DataOutputStream dos;

private Socket socket;

private boolean isRunning=true;

private String name; //用来存储检索出来的客户端的姓名

/**

 * 通过带参构造传入socket对象,用于构建输入,输出流

 * @param socket

 */

public ServerChannel(Socket socket) {

super();

this.socket = socket;

try {

dis = new DataInputStream(this.socket.getInputStream());

dos = new DataOutputStream(this.socket.getOutputStream());

//检索名字

this.name = dis.readUTF();

//给自身发送欢迎消息

this.sendMsg("系统消息:"+this.name+",欢迎你登录");

//给其他的已经登录的发送系统消息

this.sendToOthers("系统消息:"+this.name+"已经进入了聊天室",true);

} catch (IOException e) {

isRunning=false;

CloseUtil.closeAll(dos,dis);

e.printStackTrace();

}

}

/**

 * 返回读取的内容

 * @return

 */

private String getMsg(){

//读取内容

String msg=null;

try {

msg = dis.readUTF();

//存储到文本文件

saveIntoFile(msg,"E:/others/聊天内容.txt");

} catch (IOException e) {

isRunning =false;

clist.remove(this); //移除自身

CloseUtil.closeAll(dis);

e.printStackTrace();

}

return msg;

}

/**

 * 将msg包装一下存储到path所指定的路径中

 * @param msg  聊天的内容(服务器接收到的)

 * @param path E:/others/聊天内容.txt

 */

private void saveIntoFile(String msg,String path){

//数据的格式: 客户端 ip地址 + “  ”+客户端姓名: + “  ”+ 年-月-日 时:分:秒:  聊天内容 ,按行存储

String data = this.socket.getInetAddress().getHostAddress()+" "+this.name+" "+DateUtil.getStrFromDate("yyyy-MM-dd HH:mm:ss", new Date())

+" :"+msg+"\r\n";

FileOutputStream fos = null;

try {

fos = new FileOutputStream(path,true);

fos.write(data.getBytes());

fos.flush();

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}finally{

CloseUtil.closeAll(fos);

}

}

/**

 * 转发信息给其他人    @tom:afadfhello,aaaaa

 * @param msg

 * @isSys 该变量,用于表明是否是系统消息 ,如果是true 转发 “系统消息:XXX已经进入了聊天室” ,如果false 转发 "XXX对大家说:....."

 */

private void sendToOthers(String msg,boolean isSys){

//判断消息的类型,判断是否是私聊

if(msg.startsWith("@") && msg.indexOf(":")>-1){ //发给特定的人

//取出私聊对象的姓名

String toName = msg.substring(msg.indexOf("@")+1, msg.indexOf(":"));

//取出私聊内容

String toContent = msg.substring(msg.indexOf(":")+1);

System.out.println("****私聊对象="+toName+",私聊内容="+toContent);

//局部变量,声明1个boolean,用于表明,是否找到私聊对象 ,如果找到私聊对象,更改布尔值

boolean isFind = false;

//遍历集合,找到私聊对象所属的客服(ServerChannel) ,该客服给其对应的客户端发送消息

Iterator<ServerChannel> it = clist.iterator();

while(it.hasNext()){

ServerChannel chan = it.next();

if(chan.name.equals(toName) && !toName.equals(this.name)){

chan.sendMsg(this.name+"悄悄对你说: "+toContent);

isFind=true;

}

}

//循环结束后,再次判断isFind的值

if(!isFind){

if(toName.equals(this.name)){

this.sendMsg(this.name+"你不能和自身私聊"); //给自身发送提示信息

}else{

this.sendMsg(this.name+",你要私聊的对象“"+toName+"”不存在,无法私聊");

}

}

}else{ //正常的发送给其他客户端

//将读取的内容转发给其他客户端

for(ServerChannel chan:clist){

//屏蔽自身,除了自身不发送

if(chan!=this){

//chan :表示的是其他客户端对应的客服(多线程类ServerChannel)

if(isSys){ //系统消息

chan.sendMsg(msg);

}else{ //不是系统消息,正常的转发

chan.sendMsg(this.name+"对大家说: "+msg);

}

}

}

}

}

/**

 * 发送给自身对应的客户端

 * @param msg

 */

private void sendMsg(String msg){

try {

dos.writeUTF(msg);

dos.flush();

} catch (IOException e) {

isRunning =false;

clist.remove(this); //移除自身

CloseUtil.closeAll(dos);

e.printStackTrace();

}

}

@Override

public void run() {

while(isRunning){

String msg = getMsg();

if(null!=msg && msg.trim().length()!=0){

if(WordsUtil.isContain(msg)){

sendMsg(this.name+"你发送的内容包含侮辱性词语,已被屏蔽");

}else{

sendToOthers(msg,false);

}

}else{

sendMsg(this.name+"你发送的内容不能为空");

}

}

}

}

}

客户端Client代码

package com.njwb.net.socket06;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.Socket;

import java.net.UnknownHostException;

/**

 *

 * @author Administrator

 *

 */

public class Client {

public static void main(String[] args) throws UnknownHostException, IOException {

BufferedReader console = new BufferedReader(new InputStreamReader(System.in));

System.out.println("请输入姓名:");

String name = console.readLine();

//创建Socket对象,同时指定要连接的服务端的ip地址和端口号

Socket client = new Socket("127.0.0.1",8023);

//开启发送线程

new Thread(new Send(client,name),"发送线程").start();

//开启接收线程

new Thread(new Receive(client),"接收线程").start();

}

}

客户端发送线程 send.java

package com.njwb.net.socket06;

import java.io.BufferedReader;

import java.io.DataOutputStream;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.Socket;

/**

 * 发送线程

 * @author Administrator

 *

 */

public class Send implements Runnable {

private DataOutputStream dos; //声明输出流对象,用于发送消息

private BufferedReader console; //声明键盘端输入的实例变量

private Socket socket; //声明socket,用于构建输出流对象

private boolean isRunning=true; //用于控制循环是否继续

private String name; //用于保存客户端的姓名

/**

 * 往线程传递数据,此处从client类传入socket对象,用于构建输出流

 * @param socket

 */

public Send(Socket socket,String name) {

super();

this.socket = socket;

this.name = name;

console = new BufferedReader(new InputStreamReader(System.in));

try {

//根据传入的socket,构建了输出流对象

dos = new DataOutputStream(this.socket.getOutputStream());

//发送名字

dos.writeUTF(this.name);

dos.flush();

} catch (IOException e) {

isRunning=false;

CloseUtil.closeAll(dos,console);

e.printStackTrace();

}

}

/**

 * 返回的键盘端输入的字符串

 * @return

 */

private String getMsg(){

String msg="";

try {

msg = console.readLine();

} catch (IOException e) {

isRunning=false;

CloseUtil.closeAll(console);

e.printStackTrace();

}

return msg;

}

/**

 * 发送信息

 * @param msg

 */

private void sendMsg(String msg){

if(!StringUtil.isEmpty(msg)){ //发送的消息做非空验证

try {

dos.writeUTF(msg);

dos.flush();

} catch (IOException e) {

isRunning=false;

CloseUtil.closeAll(dos);

e.printStackTrace();

}

}

}

@Override

public void run() {

while(isRunning){

sendMsg(getMsg());

}

}

}

客户端接收线程 Receive.java

package com.njwb.net.socket06;

import java.io.DataInputStream;

import java.io.IOException;

import java.net.Socket;

/**

 * 接收线程

 * @author Administrator

 *

 */

public class Receive implements Runnable{

private DataInputStream dis; //声明输入流对象,用于读取服务端发送过来的

private Socket socket; //声明socket对象

private boolean isRunning=true; //声明boolean变量,用于控制循环是否继续

public Receive(Socket socket) {

super();

this.socket = socket;

//根据传入的socket对象,构建输入流对象

try {

dis = new DataInputStream(this.socket.getInputStream());

} catch (IOException e) {

isRunning=false;

CloseUtil.closeAll(dis);

e.printStackTrace();

}

}

/**

 * 返回接收的字符串

 * @return

 */

private String getMsg(){

String msg=null;

try {

msg = dis.readUTF();

} catch (IOException e) {

isRunning=false;

CloseUtil.closeAll(dis);

e.printStackTrace();

}

return msg;

}

@Override

public void run() {

while(isRunning){

System.out.println(getMsg());

}

}

}

1.3. UDP通信

基于UDP协议的通信方式,称为数据报通信方式

每个数据发送单元被统一封装成数据包的方式,发送方将数据包发送到网络中,数据包在网络中去寻找他的目的地。

l 以数据为中心,非面向连接,不安全,数据可能丢失,效率高。

l DatagramSocket:用于发送或接收数据包

l DatagramPacket:数据容器(封包)的作用

l 基本步骤:

1.创建客户端的DatagramSocket,创建时,定义客户端的监听端口。

2.创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口

3.在客户端定义DatagramPacket对象,封装待发送的数据包。

4.客户端将数据包发送出去

5.服务端接收数据包

发送引用数据类型

l 客户端:

1) 创建客户端 DatagramSocket类 +指定端口

2) 准备数据  字节数组

3) 打包 DatagramPacket +服务器地址及端口

4) 发送

5) 释放资源

l 服务器:

1) 创建服务端DatagramSocket类+指定端口

2) 准备接受容器  字节数组 封装DatagramPacket(封装成包)

3)  接收数据

4) 分析

5) 释放资源

发送基本数据类型

l 客户端:

1) 创建客户端 DatagramSocket类 +指定端口

2) 准备数据 基本数据类型转换成字节数组(字节数组输出流ByteArrayOutputStream toByteArray 数据字节输出流DataOutputStream)

3) 打包 DatagramPacket +服务器地址及端口(发送的地点以及端口)

4) 发送

5) 释放资源

l 服务器:

1) 创建服务端DatagramSocket类+指定端口

2) 准备接受容器  字节数组 封装DatagramPacket(封装成包)

3)  接收数据

4) 分析数据 字节数组转换成基本数据类型(字节数组输入流ByteArrayOutputStream 数据字节输入流DataInputStream)

5) 释放资源

例题1:对应的需求1

package com.njwb.net.udp;

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

/**

 * 只需要记住2个类DatagramSocket DatagramPacket

 * DatagramSocket(int port)

          创建数据报套接字并将其绑定到本地主机上的指定端口。

  DatagramPacket(byte[] buf, int length)

          构造 DatagramPacket,用来接收长度为 length 的数据包。 服务端

 * 需求:发送今天吃了10个包子发送给服务器

 * 需求:发送 139.87 double基本数据类型发送给服务器

 * 1)创建服务端DatagramSocket+指定端口

2)准备接受容器  字节数组 封装DatagramPacket(封装成包)

3) 接收数据

4)分析

5)释放资源

 * @author Administrator

 *

 */

public class Server {

public static void main(String[] args) throws IOException {

//1.创建服务端DatagramSocket+指定端口

DatagramSocket server = new DatagramSocket(8888);

//2.准备接受容器  字节数组

byte[] container = new  byte[1024];

//3.封装DatagramPacket(封装成包)

DatagramPacket packet = new DatagramPacket(container,container.length);

//4.包 接收数据(接收的数据存储在packet)

server.receive(packet);

//5.分析 (数据在packet中,需要取出,输出在控制台)

byte[] data = packet.getData();

int len = packet.getLength();

//转换成字符串打印输出

System.out.println(new String(data,0,len));

//6.释放资源

server.close();

}

}

package com.net.udp;

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

/**

 * DatagramSocket(int port)

          创建数据报套接字并将其绑定到本地主机上的指定端口。

          

  (要发送的数据对应的byte数组,byte数组的长度,InetAddress.getByName("服务器的ip地址"),服务器的端口号)       

  DatagramPacket(byte[] buf, int length, InetAddress address, int port)  你熟悉一点

          构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。

  (要发送的数据对应的byte数组,byte数组的长度, SocketAddress的子类对象)                

  DatagramPacket(byte[] buf, int length, SocketAddress address)

  SocketAddress抽象类,不能直接new,此处放置的是SocketAddress的子类  InetSocketAddress ,

  InetSocketAddress(String hostname, int port)

          根据主机名和端口号创建套接字地址。  (服务器的ip地址,服务器的端口号)

          

          构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。        

 * 客户端:

 * 1)创建客户端 DatagramSocket+指定端口

2)准备数据  字节数组

3)打包 DatagramPacket +服务器地址及端口

4)发送

5)释放资源

 

 * @author Administrator

 *

 */

public class Client {

public static void main(String[] args) throws IOException {

//1.创建客户端 DatagramSocket+指定端口  ,不要和服务器相同

DatagramSocket client = new DatagramSocket(7777);

//2.准备数据,要将数据转换成字节数组

String str = "今天吃了10个包子";

byte[] data = str.getBytes();

//3.打包 DatagramPacket +服务器地址及端口

DatagramPacket packet = new DatagramPacket(data,data.length,InetAddress.getByName("127.0.0.1"),8888);

//DatagramPacket packet = new DatagramPacket(data,data.length,new InetSocketAddress("127.0.0.1",8888));

//4.发送

client.send(packet);

//5.释放资源

client.close();

}

}

例题2:

package com.net.udp;

import java.io.ByteArrayInputStream;

import java.io.DataInputStream;

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import com.njwb.net.socket06.CloseUtil;

 

/**

 * 接收的基本数据类型

 * 1) 创建服务端DatagramSocket类+指定端口

2) 准备接受容器  字节数组 封装DatagramPacket(封装成包)

3) 接收数据

4) 分析数据 字节数组转换成基本数据类型(字节数组输入流ByteArrayOutputStream 数据字节输入流DataInputStream)

5) 释放资源

 * @author Administrator

 *

 */

public class Server2 {

public static void main(String[] args) throws IOException {

//1.创建服务端DatagramSocket类+指定端口

DatagramSocket server = new DatagramSocket(8888);

//2.准备接受容器  字节数组

byte[] container = new  byte[1024];

//3.封装DatagramPacket(封装成包)

DatagramPacket packet = new DatagramPacket(container,container.length);

//4.包 接收数据(接收的数据存储在packet中)

server.receive(packet);

//5.分析 (数据在packet中,需要取出,输出在控制台)

byte[] data = packet.getData();

//将byte[]数组转换成基本数据类型

double num = convert(data);

System.out.println("num="+num);

//6.释放资源

server.close();

}

/**

 * 字节数组转换成double返回

 * @param data

 * @return

 * @throws IOException

 */

public static double convert(byte[] data) throws IOException{

ByteArrayInputStream bis = new ByteArrayInputStream(data);

DataInputStream dis = new DataInputStream(bis);

double num = dis.readDouble();

CloseUtil.closeAll(dis,bis);

return num;

}

}

package com.njwb.net.udp;

 

import java.io.ByteArrayOutputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

 

import com.njwb.net.socket06.CloseUtil;

 

/**

 * 发送的基本数据类型 double

 *  1) 创建客户端 DatagramSocket类 +指定端口

2) 准备数据 基本数据类型转换成字节数组(字节数组输出流ByteArrayOutputStream toByteArray 数据字节输出流DataOutputStream)

3) 打包 DatagramPacket +服务器地址及端口(发送的地点以及端口)

4) 发送

5) 释放资源

 * @author Administrator

 *

 */

public class Client2 {

public static void main(String[] args) throws IOException {

//1.创建客户端 DatagramSocket类 +指定端口  ,不要和服务器相同

DatagramSocket client = new DatagramSocket(7777);

//2.准备数据,要将数据转换成字节数组

double num = 139.87;

// byte[] data = (num+"").getBytes();

byte[] data = convert(num);

//3.打包 DatagramPacket +服务器地址及端口

DatagramPacket packet = new DatagramPacket(data,data.length,InetAddress.getByName("127.0.0.1"),8888);

// DatagramPacket packet = new DatagramPacket(data,data.length,new InetSocketAddress("127.0.0.1",8888));

//4.发送

client.send(packet);

//5.释放资源

client.close();

}

/**

 * 将传入的double类型的数字,转换成byte[]数组返回 ,整个方法作用 : byte[] data = (num+"").getBytes();

 * @param num

 * @return

 * @throws IOException

 */

public static byte[] convert(double num) throws IOException{

ByteArrayOutputStream bos = new ByteArrayOutputStream();

DataOutputStream dos = new DataOutputStream(bos);

//将基本数据类型写出到内存中

dos.writeDouble(num);

dos.flush();

//将字节数组输出流转换成byte[]数组

byte[] data = bos.toByteArray();

CloseUtil.closeAll(dos,bos);

return data;

}

}

1.4. 作业

l 精通单线程调试,熟练多线程调试。

l 熟悉掌握Socket

l 复习之前的知识点

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值