Socket

1. Socket

1.1. 本章目标

l 网络的基本概念

l InetAddress,InetSocketAddress的使用

l Tcp协议

l Socket编程

1.2. 网络

网络:将不同区域的计算机,遵从某种通信协议连接在一起。按照物理覆盖范围,可以分为局域网,城域网,(广域网)互联网。

如何区分每一台计算机? 通过ip地址,可以唯一的标示出一台计算机。

端口号:比方说你的计算机上的QQ如何和腾讯公司的QQ服务器做交互,通过ip地址+端口号,可以具体的区分出哪一台计算机上的qq。

1.3. Ip地址

IP地址是IP Address的缩写,全称是Internet Protocol Address,中文叫互联网协议地址或网际协议地址,是为了计算机网络相互连接进行通信而设计的协议。

IP地址类似于电话号码、身份证号,用于给网络上的电脑编号,一台电脑如果想联网,必须遵守IP协议。

IP地址由4个0-255之间的数字组成,在同一个网络内部,IP地址不能相同,所以可以用来标识网络上的一个设备,网络中我们只能依赖IP地址进行数据传输。

IPv4协议的32位地址,地址总容量近43亿个。而IPv6地址采用128位标识,数量为2的128次方。“IPv6可以让地球上每一粒沙子都拥有一个IP地址。”

美国现有IP地址约12亿个左右,平均每个美国人有4个IP地址。中国目前约5400万个,大约24个中国人1个IP地址。

如何查看自己的IP地址?

Windows:ipconfig

Ubuntu:ifconfig

 

0.0.0.0:表示本机

127.0.0.1:表示本机回环地址  localhost:本机地址

255:255:255:255 :表示当前自我,一般用于向当前子网广播信息

1.4. 端口

一台拥有IP地址的电脑可以提供多种服务,如网站、FTP服务,如果仅仅有一个IP地址,无法区分具体业务。显然不能只靠IP地址,实际上是通过“IP地址+端口号”来区分不同的服务的。  

常见的服务对应的端口:

ftp:21,http:80,telnet:23,smtp:25,dns:53,https:443,tomcat:8080

一台电脑上最多只有65536(端口2个字节存储 16bit  2的16次方 =65536)个端口,每个端口对应一个程序,不同程序使用的端口不同。端口始终存在,但程序不一定启动。

注意:在写socket网络通信时,选取的端口号一般要选择1024以上, 1024以下给一些知名软件厂商预留。

1.5. 数据传输的方式有两种

TCP(Transfer Control Protocol) 传输控制协议方式,面向连接,三次握手,效率低。优点:稳定、可靠。缺点:建立连接和维持连接的代价高,传输速度不快。电话只需要拨号一次,就可以建立持久的通话,如果对方没听清你说的话,可以要求你重复,保证传输信息的可靠。

UDP(User Datagram Protocol) 用户数据报协议方式,不是面向连接,效率高。优点:开销小,传输速度快。缺点:传输方式不可靠,不稳定。类似于发短信,每次发送需要输入对方手机号,不是持久的连接,数据有可能丢失,系统只保证尽力发送。

在大型的网络编程中,会组合使用这两种方式实现数据的传递。

1.6. InetAddress,InetSocketAddress

例题1:

package com.net.address;

import java.net.InetAddress;

import java.net.UnknownHostException;

/**

 * java.net.InetAddress 此类表示互联网协议 (IP) 地址。

 * static InetAddress getByName(String host)

          在给定主机名的情况下确定主机的 IP 地址。

    String getHostAddress()

          返回 IP 地址字符串(以文本表现形式)。

String getHostName()

          获取此 IP 地址的主机名。

 * @author Administrator

 *

 */

public class InetAddressDemo01 {

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

System.out.println("----------获取本地InetAddress对象--------------");

//获取本地主机

InetAddress addr3 = InetAddress.getLocalHost();

//获取ip地址 10.0.2.98

System.out.println(addr3.getHostAddress());

//计算机名

System.out.println(addr3.getHostName());

System.out.println("----------根据域名获取InetAddress对象--------------");

//域名

//InetAddress addr2 = InetAddress.getByName("www.baidu.com");

//获取ip地址 118.34.20.100

//System.out.println(addr2.getHostAddress());

//获取的 www.baidu.com

//System.out.println(addr2.getHostName());

System.out.println("----------根据ip获取InetAddress对象--------------");

InetAddress addr = InetAddress.getByName("127.0.0.1");

//获取ip地址

System.out.println(addr.getHostAddress());

//如果addr中指定的ip地址存在,并且dns可以解析,那么此处获取的是ip地址对应的域名“WWW...com

//如果addr中指定的ip地址不存在,或者dns不可以解析,那么此处获取的是原来的ip地址

System.out.println(addr.getHostName());

}

}

