【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")); // 輸出響應報頭
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章