【HTTP协议其实很简单】04.给我报告vs我要汇报&GET vs POST&Request vs Response(自己实现Servlet)

本篇主题:理解GET和POST,Request和Response,扩展第3章的微型服务器,让其可以处理Post请求,并模仿Servlet来处理请求。

上一篇文章,我用一段代码示例了请求和响应的过程,用Socket很容易就可以实现。

其中最关键的是对HTTP协议请求报文的第一行做了处理,再来看一下。请求报文的第一行是

GET http://XXXX.com/index.html HTTP/1.1

在第3章中处理的是这一行的中间部分,也就是:

http://XXXX.com/index.html

将这一部分解析之后再映射到本地的文件即可完成。

那么第一部分的GET是干什么的呢?

其实在第1章中已经简单做了介绍,再来回顾一下:

HTTP协议中共定义了八种方法或者叫“动作”来表明对Request-URI指定的资源的不同操作方式,具体介绍如下:

OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*'的请求来测试服务器的功能性。

HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。

GET:向特定的资源发出请求。

POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。

PUT:向指定资源位置上传其最新内容。

DELETE:请求服务器删除Request-URI所标识的资源。

TRACE:回显服务器收到的请求,主要用于测试或诊断。

CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。

虽然HTTP的请求方式有8种,但是我们在实际应用中常用的也就是get和post,其他请求方式也都可以通过这两种方式间接的来实现。

简单来说这一部分表示HTTP请求的方式,总结起来主要的方式就两种:GET和POST。那GET是什么意思,POST又是什么意思呢?

来想像以下两个场景:

场景一:

员工小妖出差在外,需要查一个客户的资料,而客户的资料在公司里,由内勤人员管理,小妖要想得到这个客户资料,是不是得给内勤人员打电话:

小妖:你好,小青,我++需要++XX客户的资料。

小青:你好,小妖。这个客户的资料是:xxxxxx

场景二:

员工小妖出差在外,刚刚得到一个新客户的资料,公司规定新客户的资料必须到内勤人员处登录备案,这时小妖是这样打电话的:

小妖:你好,小青,我要++汇报++XX客户的资料。客户的关键联系人是:李逍遥,电话是:139XXXXXXXX,客户需要的产品是:xxxxx

小青:小妖,你好。客户资料我已做好备案。

看到两个的不同了吗?一个是小妖“需要",一个是小妖要"汇报"。

两个场景中小妖都是请求者的身份,也就是在http协议中的浏览器端,而小青则是服务器端。

在“需要”的场景中,小妖只告诉小青,我要XX客户的资料就好了。在“汇报”的场景中,小妖不仅说我要汇报XX客户的资料,还把客户的详细的资料做了阐述。

在“需要”的场景中,小青接到电话后,要进行查询,查到之后告诉小妖详细的资料;在“汇报”的场景中,小青接到信息后,要把客户资料保存到备案库中,不需要向小妖再说什么。

“需要”的场景就是GET,“汇报”的场景就是POST

"需要"的场景中小青说出的客户的详细资料,就是HTTP协议中响应报文的消息体,也就是响应报文中的红色框部分(如下图所示)。

image

这部分内容是由服务器端组合而成,返回给浏览器的。

“汇报”的场景中小妖告诉小青的内容,就是HTTP协议中请求报文的消息体。如下图所示的红框部分。

image
这部分内容是由浏览器组合起来提交给服务器的,服务器接收到之后,可以将这部分数据保存起来。可以保存成文件,也可以解析之后保存到数据库中。

不论是“需要”的场景,还是“汇报”的场景,小妖讲的话都是请求,也就是Reqeust,小青讲的话都是响应,也就是Response。

Request中包含着请求报文中的所有内容。Response中包含着响应报文中的所有内容。


以上的示例中浏览器和服务器之间的消息体都是以字符串表示的,这些字符串是不是随便怎么写都可以呢?

