JAVA编写HTTP代码并发布在网上

   最近在学习JAVA网络编程编写HTTP服务器,HTTP是个大协议,完整功能的HTTP服务器必须响应资源请求,将URL转换为本地系统的资源名。响应各种形式的HTTP请求(GET、POST等)。处理不存在的文件请求,返回各种形式的状态码,解析MIME类型等。但许多特定功能的HTTP服务器并不需要所有这些功能。因此可用JAVA编写一个代替

首先来看利用的云服务器,选择的是新浪SAE,因为他免费,方便。

创建应用


在语言上选择JAVA


创建完成后进入代码管理


将编写好的代码打包成WAR 上传

这里可以选择用Myeclipise打包 

首先需要注意的是,MyEclipse只能对WebProject类型的工程进行WAR包制作,对于我们常用的JavaProject则无法进行WAR包制作。

打开MyEclipse,在【Package Explorer】中选中需要压缩的项目,点击工具栏中的“File->Export…”,在弹出的【Export】对话框上,点击选中树状图中的“J2EE->WAR file  (MyEclipse)”,点击【Next  >】继续


在【WAR Export】对话框上选择需要压缩的项目名称,点击【Browse…】,在弹出的【另存为】对话框上选择WAR包保存的路径和名称,确认后点击【Finish】,开始进行压缩。

这里也可以用SVN进行代码的上传 不做说明

上传成功后就可以点击链接通过HTTP访问了

最后我们来看下代码:

1.AcceptHanlder.java

package Test;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class AcceptHandler implements Handler {
  public void handle(SelectionKey key) throws IOException {
    ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.channel();
    SocketChannel socketChannel = serverSocketChannel.accept();
    if (socketChannel== null)return;
    System.out.println("接收到客户连接,来自:" +
                   socketChannel.socket().getInetAddress() +
                   ":" + socketChannel.socket().getPort());

    ChannelIO cio =new ChannelIO(socketChannel, false/*非阻塞模式*/);
    RequestHandler rh = new RequestHandler(cio);
    socketChannel.register(key.selector(), SelectionKey.OP_READ, rh);
  }
}

2.ChannelIO.java

package Test;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class ChannelIO {
  protected SocketChannel socketChannel;
  protected ByteBuffer requestBuffer;
  private static int requestBufferSize = 4096;

  public ChannelIO(SocketChannel socketChannel, boolean blocking)
    throws IOException {
    this.socketChannel = socketChannel;
    socketChannel.configureBlocking(blocking); //设置阻塞模式
    requestBuffer = ByteBuffer.allocate(requestBufferSize);
  }

  public SocketChannel getSocketChannel() {
    return socketChannel;
  }

  /*
   * 如果原缓冲区的剩余容量不够,就创建一个新的缓冲区,容量为原来的两倍,
   * 把原来缓冲区的数据拷贝到新缓冲区
   */
  protected void resizeRequestBuffer(int remaining) {
    if (requestBuffer.remaining() < remaining) {
      // 把容量增大到原来的两倍
      ByteBuffer bb = ByteBuffer.allocate(requestBuffer.capacity() * 2);
      requestBuffer.flip();
      bb.put(requestBuffer);  //把原来缓冲区中的数据拷贝到新的缓冲区
      requestBuffer = bb;
    }
  }

  /*
   * 接收数据,把它们存放到requestBuffer中,如果requsetBuffer的剩余容量不足5%,
   * 就调用resizeRequestBuffer()方法扩充容量
   */
  public int read() throws IOException {
    resizeRequestBuffer(requestBufferSize/20);
    return socketChannel.read(requestBuffer);
  }

  /*
   * 返回requestBuffer,它存放了所有的请求数据
   */
  public ByteBuffer getReadBuf() {
      return requestBuffer;
  }

  /*
   * 发送参数指定的ByteBuffer中的数据
   */
  public int write(ByteBuffer src) throws IOException {
    return socketChannel.write(src);
  }

  /*
   * 把FileChannel中的数据写到SocketChannel中
   */
  public long transferTo(FileChannel fc, long pos, long len) throws IOException {
    return fc.transferTo(pos, len, socketChannel);
  }

  /*
   * 关闭SocketChannel
   */
  public void close() throws IOException {
    socketChannel.close();
  }
}

