9.5 基于HTTP协议的web服务器响应程序

9.5 基于HTTP协议的web服务器响应程序

介绍

      当我们在浏览器中输入网址,然后敲击回车后,浏览器会依据HTTP协议向服务器发送请求协议,服务器接收到请求协议后,获取对应数据,然后返回响应协议及数据。
      HTTP协议属于应用层协议,依据计算机网络知识及TCP/IP模型,我们知道,网络中的信息传输都是从发送端的对应层(在本文中即是应用层)将信息从上到下层层封装到最底层(物理层),然后物理层通过光纤等物理传输设备将信息传到接收端的物理层,接收端将信息从物理层向上层层解封传输到对应层。
      应用层的下一层是传输层,传输层的协议有TCP协议和UDP协议。本文将使用TCP编程进行数据传输。即在浏览器端只需要输入网址,然后提交即可,除此外不需要对浏览器端进行任何操作,它会自动将信息封装到物理层,然后进行传输,在服务器端,只需要在传输层处理数据即可,因为在服务器端的对应层已将数据进行解封。
来张图片(只是展示了从浏览器端向服务器端传送数据的过程,即请求过程):
p

目的

      编写简单的web服务器响应程序,使其可以接收请求协议,并且能够返回带有数据的响应协议。

逻辑

logic

逻辑讲解

  1. Browser端http网址访问
    p1
    (1)一个URL可以唯一定位网络中的一个资源。
    URL由三部分组成:资源类型、存放资源的主机域名、资源文件名;也可认为由4部分组成:协议、主机、端口、路径。
    URL的一般语法格式为:(带方括号[]的为可选项):
    protocol :// hostname[:port] / path / [;parameters][?query]#fragment
    协议为https协议时的一个例子:https://blog.csdn.net/qq_37665301。这是我的csdn博客首页,其中blog.csdn.net可以定位到csdn的服务器,qq_37665301定位到csdn为我组织的博客首页。
    可以认为网址就是URL。
    (2)浏览器请求数据过程。
    浏览器依据URL中的hostname[:port]将请求发送到对应的服务器,服务器依据path找到对应数据,然后将数据发送给浏览器。
    (3)浏览器端请求数据的方式。
    一般有两种:get 和 post。它们的一个区别在于数据在url中是否是可见的,这也导致了http网址的不同。
    (4)我在实验中所使用的部分测试URL。
http://localhost:8888/o
http://localhost:8888/login
http://localhost:8888/g
http://localhost:8888/reg
  1. 服务器循环接受浏览器的请求。
    p2
  • 功能:总控制程序,同时负责与浏览器端进行数据交互。
    (1)服务器程序入口。
  • 原理:server.java是服务器主程序,同时是服务器的程序入口。它负责接受浏览器的连接请求,建立与浏览器的连接,然后为此次请求建立一个线程,在线程中处理此次请求事件。
  • 技术:通过ServerSocket指定服务器端口,并启动服务器;通过accept()阻塞式接受请求连接;通过new Thread(…).start();启动线程处理程序。
    (2)多线程处理程序。
  • 原理:Dispatcher.java设置在每个线程中进行:请求处理–>服务器中请求数据对应处理类的地址查找–>获取响应数据–>构建报文并向浏览器发送此报文。
  • 技术:Dispatcher.java通过继承Runnable接口,重写run()方法来实现多线程;通过Request对象进行请求处理;通过WebAnalysis对服务器配置文件进行分析,查找请求数据对应的处理类的地址;通过反射创建Servlet对象,然后获取响应数据;通过Response对象构建协议报文,并发送给浏览器端。
  1. 请求处理
    p3
  • 功能:接受浏览器端的数据请求报文,然后解析此报文,获取URL中的path[;parameters]
  • 原理:服务器端接收到的是基于http协议下的请求报文,服务器想返回浏览器请求的数据就需要知道报文中的pathpath能帮助服务器找到对应数据。
  • 技术:通过字符串分割技术提取path,同时注意请求方式(get/post)的不同,会导致请求报文不同,需要分别分析。
  1. 服务器中请求数据对应处理类的地址查找
    p4
    (1)服务器配置文件
  • 说明:在实验中使用自己写的servlet.xml来模拟服务器配置文件。首先servlet.xml的作用是映射,是我们可以通过 URL 中的 path 就可以找到对应的处理类地址。
  • 示例:
    下面是servlet.xml中的一段数据
<servlet>
		<servlet-name>login</servlet-name>
		<servlet-class>dxt.server.user.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
	<servlet-name>login</servlet-name>
	<url-pattern>/login</url-pattern>
	<url-pattern>/g</url-pattern>