当然不是。因为浏览器最终要呈现出一个网页,所以服务器端要返回html的代码,才可以被解析成网页。服务器端需要将数据保存,那么服务器端可以识别什么格式的字符串呢?那就要看服务器端的解析程序可以支持什么了。为了保证服务器端处理程序的一致性、兼容性,各大web中间件和服务器的处理程序一般支持固定的几种格式,请看下图:

image

这是不是意味着服务器端只能处理这几种格式呢?当然不是,如果你造出来一种全新的格式,需要服务器编写一套算法去解析,浏览器端也需要重新设计一套算法去编码,而各网站也需要重新设计网站的源码,这个成本太高了,只有w3c这样的标准化组织有这个影响力。

当然了你如果想写一套自己玩,也是可以的,不过那就不是HTTP协议了。

好了,作为服务器端,只需要处理几种请求体就可以将浏览器请求来的数据保存下来了,下面我列出几种格式的示例:

  • form-data
name=张三&age=23&gender=男
  • json
{name:'张三',age:23,gender:'男'}
  • xml
<data>
    <name>张三</name>
    <age>23</age>
    <gender>男</gender>
</data>

由于几种格式统一,所以每种不同的技术都发展出来一些类库来处理这些不同的格式。

这些格式中form-data是http协议默认格式,也是历史最悠久的。所有的web服务器开发架构都默认支持这种,比如java技术中j2ee就有servlet,将请求封装成了HttpServletRequest,可以通过request.getParameter()方法来获取请求的数据。
.NET将请求封装成了HttpRequest对象,php则封装成了$_request。

而json格式和xml格式需要借助一些解析类库,使用起来更加方便和灵活,比如解析json格式有大名鼎鼎的fastjson、JackJson。解析xml格式的有Jdom、Dom4j、Xstream等等。

弄明白了这一点,接下来我们的微型服务器只需要稍做扩展就以处理Post请求。


首先,需要将请求的消息体解析出来,几种格式中当然要优先处理form-data格式,因为这种是应用最为广泛,浏览器的表彰提交默认情况也都是采用这种方式。

form-data格式其实很简单,都是以键值对的形式传递数据,每组键值对用"&”分隔开来,所以取出这些信息非常简单。

下面我将第3章的微型Web服务器稍做扩展,让其可以处理POST请求。


package com.hawkon.ch04;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class MyServer {
	public static String HOME_DIR = "d:/home"; // 定义服务器默认的文件夹在哪里
	public static String NOT_FOUND_FILE = "d:/home/404.html"; // 定义404错误页
	public static String ERR_FILE = "d:/home/err.html"; // 定义500错误页
	public static String DEFAULT_FILE = "/index.html"; // 定义默认页面

	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(8009); // 定义端口
		while (true) { // 循环是不断的接受新的请求
			Socket socket = serverSocket.accept(); // 当服务器运行起来没有请求的时候会在这里等待
			System.out.println("客户端接入:"+socket.getLocalAddress().toString());
			InputStream is = socket.getInputStream(); // 创建一个输入流
			HttpRequest request = new HttpRequest();
			try {
				request.init(is);//交由request对象初始化请求
				HttpResponse response = new HttpResponse(request,socket.getOutputStream());//将request和流对象封装进response
				HttpServlet servlet = new HttpServlet(response);//由Servlet对象来处理业务
				switch (request.getMethod()) {
				case Get:
					servlet.doGet(request);
					break;
				case Post:
					servlet.doPost(request);
					break;
				default:
				}
			} catch (Exception ex) {
				ex.printStackTrace(); // 输出异常信息
			} finally {
				is.close();
			}
		}
	}
}
package com.hawkon.ch04;

/**
 * 定义出HTTP请求的不同类型
 * @author hawkon
 *
 */
public enum HttpRequestMethod {
	Get, Post,Head,Options,Put,Delete,Trace,Connect
}

package com.hawkon.ch04;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * Request对象,封装请求相关的信息
 * @author hawkon
 *
 */