例题2:

package com.net.address;

import java.net.InetAddress;

import java.net.InetSocketAddress;

import java.net.UnknownHostException;

/**

 * InetSocketAddress 此类实现 IP 套接字地址(IP 地址 + 端口号)。 封装端口

 * InetSocketAddress(String hostname, int port)

 *

 * InetSocketAddress(InetAddress addr, int port)

          根据 IP 地址和端口号创建套接字地址。       

          根据主机名和端口号创建套接字地址。

 * InetAddress getAddress()

          获取 InetAddress

 String getHostName()

          获取 hostname

 int getPort()

          获取端口号。

 * @author Administrator

 *

 */

public class InetSocketAddressDemo01 {

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

System.out.println("--------------构造1----------------");

InetSocketAddress addr = new InetSocketAddress("127.0.0.1",8888);

//InetSocketAddress addr = new InetSocketAddress("localhost",8888);

//获取主机名

System.out.println(addr.getHostName());

//获取端口号

System.out.println(addr.getPort());

System.out.println("--------------构造2----------------");

InetSocketAddress addr2 = new InetSocketAddress(InetAddress.getByName("127.0.0.1"),8999);

//获取InetAddress对象

InetAddress inadd = addr2.getAddress();

System.out.println("*****"+inadd.getHostName()+"*****"+inadd.getHostAddress());

//获取主机名

System.out.println(addr2.getHostName());

//获取端口号

System.out.println(addr2.getPort());

}

}

1.7. 什么是Socket

Socket编程是基于TCP/IP协议的网络编程,在两台电脑之间建立连接进行通讯,需要知道电脑的IP地址进行连接,这个连接的一端称为一个Socket。两台电脑之间的通信连接总是需要发起,不会凭空出现,发起连接的是客户端(Client),等待被连接的是服务端(Server),客户端服务端表示的是谁发起连接,与是否发送接收数据无关。

java在包java.net中提供了两个类Socket和ServerSocket,分别用来表客户端和服务端。客户端对象使用new Socket()创建,服务端对象使用new ServerSocket()创建。

1.8. 创建连接

 服务端:发送读取  客户端:读取发送

分析: 服务器欢迎光临     客户端接收到 : 欢迎光临

   客户端--à我来了       服务器接收到 :我来了

package com.net.socket01;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.InetAddress;

import java.net.Socket;

import java.net.UnknownHostException;

/**

 * Socket(String host, int port)

          创建一个流套接字并将其连接到指定主机上的指定端口号。

   Socket(InetAddress address, int port)

          创建一个流套接字并将其连接到指定 IP 地址的指定端口号。

 * 指定ip地址:表明和哪台主机通信

 * 指定端口号:表示和这台主机上的哪个程序通信(一个端口号,对应一个程序)

 * @author Administrator

 *

 */

public class Client {

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

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

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

//localhost 表示本地地址 ,作用和127.0.0.1相同

// Socket client = new Socket("localhost",8023);

Socket client = new Socket(InetAddress.getByName("127.0.0.1"),8023);

//根据socket对象,创建输入流,读出服务端发送过来的信息

DataInputStream dis = new DataInputStream(client.getInputStream());

String msg = dis.readUTF();

System.out.println("客户端读取到的:"+msg);

//根据socket对象,创建输出流对象,用于给服务器发送信息

DataOutputStream dos = new DataOutputStream(client.getOutputStream());

dos.writeUTF("我来了");

dos.flush();

}

}

package com.njwb.net.socket01;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

/**

 *        

 * @author Administrator

 *

 */

public class Server {

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

//创建ServerSocket对象

ServerSocket server = new ServerSocket(8023);

//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行

Socket socket = server.accept();

//如果代码能够执行到这里,表示已经和客户端建立连接了

//发送 欢迎光临  写出 输出流

//根据socket对象,构建输出流对象,用于向客户端写出数据

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

dos.writeUTF("欢迎光临");

dos.flush();

//根据socket对象,创建输入流读取,客户端发送过来的

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

String msg = dis.readUTF();

System.out.println("服务端收到的:"+msg);

}

}

总结Socket通讯的过程

(1) 服务端创建socket监听某个端口是否有连接请求。

(2) 客户端创建socket连接该端口。

(3) 服务端Accept(接受)以后,一个连接建立成功。

(4) 双方根据socket创建输入流,输出流。

(5) 双方可以通过流互相发送信息。

1.9. 连接持久化

要求:客户端可以从键盘端循环输入(发送),并且可以接收服务端发送的,服务端必须循环读取客户端信息同时回发给客户端

