《Java I/O》Chapter 5

Chapter5 网络流

从成立之初,Java就比其他任何通用编程语言都多了网络这块。 Java是第一种为网络I/O提供尽可能多的支持的编程语言,甚至对文件I/O也提供了更多的支持(Java的URL,URLConnection,Socket和ServerSocket类都是丰富的流源)。 网络连接使用的流的确切类型通常隐藏在未记录的sun类包中。 因此,网络I/O主要依赖于基本InputStream和OutputStream方法,你可以将它们与适合你需要的任何更高级别的流进行包装:缓冲,加密,压缩或你的应用程序需要的任何内容。

URLs
java.net.URL类表示统一资源定位符,例如http://www.cafeaulait.org/books/javaio2/。 每个URL明确标识Internet上资源的位置。 URL类具有六个构造函数。 声明所有对象都抛出MalformedURLException,这是IOException的子类。

public URL(String url) throws MalformedURLException
public URL(String protocol, String host, String file)
throws MalformedURLException
public URL(String protocol, String host, int port, String file)
throws MalformedURLException
public URL(String protocol, String host, int port, String file,
URLStreamHandler handler) throws MalformedURLException
public URL(URL context, String url) throws MalformedURLException
public URL(URL context, String url, URLStreamHandler handler)
throws MalformedURLException 

如果每个构造函数的参数未指定有效的URL,则将引发MalformedURLException。 通常,这意味着特定的Java实现没有安装正确的协议处理程序。 因此,给定一个完整的绝对URL,例如http://www.poly.edu/schedule/fall2006/bgrad.html#cs,你可以像这样构造一个URL对象:

URL u = null;
try {
  u = new URL("http://www.poly.edu/schedule/fall2006/bgrad.html#cs");
} catch (MalformedURLException ex) {
  // this shouldn't happen for a syntactically correct http URL
} 

你还可以通过将URL对象的各个部分传递给构造函数来构造URL对象:

URL u = new URL("http", "www.poly.edu", "/schedule/ fall2006/bgrad.html#cs"); 

通常,你不需要为URL指定端口。 大多数协议都有默认端口。 例如,HTTP端口是80。有时使用的端口会更改,在这种情况下,你可以使用以下构造函数:

URL u = new URL("http", "www.poly.edu", 80, "/schedule/ fall2006/bgrad.html#cs "); 

最后,许多HTML文件都包含相对URL。 最后两个构造函数创建相对于给定URL的URL,在解析HTML时特别有用。 例如,以下代码创建一个指向文件08.html的URL,从u1中获取其余URL:

URL u1 = new URL("http://www.cafeaualit.org/course/week12/07.html");
URL u2 = new URL(u1, "08.html"); 

构造完URL对象后,你可以将其数据分成两部分进行检索
方法。 openStream()方法从源返回原始字节流。 getContent()方法返回代表数据的Java对象。 当你调用getContent()时,Java将寻找与数据的MIME类型匹配的内容处理程序。 本书所关注的是openStream()方法。

openStream()方法建立到URL中指定的服务器和端口的套接字连接。 它返回一个输入流,你可以从该输入流读取该URL上的数据。 在打开流之前,将剥离请求的实际数据或文件之前的所有标头。 你只能获取原始数据。

public InputStream openStream( ) throws IOException 

例5-1展示了如何连接到在命令行中输入的URL,下载其数据并将其复制到System.out。

Example 5-1. The URLTyper program
import java.net.*;
import java.io.*;
public class URLTyper {
 public static void main(String[] args) throws IOException {
  if (args.length != 1) {
     System.err.println("Usage: java URLTyper url");
     return;
  }
  InputStream in = null;
  try {
    URL u = new URL(args[0]);
    in = u.openStream( );
    for (int c = in.read(); c !=1; c = in.read( )) {
      System.out.write(c);
    }
  }catch (MalformedURLException ex) {
     System.err.println(args[0] + " is not a URL Java understands.");
  }finally {
    if (in != null) in.close( );
  }
 }
} 

例如,这是你连接到http://www.oreilly.com/时看到的前几行:
$ java URLTyper http://www.oreilly.com/

