JavaWeb系列九: 手动实现Tomcat底层机制

在这里插入图片描述

在这里插入图片描述

1. 创建maven-web项目

第一步

高版本idea 2022
在这里插入图片描述
低版本idea 2020
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第二步
在这里插入图片描述
创建完成
在这里插入图片描述

1.1 配置阿里maven镜像

  1. 把D:\Program Files\IntelliJ IDEA 2022.3.2\plugins\maven\lib\maven3\conf里的settings.xml复制到C:\Users\97896.m2
    在这里插入图片描述
  2. 修改C:\Users\97896.m2下的settings.xml
    在这里插入图片描述
<mirror>
      <id>maven-default-http-blocker</id>
      <mirrorOf>external:http:*</mirrorOf>
      <name>Pseudo repository to mirror external repositories initially using HTTP.</name>
      <url>http://0.0.0.0/</url>
      <blocked>true</blocked>
</mirror>
  1. 配置IDEA
    在这里插入图片描述
    pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.zzw</groupId>
  <artifactId>zzwtomcat</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>zzwtomcat Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <!--引入Servlet.jar-->
    <!--
        1.引入servlet-api.jar, 为了开发Servlet
        2.dependency 标签是表示要引入一个包
        3.groupId 包的开发公司/开发团队/个人信息 javax.servlet
        4.artifactId 包的项目名 java.servlet.api
          补充: groupId+artifactId 是以目录形式存在的
        5.version 版本信息
        6.scope 表示引入的包的作用范围
        7.provided 表示tomcat本身有这个jar包. 在这里引入的jar包, 只在编译和测试时有效, 但是在打包发布的时候,
        不会带上这个jar包
        8.下载的包在你指定的目录:C:\Users\97896\.m2\repository
        9.我们可以修改我们要下载包的位置 -> File | Settings | Build, Execution, Deployment |
                                      Build Tools | Maven | Local Repository
        10.我们可以去指定maven仓库, 即配置maven镜像 "C:\Users\97896\.m2\settings.xml"
    -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>1.6.1</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>zzwtomcat</finalName>
  </build>
</project>

1.2 新建Servlet项目

  1. 目录结构
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. Tomcat

在这里插入图片描述
在这里插入图片描述
3. web.xml
在这里插入图片描述
4. 前端页面

5. Servlet
在这里插入图片描述
字符串拼接简易方法
在这里插入图片描述
6. 工具类
在这里插入图片描述

2. Tomcat整体架构分析

Tomcat有三种运行模式(BIO, NIO, APR), 采用BIO线程模型来模拟Tomcat如何接收客户端请求, 解析请求, 调用Servlet, 并返回结果的机制流程
在这里插入图片描述

项目整体目录结构
在这里插入图片描述

2.1 基于socket开发服务端

Content-Type: text/html;charset=gbk 给浏览器响应时设置成gbk

转换流

/**
 * @author 赵志伟
 * @version 1.0
 * 这是第一个版本的tomcat, 可以完成接收浏览器的请求, 并返回信息
 */
@SuppressWarnings({"all"})
public class ZzwTomcatVersion1 {
    public static void main(String[] args) throws IOException {
        //1.在服务端监听8080端口
        ServerSocket serverSocket = new ServerSocket(8080);
        while (!serverSocket.isClosed()) {
            System.out.println("服务端Tomcat在 8080端口 等待连接");
            //如果有连接, 就创建一个socket, 这个socket是服务端和客户端的连接通道
            Socket socket = serverSocket.accept();

            System.out.println("接收浏览器发送的数据");
            InputStream inputStream = socket.getInputStream();
            BufferedReader bufferedReader =
                    new BufferedReader(new InputStreamReader(inputStream, "utf-8"));

            String message = "";
            while ((message = bufferedReader.readLine()) != null) {
                //判断长度是否为0
                if (message.length() == 0) {
                    break;
                }
                System.out.println(message);
            }

            //我们的Tomcat以 http协议方式 回送数据给浏览器
            OutputStream outputStream = socket.getOutputStream();
            //构建一个http响应的消息头
            // \r\n代表换行
            // 响应头和响应体之间有个空行
            String responseHeader = "HTTP/1.1 200\r\n" +
                    "Content-Type: text/html;charset=gbk\r\n\r\n";
            String response = responseHeader + "你好, 世界 521";
            System.out.println("\ntomcat响应给浏览器的数据\n" + response);
            outputStream.write(response.getBytes());//因为是字节输出流, 所以要按照字节的方式返回

            //关闭流
            outputStream.flush();
            outputStream.close();
            bufferedReader.close();
            socket.close();
        }
    }
}