package com.net.socket02;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

/**

 * address already in use  出现这个异常,Console控制台开启的多余的Server关掉

 * @author Administrator

 *

 */

public class Server {

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

//创建ServerSocket对象

ServerSocket server = new ServerSocket(8023);

//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行

Socket socket = server.accept();

//根据socket对象,创建输入流读取,客户端发送过来的

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

//根据socket对象,构建输出流对象,用于向客户端写出数据

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

while(true){

String msg = dis.readUTF();

System.out.println("服务端收到的:"+msg);

dos.writeUTF(msg);

dos.flush();

}

}

}

package com.net.socket02;

import java.io.BufferedReader;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.Socket;

import java.net.UnknownHostException;

/**

 * 需要循环输入

 * 客户端:先发送 ,再读取  

 * 需要循环转发

 * 服务端:先读取,再发送

 * 客户端:

 * 1.创建客户端的socket对象 ,同时指定连接的服务端的ip地址和端口号

 * 2.根据socket对象,创建输入,输出流

 * 3.根据流,先发送(键盘端输入的),再读取

 * 4.关闭流(暂时不关)

 * 服务端:

 * 1.服务端创建ServerSocket对象,同时指定端口号

 * 2.服务端等待客户端触发通信 accept

 * 3.根据socket对象,创建输入,输出流

 * 4.转发  先读,再发

 * 5.关闭流(暂时不关)

 * @author Administrator

 *

 */

public class Client {

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

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

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

//声明键盘端输入的实例变量

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

//根据socket对象,创建输出流对象,用于给服务器发送信息

DataOutputStream dos = new DataOutputStream(client.getOutputStream());

//根据socket对象,创建输入流,读出服务端发送过来的信息

DataInputStream dis = new DataInputStream(client.getInputStream());

while(true){

//键盘端输入的变量,存储在msg中,将msg发送出去

String msg = console.readLine();

dos.writeUTF(msg);

dos.flush();

String data = dis.readUTF();

System.out.println("客户端读取到的:"+data);

}

}

}

1.10. 双工客户端

半双工是指一个时间段内只有一个动作发生。一条窄窄的马路,同时只能有一辆车通过,当目前有两辆车对开,这种情况下就只能一辆先过,等到头后另一辆再开。早期对讲机就是半双工。

全双工的系统允许二台设备间同时进行双向资料传输。一般的电话、手机就是全双工的系统,因为在讲话时同时也可以听到对方的声音。

现在的问题是,客户端必须先发送,才能接收?

实际的聊天室,发送和读取相互分离的,互相不影响。

如何实现客户端多线程?

要求:客户端必须实现发送和接收2个线程(发送和接收分离),发送线程实现键盘端输入并发送,接收线程接收消息并输出。服务端依然循环处理,把接收到消息,回传给客户端。

package com.net.socket03;

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; //用于控制循环是否继续

/**

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

 * @param socket

 */

public Send(Socket socket) {

super();

this.socket = socket;

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

try {

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

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

} catch (IOException e) {

isRunning=false;

e.printStackTrace();

}

}

@Override

public void run() {

while(isRunning){

try {

String msg = console.readLine();

dos.writeUTF(msg);

dos.flush();

} catch (IOException e) {

isRunning=false;

e.printStackTrace();

}

}

}

}

package com.njwb.net.socket03;

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;

e.printStackTrace();

}

}

@Override

public void run() {

while(isRunning){

try {

String msg = dis.readUTF();

System.out.println("客户端收到的:"+msg);

} catch (IOException e) {

isRunning=false;

e.printStackTrace();

}

}

}

}

package com.njwb.net.socket03;

import java.io.IOException;

import java.net.Socket;

import java.net.UnknownHostException;

/**

 * 现在的是单线程的,都写在main线程里,代码写出 必须先write()发送出去,才能read()读取进来,导致发送,读取不能相互独立。

 * 有必要,拆分成2个线程,发送线程,读取(接收)线程

 * 1.双工客户端 send发送线程 : 输出流 dos,键盘接收的变量consle, 声明了Socket对象,从Client类将Scoket对象传入Send类 ,循环发送的变量控制 isRunning,

 * run线程体中,不写while(),只能发送1句

 * receive接收线程: 输入流对象dis ,socket对象 ,isRunning  

 * @author Administrator

 *

 */

public class Client {

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

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

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

//开启发送线程

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

//开启接收线程

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

}

}

1.11. 服务端并发接收多个客户端的连接

当前的问题

服务端只能与一个客户端连接,谁先到谁先聊,后来的无法接收。

服务端如何应付多个客户端的同时连接?