oreilly.com Welcome to O'Reilly Media, Inc. computer books, softwar e conferences, online publishing ... 大多数网络连接,即使在LAN上,也比文件慢,可靠性低。互联网上的连接速度甚至更慢,可靠性也不高,调制解调器之间的连接速度也更慢,可靠性也不高。在这种情况下提高性能的一种方法是缓冲数据:将尽可能多的数据读取到类内部的临时存储阵列中,然后根据需要将其打包。在下一章中,你将学习完全做到这一点的BufferedInputStream类。

在安全管理器的控制下运行的不受信任的代码(例如在Web浏览器中运行的applet)通常只允许连接到从其下载的主机。可以从Applet类的getCodeBase()方法返回的URL中确定此主机。尝试连接到其他主机会引发安全异常。你可以创建指向其他主机的URL,但不能使用openStream()或任何其他方法从它们下载数据。 (此小程序的安全限制适用于任何网络连接,无论你如何获得它。)

URL 链接
顾名思义,URL连接与URL紧密相关。 确实,您可以通过使用URL对象的openConnection()方法来获得对URLConnection的引用。 在许多方面,URL类只是URLConnection类的包装器。 URL连接提供了对客户端和服务器之间通信的更多控制。 特别是,URL连接不仅提供客户端可以通过其从服务器读取数据的输入流,而且还提供输出流以将数据从客户端发送到服务器。

java.net.URLConnection类是一个抽象类,用于处理与不同类型的服务器(例如FTP服务器和Web服务器)的通信。 隐藏在sun软件包中的URLConnection的特定于协议的子类处理不同类型的服务器。

从URL连接读取数据
URL连接分为五个步骤:
1.构造URL对象。
2. URL对象的openConnection()方法创建URLConnection对象。
3.设置客户端发送到服务器的连接参数和请求属性。
4. connect()方法建立到服务器的连接,可能使用套接字进行网络连接,或者使用文件输入流进行本地连接。从服务器读取响应头信息。
5.使用getInputStream()返回的输入流或getContent()返回的内容处理程序从连接中读取数据。可以使用getOutputStream()提供的输出流将数据发送到服务器。

该方案非常基于HTTP协议。它不适合其他具有更具交互性的“请求,响应,请求,响应,请求,响应”模式的方案,而不是HTTP / 1.0的“单个请求,单个响应,紧密连接”模式。特别是,FTP确实不适合这种模式。

URLConnection对象不是直接在你自己的程序中构造的。 而是为特定资源创建一个URL,然后调用该URL的openConnection()方法。 这为你提供了URLConnection。 然后,getInputStream()方法返回从URL读取数据的输入流。 (URL类的openStream()方法只是URLConnection类的getInputStream()方法的前置需要。)如果无法打开连接(例如因为远程主机不可访问),则connect()会抛出IOException。
例如,此代码使用URLConnection重复示例5-1的主体以打开流:

URL u = new URL(args[0]);
URLConnection connection = u.openConnection( );
in = connection.getInputStream( );
for (int c = in.read(); c !=1; c = in.read( )) {
   System.out.write(c);
} 

在URL连接上写入数据
将数据写入URLConnection类似于读取数据。但是,必须首先通知URLConnection你计划将其用于输出。然后,你无需获取连接的输入流并从中读取数据,而是获取连接的输出流并对其进行写入。这通常用于HTTP POST和PUT。以下是在URLConnection上写入数据的步骤:

1.构造URL对象。
2.调用URL对象的openConnection()方法来创建URLConnection对象。
3.将true传递给setDoOutput()以指示此URLConnection将用于输出。
4.如果你还想从流中读取输入,请调用setDoInput(true)以指示此URLConnection将用于input。
5.创建要发送的数据,最好是字节数组。
6.调用getOutputStream()以获取输出流对象。将步骤5中计算出的字节数组写入流中。
7.关闭输出流。
8.调用getInputStream()以获取输入流对象。照常读写。

例5-2使用这些步骤来实现一个简单的邮件客户端。它通过在命令行中输入的电子邮件地址形成一个mailto URL。使用StreamCopier将消息的输入从System.in复制到URLConnection的输出流。流结束字符表示消息结束。