2.2 BIO线程模型

在这里插入图片描述
线程

/**
 * @author 赵志伟
 * @version 1.0
 * ZzwRequestHandler 是一个线程对象
 * 用来处理 http请求
 */
@SuppressWarnings({"all"})
public class ZzwRequestHandler implements Runnable {
    private Socket socket;

    public ZzwRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        //对客户端和浏览器进行IO操作
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = socket.getInputStream();
            BufferedReader bufferedReader//inputStream->bufferedReader 方便按行读取
                    = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
            System.out.println("当前线程=" + Thread.currentThread().getId());
            System.out.println("==============Tomcat Version2 接收到的数据如下==============");
            String message = "";
            while ((message = bufferedReader.readLine()) != null) {
                if (message.length() == 0) {
                    break;
                }
                System.out.println(message);
            }
            //构建一个http响应头
            //响应头和响应体之间有两个换行 \r\n\r\n
            String responseHeader = "HTTP/1.1 200\r\n" +
                    "Content-Type: text/html;charset=gbk\r\n\r\n";
            String response = responseHeader + "<h1>你好,世界</h1>";
            System.out.println("==============Tomcat Version2 返回的数据如下==============");
            System.out.println(response);
            //得到输出流,将数据封装成 http响应格式 返回给浏览器/客户端
            outputStream = socket.getOutputStream();
            outputStream.write(response.getBytes());//这个方法把字符串转成字节数组
            socket.close();//一定要确保socket关闭
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
                inputStream.close();
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
/**
 * @author 赵志伟
 * @version 1.0
 * 这是第二个版本的tomcat, 可以调用线程
 */
@SuppressWarnings({"all"})
public class ZzwTomcatVersion2 {
    public static void main(String[] args) throws IOException {
        //1.在服务端监听8080端口
        ServerSocket serverSocket = new ServerSocket(8080);
        //只要servletSocket没有关闭, 就一直等待 浏览器/客户端 连接
        while (!serverSocket.isClosed()) {
            System.out.println("服务端Tomcat version2 在 8080端口 等待连接");
            //如果有连接, 就创建一个socket, 这个socket是服务端和客户端的连接通道
            Socket socket = serverSocket.accept();

            //不能直接调用run方法, 要调用start
            ZzwRequestHandler zzwRequestHandler = new ZzwRequestHandler(socket);
            new Thread(zzwRequestHandler).start();
        }
    }
}

2.3 处理Servlet

模仿Servlet规范
在这里插入图片描述
在这里插入图片描述

  1. ZzwRequest对象
/**
 * @author 赵志伟
 * @version 1.0
 * 1.ZzwRequest作用 封装http请求的数据
 * get /zzwCalServlet?num1=12&num2=21
 * 2.比如 请求方法method(get/post), uri(/zzwCalServlet), 参数(num1=12&num2=21)
 * 3.ZzwRequest等价于原生Servlet中的 HttpServletRequest
 * 4.这里只考虑get请求
 */
@SuppressWarnings({"all"})
public class ZzwRequest {

    private String method;
    private String uri;
    //存放参数列表 参数名-参数值 => 数据结构HashMap
    private HashMap<String, String> argsMapping = new HashMap<String, String>();

    //inputStream 是和对应http请求的socket关联
    public ZzwRequest(InputStream inputStream) {
        init(inputStream);
    }