服务端使用多线程,每次有客户端接入时,创建一个服务端线程与客户端对接,每个服务端线程负责一个客户端。此时服务器端,必须循环实现创建服务端线程,并且服务端线程类必须实现读出的内容回传给客户端

如何实现服务端多线程?

package com.net.socket04;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

/**

 * 现在的问题,启动1个server后,如果开启多个client,发现谁先到,谁先聊,服务端只能支持1个客户端,即使你开启了多个客户端

 * 解决办法1:将 server.accept()放到循环中,理论上可以接收多个客户端,发现仍旧解决不了问题,

 * 解决办法2:服务端开启多线程,来一个客户端,开启一个多线程类(10086客服)接待一下

 * 分析: 转发  先读取,在发送  DataInputStream dis 输入流对象,DataOutputStream dos ,输入流,输出流需要根据什么构建,

 * socket对象 ,socket对象通过带参构造传入 ,可行,能够实现1对多

 * @author Administrator

 *

 */

public class Server {

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

//创建ServerSocket对象

ServerSocket server = new ServerSocket(8023);

while(true){

//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行

Socket socket = server.accept();

System.out.println("看,有1个客户端过来了");

//根据socket对象,创建输入流读取,客户端发送过来的

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

//根据socket对象,构建输出流对象,用于向客户端写出数据

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

while(true){

String msg = dis.readUTF();

System.out.println("服务端收到的:"+msg);

dos.writeUTF(msg);

dos.flush();

}

}

}

}

package com.njwb.net.socket04;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

/**

 * 现在的问题,启动1server后,如果开启多个client,发现谁先到,谁先聊,服务端只能支持1个客户端,即使你开启了多个客户端

 * 解决办法1:将 server.accept()放到循环中,理论上可以接收多个客户端,发现仍旧解决不了问题,

 * 解决办法2:服务端开启多线程,来一个客户端,开启一个多线程类(10086客服)接待一下

 * 分析: 转发  先读取,在发送  DataInputStream dis 输入流对象,DataOutputStream dos ,输入流,输出流需要根据什么构建,

 * socket对象 ,socket对象通过带参构造传入

 * @author Administrator

 *

 */

public class Server2 {

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

//创建ServerSocket对象

ServerSocket server = new ServerSocket(8023);

while(true){

//等待客户端来连接(等待客户端触发通信) accept()阻塞方法,像next(),如果没有客户端过来,代码不会继续往下执行

Socket socket = server.accept();

System.out.println("看,有1个客户端过来了,开启一个客服(多线程类ServerChannel)接待一下");

ServerChannel chan = new ServerChannel(socket);

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

}

}

}

package com.njwb.net.socket04;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.Socket;

public class ServerChannel implements Runnable{

private DataInputStream dis;

private DataOutputStream dos;

private Socket socket;

private boolean isRunning=true;

public ServerChannel(Socket socket) {

super();

this.socket = socket;

try {

//根据传入的socket对象,构建输入流对象,用于读取

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

//根据传入的socket对象,构建输出流对象,用于写出

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

} catch (IOException e) {

isRunning=false;

e.printStackTrace();

}

}

@Override

public void run() {

//先读取,再发送

while(isRunning){

try {

String msg = dis.readUTF();

System.out.println("服务端读取的:"+msg);

dos.writeUTF(msg);

dos.flush();

} catch (IOException e) {

isRunning=false;

e.printStackTrace();

}

}

}

}

1.12. 群聊

服务端将数据分发给所有连接的客户端

要求:服务端必须有保存处理客户端信息的服务端线程类的集合.发送数据时,除自身外,其他客户端都应该收到。每个服务端线程类对应一个客户端。 

package com.njwb.net.socket06;

 

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.ArrayList;

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, 里面有多行关键词,一行一个 ,“笨蛋”,“白痴”,“傻瓜”.....

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

 *

 * @author Administrator

 *

 */

public class Server {

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

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

//对象名.方法名()

new Server().start();

// Server s = new Server();

// s.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;

/**

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

 * @param socket

 */

public ServerChannel(Socket socket) {

super();

this.socket = socket;

try {

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

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

//检索名字

//给自身发送欢迎消息

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

} catch (IOException e) {

isRunning=false;

e.printStackTrace();

}

}

@Override

public void run() {

while(isRunning){

try {

//读取内容

String msg = dis.readUTF();

System.out.println("服务端收到的:"+msg);

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

for(ServerChannel chan:clist){

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

if(chan!=this){

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

chan.dos.writeUTF(msg);

chan.dos.flush();

}

}

} catch (IOException e) {

isRunning=false;

//删除出错的客户端

for(ServerChannel chan:clist){

if(chan==this){

clist.remove(chan);

}

}

e.printStackTrace();

}

}

}

}

}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值