迷你HTTP服务器+小型博客

项目内容

1.手写一个HTTP服务器
2.手写一个博客系统运行在HTTP服务器上

项目环境/平台

Windows10+IDEA+JDK1.8+Maven+MySQL

目录

1.Http请求解析
2.Http响应
3.静态Controller处理
4.自定义类加载器
5.开启服务器Server类
6.博客代码
博客提交页面
博客列表页面
博客文章详情

Http请求

首先编写代码进行解析请求
请求包含以下部分
请求行 GET /aaa.html?id=1&password=ppp&name=10 HTTP/1.1
请求头 Host: www.aaa.com Accept:text/html等信息
请求体 请求体(get方法没有请求体)
特殊格式的请求 “GET /hello?target=%E4%B8%AD%E5%9B%BD HTTP/1.0”

解析请求行
请求的方法method
请求的参数urlall,请求的参数
请求的版本号protocol
解析请求头
请求头Host:网址
请求头Accept:text/html
请求的编码
1.首先定义请求类保存请求行请求头参数
2.在解析请求头的时候判断是post还是get方法
3.进行分步解析

package com.github7.request;
import java.io.*;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

public class Request {
    String url;
    String protocol;
    String method;
    Map<String, String> headers = new HashMap<>();
    Map<String, String> requestParam = new HashMap<>();

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }

    public void setHeaders(String key, String value) {
        this.headers.put(key, value);
    }

    public void setRequestParam(String key, String value) {
        this.requestParam.put(key, value);
    }

    public void setMethod(String method) throws IOException {
        this.method = method.toUpperCase();
        if (this.method.equals("POST") || this.method.equals("GET")) {
            return;
        }
        throw new IOException("不支持的方法");
    }

    public void setUrl(String url) throws UnsupportedEncodingException {
        this.url = URLDecoder.decode(url, "UTF-8");
    }

    public String getUrl() {
        return url;
    }

    public String getProtocol() {
        return protocol;
    }

    public String getMethod() {
        return method;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public Map<String, String> getRequestParam() {
        return requestParam;
    }

    //请求方法
    public static Request parse(InputStream inputStream) throws IOException {
        //传入浏览器请求的字节流,Server解析
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        Request request = new Request();
        //设置请求行参数
        praseRequestLine(bufferedReader, request);
        //设置请求头参数
        praseRequestHeader(bufferedReader, request);
        return request;
    }
    
    //请求第一行 请求行:方法 请求URL HTTP版本号
    //GET /aaa.html?id=1&password=ppp&name=10 HTTP/1.1

    private static void praseRequestLine(BufferedReader bufferedReader, Request request) throws IOException {
        String firstLine = bufferedReader.readLine();
        //第一行参数是以" "进行分割。然后得到请求方法,请求参数,请求版本号
        //比如请求行为GET /aaa.html?id=1&name=10 HTTP/1.1
        String[] fragments = firstLine.split(" ");
        //fragmens至少存在三个参数
        if (fragments.length < 3) {
            throw new IOException("错误的请求行,参数不够");
        }
        //第一个参数是请求方式
        String method = fragments[0];
        request.setMethod(method);
        //第二个参数是总的url请求参数
        String urlAll = fragments[1];
        //第三个参数是版本号
        String protocol = fragments[2];
        request.setProtocol(protocol);
        //获取url请求页面,是urlAll里面的
        String[] qFragmens = urlAll.split("\\?");
        request.setUrl(qFragmens[0]);
        //获取请求的参数 qFragmens里面的第二个参数
        //id=1 & name=10
        if (qFragmens.length > 1) {
            String[] mess = qFragmens[1].split("&");
            for (int i = 0; i < mess.length; i++) {
                String[] keyvalue = mess[i].split("=");
                String key = keyvalue[0];
                String value = "";
                if (keyvalue.length > 1) {
                    value = URLDecoder.decode(keyvalue[1], "utf-8");
                }
                //将获取到的键值对添加进去
                request.setRequestParam(key, value);
            }
        }

    }

    //请求第二部分,直到遇到空行结束
    // 请求头有很多信息如下:
    // Host:www.baidu.com
    // Accept:text/html
    private static void praseRequestHeader(BufferedReader bufferedReader, Request request) throws IOException {
        String secondLine;
        //将请求头的信息全部保存到HashMap中。
        while ((secondLine = bufferedReader.readLine()) != null && secondLine.trim().length() != 0) {
            String[] keyValue = secondLine.split(":");
            //分割之后可能会出现空格。所以要去掉空格
            String key = keyValue[0];
            String value = "";
            if (keyValue.length > 1) {
                value = keyValue[1].trim();
            }
            //将信息头保存到请求头里面
            request.setHeaders(key, value);
        }
    }

    @Override
    public String toString() {
        return "Request{" +
                "请求页面='" + url + '\'' +
                ", 版本号='" + protocol + '\'' +
                ", 请求方法='" + method + '\'' +
                ", 请求头=" + headers +
                ", 请求行参数=" + requestParam +
                '}';
    }
}