</servlet-mapping>

      通过 servlet-mapping 中的 url-pattern 可以找到 servlet-name,然后可以通过 servlet-name 可以在 servlet 中找到对应处理类的地址 servlet-class。当我们有了地址之后,就可以通过 反射技术 来使用此类,来处理此次请求。
      假如有一个URL:http://localhost:8888/g,我们从URL中提取出 g,然后在xml文件中找url-pattern为/g的servlet-mapping,然后获得此servlet-mapping中的servlet-name,即login,然后在找setvlet中servlet-name为login的,既可以获得值为dxt.server.user.LoginServlet的servlet-class。
(2)xml数据处理

  • 原理:WebAnalysis.java负责解析xml文件,并提供方法是使用者通过URL就可以获取xml文件中的servlet-class。WebHandler.java 、WebContext.java、Entity.java和Mapping.java都服务于WebAnalysis.java。
  • 技术:Sax方式解析xml文件;WebHandler.java即为Sax解析中的处理器类。
    (3)解释
  • 为什么要使用服务器配置文件?有时候服务器中的文件名过长,不适合直接使用在URL中;在此次实验中,我们通过URL去找一个服务器端一个处理类的地址,不应该让使用浏览器的人直接使用此地址,也不方便直接使用。
  • 为什么找对应处理类的地址,而不是找服务器中的数据?我们在对应的类中构造浏览器端请求的数据。
  1. 获取响应数据
    p5
    (1)Servlet接口
  • 解释:Servlet.java 实现了一个接口,声明了一个service()方法;LoginServlet.java 等都是不同实现Servlet接口的数据处理类。
  • 作用:我们可以声明Servlet对象,然后使用得到的对应数据处理类的地址(servlet-class),利用反射技术创建对应处理的对象,已处理不同的请求。
    (2)LoginServlet.java等
  • 功能:就是构建对应的响应信息。比如RegisterServlet.java是处理注册请求的类,在此类中就是构建一个注册页面(html),然后将此页面数据发送到浏览器端,浏览器将数据以注册网页的形式呈现给用户。
  1. 构建报文并向浏览器发送此报文
    p6
  • 功能:依据Servlet得到的数据和http响应协议的格式构建响应报文,然后发送此报文给浏览器端。
  • 技术:http响应报文格式;字符串的拼接

代码

  1. Server.java
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服务器响应程序
 * 多线程处理,可以同时处理多个请求
 * @author dxt
 *
 */
public class Server {
	private ServerSocket serverSocket;
	private boolean isRunning;
	