3.Content.java

package Test;

/**
 * 表示服务器发送给客户的正文内容
 */
public interface Content extends Sendable {
  //内容的类型
  String type();

  //在内容还没有准备之前,即还没有调用prepare()方法之前,length()方法返回-1。
  long length();
}

4.FileContent.java

package Test;

import java.io.*;
import java.net.*;
import java.nio.channels.*;
import java.nio.charset.*;

/*文件形式的响应正文*/
public class FileContent implements Content {
  //假定文件的根目录为"root"
  private static File ROOT = new File("D:\\");
  
  private File file;

  public FileContent(URI uri) {
    file = new File(ROOT,
                  uri.getPath()
                  .replace('/',File.separatorChar));
  }

  private String type = null;

  /* 确定文件类型 */
  public String type() {
    if (type != null) return type;
    String nm = file.getName();
    if (nm.endsWith(".html")|| nm.endsWith(".htm"))
        type = "text/html; charset=GBK";  //HTML网页
    else if ((nm.indexOf('.') < 0) || nm.endsWith(".txt"))
        type = "text/plain; charset=GBK";  //文本文件
    else
        type = "application/octet-stream";  //应用程序
    return type;
  }

  private FileChannel fileChannel = null;
  private long length = -1;  //文件长度
  private long position = -1; //文件的当前位置

  public long length() {
      return length;
  }

  /* 创建FileChannel对象*/
  public void prepare() throws IOException {
    if (fileChannel == null)
        fileChannel = new RandomAccessFile(file, "r").getChannel();
    length = fileChannel.size();
    position = 0;
  }

  /* 发送正文,如果发送完毕,就返回false,否则返回true */
  public boolean send(ChannelIO channelIO) throws IOException {
    if (fileChannel == null)
        throw new IllegalStateException();
    if (position < 0)
        throw new IllegalStateException();

    if (position >= length) {
        return false;  //如果发送完毕,就返回false
    }

    position += channelIO.transferTo(fileChannel, position, length - position);
    return (position < length);
  }

  public void release() throws IOException {
    if (fileChannel != null){
        fileChannel.close();  //关闭fileChannel
        fileChannel = null;
    }
  }
}

5.Hanlder.java

<span style="font-size:14px;"><strong>package Test;

import java.io.*;
import java.nio.channels.*;

public interface Handler {
    public void handle(SelectionKey key) throws IOException;
}

</strong></span>
6.HttpServer.java

package Test;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.net.*;
import java.util.*;

public class HttpServer{
  private Selector selector = null;
  private ServerSocketChannel serverSocketChannel = null;
  private int port = 80;
  private Charset charset=Charset.forName("GBK");

  public HttpServer()throws IOException{
    selector = Selector.open();
    serverSocketChannel= ServerSocketChannel.open();
    serverSocketChannel.socket().setReuseAddress(true);
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.socket().bind(new InetSocketAddress(port));
    System.out.println("服务器启动");
  }

  public void service() throws IOException{
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new AcceptHandler());
    for(;;){
      int n = selector.select();

      if(n==0)continue;
      Set readyKeys = selector.selectedKeys();
      Iterator it = readyKeys.iterator();
      while (it.hasNext()){
        SelectionKey key=null;
        try{
            key = (SelectionKey) it.next();
            it.remove();
  	    final Handler handler = (Handler)key.attachment();
            handler.handle(key);
        }catch(IOException e){
           e.printStackTrace();
           try{
               if(key!=null){
                   key.cancel();
                   key.channel().close();
               }
           }catch(Exception ex){e.printStackTrace();}
        }
      }//#while
    }//#while
  }


  public static void main(String args[])throws Exception{
    final HttpServer server = new HttpServer();
    server.service();
  }
}




7.MalformedRequestException.java

<span style="font-size:14px;"><strong>package Test;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.net.*;
import java.util.*;