    public void init(InputStream inputStream) {
        //inputStream->bufferedReader
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
            /**读取第一行(读取请求行)
             * GET /calServlet?num1=-87&num2=89 HTTP/1.1
             */
            String line1 = bufferedReader.readLine();
            String[] line1s = line1.split(" ");
            //获取method
            method = line1s[0];
            //获取uri
            int index = line1s[1].indexOf("?");
            if (index == -1) {//说没后面没有参数列表
                uri = line1s[1];
            } else {//这里有参数情况
                uri = line1s[1].substring(0, index);
                //args=>num1=-87&num2=89
                String args = line1s[1].substring(index + 1);//直接截取到最后
                //argsPair => ["num1=-87","num2=89"]
                String[] argsPair = args.split("&");
                for (String argPair : argsPair) {
                    String[] argVal = argPair.split("=");
                    if (argVal.length == 2) {
                        //放入到argsMapping
                        argsMapping.put(argVal[0], argVal[1]);
                    }
                }
            }
            //这里inputStream和socket关联, 不能在这里关闭
            //inputStream.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public String getParameter(String name) {
        if (argsMapping.containsKey(name)) {
            return argsMapping.get(name);
        } else {
            return "";
        }
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    @Override
    public String toString() {
        return "ZzwRequest{" +
                "method='" + method + '\'' +
                ", uri='" + uri + '\'' +
                ", argsMapping=" + argsMapping +
                '}';
    }
}
  1. ZzwResponse对象
    如果响应体里面有中文, 则应在响应头里设置成gbk, 至少我这台电脑是这样的, 原因不详.
/**
 * @author 赵志伟
 * @version 1.0
 * 1.ZzwResponse对象 可以封装OutputStream(和socket关联)
 * 2.可以通过ZzwResponse对象 返回HTTP响应给客户端
 * 3.ZzwResponse对象的作用等价于原生的Servlet的HttpServletResponse
 */
@SuppressWarnings({"all"})
public class ZzwResponse {
    private OutputStream outputStream;
    //设置一个http响应头
    private static final String responseHeader = "HTTP/1.1 200\r\n" +
            "Content-Type: text/html;charset=gbk\r\n\r\n";
    private String response;

    public ZzwResponse(OutputStream outputStream) {
        this.outputStream = outputStream;
    }
    public OutputStream getOutputStream() {
        return outputStream;
    }

    public void setOutputStream(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    public String getResponseHeader() {
        return responseHeader;
    }

    public String getResponse() {
        return response;
    }

    public void setResponse(String response) {
        this.response = responseHeader + response;
    }
}
  1. ZzwRequestHandler改进
public class ZzwRequestHandler implements Runnable {
    private Socket socket;
    public ZzwRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        //对客户端和浏览器进行IO操作
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = socket.getInputStream();
            ZzwRequest zzwRequest = new ZzwRequest(inputStream);
            String num1 = zzwRequest.getParameter("num1");
            String num2 = zzwRequest.getParameter("num2");
            System.out.println("num1=" + num1);
            System.out.println("num2=" + num2);
            System.out.println("zzwRequest=" + zzwRequest);

            ZzwResponse zzwResponse = new ZzwResponse(socket.getOutputStream());
            zzwResponse.setResponse("<h1>刀剑神域</h1>");
            outputStream = zzwResponse.getOutputStream();
            outputStream.write(zzwResponse.getResponse().getBytes());
            socket.close();//一定要确保socket关闭
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
                inputStream.close();
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  1. ZzwServlet接口
/**
 * @author 赵志伟
 * @version 1.0
 * 搭建结构, 由实现类写内容
 */
public interface ZzwServlet {
    void init() throws Exception;

    void service(ZzwRequest request, ZzwResponse response) throws IOException;

    void destroy();
}
  1. ZzwHttpServlet类
/**
 * @author 赵志伟
 * @version 1.0
 */
public abstract class ZzwHttpServlet implements ZzwServlet {
    public void service(ZzwRequest request, ZzwResponse response) throws IOException {
        //equalsIgnoreCase 比较字符串内容并忽略大小写
        if (request.getMethod().equalsIgnoreCase("GET")) {
            this.doGET(request, response);
        } else if (request.getMethod().equalsIgnoreCase("POST")) {
            this.doPost(request, response);
        }
    }
    
    //这里是模板设计模式,让 ZzwHttpServlet的子类来实现
    public abstract void doGET(ZzwRequest request, ZzwResponse response);

    public abstract void doPost(ZzwRequest request, ZzwResponse response);
}
  1. ZzwCalServlet实现类
public class ZzwCalServlet extends ZzwHttpServlet {

    public void doGET(ZzwRequest request, ZzwResponse response) {
        doPost(request, response);
    }

