Java 网络编程

网络编程

本文章为个人“Java零基础实战”学习笔记,侵权删
所谓的 Web 编程就是编写程序运行在同一网络下的两个终端上,使得他们之间可以进行数据传输。在正式学习 Java Web 编程之前,我么先来了解网络的相关基础知识。

​ 计算机网络就是通过硬件设施、传输媒介把各个不同的物理地址上的计算机进行连接,形成一个资源共享和数据传输的网络系统。两台终端通过网络进行连接时,需要遵守一定的规则,这个规则就是网络协议(network rotocol),网络协议主要由 3 个特征组成。

  • 语法:数据信息的结果。
  • 语义:描述请求、动作和响应。
  • 同步:动作的实现顺序。

网络通信协议有 TCP/IP 协议、IPX/SPX 协议、NetBEUI 协议等,我们常用的是 TCP/IP 协议,同时 TCP/IP 协议是分层的,分层的优点如下:

  • 各层之间相互独立,互不干扰;
  • 维护性,扩展性好;
  • 有利于系统的标准化。

分层的思想在程序开发中的应用非常普遍,它的好处是每一层只关注自己的业务,无需去关注其他层的业务,只需要获取其他层传来的信息,进行处理之后再传给下一层。例如我们在编写 Java 代码的时候,不需要考虑底层的操作系统是 Windows 还是 Mac。分层思想已经帮我们屏蔽了底层机制,我们只需要在应用层写业务代码即可。TCP/IP 协议可以分为四层,从上到下依次分为应用层、传输层、网络层和网络接口层。

在这里插入图片描述

  • 应用层(application layer)是整个体系结构中的顶层,通过应用程序之间的数据交互完成网络应用。
  • 传输层(transport layer)为两台终端中应用程序之间的数据交互提供数据传输服务。
  • 网络层(network layer)也叫 IP 层,负责为网络中不同的终端提供信息服务。
  • 网络接口层(network interface layer)包括数据链路层(data link layer)和物理层(physical layer),数据链路层的作用是为两台终端的数据传输提供链路协议;物理层是指光纤、电缆或者电磁波等正式存在的物理媒介,这些媒介可以传送网络信号。

终端 A 正在和终端 B 通过网络进行通信,整个数据的传输流程是终端 A -> 应用层 -> 传输层 -> 网络层 -> 数据链路层 -> 网络层 -> 传输层 -> 应用层 -> 终端 B

IP 与端口

IP

互联网中的每台终端设备都有一个唯一表示,网络中的请求可以根据这个标识找到具体的计算机,这个唯一表示就是 IP 地址(Internet Protocol),用户可以通过操作系统的设置来查看本机的 IP 地址。IP 地址是 32 位的二进制数据,但是我们所看到的 IP 地址已经转为了十进制数据。

IP 地址 = {<网络地址>, <主机地址>}, 网络地址的作用是找到主机所在的网络,主机地址的作用是找到网络中的主机。IP 地址分为 5 类,各类地址可使用的 IP数量不同,棘突范围如表:

分类范围
A 类1.0.0.1 ~ 126.255.255.254
B 类128.0.0.1 ~ 191.255.255.254
C 类192.0.0.1 ~ 223.225.225.254
D 类224.0.0.1 ~ 239.255.255.254
E 类240.0.0.1 ~ 255.255.255.254

​ 需要注意的是我们在实际开发中斌不需要 记住本机的 IP 地址,可以使用 127.0.0.1 或者 localhost 来表示本机 IP 地址, Java 中有专门的类来描述 IP 地址,这个类是 java.net.InetAddress,该类的常用方法如表:

方法描述
public static InetAddress getLocalHost() throws UnknownHostException获取本地主机的 InetAddress 对象
public static InetAddress getByName(String host) throws UnknownHostException通过主机名称创建 InetAddress 对象
String getHostName()获取主机名称
public String getHostAddress()获取主机 IP 地址
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException通过主机名称和 IP 地址创建 InetAddress 对象
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException通过 IP 地址创建 InetAddress 对象
public class TestIp {
	public static void main(String[] args) {
		try {
			InetAddress inetAddress = InetAddress.getLocalHost();
			System.out.println(inetAddress.getHostName());
			System.out.println(inetAddress.getHostAddress());
			InetAddress inetAddress2 = InetAddress.getByName("127.0.0.1");
			System.out.println(inetAddress2);
			inetAddress2 = InetAddress.getByName("localhost");
			System.out.println(inetAddress2);
		} catch (UnknownHostException e) {
			// TODO: handle exception
		}
	}
}