public class HttpServer{
  private Selector selector = null;
  private ServerSocketChannel serverSocketChannel = null;
  private int port = 80;
  private Charset charset=Charset.forName("GBK");

  public HttpServer()throws IOException{
    selector = Selector.open();
    serverSocketChannel= ServerSocketChannel.open();
    serverSocketChannel.socket().setReuseAddress(true);
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.socket().bind(new InetSocketAddress(port));
    System.out.println("服务器启动");
  }

  public void service() throws IOException{
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new AcceptHandler());
    for(;;){
      int n = selector.select();

      if(n==0)continue;
      Set readyKeys = selector.selectedKeys();
      Iterator it = readyKeys.iterator();
      while (it.hasNext()){
        SelectionKey key=null;
        try{
            key = (SelectionKey) it.next();
            it.remove();
  	    final Handler handler = (Handler)key.attachment();
            handler.handle(key);
        }catch(IOException e){
           e.printStackTrace();
           try{
               if(key!=null){
                   key.cancel();
                   key.channel().close();
               }
           }catch(Exception ex){e.printStackTrace();}
        }
      }//#while
    }//#while
  }


  public static void main(String args[])throws Exception{
    final HttpServer server = new HttpServer();
    server.service();
  }
}




</strong></span>
8.Request.java
<strong><span style="font-size:14px;">package Test;

import java.net.*;
import java.nio.*;
import java.nio.charset.*;
import java.util.regex.*;
/* 代表客户的HTTP请求 */
public class Request {
  static class Action {  //枚举类,表示HTTP请求方式
    private String name;
    private Action(String name) { this.name = name; }
    public String toString() { return name; }

    static Action GET = new Action("GET");
    static Action PUT = new Action("PUT");
    static Action POST = new Action("POST");
    static Action HEAD = new Action("HEAD");

    public static Action parse(String s) {
        if (s.equals("GET"))
            return GET;
        if (s.equals("PUT"))
            return PUT;
        if (s.equals("POST"))
            return POST;
        if (s.equals("HEAD"))
            return HEAD;
        throw new IllegalArgumentException(s);
    }
  }

  private Action action;
  private String version;
  private URI uri;

  public Action action() { return action; }
  public String version() { return version; }
  public URI uri() { return uri; }

  private Request(Action a, String v, URI u) {
    action = a;
    version = v;
    uri = u;
  }

  public String toString() {
    return (action + " " + version + " " + uri);
  }

  private static Charset requestCharset = Charset.forName("GBK");

  /* 判断ByteBuffer是否包含了HTTP请求的所有数据。
   * HTTP请求以“\r\n\r\n”结尾。
   */
  public static boolean isComplete(ByteBuffer bb) {
    ByteBuffer temp=bb.asReadOnlyBuffer();
    temp.flip();
    String data=requestCharset.decode(temp).toString();
    if(data.indexOf("\r\n\r\n")!=-1){
      return true;
    }
    return false;
  }

  /*
   * 删除请求正文,本例子仅支持GET和HEAD请求方式,忽略HTTP请求中的正文部分
   */
  private static ByteBuffer deleteContent(ByteBuffer bb) {
    ByteBuffer temp=bb.asReadOnlyBuffer();
    String data=requestCharset.decode(temp).toString();
    if(data.indexOf("\r\n\r\n")!=-1){
        data=data.substring(0,data.indexOf("\r\n\r\n")+4);
        return requestCharset.encode(data);
    }
    return bb;
  }

  /*
   * 设定用于解析HTTP请求的字符串匹配模式。对于以下形式的HTTP请求:
   *
   *     GET /dir/file HTTP/1.1
   *     Host: hostname
   *
   * 将被解析成:
   *
   *     group[1] = "GET"
   *     group[2] = "/dir/file"
   *     group[3] = "1.1"
   *     group[4] = "hostname"
   */
  private static Pattern requestPattern
      = Pattern.compile("\\A([A-Z]+) +([^ ]+) +HTTP/([0-9\\.]+)$"
                        + ".*^Host: ([^ ]+)$.*\r\n\r\n\\z",
                        Pattern.MULTILINE | Pattern.DOTALL);

