小型web服务器的编写

此代码实现了web服务器的基本功能编写

首先我们来在来介绍一下web服务器**
tomcat现在较为流行的服务器

这里来一个小插曲,想必大家都使用过tomcat,在使用tomcat的时候,我们首先要编写一个servlet类,然后让这个类来实现servlet接口,然后我们重写这个接口中的service方法,在service接口中来编写我们的逻辑代码,然后我们通过在web.xml中配置这个servlet的全路径类名.最后在我们访问到这个servlet的url路径地址的时候,这个servlet就会执行, 相信我们都会使用,但是我们想过没有,为什么我们要实现servlet接口(当然也可以继承HttpServlet类,道理是一样的)
在servlet中我们没有实现任何逻辑代码的编写,仅仅定义了一些基本的方法,
为什么我们直接创建一个类来编写service不可以,而必须要实现servlet接口这个servlet才可以被执行.

/**
 * @author 史锦泽
 * @date 2020-08-23 16:56
 */
public class servlet0101 implements Servlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
				  System.out.println("我是servlet");
    	}
    }
    

这就要说说我们的反射了
tomcat之所以能够执行我们编写的servlet,就是因为,在tomcat启动过后,就会一直监听浏览器开启的端口,一旦有请求来了,就会获取到请求地址的url,然后去web.xml中的url-parren去寻找有没有配对的servlet,如果有,那么就根据servlet-class去找到这个servlet的全路径类名,拿到这个类的全路径类名,就会通过class.forName(“全路径类名”),去获取到这个类的全路径类名,然后通过 aClass.getConstructor().newInstance();去创建这个对象,那么问题来了,我们可以获取到这个对象,我们用什么去接受这个对象,tomcat是已经写好了的,它不可能知道我们创建的servlet是什么名字,例如我们写了一个LoginServlet,我们可以用

LoginServlet  loginServlet=aClass.getConstructor().newInstance();

但是tomcat不知道我们的servlet名字是什么,所以我们所些的servlet都需要实现servlet或者去继承servlet的子类,这个我们就可以用servlet去接受这个对象,然后通过多态用父接口去调用我们自己编写的子类servlet,这样就可以实现无论我们servlet使用了什么名字.只要实现了servlet这个接口,tomcat就可以执行我们自己的servlet,你了解了吗?

// 就像这样
  Servlet servlet=(Servlet)aClass.getConstructor().newInstance();
  servlet.servlce();

介绍完了上面的问题.我们来谈谈浏览器
现在的软件通常分为BS架构和CS架构,但是随着互联网的发展,网络传输的速率越来越高,因此就给BS架构带来了一些便利,在网速达到一定程度的时候,我们的大部分工作都可以通过浏览器来实现,即使有些游戏也一样,如果如果网络传输的速度跟得上画面渲染的速度,那么像一些3d游戏,类似吃鸡的游戏同样可以在浏览器中完成,或许未来的某一天,我们的电脑上可能仅仅只拥有一个浏览器,而不需要其他软件;
对于浏览器,也是一个和其他软件一样,例如网易云,qq,微信一样.简单来说,我们在上网的时候,浏览器会创建一个socket套接字,然后根据我们输入的url来确定端口,默认的是通过80端口,通过这个套接字来向指定的主机发送请求,

所谓的请求也就是一组支持指定格式的数据
包含了:请求行,请求头,请求空行,请求体

例如下面这样:

Accept
	text/plain, */*; q=0.01
Accept-Encoding
	gzip, deflate, br
Accept-Language
	zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Cache-Control
	no-cache
Connection
	keep-alive
Cookie
	BAIDUID=1C21C1DB1657AD1D6EE2BB5E0C691A6F:FG=1; BIDUPSID=1C21C1DB1657AD1DE27773F073D67933; PSTM=1597720357; BD_UPN=13314752; COOKIE_SESSION=338566_0_3_3_3_2_0_0_3_1_0_1_338555_0_0_0_1598163160_0_1598163160%7C5%230_0_1598163160%7C1; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; BDRCVFR[Hp1ap0hMjsC]=mk3SLVN4HKm; delPer=0; BD_CK_SAM=1; PSINO=7; H_PS_PSSID=1455_32571_31660_32350_32046_32115_31709_32618_32508; H_PS_645EC=8b8aEH1b5RTfC0SPmE%2Frlc0moqtslcTW4o%2Fdbg88Yh1bURtX%2Blct34AoA2pU4iZCPUQn; BD_HOME=1
Host
	www.baidu.com
Pragma
	no-cache
Referer
	https://www.baidu.com/
User-Agent
	Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0
X-Requested-With
	XMLHttpRequest

其中包含了许多信息,例如请求地址,请求方式,支持的协议.编码信息,缓存信息
而我们知道要在代码中创建一个tcp 服务器对象去监听浏览器开启的端口,也就是80,我们也就能通过这个tcp服务器对象来获得这些数据,获取的代码如下:

/**
 * @author 史锦泽
 * @date 2020-08-23 16:10
 */