Http响应

响应主要包括三部分 1.响应行 2.响应头 3.响应体
第一行版本信息和状态码:HTTP/1.1 200 OK
第二行响应头里面有:
Server HTTP 服务器编号
Date Wed, 07 Aug 2019 10:14:53 GMT 指定格式的响应时间说明
Content-Type 响应体的格式说明
Content-Length 响应体的长度说明
常见状态码
200 OK
302 Temporarily Moved
400 Bad Request
404 Not Found
405 Method Not Allowed
500 Internal Server Error
编码
1.首先我们创建一个输出流数组,保存所有的信息
2.创建一个枚举类保存状态码
3.设置我们的响应服务器,类型等信息。
4.设置好之后,将响应行,响应头,响应体写到字节数组中去
5.将字节数组中的内容拷贝到输出流中,并设置长度。

package com.github7.response;

import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.*;

public class Response {
    public static Response start(OutputStream outputStream) throws IOException {
        Response response = new Response();
        response.setServer();
        response.setOutputStream(outputStream);
        response.setContentType("text/html");
        response.setDate();
        return response;
    }

    //保存输出流
    private byte[] resByteArray = new byte[8192];
    private OutputStream outputStream;

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

    private int contentLength = 0;
    //保存以下信息Content-Type:Content-Lenth:Data:Server:
    private Map<String, String> headers = new HashMap<>();

    //将默认状态置为200 ok
    private State state = State.OK;

    public void setState(State state) {
        this.state = state;
    }

    private void setServer() {
        headers.put("Server", "LR/1.0");
    }


    public void setContentType(String contentType) {
        headers.put("Content-Type", contentType + ";charset=utf-8");
    }

    private void setDate() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("E,dd MMM yyyy HH:mm:ss z");
        headers.put("Date", dateFormat.format(new Date()));
    }

    //将信息写入到响应输出流中的方法。
    public void write(byte[] bytes, int off, int len) throws IOException {
        if (contentLength + len > 8192) {
            throw new IOException("超过请求头最大长度");
        }
        //将写入的东西先保存在自己的byte数组中
        System.arraycopy(bytes, off, resByteArray, contentLength, len);
        contentLength += len;
    }

    public void write(byte[] bytes, int len) throws IOException {
        write(bytes, 0, len);
    }

    public void write(byte[] bytes) throws IOException {
        write(bytes, bytes.length);
    }

    //格式化响应
    public void print(String string, Object... args) throws IOException {
        write(new Formatter().format(string, args).toString().getBytes("utf-8"));
    }

    public void println(Object o) throws IOException {
        print("%s%n", o.toString());
    }

    public void flush() throws IOException {
        headers.put("Content-Length", String.valueOf(contentLength));
        sendResponseLine();
        sendResponseHeads();
        outputStream.write(resByteArray, 0, contentLength);
    }

    //将响应行加入到输出流中
    public void sendResponseLine() throws IOException {
        String responseLine = String.format("HTTP/1.0 %d %s \r\n", state.getCode(), state.getReason());
        outputStream.write(responseLine.getBytes());
    }

    //将响应头加入到输出流中
    public void sendResponseHeads() throws IOException {
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            String header = String.format("%s:%s\r\n", entry.getKey(), entry.getValue());
            outputStream.write(header.getBytes("utf-8"));
        }
        outputStream.write("\r\n".getBytes("UTF-8"));
    }

    @Override
    public String toString() {
        return "Response{" +
                " headers=" + headers +
                ", state=" + state +
                '}';
    }
}
package com.github7.response;

public enum State {
    OK(200, "OK"),
    Temporarily_Moved(302, "Temporarily Moved"),
    BAD_REQUEST(400, "BAD_REQUEST"),
    NOT_FOUND(404, "NOT_FOUND"),
    METHOD_NOT_ALLOWED(405,"Not_Allowed"),
    INTERNAL_SERVER_ERROR(500, "INTERNAL SERVER ERROR");