  /* 解析HTTP请求,创建相应的Request对象 */
  public static Request parse(ByteBuffer bb) throws MalformedRequestException {
    bb=deleteContent(bb); //删除请求正文
    CharBuffer cb = requestCharset.decode(bb); //解码
    Matcher m = requestPattern.matcher(cb);  //进行字符串匹配
    //如果HTTP请求与指定的字符串模式不匹配,说明请求数据不正确
    if (!m.matches())
        throw new MalformedRequestException();
    Action a;
    try {  //获得请求方式
        a = Action.parse(m.group(1));
     } catch (IllegalArgumentException x) {
        throw new MalformedRequestException();
    }
    URI u;
    try { //获得URI
        u = new URI("http://"
                    + m.group(4)
                    + m.group(2));
    } catch (URISyntaxException x) {
        throw new MalformedRequestException();
    }
    //创建一个Request对象,并将其返回
    return new Request(a, m.group(3), u);
  }
}


</span></strong>
9.RequestHanlder..java

<strong><span style="font-size:14px;">package Test;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class RequestHandler implements Handler {
  private ChannelIO channelIO;
  private ByteBuffer requestByteBuffer = null;  //存放HTTP请求的缓冲区

  private boolean requestReceived = false;  //是否已经接收到了所有的HTTP请求
  private Request request = null;  //表示HTTP请求
  private Response response = null;  //表示HTTP响应

  RequestHandler(ChannelIO channelIO) {
    this.channelIO = channelIO;
  }

  /* 
   * 接收HTTP请求,如果已经接收到了所有的HTTP请求数据,就返回true,否则返回false
   */
  private boolean receive(SelectionKey sk) throws IOException {
    ByteBuffer tmp = null;

    if (requestReceived)return true;  //如果已经接收到所有HTTP请求数据,返回true
    
    //如果已经读到通道的末尾,或者已经读到HTTP请求数据的末尾标志“\r\n”,就返回true
    if ((channelIO.read() < 0) || Request.isComplete(channelIO.getReadBuf())) {
      requestByteBuffer = channelIO.getReadBuf();
      return (requestReceived = true);
    }
    return false;
  }

  /*
   * 通过Request类的parse()方法,解析requestByteBuffer中的HTTP请求数据,构造相应的Request对象 
   */
  private boolean parse() throws IOException {
    try {
      request = Request.parse(requestByteBuffer);
      return true;
    } catch (MalformedRequestException x) {  
      //如果HTTP请求的格式不正确,就发送错误信息
      response = new Response(Response.Code.BAD_REQUEST,
                          new StringContent(x));
    }
    return false;
  }

  /*
   * 创建HTTP响应 
   */
  private void build() throws IOException {
    Request.Action action = request.action();
    //仅仅支持GET和HEAD请求方式
    if ((action != Request.Action.GET) &&
            (action != Request.Action.HEAD)){
       response = new Response(Response.Code.METHOD_NOT_ALLOWED,
                          new StringContent("Method Not Allowed"));
    }else{
       response = new Response(Response.Code.OK,
                      new FileContent(request.uri()), action);
    }
  }
  
  /*  接收HTTP请求,发送HTTP响应 */
  public void handle(SelectionKey sk) throws IOException {
    try {
        if (request == null) { //如果还没有接收到HTTP请求的所有数据
            //接收HTTP请求      
            if (!receive(sk))return;
            requestByteBuffer.flip();
     
            //如果成功解析了HTTP请求,就创建一个Response对象
            if (parse())build();
     
            try {
                response.prepare();  //准备HTTP响应的内容
            } catch (IOException x) {
                response.release();  
                response = new Response(Response.Code.NOT_FOUND,
                                  new StringContent(x));
                response.prepare();
            }

            if (send()) {  
               //如果HTTP响应没有发送完毕,则需要注册写就绪事件,
               //以便在写就绪事件发生时继续发送数据
               sk.interestOps(SelectionKey.OP_WRITE);
            } else {
               //如果HTTP响应发送完毕,就断开底层的连接,并且释放Response占用的资源
               channelIO.close();
               response.release();
            }
        } else {  //如果已经接收到HTTP请求的所有数据
            if (!send()) {  //如果HTTP响应发送完毕
              channelIO.close();
              response.release();
            }
        }
    } catch (IOException x) {
        x.printStackTrace();
        channelIO.close();
        if (response !=  null) {
            response.release();
        }
    }
  }

  /* 发送HTTP响应,如果全部发送完毕,就返回false,否则返回true */  
  private boolean send() throws IOException {
    return response.send(channelIO);
  }
}