    public void doPost(ZzwRequest request, ZzwResponse response) {
        String num1 = request.getParameter("num1");
        String num2 = request.getParameter("num2");
        int sum = WebUtils.parseInt(num1, 0) + WebUtils.parseInt(num2, 0);
        try {
            OutputStream outputStream = response.getOutputStream();
            response.setResponse("<h1>" + num1 + "+" + num2 + "=" + sum + " ZzwTomcatVersion3</h1>");
            outputStream.write(response.getResponse().getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void init() throws Exception {

    }

    public void destroy() {

    }
}
  1. ZzwRequestHandler改进2
/**
 * @author 赵志伟
 * @version 1.0
 * ZzwRequestHandler 是一个线程对象
 * 用来处理 http请求
 */
@SuppressWarnings({"all"})
public class ZzwRequestHandler implements Runnable {
    private Socket socket;

    public ZzwRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        //对客户端和浏览器进行IO操作
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();

            ZzwRequest zzwRequest = new ZzwRequest(inputStream);
            ZzwResponse zzwResponse = new ZzwResponse(outputStream);

            //创建ZzwCalServlet对象
            ZzwCalServlet zzwCalServlet = new ZzwCalServlet();
            zzwCalServlet.doGET(zzwRequest, zzwResponse);

            socket.close();//一定要确保socket关闭
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
                inputStream.close();
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2.4 容器实现

在这里插入图片描述
在这里插入图片描述
因为我们的Servlet是自己设计的, web.xml检查报红, 直接忽略, 同时要在target/classes目录手动拷贝一份web.xml(平时是自动拷贝). 然后把上面的CalServlet的配置注释掉, 不然反射实例的时候会出错.
在这里插入图片描述
如果想要取消报红, 那么
在这里插入图片描述

shortcuts: ctrl+b 定位到声明的位置
在这里插入图片描述

  1. ZzwTomcatVersion3
/**
 * @author 赵志伟
 * @version 1.0
 * 第3版Tomcat, 实现通过xml+反射 初始化容器
 */
@SuppressWarnings({"all"})
public class ZzwTomcatVersion3 {
    /*
        容器 servletMapping
         - ConcurrentHashMap
         - HashMap
               key        -       value
           ServletName          Servlet实例
     */
    //因为ZzwHttpServlet是所有业务Servlet的父类, 所以这里可以存放子类Servlet的对象
    public static final ConcurrentHashMap<String, ZzwHttpServlet> servletMapping
            = new ConcurrentHashMap<String, ZzwHttpServlet>();
    /*
        容器 servletMapping
         - ConcurrentHashMap
         - HashMap
               key       -      value
           url-pattern       ServletName
     */
    public static final ConcurrentHashMap<String, String> servletUriMapping
            = new ConcurrentHashMap<String, String>();

    //直接对两个容器进行初始化
    public void init() {
        //读取web.xml文件 => dom4j
        // 得到web.xml文件[拷贝一份]的路径 => 定位到target/classes
        //D:/idea_project/zzw_springmvc/zzwtomcat/target/classes/
        String path = ZzwTomcatVersion3.class.getResource("/").getPath();
        //使用dom4j完成xml文件的提取
        // 获取解析器
        SAXReader reader = new SAXReader();
        try {
            Document document = reader.read(new File(path + "web.xml"));// 加不加/都行
            System.out.println(document);
            // 获取rootElement
            Element rootElement = document.getRootElement();
            List<Element> elements = rootElement.elements();
            // 遍历元素并过滤出 servlet servlet-mapping
            for (Element element : elements) {
                if ("servlet".equalsIgnoreCase(element.getName())) {
                    //如果这是一个servlet配置
                    //System.out.println("servlet\n" + element);
                    String servletName = element.element("servlet-name").getText();
                    String servletClass = element.element("servlet-class").getText();
                    //使用反射将该servlet实例放入到servletMapping集合
                    servletMapping.put(servletName, (ZzwHttpServlet) Class.forName(servletClass).newInstance());

                } else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
                    //如果这是一个servlet-mapping配置
                    //System.out.println("servlet-mapping\n" + element);
                    Element urlPattern = element.element("url-pattern");
                    Element serlvetName = element.element("servlet-name");
                    servletUriMapping.put(urlPattern.getText(), serlvetName.getText());
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        System.out.println(servletMapping);
        System.out.println(servletUriMapping);
    }

    public static void main(String[] args) {
        //String path = ZzwTomcatVersion3.class.getResource("/").getPath();
        //System.out.println(path);
        ZzwTomcatVersion3 zzwTomcatVersion3 = new ZzwTomcatVersion3();
        zzwTomcatVersion3.init();
        //启动zzwTomcatVersion3容器
        zzwTomcatVersion3.run();
    }

    启动ZzwTomcatVersion3容器, 这只是一个普通方法
    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            while (!serverSocket.isClosed()) {
                System.out.println("服务端Tomcat version3 在 8080端口 等待连接");
                Socket socket = serverSocket.accept();
                ZzwRequestHandler zzwRequestHandler = new ZzwRequestHandler(socket);
                new Thread(zzwRequestHandler).start();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  1. ZzwRequestHandler改进3
/**
 * @author 赵志伟
 * @version 1.0
 * ZzwRequestHandler 是一个线程对象
 * 用来处理 http请求
 */
@SuppressWarnings({"all"})
public class ZzwRequestHandler implements Runnable {
    private Socket socket;

    public ZzwRequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            ZzwRequest zzwRequest = new ZzwRequest(socket.getInputStream());//socket关闭后,这些流也就没有了
            ZzwResponse zzwResponse = new ZzwResponse(socket.getOutputStream());

            //1.得到uri => servletUriMapping的urlPattern
            String uri = zzwRequest.getUri();
            String servletName = ZzwTomcatVersion3.servletUriMapping.get(uri);
            //2.uri->得到servletName->得到servlet实例, 其真正的运行类型是其子类 ZzwCalServlet
            //  细节:这里的servletName可能是空, ConcurrentHashMap的get(空)会报错, HashMap的get(空)不会报错
            //  解决方案一: 换成HashMap
            //  解决方案二: 如果是空换成空串
            servletName = (servletName == null) ? "" : servletName;
            ZzwHttpServlet zzwHttpServlet = ZzwTomcatVersion3.servletMapping.get(servletName);
            //3.调用service方法, 通过OOP的动态绑定机制 调用到真正运行类型的doGet或者doPost
            if (zzwHttpServlet != null) {
                zzwHttpServlet.service(zzwRequest, zzwResponse);
            } else {
                //请求的地址不存在, 返回404
                zzwResponse.setResponse("<h1>404 Not Found!</h1>");
                OutputStream outputStream = zzwResponse.getOutputStream();
                outputStream.write(zzwResponse.getResponse().getBytes());
                outputStream.flush();
                outputStream.close();
            }
            socket.close();//一定要确保socket关闭
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2.5 访问静态页面

在ZzwTomcatHandler类的try代码块的中上位置加入以下代码
在这里插入图片描述

if (!WebUtils.isExist(uri.substring(1))) {
                OutputStream outputStream = zzwResponse.getOutputStream();
                zzwResponse.setResponseBody("404 Not Found!");
                outputStream.write(zzwResponse.getResponse().getBytes());
                outputStream.flush();
                outputStream.close();
                socket.close();
                return;
            }
if (WebUtils.isHtml(uri)) {
                String html = WebUtils.readHtml(uri.substring(1));
                OutputStream outputStream = zzwResponse.getOutputStream();
                zzwResponse.setResponseBody(html);
                outputStream.write(zzwResponse.getResponse().getBytes());
                outputStream.flush();
                outputStream.close();
                socket.close();
                return;
            }

在WebUtils工具类中增加以下代码

public static boolean isExist(String fileName) {
        String path = WebUtils.class.getResource("/").getPath();
        File file = new File(path + fileName);
        return file.exists();
    }

public static boolean isHtml(String uri) {
        return uri.endsWith(".html");
    }

    //读取该网页
    public static String readHtml(String htmlName) {
        String path = WebUtils.class.getResource("/").getPath();
        StringBuffer stringBuffer = new StringBuffer();
        String line = "";
        try {
	        //bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(path + fileName), "utf-8"));
            BufferedReader bufferedReader = new BufferedReader(new FileReader(path + htmlName));
            while ((line = bufferedReader.readLine()) != null) {
                stringBuffer.append(line);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return stringBuffer.toString();
    }
```

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~ 小团子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值