public class main1 {
    public static void main(String[] args)  throws Exception{
        ServerSocket serverSocket=new ServerSocket(80);
        Socket accept = serverSocket.accept();
        InputStream inputStream = accept.getInputStream();
        byte[] bytes=new byte[1024*1024];
        int len = inputStream.read(bytes);
        System.out.println(new String(bytes,0,len));
    }
}

接受到的request如下:

GET / HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: Idea-fa18f224=ff7774f8-f02e-43e2-8e7f-6dccdecbe600
Upgrade-Insecure-Requests: 1

我们用浏览器去搜索相当于浏览器帮助我们去发送请求,我们需要的是结果,所以浏览器会将服务器根据请求的信息返回的结果解析成我们想要看到的页面.
即浏览器在发送了请求信息过后就会监听这个端口,也就是80端口,我们通过套接字去发送一些数据,那么浏览器也会根据这这些数据去按照指定的格式去解析,最后将页面内容展现给我们
我们所发送的数据必须要遵循一些格式,也就是http协议

下面是一个简单的response

状态200
OK
版本HTTP/1.1
传输180 字节(大小 114 字节)
Content-length
	114
Content-type
	text/html

我们将这些数据通过80端口发送到浏览器,那么浏览器也就会成功解析为我们想要的数据

那么我们开始吧
第一步:

我们要为我们的web服务器编写一个web.xml

<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
    <servlet>
        <servlet-name>login</servlet-name>
        <servlet-class>com.sjz.service.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/login</url-pattern>
        <url-pattern>/g</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>regist</servlet-name>
        <servlet-class>com.sjz.service.RegisterServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>regist</servlet-name>
        <url-pattern>/regist</url-pattern>
        <url-pattern>/r</url-pattern>
    </servlet-mapping>
</web-app>

然后我们要去解析这个web.xml文件,通过url-pattern去找到相应的servlet全路径类名

package com.sjz.service;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

/**
 * @author 史锦泽
 * @date 2020-08-20 17:49
 */
public class WebApp {
   private static WebContext webContext;
    static {
        try {
            //SAX解析,流解析
            //1,获取解析工厂
            SAXParserFactory factory=SAXParserFactory.newInstance();
            //2,从解析工厂获取解析器
            SAXParser parser=factory.newSAXParser();
            //3,编写文档Document注册处理器
            //4,编写处理器
            WebHandler handler=new WebHandler();
            parser.parse(Thread.currentThread().getContextClassLoader().getResourceAsStream("p.xml"),handler);
            List<Entity> entities=handler.getEntities();
            List<Mapping> mappings=handler.getMappings();
            webContext=new WebContext(entities,mappings);
        }catch (Exception e){
            System.out.println("解析配置文件错误");
        }
    }

    //通过url获取配置文件的url
    public  static Servlet getSerVletFromUrl(String url){
        //        假设你输入了/login
        //获取数据
        String calssName=webContext.getClz("/"+url);
        Class aClass = null;
        try {
            aClass = Class.forName(calssName);
            System.out.println(aClass);
            Servlet servlet=(Servlet)aClass.getConstructor().newInstance();
            return servlet;
        } catch (Exception e) {
            return null;
        }
    }
}

编写处理器

package com.sjz.service;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 史锦泽
 * @date 2020-08-08 16:30
 */