</span></strong>

10.Response.java

<strong><span style="font-size:14px;">package Test;

import java.io.*;
import java.nio.*;
import java.nio.charset.*;

public class Response implements Sendable {
  static class Code {  //枚举类,表示状态代码
    private int number;
    private String reason;
    private Code(int i, String r) { number = i; reason = r; }
    public String toString() { return number + " " + reason; }

    static Code OK = new Code(200, "OK");
    static Code BAD_REQUEST = new Code(400, "Bad Request");
    static Code NOT_FOUND = new Code(404, "Not Found");
    static Code METHOD_NOT_ALLOWED = new Code(405, "Method Not Allowed");
  }

  private Code code;  //状态代码
  private Content content;  //响应正文
  private boolean headersOnly;  //表示HTTP响应中是否仅包含响应头
  private ByteBuffer headerBuffer = null;  //响应头

  public Response(Code rc, Content c) {
    this(rc, c, null);
  }

  public Response(Code rc, Content c, Request.Action head) {
    code = rc;
    content = c;
    headersOnly = (head == Request.Action.HEAD);
  }

  private static String CRLF = "\r\n";
  private static Charset responseCharset = Charset.forName("GBK");

  /* 创建响应头的内容,把它存放到一个ByteBuffer中 */
  private ByteBuffer headers() {
    CharBuffer cb = CharBuffer.allocate(1024);
    for (;;) {
        try {
            cb.put("HTTP/1.1 ").put(code.toString()).put(CRLF);
            cb.put("Server: nio/1.1").put(CRLF);
            cb.put("Content-type: ").put(content.type()).put(CRLF);
            cb.put("Content-length: ")
                .put(Long.toString(content.length())).put(CRLF);
            cb.put(CRLF);
            break;
        } catch (BufferOverflowException x) {
            assert(cb.capacity() < (1 << 16));
            cb = CharBuffer.allocate(cb.capacity() * 2);
            continue;
        }
    }
    cb.flip();
    return responseCharset.encode(cb);  //编码
  }

  /* 准备HTTP响应中的正文以及响应头的内容 */
  public void prepare() throws IOException {
    content.prepare();
    headerBuffer= headers();
  }

  /* 发送HTTP响应,如果全部发送完毕,返回false,否则返回true */
  public boolean send(ChannelIO cio) throws IOException {
    if (headerBuffer == null)
        throw new IllegalStateException();

    //发送响应头
    if (headerBuffer.hasRemaining()) {
        if (cio.write(headerBuffer) <= 0)
            return true;
    }

    //发送响应正文
    if (!headersOnly) {
        if (content.send(cio))
            return true;
    }

    return false;
  }

  /* 释放响应正文占用的资源 */
  public void release() throws IOException {
    content.release();
  }
}



</span></strong>

11.Sendable.java

<strong><span style="font-size:14px;">package Test;

import java.io.*;
/*
 *表示服务器可以发送给客户端的东西
 */
public interface Sendable {
  // 准备发送的内容
  public void prepare() throws IOException;

  // 利用通道发送部分内容,如果所有内容发送完毕,就返回false
  // 如果还有内容未发送,就返回true
  // 如果内容还没有准备好,就抛出IllegalStateException
  public boolean send(ChannelIO cio) throws IOException;

  //当服务器发送内容完毕,就调用此方法,释放内容占用的资源
  public void release() throws IOException;
}
</span></strong>
12.SimpleHttpServer.java

<strong><span style="font-size:14px;">package Test;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

public class SimpleHttpServer {
  private int port=80;
  private ServerSocketChannel serverSocketChannel = null;
  private ExecutorService executorService;
  private static final int POOL_MULTIPLE = 4;