public class HttpRequest {
	public HttpRequest() {
		header = new HashMap<>();
		form = new HashMap<>();
	}

	/**
	 * 请求类型:Get,Post
	 */
	private HttpRequestMethod method;

	/**
	 * 存放请求地址
	 */
	private String url;

	/**
	 * 存放HTTP请求头信息
	 */
	private Map<String, String> header;

	/**
	 * 存放form-data键值对数据
	 */
	private Map<String, String> form;

	public HttpRequestMethod getMethod() {
		return method;
	}

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

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public Map<String, String> getHeader() {
		return header;
	}

	public void setHeader(Map<String, String> header) {
		this.header = header;
	}

	public Map<String, String> getForm() {
		return form;
	}

	public void setForm(Map<String, String> form) {
		this.form = form;
	}

	public void init(InputStream httpStream) throws Exception {
		InputStreamReader sr = new InputStreamReader(httpStream); // 过渡代码主要是为下一行创建对象
		BufferedReader br = new BufferedReader(sr); // 创建BufferedReader对象,在本文中采用一行一行的读的方式,比较方便。

		try {
			String line = "";
			line = br.readLine(); // 只读取了第一行,后面报头本文暂时没用。
			String[] arr = line.split(" "); // 拆分出第一行的三部分内容
			if (arr.length != 3) { // 拆分的结果不是三部分内容,说明不是HTTP协议
				throw new Exception("Http协议格式错误:第一行不符合规范");
			} else {
				String method = arr[0];
				switch (method) {
				case "GET":
					this.setMethod(HttpRequestMethod.Get);
					break;
				case "POST":
					this.setMethod(HttpRequestMethod.Post);
					break;
				default: // 暂时不处理其它请求类型,为保持服务器程序的严谨性,抛出异常
					throw new Exception("服务器暂不支持GET、POST以外的请求类型");
				}
				String path = arr[1]; // 取出请求的资源路径
				this.setUrl(path);
				//String version = arr[2]; // 版本暂无用处,不做处理
			}
			System.out.println("HttpHeader");
			// 读取头信息
			do {
				// 头信息的格式为 Host: 127.0.0.1:80,第一个冒号前是键,后面的是值,每个键一行,处理起来也很简单
				line = br.readLine();
				// 头信息结束后会有两个空行,之后是Post数据,如果是Post请求,头信息中会有Content-Length来表示消息体有多少字节
				if (line.equals("")) {
					break;
				}
				// 此处不可用splite,头信息的值中有可能包含":",如果用splite分割,可能会分出两截以上。
				int index = line.indexOf(":");
				String key = line.substring(0, index);//取出头信息的关键字
				String value = line.substring(index + 2);
				this.getHeader().put(key.toLowerCase(), value); //往map中存的时候统一转为小写,因为不同浏览器的大小写规范有所不同
				System.out.println(key+":"+value);
			} while (true);
			// 判断是否是Post请求,读取消息体
			System.out.println("PostBody");
			if (this.getMethod().equals(HttpRequestMethod.Post)) {//如果是POST请求则读取消息体
				// 取出Content-length,开始读取消息体
				int contentLength = Integer.parseInt(this.getHeader().get("content-length"));
				char[] body = new char[contentLength];
				br.read(body, 0, contentLength);
				String bodyStr = new String(body);
				String[] key_value = bodyStr.split("&");
				for (int i = 0; i < key_value.length; i++) {
					String[] arr_key_value = key_value[i].split("=");
					this.getForm().put(arr_key_value[0], arr_key_value[1]);
				}
			}
		} catch (Exception ex) {
			ex.printStackTrace(); // 输出异常信息
		} finally {
//			br.close(); //这里不能关闭,关闭的同时流会关闭,无法向客户端返回响应
//			sr.close();
		}

	}
}

package com.hawkon.ch04;

import java.io.OutputStream;

/**
 * Response对象,封装响应所需的类
 * @author hawkon
 *
 */