    private int code;
    private String reason;

    public int getCode() {
        return code;
    }

    public String getReason() {
        return reason;
    }

    State(int code, String reason) {
        this.code = code;
        this.reason = reason;
    }
}

创建父类Controller控制器

该类有两个方法默认执行的doGet方法

package com.github7.controller;

import com.github7.request.Request;
import com.github7.response.Response;
import com.github7.response.State;

import java.io.IOException;

public class Controller {
    public void doGet(Request request, Response response) throws IOException {
        if (request.getProtocol().endsWith("1.0")) {
            response.setState(State.METHOD_NOT_ALLOWED);
            response.println("版本号不支持,仅支持1.1");
        } else {
            response.setState(State.BAD_REQUEST);
            response.println("请求错误,仅支持HTTP/1.1");
        }
    }

    public void doPost(Request request, Response response) throws IOException {
        this.doGet(request, response);
    }
}

静态页面加载

StaticContraller继承自Controller覆写其方法
编码
1.首先我们根据url获取要链接的静态页面。
2.如果要获取的页面是当前路径/就加载指定静态页面
3.读取文件内容,将文件写到byte数组中
4.根据文件后缀比如.html设置ContentType

package com.github7.controller;

import com.github7.request.Request;
import com.github7.response.Response;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

public class StaticController extends Controller {
    private static final String HOME = System.getenv("LR_HOME");
    //保存ContentType  支持三种类型用HashMap先保存起来
    private final Map<String, String> Content_TYPE = new HashMap<String, String>() {
        {
            put("html", "text/html");
        }
    };

    @Override
    public void doPost(Request request, Response response) throws IOException {
        this.doGet(request, response);
    }

    @Override
    public void doGet(Request request, Response response) throws IOException {
        //1.获取文件地址 request.getUrl()得到的是/hh.html
        String filename = getFileName(request.getUrl());
        //2.开始读取文件内容
        System.out.println(filename);
        InputStream inputStream = new FileInputStream(filename);
        //缓冲流 一次读取1024字节内容
        byte[] buf = new byte[1024];
        int len;
        while ((len = inputStream.read(buf)) != -1) {
            response.write(buf, 0, len);
        }
        //3.获取类型content-type,根据文件名的后缀
        String suffix = getSuffix(filename);
        String contype = getContentType(suffix);
        //然后设置响应头里面的contenttype
        response.setContentType(contype);
    }

    private String getContentType(String suffix) {
        String contentType = Content_TYPE.get(suffix);
        if (contentType == null) {
            //说明没有这种类型的文件
            contentType = "text/html";
        }
        return contentType;
    }

    private String getSuffix(String filename) {
        //找到最后一个点,点后面的就是文件类型
        int index = filename.lastIndexOf('.');
        if (index == -1) {
            return null;
        }
        //返回文件名后缀
        return filename.substring(index + 1);
    }

    private String getFileName(String url) {
        if (url.equals("/")) {
            url = "/index.html";
        }
        return "F:\\\\httpProject\\webapps\\"+ url.replace("/", File.separator);
    }
}


自定义类加载器

首先要定义一个类加载器,我们如果在静态页面找不到页面。就找动态的类进行加载
1.根据传进来的url进行解析得到我们的文件
2.通过继承ClassLoader找到我们所需要的文件.class文件。
3.读取内容返回生成Class对象
4.通过返回的Class对象创建controller实例

package com.github7.classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            return super.findClass(name);
        } catch (ClassNotFoundException e) {

            //1.根据类名称找到name对应的.class文件
            //String fileLocation =  "F:\\\\LRhttp\\webapps\\WEB-INF\\classes\\"+name.replace("/","")+".class";
            String fileLocation = "F:\\\\httpProject\\target\\classes\\" + name.replace("/", "") + ".class";
            File file = new File(fileLocation);
            if (file.exists()) {
                //2.读取文件内容。
                byte[] buf = new byte[8192];
                int len = 0;
                try {
                    len = new FileInputStream(fileLocation).read(buf);
                } catch (IOException e1) {
                   throw new ClassNotFoundException("文件出错",e);
                }
                //3.调用defineClass 转为Class<?>
                System.out.println("类名" + name.replaceAll("/", ""));
                return defineClass(name.replaceAll("/", ""), buf, 0, len);
            } else {
               throw new ClassNotFoundException("类没找到");
            }
        }
    }
}