  public SimpleHttpServer() throws IOException {
    executorService= Executors.newFixedThreadPool(
	    Runtime.getRuntime().availableProcessors() * POOL_MULTIPLE);
    serverSocketChannel= ServerSocketChannel.open();
    serverSocketChannel.socket().setReuseAddress(true);
    serverSocketChannel.socket().bind(new InetSocketAddress(port));
    System.out.println("服务器启动");
  }

  public void service() {
    while (true) {
      SocketChannel socketChannel=null;
      try {
        socketChannel = serverSocketChannel.accept();
        executorService.execute(new Handler(socketChannel));
      }catch (IOException e) {
         e.printStackTrace();
      }
    }
  }

  public static void main(String args[])throws IOException {
    new SimpleHttpServer().service();
  }
  class Handler implements Runnable{
  private SocketChannel socketChannel;
  public Handler(SocketChannel socketChannel){
    this.socketChannel=socketChannel;
  }
  public void run(){
    handle(socketChannel);
  }

  public void handle(SocketChannel socketChannel){
    try {
        Socket socket=socketChannel.socket();
        System.out.println("接收到客户连接,来自: " +
        socket.getInetAddress() + ":" +socket.getPort());

         ByteBuffer buffer=ByteBuffer.allocate(1024);
         socketChannel.read(buffer);
         buffer.flip();
         String request=decode(buffer);
         System.out.print(request);  //打印HTTP请求

         //输出HTTP响应结果
         StringBuffer sb=new StringBuffer("HTTP/1.1 200 OK\r\n");
         sb.append("Content-Type:text/html\r\n\r\n");
         socketChannel.write(encode(sb.toString()));//输出响应头

         FileInputStream in;
         //获得HTTP请求的第一行
         String firstLineOfRequest=request.substring(0,request.indexOf("\r\n"));
         if(firstLineOfRequest.indexOf("login.htm")!=-1)
            in=new FileInputStream("root/login.htm");
         else
            in=new FileInputStream("root/hello.htm");

         FileChannel fileChannel=in.getChannel();
         fileChannel.transferTo(0,fileChannel.size(),socketChannel);
         fileChannel.close();
      }catch (Exception e) {
         e.printStackTrace();
      }finally {
         try{
           if(socketChannel!=null)socketChannel.close();
         }catch (IOException e) {e.printStackTrace();}
      }
  }
  private Charset charset=Charset.forName("GBK");
  public String decode(ByteBuffer buffer){  //解码
    CharBuffer charBuffer= charset.decode(buffer);
    return charBuffer.toString();
  }
  public ByteBuffer encode(String str){  //编码
    return charset.encode(str);
  }
 }

}
</span></strong>
13.StringContent.java

<strong><span style="font-size:14px;">package Test;

import java.io.*;
import java.nio.*;
import java.nio.charset.*;
/* 字符串形式的内容 */
public class StringContent implements Content {

    private static Charset charset = Charset.forName("GBK");
    private String type;		// MIME type
    private String content;

    public StringContent(CharSequence c, String t) {
	content = c.toString();
	if (!content.endsWith("\n"))
	    content += "\n";
	type = t + "; charset=GBK";
    }

    public StringContent(CharSequence c) {
	this(c, "text/plain");
    }

    public StringContent(Exception x) {
	StringWriter sw = new StringWriter();
	x.printStackTrace(new PrintWriter(sw));
	type = "text/plain; charset=GBK";
	content = sw.toString();
    }

    public String type() {
	return type;
    }

    private ByteBuffer bb = null;

    private void encode() {
	if (bb == null)
	    bb = charset.encode(CharBuffer.wrap(content));
    }

    public long length() {
	encode();
	return bb.remaining();
    }

    public void prepare() {
	encode();
	bb.rewind();
    }

    public boolean send(ChannelIO cio) throws IOException {
	if (bb == null)
	    throw new IllegalStateException();
	cio.write(bb);

	return bb.hasRemaining();
    }

    public void release() throws IOException {}
}


</span></strong>


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值