關于Java實現(xiàn)HttpServer模擬前端接口調(diào)用
Java 實現(xiàn) Http Server,模擬前端接口調(diào)用
前言: 最近看到一個很有意思的東西,手寫簡單的 Http Server,而且只需要使用 Socket 編程就行了。當然了,才學過計算機網(wǎng)絡,所以知道 Http 協(xié)議是建立在 TCP 協(xié)議之上的協(xié)議,所以能用 TCP 來自己模擬一個簡單的 Http Server 當然是可以的。所以,自己實現(xiàn)了一個簡單的可以進行 請求、響應 的 Http Server,但是 Http 報文解析本身還是比較復雜的,所以只能處理簡單的情況。對于涉及二進制數(shù)據(jù)的報文解析,還是一頭霧水。不過,這也沒關系,只是現(xiàn)在做不到而已。 因為我可以判斷前端的請求是什么,所以我也就能根據(jù)特定的請求去響應特定的數(shù)據(jù)給前端,這個就和那個前端模擬接口調(diào)用有點相似了。(當然了,都是一些簡單的數(shù)據(jù),哈哈!)所以,就做了這個小工具,它可以根據(jù)用戶的請求執(zhí)行特定的響應數(shù)據(jù)。(而且不需要使用網(wǎng)絡了,只需要在本地就可以使用,并且我覺得對于簡單的情況,使用還是挺方便的。)
執(zhí)行結果展示
這里是兩個簡單的測試展示,最后有更多的測試示例。
訪問根目錄,然后會返回一句話(字符串)(注意:那條綠色的龍,只有使用瀏覽器訪問才能看到的,圖片本身也是屬于一個請求的。) Content-Type: application/json

單獨訪問這張圖片,返回值是圖片(二進制數(shù)據(jù))