**Example 5-2. The MailClient class **

import java.net.*;
import java.io.*;
public class MailClient {
public static void main(String[] args) {
  if (args.length == 0) {
    System.err.println("Usage: java MailClient username@host.com");
    return;
  }
  try {
    URL u = new URL("mailto:" + args[0]);
    URLConnection uc = u.openConnection( );
    uc.setDoOutput(true);
    uc.connect( );
    OutputStream out = uc.getOutputStream( );
    for (int c = System.in.read(); c !=1; c = System.in.read( )) {
       out.write(c);
    }
    out.close( );
  }catch (IOException ex) {
    System.err.println(ex);
  }
 }
} 

例如,要将电子邮件发送给本书的作者,请执行以下操作:
$ java MailClient elharo@metalab.unc.edu
hi there!
^D

MailClient受到一些限制。 检测消息结束的正确方法是自己在一条线上寻找一个句点。 正确与否,那种风格的用户界面确实过时了,所以我不必费心去实现它。 为此,你需要使用Reader或Writer; 在第20章中将对它们进行讨论。 此外,它仅在支持mailto协议的Java环境中有效; 因此,它可以在Sun的JDK下运行,但可能无法在其他VM上运行。 它还要求本地主机正在运行SMTP服务器,或者系统属性mail.host必须包含可访问的SMTP服务器的名称,或者要求本地域中名为mailhost的计算机正在运行SMTP服务器。 最后,安全管理器必须允许与该服务器的网络连接,尽管通常在应用程序中这不是问题。

Sockets
在数据从一台主机通过Internet发送到另一台主机之前,它会被分成大小不同但数量有限的数据包,称为数据报。数据报的大小范围从几十个字节到大约60,000字节。任何较大的东西(通常是较小的东西)都必须先分成较小的数据包,然后再进行传输。该方案的优势在于,如果丢失了一个数据包,则可以重新传输该数据包,而无需重新交付所有其他数据包。此外,如果数据包到达的顺序不正确,则可以在连接的接收端对其进行重新排序。

幸运的是,Java程序员看不到数据包。主机的本地网络软件会在发送端将数据拆分为数据包,然后在接收端重新组合数据包。取而代之的是,向Java程序员提供了称为socket的更高级别的抽象。套接字为两个主机之间的数据传输提供了可靠的连接。它将你与数据包编码,丢失和重新传输的数据包以及乱序到达的数据包的详细信息区分开。套接字执行四个基本操作:

1.连接到远程计算机
2.发送数据
3.接收数据
4.关闭连接

一个套接字不能连接到多个远程主机。但是,套接字可能会向与其连接的远程主机发送数据,也可能会从远程主机接收数据。

java.net.Socket类是Java到网络套接字的接口,并允许你执行所有四个基本的套接字操作。它提供了两个主机之间未经处理的原始通信。你可以连接到远程计算机。你可以发送数据;你可以接收数据;你可以关闭连接。没有像URL和URLConnection那样抽象出协议的任何部分。程序员完全负责客户端和服务器之间的交互。

要打开连接,请调用Socket构造函数之一,指定要连接的主机。 每个Socket对象都与一个远程主机正好关联。 要连接到其他主机,必须创建一个新的Socket对象:

public Socket(String host, int port)throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException
public Socket(InetAddress address, int port, InetAddress localAddress,
int localPort) throws IOException 

host参数是一个字符串,例如“ http://www.oreilly.com”或“ http://duke.poly.edu”,它指定要连接的特定主机。它甚至可以是数字的点分四进制字符串,例如“ 199.1.32.90”。此参数也可以作为java.net.InetAddress对象传递。

port参数是远程主机上要连接的端口。一台计算机的网络接口在逻辑上细分为65,536个不同的端口。当数据以数据包的形式遍历Internet时,每个数据包都包含要访问的主机的地址和该主机上的端口号。主机从接收到的每个数据包中读取端口号,以确定哪个程序应接收该数据块。许多服务在众所周知的端口上运行。例如,HTTP服务器通常在端口80上侦听。