public class WebHandler extends DefaultHandler {
    private List<Entity> entities = new ArrayList<>();
    private List<Mapping> mappings = new ArrayList<>();
    private Entity entity=new Entity();
    private Mapping mapping=new Mapping();
    private String tag;
    private boolean isMapping = false;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if (null != qName) {
            tag = qName;//存储标签名
            if (qName.equals("servlet")) {
                entity = new Entity();
                isMapping = false;
            } else if (tag.equals("servlet-mapping")) {
                mapping = new Mapping();
                isMapping = true;
            }
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contens = new String(ch, start, length).trim();
        if (null != tag) {
            if (isMapping) {
                if (tag.equals("servlet-name")) {
                    mapping.setName(contens);
                } else if (tag.equals("url-pattern")) {
                    mapping.addPattern(contens);
                }
            } else {
                if (tag.equals("servlet-name")) {
                    entity.setName(contens);
                } else if (tag.equals("servlet-class")) {
                    entity.setClz(contens);
                }
            }

        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if (null != qName) {
            if (qName.equals("servlet")) {
                entities.add(entity);
            }else if (qName.equals("servlet-mapping")){
                mappings.add(mapping);
            }
            tag = null;//tag丢弃
        }
    }

    public List<Entity> getEntities() {
        return entities;
    }

    public void setEntities(List<Entity> entities) {
        this.entities = entities;
    }

    public List<Mapping> getMappings() {
        return mappings;
    }

    public void setMappings(List<Mapping> mappings) {
        this.mappings = mappings;
    }

    public Entity getEntity() {
        return entity;
    }

    public void setEntity(Entity entity) {
        this.entity = entity;
    }

    public Mapping getMapping() {
        return mapping;
    }

    public void setMapping(Mapping mapping) {
        this.mapping = mapping;
    }

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public boolean isMapping() {
        return isMapping;
    }

    public void setMapping(boolean mapping) {
        isMapping = mapping;
    }
}

获取全路径类名

package com.sjz.service;

import com.sjz.service.Entity;
import com.sjz.service.Mapping;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author 史锦泽
 * @date 2020-08-08 18:41p'
 */
public class WebContext {
    private List<Entity> entities=null;
    private List<Mapping> mappings=null;
    //key-->servlet-name,value-->servlet-class
    private Map<String,String> entityMap =new HashMap<>();
    //key-->url-pattern,value-->servlet-name
    private Map<String,String> mappingMap =new HashMap<>();
    public WebContext(List<Entity> entities, List<Mapping> mappings) {
        this.entities = entities;
        this.mappings = mappings;
        for (Entity entity : entities) {
            entityMap.put(entity.getName(),entity.getClz());
        }
        for (Mapping mapping : mappings) {
            Set<String> patterns = mapping.getPatterns();
            for (String pattern : patterns) {
                mappingMap.put(pattern,mapping.getName());
            }
        }
    }
/**
 * @author:  史锦泽
 * @version 创建时间:2020/8/8
 * @方法作用:通过url的路径找到了对应的class
 */
    public  String getClz(String pattern){
        String name=mappingMap.get(pattern);
        return entityMap.get(name);
    }

}

实体

package com.sjz.service;

/**
 * @author 史锦泽
 * @date 2020-08-08 16:22
 */
public class Entity {
    private String name;
    private String clz;

    public Entity(String name, String clz) {
        this.name = name;
        this.clz = clz;
    }

    public Entity() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClz() {
        return clz;
    }

    public void setClz(String clz) {
        this.clz = clz;
    }

    @Override
    public String toString() {
        return "Entity{" +
                "name='" + name + '\'' +
                ", clz='" + clz + '\'' +
                '}';
    }
}

package com.sjz.service;

import java.util.HashSet;
import java.util.Set;

/**
 * @author 史锦泽
 * @date 2020-08-08 16:24
 */
public class Mapping {
    private String name;
    private Set<String> patterns;
    public Mapping(){
        patterns=new HashSet<String>();
    }

    public Mapping(String name, Set<String> patterns) {
        this.name = name;
        this.patterns = patterns;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<String> getPatterns() {
        return patterns;
    }

    public void setPatterns(Set<String> patterns) {
        this.patterns = patterns;
    }

    public void addPattern(String pattern){
        this.patterns.add(pattern);
    }


    @Override
    public String toString() {
        return "Mapping{" +
                "name='" + name + '\'' +
                ", patterns=" + patterns +
                '}';
    }
}

封装request

package com.sjz.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.security.PublicKey;
import java.util.*;

/**
 * 封装请求协议,获取method url 以及请求参数
 *
 * @author 史锦泽
 * @date 2020-08-09 15:27
 */
public class Request {
    //协议信息
    private String requestInfo;
    //请求方式
    private String method;
    //请求url
    private String url;
    //请求的参数
    private String queryStr;
    private Map<String, List<String>> parameterMap;
    private final String CRLF = "\r\n";