請求和響應配置文件
所以只要用戶提前設置好請求信息和響應信息,在訪問特定請求時,就能返回特定數(shù)據(jù)。所以,我設計了一個簡單的 xml 文件用來配置這些信息,使用 xml 配置比較方便,properties 配置文件無法表達層次信息,只能適用于簡單的配置要求。
一個大的 request_and_responses 代表許多個請求和響應配置,每一個 request_and_response 節(jié)點代表一個request 請求和 response 響應信息,里面包含了請求和響應的基本信息。GET 方式請求主要包括:(method) 請求方法 和 (path) 請求路徑和參數(shù)。 POST 方法請求還包括一個 (param )請求參數(shù)。 response 包括:content_type(響應內(nèi)容類型) 和 value(響應內(nèi)容)。
GET 和 POST 方式的區(qū)別在于,GET 方式的請求路徑和請求參數(shù)是在一起的(都在請求頭中,沒有請求體),而 POST 方式的請求參數(shù)則是在請求體里面的,請求頭和請求體之間有一個 CRLF 分隔。
xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<request_and_responses>
<!-- & 需要使用轉義字符 & -->
<request_and_response>
<request>
<method>GET</method>
<path>/</path>
</request>
<response>
<content_type>application/json</content_type>
<value>I love you yesterday and today!</value>
</response>
</request_and_response>
<request_and_response>
<request>
<method>GET</method>
<path>/login?account=123&pwd=456</path>
</request>
<response>
<content_type>application/json</content_type>
<value>success</value>
</response>
</request_and_response>
<request_and_response>
<request>
<method>GET</method>
<path>/query/龍</path>
</request>
<response>
<content_type>application/json</content_type>
<value>龍是中國等東亞國家古代神話傳說生活于海中的神異生物。</value>
</response>
</request_and_response>
<request_and_response>
<request>
<method>POST</method>
<path>/login</path>
<param>account=123&pwd=456</param>
</request>
<response>
<content_type>application/json</content_type>
<value>{"result":success}</value>
</response>
</request_and_response>
<request_and_response>
<request>
<method>POST</method>
<path>/login</path>
<param>workId=12345</param>
</request>
<response>
<content_type>application/json</content_type>
<value>{"result":"success", "data":{"name":"李工", "sex":"男", "age":35}}</value>
</response>
</request_and_response>
<request_and_response>
<request>
<method>GET</method>
<path>/pictures/husky.jpeg</path>
</request>
<response>
<content_type>image/jpeg</content_type>
<value>D:/DB/husky.jpeg</value>
</response>
</request_and_response>
<!-- 瀏覽器訪問時的圖標 -->
<request_and_response>
<request>
<method>GET</method>
<path>/favicon.ico</path>
</request>
<response>
<content_type>image/webp</content_type>
<value>D:/DB/favicon.ico</value>
</response>
</request_and_response>
</request_and_responses>
xml 映射的實體類
xml 中的信息,讀取到內(nèi)存中,使用一個實體類來對信息進行封裝。
package com.dragon;
public class RequestAndResponse {
private String method;
private String path;
private String param;
private String content_type;
private String value;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getContent_type() {
return content_type;
}
public void setContent_type(String content_type) {
this.content_type = content_type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "RequestAndResponse [method=" + method + ", path=" + path + ", param=" + param + ", content_type="
+ content_type + ", value=" + value + "]";
}
}
xml 文件解析器類
使用一個類專門解析 xml 文件為Java 對象,然后使用一個 List 集合來存儲所有的對象。
注:不太會取名字,有點太長了,湊合著看吧!哈哈。 注:這里使用一個xml解析的jar包:dom4j。
package com.dragon;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
* 解析 xml 文件中配置的用戶請求和響應數(shù)據(jù)。
* */
public class RequestAndResponseResolver {
private static final String method = "method";
private static final String path = "path";
private static final String param = "param";
private static final String content_type = "content_type";
private static final String value = "value";
public static List<RequestAndResponse> listRequestAndResponse(String filePath) throws DocumentException{
File file = new File(filePath);
SAXReader reader = new SAXReader();
Document doc = reader.read(file);
Element root = doc.getRootElement();
//獲取根元素下面所有的子元素,利用迭代器方式
Iterator<?> it = root.elementIterator();
List<RequestAndResponse> requestAndResponses = new ArrayList<>();
while (it.hasNext()) {
//取出元素request_and_response
Element e = (Element)it.next();
//依次遍歷每一個 request_and_response,獲取相應的信息
Element request = e.element("request");
Element response = e.element("response");
RequestAndResponse requestAndResponse = new RequestAndResponse();
requestAndResponse.setMethod(request.elementText(method));
requestAndResponse.setPath(request.elementText(path));
requestAndResponse.setParam(request.elementText(param)); //GET 方式,這個屬性為 null
requestAndResponse.setContent_type(response.elementText(content_type));
requestAndResponse.setValue(response.elementText(value));
requestAndResponses.add(requestAndResponse);
}
return requestAndResponses;
}
}
接收請求并處理的部分
下面介紹一下,使用 Socket 接收并處理請求的部分。 這里涉及的知識和使用 Socket 基本上都是一樣的,唯一的區(qū)別就是對于內(nèi)容本身的處理,因為內(nèi)容本身是包含數(shù)據(jù)和非數(shù)據(jù)部分的。(站在 HTTP 的角度,只能看到數(shù)據(jù)部分。) 使用 Socket 編程,簡單來說就是監(jiān)聽一個端口,一旦有連接到來,就進行處理。(這里使用傳統(tǒng)的 BIO,NIO 那部分我不會。)
這里我的處理是,使用一個線程池進行處理,每一個連接使用一個線程進行處理。關于這個類(Server 類)的完整代碼,見下面。
public void receive() {
//使用線程池處理請求
ExecutorService pool = Executors.newFixedThreadPool(THREAD_NUMBER);
while (true) {
try {
Socket connection = server.accept();
pool.submit(new UserConnection(connection));
} catch (IOException e) {
System.out.println(this.getDate()+" 用戶連接斷開");
e.printStackTrace();
}
}
}
接收請求的代碼:Server 類
package com.dragon;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
private static final int THREAD_NUMBER = 10;
private ServerSocket server;
private int port;
public Server(int port) {
this.port = port;
}
//啟動服務。
public void start() {
try {
server = new ServerSocket(port);
System.out.println(this.getDate()+" 服務啟動!");
this.receive();
} catch (IOException e) {
System.out.println(this.getDate()+" 服務啟動失??!");
e.printStackTrace();
}
}
public void receive() {
//使用線程池處理請求
ExecutorService pool = Executors.newFixedThreadPool(THREAD_NUMBER);
while (true) {
try {
Socket connection = server.accept();
pool.submit(new UserConnection(connection));
} catch (IOException e) {
System.out.println(this.getDate()+" 用戶連接斷開");
e.printStackTrace();
}
}
}
private String getDate() {
String format = "yyyy-MM-dd HH:mm:ss";
Date now = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
return dateFormat.format(now);
}
}
Http 請求報文從 TCP 的層次來看就是一段二進制數(shù)據(jù)流(網(wǎng)絡是分層的),所以我們可以直接使用 TCP 接收這個流, 因為涉及二進制數(shù)據(jù)(如文件上傳)的報文解析比較復雜,我也不知道該怎么做,所以這里我只是測試簡單的不含有二進制文件的請求。)
注:因為具體的解析也是很復雜的,這涉及到 HTTP 報文的結構,但是如果不涉及文件上傳,那么整個報文都是一些字符數(shù)據(jù),所以一次性讀取全部請求報文,然后轉成字符串,使用字符串來進行解析。
in = connection.getInputStream();
out = connection.getOutputStream();
//這個數(shù)字是隨便設置的,因為要一次性讀取整個請求報文,不能太小。(但是其實這個也很大了)
byte[] b = new byte[5*1024];
BufferedInputStream input = new BufferedInputStream(in);
int count = input.read(b);
String requestMessage = new String(b, 0, count);
System.out.println("====================報文分隔符上界===================");
System.out.println(requestMessage);
System.out.println("====================報文分隔符下界===================");
處理請求代碼:UserConnection 類
請求和響應信息初始化 說明:使用靜態(tài)初始化塊來初始化信息,將用戶提前配置好的 xml 信息讀取入內(nèi)存,前面提到過這部分。
// 初始化配置好的信息
static {
try {
requestAndResponses = RequestAndResponseResolver.listRequestAndResponse("./src/com/dragon/request_and_response.xml");
} catch (DocumentException e) {
e.printStackTrace();
}
}
請求處理和獲取響應信息 因為這里是模擬調(diào)用,所以我主要關注請求頭中的三個部分數(shù)據(jù):請求方法(method)、請求路徑(path)、請求參數(shù)(param)。 對于 GET 方式和 POST 方式分開進行處理,上面簡單介紹了 GET 和 POST 的區(qū)別(但是不夠詳細,可以去參考網(wǎng)上的其它資料了解)。
通過這段代碼,如果是 GET 方式,就將 RequestAndResponse 對象中的 content_type(返回值數(shù)據(jù)類型) 和 value (返回值數(shù)據(jù))取出來,并賦值給局部變量 content_type 和 value。
if ("GET".compareTo(method) == 0) {
for (RequestAndResponse r : requestAndResponses) {
//這里需要對 get 方式時的請求進行解碼,因為一些非 ASCII 碼字符會被編碼,比如漢字。
path = URLDecoder.decode(path, ENCODE);
if (r.getMethod().equals(method) && r.getPath().equals(path)) {
content_type = r.getContent_type();
value = r.getValue();
break;
}
}
} else {
//POST 方式,請求參數(shù)是在請求體中的,請求頭和請求體中間有一個換行符。
String param = requestMessage.substring(requestMessage.lastIndexOf(CRLF) + 2); //這里是不包括 CRLF 的兩個字符的。
for (RequestAndResponse r : requestAndResponses) { //因為這個get方式的 參數(shù)為空,所以這里必須是 param 在前。
if (r.getMethod().equals(method) && r.getPath().equals(path) && param.equals(r.getParam())) {
content_type = r.getContent_type();
value = r.getValue();
break;
}
}
}
這里介紹一個知識:URL 中的字符是特定的,不允許中文等字符的出現(xiàn),所以發(fā)送請求時會對中文等字符進行編碼,如果直接使用 equals 方法的,當然不會相等了,所以需要先對數(shù)據(jù)進行解碼,然后再調(diào)用 equals 方法進行處理。這個是我們平時廣泛使用 的東西,有時候使用瀏覽器可以看到帶很多奇怪字符 URL,它們都是被處理過的。
舉一個簡單的例子:
String str = "我愛你";
String en_str = java.net.URLEncoder.encode(str, "UTF-8");
String de_str = java.net.URLDecoder.decode(en_str, "UTF-8");
System.out.println("編碼字符:" + en_str);
System.out.println("解碼字符:" + de_str);