可选的localAddress和localPort参数指定套接字连接的本地主机上的哪个地址和端口(假设有多个可用地址)。大多数主机有许多可用端口,但只有一个地址。这两个参数是可选的。如果不考虑它们,构造函数将选择合理的值。

数据通过流,通过套接字发送。这些是为套接字获取两个流的方法:

public InputStream getInputStream( ) throws IOException
public OutputStream getOutputStream( ) throws IOException 

也有关闭socket的方法:

public void close( ) throws IOException 

这也将关闭套接字的输入和输出流。 关闭套接字后对它们进行的任何读取或写入尝试都将引发IOException。

例5-3是连接到Web服务器并下载指定URL的另一个程序。 但是,由于此程序使用原始套接字,因此它既需要发送HTTP请求,又需要读取响应中的标头。 URL和URLConnection类没有将它们解析掉;它们没有被解析。 你可以使用输出流来显式发送请求,并使用输入流来读回包含HTTP标头的数据。 仅支持HTTP URL。

**Example 5-3. The SocketTyper program **

import java.net.*;
import java.io.*;
public class SocketTyper {
    public static void main(String[] args) throws IOException {
        if (args.length != 1) {
            System.err.println("Usage: java SocketTyper url1");
            return;
        }
        URL u = new URL(args[0]);
        if (!u.getProtocol().equalsIgnoreCase("http")) {
            System.err.println("Sorry, " + u.getProtocol()
                    + " is not supported");
            return;
        }
        String host = u.getHost();
        int port = u.getPort();
        String file = u.getFile();
        if (file == null) file = "/";
       // default port
        if (port <= 0) port = 80;
        Socket s = null;
        try {
            s = new Socket(host, port);
            String request = "GET " + file + " HTTP/1.1\r\n"
                    + "User-Agent: SocketTyper\r\n"
                    + "Accept: text/*\r\n"
                    + "Host: " + host + "\r\n"
                    + "\r\n";
            byte[] b = request.getBytes("US-ASCII");
            OutputStream out = s.getOutputStream();
            InputStream in = s.getInputStream();
            out.write(b);
            out.flush();
            for (int c = in.read(); c != -1; c = in.read()) {
                System.out.write(c);
            }
        } finally {
            if (s != null && s.isConnected()) s.close();
        }
    }
}

例如,当SocketTyper连接到http://www.oreilly.com/时,你将看到以下内容:
$ java SocketTyper http://www.oreilly.com/ HTTP/1.1 200 OK
Date: Mon, 23 May 2005 14:03:17 GMT
Server: Apache/1.3.33 (Unix) PHP/4.3.10 mod_perl/1.29
P3P: policyref=“http://www.oreillynet.com/w3c/p3p.xml”,CP=“CAO DSP COR CURa ADMa
DEVa TAIa PSAa PSDa IVAa IVDa CONo OUR DELa PUBi OTRa IND PHY
ONL UNI PUR COM N
AV INT DEM CNT STA PRE”
Last-Modified: Mon, 23 May 2005 08:20:30 GMT
ETag: “20653-db8c-4291924e”
Accept-Ranges: bytes
Content-Length: 56204
Content-Type: text/html
X-Cache: MISS from www.oreilly.com

...

请注意此处的标题行,你在示例5-1中没有看到这些行。 当你使用URL类下载网页时,关联的协议处理程序将在获取流之前使用HTTP标头。

Server Sockets
每个连接都有两端:启动连接的客户端和响应连接的服务器。到目前为止,我只讨论了客户端,并假设那里有一个服务器供客户端与之交谈。要实现服务器,你需要编写一个程序,等待其他主机连接到该服务器。服务器套接字绑定到本地计算机(服务器)上的特定端口。然后,它侦听来自远程计算机(客户端)的传入连接尝试。服务器检测到连接尝试后,即接受连接。这将在客户端和服务器通过其通信的两台计算机之间创建一个套接字。

多个客户端可以同时连接到服务器。传入数据通过寻址到的端口以及客户端主机和传入的端口来区分。服务器可以通过检查数据到达的端口来判断数据打算用于哪个服务(例如HTTP或FTP)。通过查看与数据一起存储的客户端地址和端口,它知道将响应发送到哪里。