	public static void main(String[] args){
		Server s = new Server();
		s.start();
	}
	/**
	 * 启动服务器响应程序,开始接收请求信息
	 */
	public void start(){
		try {
			serverSocket = new ServerSocket(8888);
			isRunning = true;
			receive();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("服务器启动失败。");
			stop();
		}
	}
	/**
	 * 接收请求信息,返回响应信息
	 */
	public void receive(){
		while(isRunning){
			try {
				Socket client = serverSocket.accept();
				System.out.println("一个客户端已建立连接");
			
				//多线程处理
				new Thread(new Dispatcher(client)).start();
			
			} catch (IOException e) {
				System.out.println("客户端错误。");
				e.printStackTrace();
			}
		}
	}
	/**
	 * 关闭服务器
	 * 服务器监听程序应该一直处于运行状态
	 */
	public void stop(){
		isRunning = false;
		try {
			serverSocket.close();
			System.out.println("服务器停止运行。");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
  1. Dispatcher.java
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

/**
 * 多线程实现 
 * @author dxt
 *
 */
public class Dispatcher implements Runnable{

	private Socket client;
	private Request request;
	private Response response;
	
	public Dispatcher(Socket client){
		this.client = client;
		try {
			this.request = new Request(client);
			
			this.response = new Response(client);
		} catch (IOException e) {
			e.printStackTrace();
			this.release();
		}
	}
	
	public void run() {
		try{
			//获取servlet,得到url(请求处理+服务器文件地址查找)
			Servlet servlet = WebAnalysis.getServletFromUrl(request.getUrl());
			if(null != servlet){
				//运行对应的服务(对应处理类获取对应数据)
				servlet.service(request, response);
				//服务器返回响应信息
				response.push(200);
			}else{
				//错误页面
				InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("./error.html");
				response.print(is.toString());
				is.close();
				response.push(404);
			}
		}catch(Exception e){
			response.print("");
			response.push(500);
		}
		
		//处理完即释放资源
		this.release();
	}
	/**
	 * 释放client资源
	 */
	private void release(){
		try {
			client.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
  1. Request.java
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 解析请求协议:
 * (1)获取method (get还是post或...)、url以及请求参数
 * (2)将请求参数存储到Map结构中
 * @author dxt
 *
 */
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(InputStream is){
		parameterMap = new HashMap<String, List<String>>();
		byte[] datas = new byte[1024*1024];
		int len;
		try {
			len = is.read(datas);
			requestInfo = new String(datas, 0, len);
			System.out.println(requestInfo);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		//分解字符串
		parseRequestInfo();
	}
	public Request(Socket client) throws IOException{
		this(client.getInputStream());
	}
	
	public String getMethod(){
		return this.method;
	}
	public String getUrl(){
		return this.url;
	}
	public String getQueryStr(){
		return this.queryStr;
	}
	/**
	 * 分解请求协议,获取对应信息
	 */
	private void parseRequestInfo(){
		//1. 获取请求方法,转成小写。请求方法:开始到第一个"/"
		this.method = this.requestInfo.substring(0, this.requestInfo.indexOf("/")).trim().toLowerCase();
		System.out.println(this.method);
		
		//2. 获取请求的url。第一个"/" 到 "HTTP/",如果有"?",则"?"前的为url
		// 如果是get方法,则url中有请求参数
		// 如果是post方法,则url中没有请求参数
		//2.1 获取/的位置
		int startIdx = this.requestInfo.indexOf("/")+1;
		//2.2 获取HTTP/的位置
		int endIdx = this.requestInfo.indexOf("HTTP/");
		//2.3 分割字符串
		this.url = this.requestInfo.substring(startIdx, endIdx);
		//2.4 获取?的位置
		int queryIdx = this.url.indexOf("?");
		if(queryIdx >= 0){	//表示存在?,?前为url,?后为请求参数
			String[] urlArray = this.url.split("\\?");
			this.url = urlArray[0].trim();
			queryStr = urlArray[1].trim();	//请求参数
		}
		System.out.println(this.url);
		
		//3. 获取请求参数,如果是get,则已获取;如果是post,则参数在请求体中
		if(method.equals("post")){
			String qStr = this.requestInfo.substring(this.requestInfo.lastIndexOf(CRLF)).trim();
			if(null == queryStr){
				queryStr = qStr;
			}else{
				queryStr += "&" + qStr;
			}
		}
		
		queryStr = null == queryStr?"":queryStr;
		System.out.println(this.queryStr);
		System.out.println(this.method+"---"+this.url+"---"+this.queryStr);
		
		//转成Map
		convertMap();
	}
	/**
	 * 将请求参数以map形式存储
	 */
	private void convertMap(){
		//分割字符串
		String[] keyValues = this.queryStr.split("&");
		for(String queryInfo:keyValues){
			String[] kv = queryInfo.split("=");
			kv = Arrays.copyOf(kv, 2);
			//获取key和value
			String key = kv[0];
			String value = kv[1]==null?null:decode(kv[1], "utf-8");
			//存储到map中
			if(!parameterMap.containsKey(key)){	//第一次
				parameterMap.put(key, new ArrayList<String>());
			}
			parameterMap.get(key).add(value);	//添加内容
		}
	}
	/**
	 * 处理中文
	 * @param value
	 * @param enc
	 * @return
	 */
	private String decode(String value, String enc){
		try {
			return java.net.URLDecoder.decode(value, enc);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return null;
	}
	/**
	 * 通过name返回对应的多个值
	 * @param key
	 * @return
	 */
	public String[] getParameterValues(String key){
		List<String> values = this.parameterMap.get(key);
		if(null==values || values.size()<1){
			return null;
		}
		return values.toArray(new String[0]);
	}
	
	/**
	 * 通过name获取对应的一个值
	 * @param key
	 * @return
	 */
	public String getParameter(String key){
		String[] values = getParameterValues(key);
		return values==null ? null:values[0];
	}
}
  1. WebAnalysis.java
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;


/**
 * 解析xml数据
 * @author dxt
 *
 */
public class WebAnalysis {
	private static WebContext webContext;
	static{
		try{
			//1. 获取解析工厂
			SAXParserFactory factory = SAXParserFactory.newInstance();
			//2. 从解析工厂获取解析器
			SAXParser parse = factory.newSAXParser();
			
			//3. 编写处理器 WebHandler类
			
			//4. 加载文档注册处理器
			WebHandler handler = new WebHandler();
			//5. 进行解析
			parse.parse(Thread.currentThread().getContextClassLoader()
					.getResourceAsStream("servlet.xml"), handler);
			
			//获取数据
			webContext = new WebContext(handler.getEntitys(), handler.getMappings());
		}catch(Exception e){
			System.out.println("解析错误");
			e.printStackTrace();
		}
	}
	
	/**
	 * 通过url获取配置文件对应的servlet
	 * @param url
	 * @return
	 */
	public static Servlet getServletFromUrl(String url){
		//反射事件
		//2. 依据WebContext 获取我们想要得到的 对应的类名
		//2.1 我们输入的是 url
		String name = webContext.getClz("/" + url);
		//3. 有了类名 ---> 使用反射   获取对应类的信息, 类名就是xml数据中的<servlet-class>
		Class clz;
		try {
			clz = Class.forName(name);
			//4. 使用反射进行实例化
			Servlet s = (Servlet)clz.getConstructor(null).newInstance(null);
			return s;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}
  1. WebHandler.java
import java.util.ArrayList;
import java.util.List;

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


public class WebHandler extends DefaultHandler{
	private List<Entity> entitys;
	private List<Mapping> mappings;
	private Entity entity;
	private Mapping mapping;
	private String tag; //存储操作标签
	private boolean isMapping = false;
	
	
	public void startDocument(){
		System.out.println("------文档解析开始------");
		entitys = new ArrayList<Entity>();
		mappings = new ArrayList<Mapping>();
		
	}
	
	public void startElement(String uri, String localName, String qName, Attributes attributes){
		if(null != qName){	//处理换行与空格
			tag = qName;
			//处理对应的标签
			if(tag.equals("servlet")){
				entity = new Entity();
				isMapping = false;
			}else if(tag.equals("servlet-mapping")){
				mapping = new Mapping();
				isMapping = true;
			}
		}
	}
	
	public void characters(char[] ch, int start, int length){
		String contents = new String(ch, start, length).trim();
		if(null != tag){
			if(isMapping){	//处理 mapping
				if(tag.equals("servlet-name")){
					mapping.setName(contents);
				}else if(tag.equals("url-pattern")){
					mapping.addPattern(contents);
				}
			}else{	//处理entity
				if(tag.equals("servlet-name")){
					entity.setName(contents);
				}else if(tag.equals("servlet-class")){
					entity.setClz(contents);
				}
			}
		}
	}
	
	public void endElement(String uri, String localName, String qName){
		if(null != qName){
			if(qName.equals("servlet")){
				entitys.add(entity);
			}else if(qName.equals("servlet-mapping")){
				mappings.add(mapping);
			}
		}
		tag = null;
	}
	
	public void endDocument(){
		System.out.println("------文档解析结束------");
	}
	
	
	public List<Entity> getEntitys(){
		return this.entitys;
	}
	public List<Mapping> getMappings(){
		return this.mappings;
	}
}
  1. WebContext.java
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 目的: 根据url-pattern 找 servlet-class
 * 对于webxml数据
 * 可以依据<servlet-mapping> 中的pattern 找到 name
 * 在根据 name 在<servlet> 中找到class
 * @author dxt
 *
 */
public class WebContext {
	private List<Entity> entitys;
	private List<Mapping> mappings;
	//创建两个map数据,帮助解决问题
	//key:servlet-name  value:servlet-class
	private Map<String, String> entityMap = new HashMap<String, String>();
	//key:url-pattern  value:servlet-name
	private Map<String, String> mappingMap = new HashMap<String, String>();
	
	public WebContext(List<Entity> entitys, List<Mapping> mappings){
		this.entitys = entitys;
		this.mappings = mappings;
		
		//将entity的List转成了map
		for(Entity entity : entitys){
			entityMap.put(entity.getName(), entity.getClz());
		}
		//将mapping的List转为map
		for(Mapping mapping : mappings){
			for(String pattern : mapping.getPatterns()){
				mappingMap.put(pattern, mapping.getName());
			}
		}
	}
	/**
	 * 依据url-pattern 找对应的 servlet-class
	 * 一个url-pattern唯一对应一个class
	 * 一个class可对应多个url-pattern
	 * 返回servlet-class
	 * @param pattern
	 * @return
	 */
	public String getClz(String pattern){
		String name = mappingMap.get(pattern);	//找到对应name
		
		return entityMap.get(name);	//找到对应class
	}
}
  1. Entity.java
/**
 * 针对xml数据建立的类
 * 请求姓名和请求类
 * @author dxt
 *
 */
public class Entity {
	private String name;
	private String clz;
	
	public Entity(){}
	public Entity(String name, String clz){
		super();
		this.name = name;
		this.clz = clz;
	}
	
	public void setName(String name){
		this.name = name;
	}
	public String getName(){
		return this.name;
	}
	public void setClz(String clz){
		this.clz = clz;
	}
	public String getClz(){
		return this.clz;
	}
}
  1. Mapping.java
import java.util.HashSet;
import java.util.Set;
/**
 * 针对<servlet-mapping>
 * @author dxt
 *
 */
public class Mapping {
	private String name;
	private Set<String> patterns;
	
	public Mapping(){
		patterns = new HashSet<String>();
	}
	
	public void setName(String name){
		this.name = name;
	}
	public String getName(){
		return this.name;
	}
	
	public Set<String> getPatterns(){
		return this.patterns;
	}
	public void addPattern(String pattern){
		this.patterns.add(pattern);
	}
}
  1. Servlet.java
/**
 * 服务器脚本 接口
 * @author dxt
 *
 */
public interface Servlet {
	void service(Request request, Response response);
}
  1. LoginServlet.java
import dxt.server.core.Request;
import dxt.server.core.Response;
import dxt.server.core.Servlet;

/**
 * 登录响应处理程序
 * @author dxt
 *
 */
public class LoginServlet implements Servlet{
	/**
	 * 构建响应数据
	 */
	public void service(Request request, Response response) {
		response.print("<html>");
		response.print("<head>");
		response.print("<title>");
		response.print("服务器响应信息");
		response.print("</title>");
		response.print("</head>");
		response.print("<body>");
		response.print("我就是响应信息"+request.getParameter("uname"));
		response.print("</body>");
		response.print("</html>");
	}
}
  1. Response.java
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 dxt
 *
 */
public class Response {
	private BufferedWriter bw;
	//正文
	private StringBuilder content;
	//协议头(状态行+响应头+空行)
	private StringBuilder headInfo;
	private int len;	//正文字节数
	
	private final String BLANK = " ";	//空格
	private final String CRLF = "\r\n";	//换行符
	
	private Response(){
		super();
		content = new StringBuilder();
		headInfo = new StringBuilder();
		len = 0;
	}
	public Response(Socket client){
		this();
		try {
			bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	public Response(OutputStream os){
		this();
		bw = new BufferedWriter(new OutputStreamWriter(os));
	}
	
	/**
	 * 构建头信息  状态行+响应头+空行
	 * @param code 状态码
	 */
	private void createHeadInfo(int code){
		//1. 响应行
		headInfo.append("HTTP/1.1").append(BLANK);
		headInfo.append(code).append(BLANK);
		switch(code){
			case 200:
				headInfo.append("OK").append(CRLF);
				break;
			case 404:
				headInfo.append("NOT FOUND").append(CRLF);
				break;
			case 505:
				headInfo.append("SERVER ERROR").append(CRLF);
				break;
		}
		//2. 响应头
		headInfo.append("Date:").append(new Date()).append(CRLF);
		headInfo.append("Server:").append("dxt Server/0.0.1;charset=GBK").append(CRLF);
		headInfo.append("Content-type:text/html").append(CRLF);
		headInfo.append("Content-length:").append(len).append(CRLF);
		//3. 空行
		headInfo.append(CRLF);	//空行
	}
	/**
	 * 动态添加内容
	 * 构建响应信息--content
	 * @param info
	 * @return
	 */
	public Response print(String info){
		content.append(info);
		len+=info.getBytes().length;
		return this;
	}
	public Response println(String info){
		content.append(info).append(CRLF);
		len+=(info+CRLF).getBytes().length;
		return this;
	}
	/**
	 * 服务器返回响应信息
	 * @param code
	 */
	public void push(int code){
		if(headInfo == null){
			code = 505;
		}
		this.createHeadInfo(code);	//封装协议信息
		try {
			bw.append(headInfo);
			bw.append(content);
			bw.flush();
		} catch (IOException e) {
			System.out.println("发送出错");
			e.printStackTrace();
		}
	}
}
  1. servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
	<servlet>
		<servlet-name>login</servlet-name>
		<servlet-class>dxt.server.user.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>reg</servlet-name>
		<servlet-class>dxt.server.user.RegisterServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>reg</servlet-name>
		<url-pattern>/reg</url-pattern>
	</servlet-mapping>
	<servlet>
		<servlet-name>others</servlet-name>
		<servlet-class>dxt.server.user.OthersServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>others</servlet-name>
		<url-pattern>/o</url-pattern>
	</servlet-mapping>
</web-app>

总结

      相比于每一部分功能的实现,更需要学习代码的组织结构。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值