Main类 Server端

1.创建ServerSocket监听端口
创建Socket从网络中读取和写入数据,不同计算机上的两个应用可以通过连接发送和接受字节流。
2.分别创建Request和Response引用指向socket的输入输出流
3.获取url判断调用静态页面还是加载动态类
4.创建实例调用doGet/doPost方法
5.调用flush方法,更新数据

package com.github7.server;

import com.github7.classloader.MyClassLoader;
import com.github7.controller.Controller;
import com.github7.controller.StaticController;
import com.github7.request.Request;
import com.github7.response.Response;
import com.github7.response.State;
import org.dom4j.DocumentException;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server {

    public final Controller staticController = new StaticController();

    public Server() throws DocumentException {
    }

    public static void main(String[] args) throws IOException, DocumentException {
        Server server = new Server();
        server.run(1234);
    }

    public void run(int port) throws IOException {
        ExecutorService pool = Executors.newFixedThreadPool(20);
        //监听端口
        ServerSocket serverSocket = new ServerSocket(port);
        // 查看端口netstat -ano
        while (true) {

            Socket socket = serverSocket.accept();
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        //初始化
                        Request request = Request.parse(socket.getInputStream());
                        Response response = Response.start(socket.getOutputStream());
                        //查找请求url是否存在
                        String filelocation = request.getUrl();
                        String filename = "";
                        if (filelocation.equals("/")) {
                            filename = "F:\\\\httpProject\\webapps\\index.html";
                        }
                        filename = "F:\\\\httpProject\\webapps\\" + filelocation.replace("/", File.separator);
                        //根据 URL 的不同,用不用的 controller 去处理
                        File file = new File(filename);
                        Controller controller = null;
                        if (file.exists()) {
                            System.out.println("静态controller");
                            controller = staticController;
                        } else {
                            System.out.println("动态controller");
                            Class<?> cla = null;
                            String name = request.getUrl().replace("/", "");
                            if (name.equals("article")) {
                                cla = new MyClassLoader().loadClass("ArticleController");
                            } else if (name.equals("postArticle")) {
                                cla = new MyClassLoader().loadClass("PostController");
                            } else {
                                cla = new MyClassLoader().loadClass(name);
                            }
                            if (cla != null) {
                                System.out.println(cla.getName());
                                controller = (Controller) cla.newInstance();
                            }
                        }
                        //动态也找不到 返回错误
                        if (controller == null) {
                            response.setState(State.NOT_FOUND);
                            response.println("<h1>" + State.NOT_FOUND.getCode() + "    " + State.NOT_FOUND.getReason() + " 页面没有找到</h1>");
                        } else {
                            if (request.getMethod().equals("GET")) {
                                controller.doGet(request, response);
                            } else if (request.getMethod().equals("POST")) {
                                controller.doPost(request, response);

                            } else {
                                //不支持的方法
                                response.setState(State.METHOD_NOT_ALLOWED);
                                response.println(State.METHOD_NOT_ALLOWED.getReason());
                            }
                        }
                        response.flush();
                        System.out.println(request.toString());
                        System.out.println(response.toString());
                        System.out.println();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

小型博客

博客主要由以下部分构成
post.html博客提交页面
PostController博客提交:获取到html表单内容,往数据库中写入数据
ListController博客列表页面:获取数据库中的所有标题已经作者
ArticleController博客文章详情:点击超链接,连接数据库获取数据信息。

post.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>发表文章</title>
    <script>
        function getNewDate() {
            //创建日期对象
            var date = new Date();
            //拼接年月日
            var datestr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()
                + "&nbsp&nbsp" + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
            //通过id来获取span对象
            var span = document.getElementById("span_id");
            //设置span对象的innerHTML属性
            span.innerHTML = datestr;
        }

        window.setInterval("getNewDate()", 1000);
    </script>
</head>

<body>
<center><h1>发表文章</h1><a href='/ListController'>首页</a>
    <h4><span id="span_id"></span></h4>
    <form action="/postArticle" method="get">
        标题:<input type="text" name="title"  /></br>
        作者:<input type="text" name="author" /></br>
        </br><textarea name="content" style="width:600px;height:400px;" ></textarea> </br>
        <input type="submit" name="" value="提交"/>
    </form>
</center>
</body>
</html>

获取html表单内容,发表文章

/*
   Author:linrui
   Date:2019/8/26
   Content:
*/

import com.github7.controller.Controller;
import com.github7.request.Request;
import com.github7.response.Response;
import com.github7.response.State;

import java.io.IOException;
import java.sql.*;
import java.util.UUID;

public class PostController extends Controller {
    @Override
    public void doGet(Request request, Response response) throws IOException {
        this.doPost(request, response);
    }
    @Override
    public void doPost(Request request, Response response) throws IOException {
        //获取url的id 和内容
        String id = UUID.randomUUID().toString();//随机ID
        String title = request.getRequestParam().get("title");
        String author = request.getRequestParam().get("author");
        String content = request.getRequestParam().get("content");
        if(title.length()==0){
            response.println("<center>");
            response.println("输入标题</br>");
            response.println("</h4><a href='/post.html'>点击返回发表文章</a></h4></center>");
            response.println("</center>");
            return;
        }
        if(author.length()==0){
            response.println("<center>");
            response.println("对不起,未输入作者姓名</br>");
            response.println("</h4><a href='/post.html'>点击返回发表文章</a></h4></center>");
            response.println("/<center>");
            return;
        }
        if(content.length()==0){
            response.println("<center>");
            response.println("未输入内容</br>");
            response.println("</h4><a href='/post.html'>点击返回发表文章</a></h4></center>");
            response.println("</center>");
            return;
        }
        //然后将所有内容存储到数据库中
        //连接到数据库
        Connection connection = null;
        Statement statement = null;
        PreparedStatement preparedStatement = null;
        String url = "jdbc:mysql://localhost:3307/blogs?useSSL=false";
        String sql = "insert into blog values(?,?,?,?)";
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection(url, "root", "aaaaaa");
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, id);
            preparedStatement.setString(2, title);
            preparedStatement.setString(3, author);
            preparedStatement.setString(4, content);
            preparedStatement.executeUpdate();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        //重定向首页
        response.setState(State.Temporarily_Moved);
        System.out.println("进入到PostController");
        response.setHeaders("Location", "/article?id=" + id);
    }
}

文章 title


import com.github7.controller.Controller;
import com.github7.request.Request;
import com.github7.response.Response;

import java.io.IOException;
import java.sql.*;

public class ListController extends Controller {
    @Override
    public void doGet(Request request, Response response) throws IOException {
        this.doPost(request, response);
    }
    @Override
    public void doPost(Request request, Response response) throws IOException {

        response.println("<center><h1>所有文章</h1>");
        response.println("</h4><a href='/post.html'>发表文章</a></h4></center>");
        Connection connection = null;
        Statement statement = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        String url = "jdbc:mysql://localhost:3307/blogs?useSSL=false";
        String sql = "select article_id,article_title,article_author from blog";
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection(url, "root", "aaaaaa");
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                String id = resultSet.getString(1);
                String title = resultSet.getString(2);
                String author=resultSet.getString(3);
                response.println("<li><a href='/article?id=" + id + "'>" + title + "</a>"+"&nbsp;&nbsp;&nbsp;作者:"+author+"</li>");

            }

        } catch (ClassNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                resultSet.close();
                preparedStatement.close();
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

文章详情页面


import com.github7.controller.Controller;
import com.github7.request.Request;
import com.github7.response.Response;

import java.io.IOException;
import java.sql.*;

public class ArticleController extends Controller {
    @Override
    public void doGet(Request request, Response response) throws IOException {
        this.doPost(request, response);
    }

    @Override
    public void doPost(Request request, Response response) throws IOException {
        //从url 获取id
        String id = request.getRequestParam().get("id");
        Connection connection = null;
        Statement statement = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        String url = "jdbc:mysql://localhost:3307/blogs?useSSL=false";
        String sql = "select article_title,article_author,article_content from blog where article_id='" + id + "'";
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection(url, "root", "aaaaaa");
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            response.println("</h6><a href='/ListController'>回到首页</a></h6>");
            while (resultSet.next()) {
                String title = resultSet.getString(1);
                String author = resultSet.getString(2);
                String content = resultSet.getString(3);
                response.println("<h3><center>" + title + "</center></h3>");
                response.println("<h5><center>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" +
                        "作者:" + author + "</center></h5>");
                response.println(content);
            }
        } catch (ClassNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                resultSet.close();
                preparedStatement.close();
                connection.close();
            } catch (SQLException e) {
                response.println(e.getMessage());
            }
        }

    }
}

首页
在这里插入图片描述
文章title页
在这里插入图片描述

文章详情页
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值