public class HttpResponse {
	public HttpResponse(HttpRequest request,OutputStream os) {
		this.request = request;
		this.outputStream = os;
	}
	/**
	 * 流对象
	 */
	private OutputStream outputStream;
	/**
	 * 请求对象
	 */
	private HttpRequest request;


	public HttpRequest getRequest() {
		return request;
	}

	public void setRequest(HttpRequest request) {
		this.request = request;
	}

	public OutputStream getOutputStream() {
		return outputStream;
	}

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

package com.hawkon.ch04;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Date;
import java.util.Map;

/**
 * 处理请求的核心类,模仿servlet的思路
 * @author hawkon
 *
 */
public class HttpServlet {

	public static String HOME_DIR = "d:/home"; // 定义服务器默认的文件夹在哪里
	public static String NOT_FOUND_FILE = "d:/home/404.html"; // 定义404错误页
	public static String ERR_FILE = "d:/home/err.html"; // 定义500错误页
	public static String DEFAULT_FILE = "/index.html"; // 定义默认页面

	public HttpServlet(HttpResponse response) {
		this.response = response;
	}

	private HttpResponse response;

	/**
	 * Get请求
	 * @param request
	 * @throws IOException
	 */
	public void doGet(HttpRequest request) throws IOException {
		String path = request.getUrl();
		if (path.equals("/")) // 如果请求的内容没有指定文件,则返回默认的页面
			path = DEFAULT_FILE;
		if (Files.exists(Paths.get(HOME_DIR + path))) { // 判断文件是否存在
			outPutHeader(200);
			outPutFile(HOME_DIR + path);
		} else { // 如果文件不存在,返回404页面
			outPutHeader(404);
			outPutFile(NOT_FOUND_FILE);
		}
	}

	/**
	 * Post请求的处理
	 * @param request
	 * @throws IOException
	 */
	public void doPost(HttpRequest request) throws IOException {
		Map<String,String> forms = request.getForm(); //取出键值对。
		for (String key : forms.keySet()) {
			System.out.println(key+":"+forms.get(key)); // 这里仅作输出,想存数据、存文件也可以,随你喽
		}
		//输出头信息,返回200状态
		outPutHeader(200);
		//响应消息休返回true
		outPutString("true");
	}

	/**
	 * 返回字符串响应体
	 * @param context
	 * @throws IOException
	 */
	public void outPutString(String context) throws IOException{
		OutputStream os = this.response.getOutputStream();
		os.write(context.getBytes("UTF-8")); // 输出响应报头
		os.close();
	}
	/**
	 * 返回文件响应
	 * @param fileName
	 * @throws IOException
	 */
	public void outPutFile(String fileName) throws IOException {
		FileInputStream fis = new FileInputStream(fileName);
		byte[] bytes = new byte[1024];
		int len;
		OutputStream os = this.response.getOutputStream();
		while ((len = fis.read(bytes)) != -1) { // 输出文件内容
			os.write(bytes, 0, len);
		}
		os.flush();
		os.close();
		fis.close();
	}
	public void outPutHeader( int status) throws IOException{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.append("HTTP/1.1 " + status + " OK\r\n"); // 输出状态码
		stringBuilder.append("Date: " + (new Date()).toString() + "\r\n");
		stringBuilder.append("Server: MyServer 0.0.1\r\n"); // 代表服务器的软件名称
		stringBuilder.append("X-Powered-By: Hawkon\r\n"); // 这行可以换成你的英文名,看起来会有点diao
		stringBuilder.append("Keep-Alive: timeout=5, max=100\r\n");
		stringBuilder.append("Connection: Keep-Alive\r\n");
		stringBuilder.append("Content-Type: text/html;charset=utf-8\r\n"); // 字符串编码
		stringBuilder.append("\r\n");// 多输出一个空行,用来分割报头和报体,HTTP协议要求
		OutputStream os = this.response.getOutputStream();
		os.write(stringBuilder.toString().getBytes("UTF-8")); // 输出响应报头
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章