注意:這里有一個特殊的情況,如果發(fā)起了沒有配置的請求方法和路徑,那么程序會出錯。所以,這里的 content_type 和 value 有一個默認的值,而且非常有趣!

執(zhí)行響應 響應信息主要關注幾點:響應信息長度(Content-Length)(按字節(jié)數(shù)計算)、響應內(nèi)容類型(Content-Type)。
雖然發(fā)送的請求里不能帶二進制文件,但是響應信息是可以返回文件的,而且使用 Content-Length (一次性發(fā)送),不使用 Chunked 分塊發(fā)送(這里我還不太明白,而且只是模擬,應該使用一些簡單的小文件。)。
下面是區(qū)分響應類型為 json (字符串) 還是 文件(二進制數(shù)據(jù)) 的代碼:
如果是字符串,則 value 的值是字符串的值,如果是文件,則 value 的值為一個具體的本地路徑。(不應該使用網(wǎng)絡圖片,即使修改程序可以做到也沒有必要,因為這樣就需要依賴網(wǎng)絡了。)
//這里我只處理字符串類和文件類兩種響應體
//響應體
int len = 0;
String responseBody = null; //響應值是 json 數(shù)據(jù)
File file = null; //響應值是 文件
if (content_type.equals("application/json")) { //如果是 json 數(shù)據(jù),否則就是 文件類數(shù)據(jù)(圖片、文檔或其它文件)
responseBody = value;
len = responseBody.getBytes().length; //響應體的字節(jié)數(shù),注意是字節(jié)數(shù)!
} else {
file = new File(value);
len = (int) file.length();
}
然后就可以準備發(fā)送響應數(shù)據(jù)了,下面是發(fā)送響應的代碼,注意報文的具體結構。
//響應頭
responseHeader.append("HTTP/1.1").append(BLANK);
responseHeader.append(200).append(BLANK);
responseHeader.append("OK").append(CRLF);
responseHeader.append("Server:"+"CrazyDragon").append(CRLF);
responseHeader.append("Date:").append(BLANK).append(date).append(CRLF);
responseHeader.append("Content-Type:").append(BLANK).append(content_type).append(CRLF);
responseHeader.append("Content-Length:").append(BLANK).append(len).append(CRLF);
responseHeader.append(CRLF);
//如果 字符串變量 responseBody 不為空,則說明返回值是 json 數(shù)據(jù)(字符串)
//否則就是文件類的流了。
if (responseBody != null) {
String response = responseHeader.toString() + responseBody;
out.write(response.getBytes("UTF-8"));
} else {
out.write(responseHeader.toString().getBytes("UTF-8"));
int hasRead = 0;
byte[] data = new byte[4*1024];
try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
while ((hasRead = inputStream.read(data)) != -1) {
out.write(data, 0, hasRead);
}
}
}
out.flush(); //必要的刷新流操作。
User Connection 的完整代碼:
package com.dragon;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.dom4j.DocumentException;
public class UserConnection implements Runnable{
private static final String BLANK = " ";
private static final String CRLF = "\r\n"; //換行符,不能寫反了!
private static final String ENCODE = "UTF-8";
private static final String default_content_type = "application/json"; //當任何匹配路徑都沒有時。
private static final String default_value = "404 NOT FOUND!\n沒有找到你配置的請求!";
private static List<RequestAndResponse> requestAndResponses;
private Socket connection;
// 初始化配置好的信息
static {
try {
requestAndResponses = RequestAndResponseResolver.listRequestAndResponse("./src/com/dragon/request_and_response.xml");
} catch (DocumentException e) {
e.printStackTrace();
}
}
public UserConnection(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
InputStream in = null;
OutputStream out = null;
try {
in = connection.getInputStream();
out = connection.getOutputStream();
//這個數(shù)字是隨便設置的,因為要一次性讀取整個請求報文,不能太小。(但是其實這個也很大了)
byte[] b = new byte[5*1024];
BufferedInputStream input = new BufferedInputStream(in);
int count = input.read(b);
String requestMessage = new String(b, 0, count);
System.out.println("====================報文分隔符上界===================");
System.out.println(requestMessage);
System.out.println("====================報文分隔符下界===================");
//以第一個 換行符 CRLF 為界限取出 請求路徑和請求參數(shù)
String requestLine = requestMessage.substring(0, requestMessage.indexOf(CRLF));
String[] line = requestLine.split("\\s");
String method = line[0]; //考慮大小寫。
String path = line[1];
//這個數(shù)組是有三個元素,最后一個是 協(xié)議的版本,這里不需要,就不處理了。
String content_type = default_content_type;
String value = default_value;
if ("GET".compareTo(method) == 0) {
// System.out.println("請求方式:" + method + " 請求路徑(含參數(shù)):" + path);
for (RequestAndResponse r : requestAndResponses) {
//這里需要對 get 方式時的請求進行解碼,因為一些非 ASCII 碼字符會被編碼,比如漢字。
path = URLDecoder.decode(path, ENCODE);
if (r.getMethod().equals(method) && r.getPath().equals(path)) {
content_type = r.getContent_type();
value = r.getValue();
break;
}
}
} else {
//POST 方式,請求參數(shù)是在請求體中的,請求頭和請求體中間有一個換行符。
String param = requestMessage.substring(requestMessage.lastIndexOf(CRLF) + 2); //這里是不包括 CRLF 的兩個字符的。
for (RequestAndResponse r : requestAndResponses) { //因為這個get方式的 參數(shù)為空,所以這里必須是 param 在前。
if (r.getMethod().equals(method) && r.getPath().equals(path) && param.equals(r.getParam())) {
content_type = r.getContent_type();
value = r.getValue();
System.out.println(content_type+" "+value);
break;
}
}
}
StringBuilder responseHeader = new StringBuilder();
String date = this.getDate();
//這里我只處理字符串類和文件類兩種響應體
//響應體
int len = 0;
String responseBody = null; //響應值是 json 數(shù)據(jù)
File file = null; //響應值是 文件
if (content_type.equals("application/json")) { //如果是 json 數(shù)據(jù),否則就是 文件類數(shù)據(jù)(圖片、文檔或其它文件)
responseBody = value;
len = responseBody.getBytes().length; //響應體的字節(jié)數(shù),注意是字節(jié)數(shù)!
} else {
file = new File(value);
len = (int) file.length();
}
//響應頭
responseHeader.append("HTTP/1.1").append(BLANK);
responseHeader.append(200).append(BLANK);
responseHeader.append("OK").append(CRLF);
responseHeader.append("Server:"+"CrazyDragon").append(CRLF);
responseHeader.append("Date:").append(BLANK).append(date).append(CRLF);
responseHeader.append("Content-Type:").append(BLANK).append(content_type).append(CRLF);
responseHeader.append("Content-Length:").append(BLANK).append(len).append(CRLF);
responseHeader.append(CRLF);
//如果 字符串變量 responseBody 不為空,則說明返回值是 json 數(shù)據(jù)(字符串)
//否則就是文件類的流了。
if (responseBody != null) {
String response = responseHeader.toString() + responseBody;
out.write(response.getBytes("UTF-8"));
} else {
out.write(responseHeader.toString().getBytes("UTF-8"));
int hasRead = 0;
byte[] data = new byte[4*1024];
try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
while ((hasRead = inputStream.read(data)) != -1) {
out.write(data, 0, hasRead);
}
}
}
out.flush(); //必要的刷新流操作。
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null)
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String getDate() {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.CHINA);
format.setTimeZone(TimeZone.getTimeZone("GMT")); // 設置時區(qū)為GMT
return format.format(date);
}
}
主程序類:Main
package com.dragon;
public class Main {
public static void main(String[] args) {
Server server = new Server(9000);
server.start();
}
}
更多的測試示例
請求方式:GET 請求路徑和參數(shù):/query/龍 預期的響應類型:application/json 預期的響應值:龍是中國等東亞國家古代神話傳說生活于海中的神異生物。 測試結果:

請求方式:GET 請求路徑和參數(shù):/login?account=123&pwd=456 預期的響應類型:application/json 預期的響應值:success 測試結果:

請求方式:GET 請求路徑和參數(shù):/pictures/husky.jpeg 預期的響應類型:image/jpeg 預期的響應值:一張圖片(地址為:D:/DB/husky.jpeg) 測試結果:

請求方式:POST 請求路徑:/login 請求參數(shù):account=123&pwd=456 預期的響應類型:application/json 預期的響應值:{“result”:success} 測試結果:

注:這是使用 HttpClient 發(fā)送的 POST 請求。

接收到的 POST 請求:

接收到的 GET 請求(含中文參數(shù)): /query/龍 注意:“龍” 已經(jīng)被編碼了。

總結
最近抽了一點時間,寫這個小玩具(也許并沒有什么用吧),中間也思考了很多東西,發(fā)現(xiàn)這個關于協(xié)議這個東西還是挺有趣的,不過我的知識似乎也不夠了。對于報文解析這種事情,沒有一定的基礎知識是做不來的,特別是復雜的報文(Content-Type 為 multipart/form-data)。這些以后有機會再去嘗試吧,就先到這里了。
到此這篇關于關于Java實現(xiàn)HttpServer模擬前端接口調(diào)用的文章就介紹到這了,更多相關HttpServer模擬前端接口調(diào)用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MyBatis通過JDBC數(shù)據(jù)驅動生成的執(zhí)行語句問題
這篇文章主要介紹了MyBatis通過JDBC數(shù)據(jù)驅動生成的執(zhí)行語句問題的相關資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08