运行结果:

LAPTOP-N9OVN3H4
169.254.64.126
/127.0.0.1
localhost/127.0.0.1

端口

​ 如果把 IP 比作一栋大厦的地址,那么端口(port)就是不同房间的门牌号。IP 地址需要和端口结合起来使用,好比快递小哥必须要通过大厦地址和房间号才能准确找到你。计算机主机就相当于大厦,网络中的请求需要通过 IP 地址来找到主机,同时一台主机上会同时运行很多个服务,如何把不同的请求分配给不同的服务就需要使用到端口了。

​ 例如你的计算机同时打开了微信和QQ,这是一台主机上的两个服务,朋友通过 QQ 给你发送了一条信息,那么为什么请求来到主机被QQ服务所接收到而不是微信呢?就是应为不同的服务会有不同的端口,请求根据 “IP 地址+端口” 就可以准确地找到互联网中的接收它的服务了。比如要连接本地的MySQL 数据库服务,MySQL 数据库服务的端口是 3306,那么完整的 URL 请求就是 localhost:3306。


URL 和 URLConnection

URL

通常讲的网络资源实际是指网络中真实存在的一个实体,比如文字、图片、视频、音频等,如果我们要在程序获取网络实体,应该怎么做呢?可以用 URI (Uniform Resource Identifier)统一资源定位符指向目标实体,URI 的作用是用特定的语法来标识某个网络资源。 Java 中专门封装了一个类用来描述 URI,这个类就是 java.net.URI,使用 URI 的实例化对象,可以用面向对象的方式来管理网络资源,如获取主机地址、端口等,在本机(127.0.0.1)上部署服务(web_need),通过 URI 读取该服务的 login.jsp 资源的具体操作如下:

public class TestURI {
	public static void main(String[] args) {
		try {
			URI uri = new URI("http://localhost:8080/web_need/login.jsp");
			System.out.println(uri.getHost());
			System.out.println(uri.getPort());
			System.out.println(uri.getPath());
		} catch (URISyntaxException e) {
			// TODO: handle exception
		}
		
	}
}

运行结果:

localhost
8080
/web_need/login.jsp

那什么是 URL 呢?URL 和 URI 有什么区别呢?URL(Uniform Resource Locator)是统一资源位置,在 URI 的基础上进行了扩充,在定位资源的同时还提供了对应的网络地址,Java 也对 URL 进行了封装,java.net.URL 类常用方法如表:

方法描述
public URL(String protocol,String host,int port,String file) throws MalformedURLException根据协议、ip地址、端口号、资源名称获取 URL 对象
public final InputStream openStream() throws java.io.IOException获取输入流对象

在本机(127.0.0.1)上部署服务(web_need),使用 URL 读取该服务的 login.jsp 资源,代码如下:

public class TestURL {
	public static void main(String[] args) {
		InputStream inputStream = null;
		Reader reader = null;
		BufferedReader bufferedReader = null;
		try {
			URL url = new URL("http","127.0.0.1",8080,"/web_need/login.jsp");
			inputStream = url.openStream();
			reader = new InputStreamReader(inputStream);
			bufferedReader = new BufferedReader(reader);
			String str = null;
			while((str = bufferedReader.readLine()) != null) {
				System.out.println(str);
			}
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			try {
				bufferedReader.close();
				reader.close();
				inputStream.close();
			} catch (IOException e2) {
				// TODO: handle exception
			}
		}
	}
}

运行结果:

在这里插入图片描述

同时也可以通过 URL 获取资源的其他信息,如主机地址、端口等,具体操作如下:

public class TestURL {
	public static void main(String[] args) {
		try {
			URL url = new URL("http","127.0.0.1",8080,"/web_need/login.jsp");
			System.out.println(url.getHost());
            System.out.println(url.getPort());
            System.out.println(url.getPath());
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}

运行结果:

127.0.0.1
8080
/web_need/login.jsp

URLConnection

​ URLConnection 用来描述 URL 指定资源的连接,是一个抽象类,常用的子类有 HttpURLConnection,URLConnection 底层是通过 HTTP 协议来处理的,它定义了访问远程网络资源的方法。通过 URLConnection 可以获取到 URL 资源的相关信息,该类常用的方法如表:

方法描述
public int getContentLength()返回资源的长度,返回值为 int 类型
public long getContentLengthLong()返回资源的长度,返回值为 long 类型
public String getContentType()返回资源的类型
public abstract void connect() throws IOException判断连接的打开或关闭状态
public Inputstream getInputStream() throws IOException获取输入流对象

​ 接下来我们学习 URLConnection 如何使用,例如要获取上节 URL 资源的相关信息和具体内容,就可以通过 URLConnection 来完成

public class TestURLConnection {
	public static void main(String[] args) {
		InputStream inputStream = null;
        Reader reader = null;
        BufferedReader bufferedReader = null;
        try{
            URL url = new URL("http://localhost:8080/web_need/login.jsp");
            URLConnection urlConnection = rul.openConnection();
            System.out.println(urlConnection.getURL());
            System.out.println(urlConnection.getContentLengthLong());
            System.out.println(urlConnection.getContentLength());
            System.out.println(urlConnection.getContentType());
            inputStream = urlConnection.getInputStream();
            reader = new InputStreamReader(inputStream);
            bufferedReader = new BufferedReader(reader);
            String str = null;
            while((str = bufferedReader.readLine()) != null){
                System.out.println(str);
            }
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			try {
				bufferedReader.close();
				reader.close();
				inputStream.close();
			} catch (IOException e2) {
				// TODO: handle exception
			}
		}
	}
}

运行结果:

在这里插入图片描述


TCP 协议

TCP 是面向连接的运输层协议,比较复杂,应用程序在使用 TCP 协议前必须先建立连接,才能传输数据,数据传输完毕之后需要释放已经建立的连接。TCP 的有点是非常可靠,通过 TCP 传输数据,不会出现丢失的情况,并且数据是暗号先后顺序依次到达的。缺点是速度慢、效率低,实际开发中需要根据具体的业务需求来选择,对安全性要求较高的系统需要使用 TCP 协议 (例如金融系统),必须先确保用户成功登录,才能进行后续的操作。

​ Java 通过 Socket 来完成 TCP 程序的开发, Socket 是一个类,使用该类可以在服务端与客户端之间建立可靠的连接。在实际开发中,Socket 表示客户端,服务端使用 ServerSocket 来表示,ServerSocket 也是一个类,ServerSocket 和 Socket 都存放在 java.net 包中。具体的开发思路是在服务端创建 ServerSocket 对象,然后通过该对象的 accept() 方法可以接受到若干个表示客户端的 Socket 对象。

ServerSocket 类的常用方法如表:

方法描述
public ServerSocket(int port) throws IOException根据端口创建 ServerSocket 实例对象
public ServerSocket(int port,int backlog) throws IOException根据端口和 backlog 创建 ServerSocket 实例对象
public ServerSocket(int port,int backlog,InetAddress address) throws IOException根据端口、backlog 和 IP 地址创建 ServerSocket 对象
public ServerSocket() throws IOException创建没有绑定服务器的 ServerSocket 实例对象
public synchronized int getSoTimeout() throws IOException获取 Sotimeout 的设置
public InetAddress getInetAddress()获取服务器的 IP 地址
public Socket accept() throws IOException等待客户端请求,并返回 Socket 对象
public void close() throws IOException关闭 ServerSocket
public boolean isClosed()返回 ServerSocket 的关闭状态
public void bind(SocketAddress endpoint) throws IOException将 ServerSocket 实例对象绑定到指定地址
public int getLocalPort()返回 ServerSocket 的端口

Socket 类的常用方法如表:

方法描述
public Socket(String host, int port) throws UnknownHostException,IOException根据主机、端口创建要连接的 Socket 对象
public Scoket(InetAddress host,int port) throws IOException根据 IP 地址、端口创建要连接的 Socket 对象
public Socket(String host,int port,InetAddress localAddress,int localPort) throws IOException根据主句、端口创建要连接的 Socket 对象并将其连接到指定远程主机上的指定端口
public Socket(InetAddress host,int port,InetAddress localAddress,int localPort) throws IOException根据主机、端口创建要连接的 Socket 对象并将其连接到指定远程地址上的指定端口
public Socket()创建没有连接的 Socket 对象
public InputStream getInputStream() throws IOException返回 Socket 的输入流
public synchronized void close() throws IOException关闭 Socket
public boolean isClose()返回 Sockeet 的关闭状态

​ 下面来看 ServerSocket 和 Socket 的实际应用,首先启动 ServerSocket,等待接收客户端请求。当接受盗客户端请求后,打印“接收到了客户端请求” 信息,同时向客户端返回 “Hello World”。

服务端代码:

public class ServerSocketDemo {
	public static void main(String[] args) {
		ServerSocket serverSocket = null;
		Socket socket = null;
		OutputStream outputStream = null;
		InputStream inputStream = null;
		DataInputStream dataInputStream = null;
		DataOutputStream dataOutputStream = null;
		try {
			serverSocket = new ServerSocket(8080);
			System.out.println("------服务端-------");
			System.out.println("已启动,等待接收客户端请求...");
			while(true) {
				socket = serverSocket.accept();
				inputStream = socket.getInputStream();
				dataInputStream = new DataInputStream(inputStream);
				String request = dataInputStream.readUTF();
				System.out.println("接收到客户端请求:"+request);
				outputStream = socket.getOutputStream();
				dataOutputStream = new DataOutputStream(outputStream);
				String response = "Hello World";
				dataOutputStream.writeUTF(response);
				System.out.println("给客户端做出响应:"+response);
				
			}
		} catch (IOException e) {
			// TODO: handle exception
		} finally {
			try {
				dataOutputStream.close();
				outputStream.close();
				dataInputStream.close();
				inputStream.close();
				socket.close();
				serverSocket.close();
			} catch (IOException e2) {
				// TODO: handle exception
			}
			System.out.println("服务端已结束服务!");
		}
	}
}

运行结果:

------服务端-------
已启动,等待接收客户端请求…

此时程序并没有结束,在等待客户端的连接请求。我们写好客户端代码并运行:

public class SocketDemo {
	public static void main(String[] args) {
		Socket socket = null;
		OutputStream outputStream = null;
		InputStream inputStream = null;
		DataOutputStream dataOutputStream = null;
		DataInputStream dataInputStream = null;
		try {
			socket = new Socket("127.0.0.1",8080);
			System.out.println("-----客户端-----");
			String request = "你好!";
			System.out.println("客户端说:"+request);
			outputStream = socket.getOutputStream();
			dataOutputStream = new DataOutputStream(outputStream);
			dataOutputStream.writeUTF(request);
			inputStream = socket.getInputStream();
			dataInputStream = new DataInputStream(inputStream);
			String response = dataInputStream.readUTF();
			System.out.println("服务端响应:"+response);
		} catch (IOException e) {
			// TODO: handle exception
		} finally {
			try {
				dataOutputStream.close();
				outputStream.close();
				dataInputStream.close();
				inputStream.close();
				socket.close();
			} catch (IOException e2) {
				// TODO: handle exception
			}
		}
	}
}

运行结果:

------服务端-------
已启动,等待接收客户端请求…
接收到客户端请求:你好!
给客户端做出响应:Hello World

-----客户端-----
客户端说:你好!
服务端响应:Hello World


UDP 协议

​ TCP 协议可以建立稳定可靠的连接,保证数据的完整性。但是 TCP协议的确定也很明显,先建立可靠连接,再进行操作的方法必然会造成系统运行效率低下。在实际开发中某些业务场景对系统的运行效率要求较高,使用 TCP 协议很显然就不合适了,这时候就需要使用另一种传输协议——UDP。

​ UDP 所有的连接都是不可靠的,既不需要建立连接,直接发送数据即可。所以 UDP 的速度更快,但是可能会造成数据丢失,安全性不高,追求速度的应用可以选择 UDP。例如语音聊天或者视频聊天,对弈这类应用流畅性更重要,偶尔丢失几个数据包并不会有太大影响。 Java 提供了 DatagramSocket 类和 DatagramPacket 类,来帮助开发者编写基于 UDP 协议的程序,DatagramSocket 类的常用方法如表:

方法描述
public DatagramSocket(int port) throws SocketException根据端口创建 DatagramSocket 实例对象
public void send(DatagramPacket p) throws IOException发送数据报
public synchronized void receive(DatagramPacket p) throws IOException接收数据报
public InetAddress getInetAddress()获取 DatagramSocket 对应的 InetAddress 对象
public boolean isConnected判断是否连接到服务

DatagramPacket 类的常用方法如表:

方法描述
public DatagramPacket(byte[] buf[],int length,InetAddress address,int port)根据发送的数据、数据长度、IP地址、端口,创建 DatagramPacket 实例对象
public synchronized byte[] getData()获取接收的数据
public synchronized int getLength()获取数据长度
public synchronized int getPort()获取发送数据的 Socket 端口
public synchronized SocketAddress getSocketAddress()获取发送数据的 Socket 信息

两个终端通过 UDP 协议进行通信的具体实现如下:

public class TerminalA {
	public static void main(String[] args) throws Exception {
		//接收数据
		byte[] buff = new byte[1024];
		DatagramPacket datagramPacket = new DatagramPacket(buff,buff.length);
		DatagramSocket datagramSocket = new DatagramSocket(8181);
		datagramSocket.receive(datagramPacket);
		String mess = new String(datagramPacket.getData(),0,datagramPacket.getLength());
		System.out.println("我是 TerminalA ,接收到了"+datagramPacket.getPort()+"传来的数据:"+mess);
		//发送数据
		String reply = "我是 TerminalA ,已接收到你发来的数据";
		SocketAddress socketAddress = datagramPacket.getSocketAddress();
		DatagramPacket datagramPacket2 = new DatagramPacket(reply.getBytes(),reply.getBytes().length,socketAddress);
		datagramSocket.send(datagramPacket2);
	}
}
public class TerminalB {
	public static void main(String[] args) throws Exception  {
		String mess = "我是 TerminalB,你好!";
		//发送数据
		InetAddress inetAddress = InetAddress.getByName("localhost");
		DatagramPacket datagramPacket = new DatagramPacket(mess.getBytes(),mess.getBytes().length,
				inetAddress,8181);
		DatagramSocket datagramSocket = new DatagramSocket(8080);
		datagramSocket.send(datagramPacket);
		//接收数据
		byte[] buff = new byte[1024];
		DatagramPacket datagramPacket2 = new DatagramPacket(buff,buff.length);
		datagramSocket.receive(datagramPacket2);
		String reply = new String(datagramPacket2.getData(),0,datagramPacket2.getLength());
		System.out.println("我是 TerminalB,接收到了"+datagramPacket2.getPort()+"返回的数据:"+reply);
	}
}

运行结果:

我是 TerminalA ,接收到了8080传来的数据:我是 TerminalB,你好!

我是 TerminalB,接收到了8181返回的数据:我是 TerminalA ,已接收到你发来的数据


多线程下的网络编程

​ 前面章节的代码都是基于单点连接的方式,即一个服务端对应一个客户端。实际运行环境中是一个服务端需要对应多个客户端的,这种情况我们可以使用多线程来模拟。

public class ServerThread {
	public static void main(String[] args) {
		ServerSocket serverSocket = null;
		try {
			serverSocket = new ServerSocket(8080);
			System.out.println("----服务器已启动-----");
			while(true) {
				Socket socket = serverSocket.accept();
				new Thread(new ServerRunnable(socket)).start();
			}
		} catch (IOException e) {
			// TODO: handle exception
		}
	}
}

class ServerRunnable implements Runnable{
	private Socket socket;
	public ServerRunnable(Socket socket) {
		this.socket = socket;
	}
	
	@Override
	public void run() {
		InputStream inputStream = null;
		DataInputStream dataInputStream = null;
		try {
			inputStream = this.socket.getInputStream();
			dataInputStream = new DataInputStream(inputStream);
			String message = dataInputStream.readUTF();
			System.out.println(message);
		} catch (IOException e) {
			// TODO: handle exception
		} finally {
			try {
				dataInputStream.close();
				inputStream.close();
				socket.close();
			} catch (IOException e2) {
				// TODO: handle exception
			}
		}
	}
}
public class ClientThread {
	public static void main(String[] args) {
		for(int i = 0; i < 20; i++) {
			new Thread(new ClientRunnable(i)).start();
		}
	}
}

class ClientRunnable implements Runnable{
	private int num;
	public ClientRunnable(int num) {
		this.num = num;
	}
	@Override
	public void run() {
		Socket socket = null;
		OutputStream outputStream = null;
		DataOutputStream dataOutputStream = null;
		try {
			socket = new Socket("localhost",8080);
			String mess = "我是客户端"+this.num;
			outputStream = socket.getOutputStream();
			dataOutputStream = new DataOutputStream(outputStream);
			dataOutputStream.writeUTF(mess);
		} catch (IOException e) {
			// TODO: handle exception
			e.printStackTrace();
		} finally {
			try {
				dataOutputStream.close();
				outputStream.close();
				socket.close();
			} catch (IOException e2) {
				// TODO: handle exception
			}
		}
	}
}

运行结果:

----服务器已启动-----
我是客户端3
我是客户端15
我是客户端11
我是客户端12
我是客户端16
我是客户端7


综合练习

​ 使用 Socket 和 多线程编写一个简单的聊天小程序,要求客户端和服务端交替发送消息,在客户端和服务端都能看见到彼此的聊天记录。

服务端代码:

public class Server {
	public static void main(String[] args) {
		ServerSocket serverSocket = null;
		Socket socket = null;
		try {
			serverSocket = new ServerSocket(8080);
			System.out.println("服务器已启动...");
			while(true) {
				socket = serverSocket.accept();
				new Thread(new SocketThread(socket)).start();
			}
		} catch (IOException e) {
			// TODO: handle exception
		}
	}
}

class SocketThread implements Runnable{
	private Socket socket;
	public SocketThread(Socket socket) {
		this.socket = socket;
	}
	@Override
	public void run() {
		InputStream inputStream = null;
		DataInputStream dataInputStream = null;
		OutputStream outputStream = null;
		DataOutputStream dataOutputStream = null;
		String message = null;
		Scanner scanner = new Scanner(System.in);
		try {
			while(true) {
				//读
				inputStream = socket.getInputStream();
				dataInputStream = new DataInputStream(inputStream);
				message = dataInputStream.readUTF();
				System.out.println("客户端:"+message);
				
				//写
				outputStream = socket.getOutputStream();
				dataOutputStream = new DataOutputStream(outputStream);
				System.out.print("服务器:");
				message = scanner.nextLine();
				dataOutputStream.writeUTF(message);
			}
		} catch (IOException e) {
			// TODO: handle exception
		}
	}
	
}

客户端:

public class Client {
	public static void main(String[] args) {
		Socket socket = null;
		InputStream inputStream = null;
		DataInputStream dataInputStream = null;
		OutputStream outputStream = null;
		DataOutputStream dataOutputStream = null;
		String message = null;
		Scanner scanner = new Scanner(System.in);
		try {
			System.out.println("客户端已经启动...");
			socket = new Socket("127.0.0.1",8080);
			while(true) {
				//写
				outputStream = socket.getOutputStream();
				dataOutputStream = new DataOutputStream(outputStream);
				System.out.print("客户端:");
				message = scanner.nextLine();
				dataOutputStream.writeUTF(message);
				
				//读
				inputStream = socket.getInputStream();
				dataInputStream = new DataInputStream(inputStream);
				message = dataInputStream.readUTF();
				System.out.println("服务端:"+message);
			}
		} catch (IOException e) {
			// TODO: handle exception
		}
	}
}

运行结果:

客户端已经启动…
客户端:问你个问题
服务端:嗯嗯,你讲
客户端:土豆可以做成土豆泥,红豆可以做成红豆泥,你觉得你可以做成什么?
服务端:不知道呐
客户端:笨蛋,是我爱泥

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值