本篇主題:理解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協議中響應報文的消息體,也就是響應報文中的紅色框部分(如下圖所示)。
這部分內容是由服務器端組合而成,返回給瀏覽器的。
“彙報”的場景中小妖告訴小青的內容,就是HTTP協議中請求報文的消息體。如下圖所示的紅框部分。
這部分內容是由瀏覽器組合起來提交給服務器的,服務器接收到之後,可以將這部分數據保存起來。可以保存成文件,也可以解析之後保存到數據庫中。
不論是“需要”的場景,還是“彙報”的場景,小妖講的話都是請求,也就是Reqeust,小青講的話都是響應,也就是Response。
Request中包含着請求報文中的所有內容。Response中包含着響應報文中的所有內容。
以上的示例中瀏覽器和服務器之間的消息體都是以字符串表示的,這些字符串是不是隨便怎麼寫都可以呢?
當然不是。因爲瀏覽器最終要呈現出一個網頁,所以服務器端要返回html的代碼,纔可以被解析成網頁。服務器端需要將數據保存,那麼服務器端可以識別什麼格式的字符串呢?那就要看服務器端的解析程序可以支持什麼了。爲了保證服務器端處理程序的一致性、兼容性,各大web中間件和服務器的處理程序一般支持固定的幾種格式,請看下圖:
這是不是意味着服務器端只能處理這幾種格式呢?當然不是,如果你造出來一種全新的格式,需要服務器編寫一套算法去解析,瀏覽器端也需要重新設計一套算法去編碼,而各網站也需要重新設計網站的源碼,這個成本太高了,只有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")); // 輸出響應報頭
}
}