一次最多只能有一个服务器套接字侦听一个特定的端口。因此,由于服务器可能需要一次处理多个连接,因此服务器程序倾向于是多线程的。 (或者,他们可以使用非阻塞I / O。我们将从第16章开始进行探讨。)通常,侦听端口的服务器套接字仅接受连接。它将每个连接的实际处理传递到一个单独的线程。传入连接存储在队列中,直到服务器可以接受它们为止。在大多数系统上,默认队列长度在5到50之间。一旦队列填满,将拒绝进一步的传入连接,直到队列中的空间打开为止。

java.net.ServerSocket类表示服务器套接字。构造函数接收要绑定的端口,传入连接的队列长度以及IP地址:public ServerSocket(int port) throws IOException

public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr)
throws IOException 

通常,你仅指定要监听的端口:

ServerSocket ss = new ServerSocket(80); 

创建ServerSocket对象时,它将尝试绑定到port参数给定的端口。 如果另一个服务器套接字已经在侦听该端口,则构造方法将抛出IOException,即java.net.BindException。 一次只能有一个服务器套接字可以监听特定的端口。 这包括由非Java程序打开的服务器套接字。 例如,如果端口80上已经有HTTP服务器运行,则你将无法绑定到端口80。

Tip
在包括Mac OS X但不包括Windows的Unix系统上,该程序必须以root用户身份运行才能绑定到1到1023之间的端口。否则,accept()会抛出BindException。

0是特殊端口号。 它告诉Java选择一个可用的端口。 然后,你可以使用getLocalPort()方法找出它选择了哪个端口:

public int getLocalPort( ) 

如果客户端和服务器已经建立了单独的通信通道,可以通过该通道传递所选的端口号,则此功能很有用。 例如,FTP协议使用两个套接字。 客户端在用于发送命令的套接字上建立与服务器的初始连接。 客户端还会在本地主机上的随机端口上打开服务器套接字。 它发送的命令之一告诉服务器客户端正在监听的端口号。 然后,服务器打开客户端服务器端口的套接字,用于发送文件。 由于命令和数据是通过两个不同的套接字发送的,因此长文件不会占用命令通道。

一旦有了ServerSocket,你就可以通过调用accept()方法来等待传入的连接。 该方法将阻塞,直到尝试进行连接,然后返回可用于与客户端通信的Socket。

public Socket accept( ) throws IOException 

close()方法终止ServerSocket:

public void close( ) throws IOException

除了一些处理套接字选项的方法和其他一些细节外,ServerSocket几乎就是所有这些。 特别是,没有获取输入和输出流的方法。 相反,accept()返回一个客户端Socket对象:此Socket的getInputStream()或getOutputStream()方法返回用于通信的流。 例如:

ServerSocket ss = new ServerSocket(2345);
Socket s = ss.accept( );
OutputStream out = s.getOutputStream( );
// send data to the client...
s.close( ); Notice in this example, I closed the Socket s , not the ServerSocket ss . ss is still bound to port 2345. You get a new socket for each connection and reuse the server socket. For example, the next code fragment repeatedly accepts connections: 
```java
ServerSocket ss = new ServerSocket(2345);
while (true) {
Socket s = ss.accept( );
OutputStream out = s.getOutputStream( );
// send data to the client...
s.close( );
} 

例5-4中的程序侦听端口2345上的传入连接。检测到端口2345时,它将使用客户端的地址和端口以及自己的地址进行应答。 然后关闭连接。

**Example 5-4. The HelloServer program **

import java.net.*;
import java.io.*;

public class HelloServer {
    public static void main(String[] args) throws IOException {
        int port = 2345;
        ServerSocket ss = new ServerSocket(port);
        while (true) {
            try {
                Socket s = ss.accept();
                String response = "Hello " + s.getInetAddress() + " on port "
                        + s.getPort() + "\r\n";
                response += "This is " + s.getLocalAddress() + " on port "
                        + s.getLocalPort() + "\r\n";
                OutputStream out = s.getOutputStream();
                out.write(response.getBytes("US-ASCII"));
                out.flush();
                s.close();
            } catch (IOException ex) {
          // This is an error on one connection. Maybe the client crashed.
          // Maybe it broke the connection prematurely. Whatever happened,
          // it's not worth shutting down the server for.
            }
        } // end while
    } // end main
}

Here’s some output from this server. The server is running on utopia.poly.edu . The client is connecting from titan.oit.unc.edu . Note how the port from which the connection comes changes each time; like most client programs, the telnet program picks an arbitrary local port for outgoing connections:
$ telnet utopia.poly.edu Trying 128.238.3.21…
Connected to utopia.poly.edu.
Escape character is ‘^]’.
Hello titan.oit.unc.edu/152.2.22.14 on port 50361
This is utopia.poly.edu/128.238.3.21 on port 2345
Connection closed by foreign host.
% telnet utopia.poly.edu Trying 128.238.3.21…
Connected to utopia.poly.edu.
Escape character is ‘^]’.
Hello titan.oit.unc.edu/152.2.22.14 on port 50362
This is utopia.poly.edu/128.238.3.21 on port 2345
Connection closed by foreign host.

Tip
如果你无法建立与此服务器的连接,请检查防火墙
规则。 为了安全起见,大多数现代网络都在路由器,本地主机或两者中都安装了防火墙,以阻止所有无法识别的服务和端口的连接。 你可能需要配置防火墙,以允许连接到端口2345才能运行此程序。

URLViewer
示例5-5是第2章中预示的URLViewer程序。 URLViewer是一个简单的应用程序,提供了一个窗口,你可以在其中查看URL的内容。 假定这些内容或多或少是ASCII文本。 (在以后的章节中,我将消除该限制。)该应用程序具有用户可以在其中键入URL的文本字段,用户按下以加载指定URL的Load按钮以及显示第2章的JStreamedTextArea组件。 URL中的文本。 这些每个都对应于URLViewer类中的一个字段。

Example 5-5. The URLViewer program

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import java.net.*;
import com.elharo.io.ui.*;
public class URLViewer extends JFrame implements ActionListener {
    private JTextField theURL = new JTextField( );
    private JButton loadButton = new JButton("Load");
    private JStreamedTextArea theDisplay = new JStreamedTextArea(60, 72);
    public URLViewer( ) {
        super("URL Viewer");
        this.getContentPane( ).add(BorderLayout.NORTH, theURL);
        JScrollPane pane = new JScrollPane(theDisplay);
        this.getContentPane( ).add(BorderLayout.CENTER, pane);
        Panel south = new Panel( );
        south.add(loadButton);
        this.getContentPane( ).add(BorderLayout.SOUTH, south);
        theURL.addActionListener(this);
        loadButton.addActionListener(this);
        this.setLocation(50, 50);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.pack( );
    }
    public void actionPerformed(ActionEvent event) {
        try {
            URL u = new URL(theURL.getText( ));
            InputStream in = u.openStream( );
            OutputStream out = theDisplay.getOutputStream( );
            theDisplay.setText("");
            for (int c = in.read(); c != -1; c = in.read()) {
                out.write(c);
            }
            in.close( );
        }
        catch (IOException ex) {
            theDisplay.setText("Invalid URL: " + ex.getMessage( ));
        }
    }
    public static void main(String args[]) {
        final URLViewer me = new URLViewer( );
        // To avoid deadlock don't show frames on the main thread
        SwingUtilities.invokeLater(
                new Runnable( ) {
                    public void run( ) {
                        me.show( );
                    }
                }
        );
    }
}

URLViewer类本身扩展了JFrame。 构造函数将构建接口,该接口包括一个用于输入URL的JTextField,一个来自第2章的JStreamedTextArea组件(该组件位于JScrollPane内)和一个Load按钮,可以按下该按钮下载URL的内容。

当用户单击“加载”按钮或在URL文本字段内按Enter时,将填充流文本区域。 URLViewer对象侦听这两个组件。 URLViewer的actionPerformed()方法从文本字段中的文本构造一个URL,然后从文本字段中的URL打开输入流。 URL输入流中的数据倒入文本区域的输出流中。 完成后,输入流将关闭。 但是,输出流保持打开状态,因此用户可以查看新的URL。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值