    public Request(Socket client) throws IOException {
        this(client.getInputStream());
        parseRequestInfo();
    }

    public Request(InputStream is) {
        parameterMap = new HashMap<>();
        byte[] datas = new byte[1024 * 1024];
        int len;
        try {
            len = is.read(datas);
            this.requestInfo = new String(datas, 0, len);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }

    //分解字符串
    private void parseRequestInfo() {
        this.method = this.requestInfo.substring(0, this.requestInfo.indexOf("/")).trim().toLowerCase();
        //1,获取/的位置
        int startidx = this.requestInfo.indexOf("/") + 1;
//        第二部获取http的位置
        int endldx = this.requestInfo.indexOf("HTTP/");
        //分割字符串
        this.url = this.requestInfo.substring(startidx, endldx).trim();
        //4获取问好的位置
        int queryldx = this.url.indexOf("?");
        if (queryldx > 0) {
            //表示存在参数请求
            String[] urlArry = this.url.split("\\?");
            this.url = urlArry[0];
            queryStr = urlArry[1];
        }
        if (method.equals("post")) {
            String qStr = this.requestInfo.substring(this.requestInfo.lastIndexOf(CRLF)).trim();
            if (null == queryStr) {
                queryStr = qStr;
            } else {
                queryStr += "&" + qStr;
            }
        }

        System.out.println("方法是" + method);
        System.out.println("url是" + url);
        System.out.println("参数是" + queryStr);
        convertMap();
    }

    //处理请求参数为Map
    private void convertMap() {
        if (null!=queryStr) {
            //分割字符串
            String[] keyValues = this.queryStr.split("&");
            for (String queryStr : keyValues) {
                String[] kv = queryStr.split("=");
                //我擦,这个牛逼了
                kv = Arrays.copyOf(kv, 2);
//            获取key和value
                String key = kv[0];
                String value = kv[1] == null ? null : decode(kv[1], "gbk");
                //存储到map中
                if (!parameterMap.containsKey(key)) {
                    parameterMap.put(key, new ArrayList<>());
                }
                parameterMap.get(key).add(value);
            }
        }


    }

    public String[] getParameterValues(String key) {
        List<String> list = this.parameterMap.get(key);
        if (null == list || list.size() < 1) {
            return null;
        }
        return list.toArray(new String[0]);
    }
    public String getParameter(String key){
        String[] values = getParameterValues(key);
        return values==null?null:values[0];
    }

    private String decode(String value,String enc){

        try {
            return java.net.URLDecoder.decode(value,enc);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    public String getMethod() {
        return method;
    }


    public String getUrl() {
        return url;
    }


    public String getQueryStr() {
        return queryStr;
    }
}

封装response

package com.sjz.service;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;

/**
 * @author 史锦泽
 * @date 2020-08-09 19:02
 */
public class Response {
    private BufferedWriter bw;
    //正文,
    private StringBuilder content;
    //协议头信息
    private StringBuilder heaferInfo;
    private int len=0;//正文的字节输
    private final String BLANK="";
   private final String CRLF="\r\n";
    public Response() {
        content =new StringBuilder();
        heaferInfo =new StringBuilder();
        len =0;
    }
    public Response(Socket cilent) {
        this();
        try {
            bw = new BufferedWriter(new OutputStreamWriter(cilent.getOutputStream()));
        } catch (IOException e) {
            e.printStackTrace();
            heaferInfo=null;
        }
    }

    public Response(OutputStream os) {
        this();
        bw = new BufferedWriter(new OutputStreamWriter(os));
    }

//    动态添加内容
    public Response print( String info){
        content.append(info);
        len+=info.getBytes().length;
        return this;
    }

    public Response println(String info){
        content.append(info).length();
        len+=(info).getBytes().length;
        return this;
    }
    //构建头信息
    private void createHeadeInfo(int code){
        heaferInfo.append("HTTP/1.1 ").append(BLANK);
        heaferInfo.append(code).append(BLANK);
        switch (code){
            case 200:
                heaferInfo.append(" OK ").append(CRLF);
                break;
            case 404:
                heaferInfo.append(" NOT FOUND ").append(CRLF);
                break;
            case 500:
                heaferInfo.append(" SERVICE ERROR ").append(CRLF);
                break;
        }
//        2.响应头,最后一行存在空行
        heaferInfo.append("Date").append(new Date()).append(CRLF);
        heaferInfo.append("Server").append("shsxt Server/0.0.1;charse=GBK").append(CRLF);
        heaferInfo.append("Content-type:text/html").append(CRLF);
        heaferInfo.append("Content-length:").append(len).append(CRLF);
        heaferInfo.append(CRLF);
    }

//    推送头信息
    public void pushToBrowser(int code) throws IOException {
        if (null==heaferInfo){
            code=505;
        }
        createHeadeInfo(code);
        bw.append(heaferInfo);
        bw.append(content);
        bw.flush();
    }

}

程序的主执行类

package com.sjz.service;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

/**
 * @author 史锦泽
 * @date 2020-08-08 19:50
 */
public class Service01 {
    private ServerSocket serverSocket;
    private boolean isRunning;

    public static void main(String[] args) {
        Service01 service01 = new Service01();
        service01.start();
    }

    //    启动服务
    public void start() {
        try {
            serverSocket = new ServerSocket(8888);
            isRunning=true;
            receive();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服务器启动失败");
        }
    }

    //    启动连接
    public void receive() {
        while (isRunning){
        try {
            Socket client = serverSocket.accept();

                new Thread(new Dispatcher(client)).start();


//            关注了内容和状态码

        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客户端错误");
            stop();
        }
        }
    }

    //    停止服务
    public void stop() {
        isRunning=false;
        try {
            this.serverSocket.close();
            System.out.println("服务器已停止");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用多线程,编写转发器

package com.sjz.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * @author 史锦泽
 * @date 2020-08-23 11:30
 */
public class Dispatcher implements Runnable{
    private Socket client;
    private Request request;
    private Response response;
    public Dispatcher(Socket client){
        this.client=client;
        try {
            request=new Request(client);
            response=new Response(client);
        } catch (IOException e) {
            e.printStackTrace();
            this.release();
        }

    }
    @Override
    public void run() {

        try {
            if (null ==request.getUrl()||request.getUrl().equals("")){
                System.out.println("niaho");
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("index.html");
                System.out.println(is);
                byte[] by=new byte[1024*1024];
                int len = is.read(by);
                System.out.println(new String (by,0,len));
                response.println(new String (by,0,len));
                response.pushToBrowser(200);
                is.close();
                return;
            }
            //获取请求协议
            Servlet servlet=WebApp.getSerVletFromUrl(request.getUrl());
            if (null!=servlet){
                servlet.service(request,response);
                response.pushToBrowser(200);
            }else {
                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("error.html");
                byte[] by=new byte[1024*1024];
                int len = is.read(by);
                response.println(new String (by,0,len));
                response.pushToBrowser(404);
                is.close();
            }
        }catch (Exception e){
            try {
                response.println("你好我不好,我会马上好");
                response.pushToBrowser(500);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
        release();
    }


    //释放资源
    private void release(){
        try {
            client.close();
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

servlet接口

package com.sjz.service;

/**
 * @author 史锦泽
 * @date 2020-08-08 19:20
 */
public interface Servlet {
    void service(Request request,Response response);
}

LoginServlet

package com.sjz.service;


/**
 * @author 史锦泽
 * @date 2020-08-08 19:19
 */
public class LoginServlet implements Servlet {
    @Override
    public void service(Request request,Response response) {
        System.out.println(1111);
        response.print("<html>");
        response.print("<head>");
        response.print("<title>");
        response.print("<服务器响应成功>");
        response.print("</title>");
        response.print("</head>");
        response.print("<body>");
        response.print("史锦泽 service终于回来了"+request.getParameter("sjz"));
        response.print("</body>");
        response.print("</html> ");
    }
}

registerServlet

package com.sjz.service;

/**
 * @author 史锦泽
 * @date 2020-08-08 19:22
 */
public class RegisterServlet implements Servlet {
    @Override
    public void service(Request request,Response response) {
        System.out.println("registerServlet");
    }
}

404页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>404页面</title>
</head>
<body>
<hi>页面未找到</hi>
</body>
</html>

首页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<hi>首页</hi>
</body>
</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值