使用Java Socket手擼一個http服務器
原文連接:使用Java Socket手擼一個http服務器
作為一個java後端,提供http服務可以說是基本技能之一了,但是你真的了解http協議麽?你知道知道如何手擼一個http服務器麽?tomcat的底層是怎麽支持http服務的呢?大名鼎鼎的Servlet又是什麽東西呢,該怎麽使用呢?
在初學java時,socket編程是逃不掉的一章;雖然在實際業務項目中,使用這個的可能性基本為0,本篇博文將主要介紹如何使用socket來實現一個簡單的http服務器功能,提供常見的get/post請求支持,並再此過程中了解下http協議
I. Http服務器從0到1
既然我們的目標是借助socket來搭建http服務器,那麽我們首先需要確認兩點,一是如何使用socket;另一個則是http協議如何,怎麽解析數據;下面分別進行說明
1. socket編程基礎
我們這裏主要是利用ServerSocket來綁定端口,提供tcp服務,基本使用姿勢也比較簡單,一般套路如下
- 創建ServerSocket對象,綁定監聽端口
- 通過accept()方法監聽客戶端請求
- 連接建立後,通過輸入流讀取客戶端發送的請求信息
- 通過輸出流向客戶端發送鄉音信息
- 關閉相關資源
對應的偽代碼如下:
ServerSocket serverSocket = new ServerSocket(port, ip) serverSocket.accept(); // 接收請求數據 socket.getInputStream(); // 返回數據給請求方 out = socket.getOutputStream() out.print(xxx) out.flush();; // 關閉連接 socket.close()
2. http協議
我們上面的ServerSocket走的是TCP協議,HTTP協議本身是在TCP協議之上的一層,對於我們創建http服務器而言,最需要關註的無非兩點
- 請求的數據怎麽按照http的協議解析出來
- 如何按照http協議,返回數據
所以我們需要知道數據格式的規範了
請求消息
響應消息
上面兩張圖,先有個直觀映象,接下來開始抓重點
不管是請求消息還是相應消息,都可以劃分為三部分,這就為我們後面的處理簡化了很多
- 第一行:狀態行
- 第二行到第一個空行:header(請求頭/相應頭)
- 剩下所有:正文
3. http服務器設計
接下來開始進入正題,基於socket創建一個http服務器,使用socket基本沒啥太大的問題,我們需要額外關註以下幾點
- 對請求數據進行解析
- 封裝返回結果
a. 請求數據解析
我們從socket中拿到所有的數據,然後解析為對應的http請求,我們先定義個Request對象,內部保存一些基本的HTTP信息,接下來重點就是將socket中的所有數據都撈出來,封裝為request對象
@Data
public static class Request {
/**
* 請求方法 GET/POST/PUT/DELETE/OPTION...
*/
private String method;
/**
* 請求的uri
*/
private String uri;
/**
* http版本
*/
private String version;
/**
* 請求頭
*/
private Map<String, String> headers;
/**
* 請求參數相關
*/
private String message;
}
根據前面的http協議介紹,解析過程如下,我們先看請求行的解析過程
請求行,包含三個基本要素:請求方法 + URI + http版本,用空格進行分割,所以解析代碼如下
/**
* 根據標準的http協議,解析請求行
*
* @param reader
* @param request
*/
private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {
String[] strs = StringUtils.split(reader.readLine(), " ");
assert strs.length == 3;
request.setMethod(strs[0]);
request.setUri(strs[1]);
request.setVersion(strs[2]);
}
請求頭的解析,從第二行,到第一個空白行之間的所有數據,都是請求頭;請求頭的格式也比較清晰, 形如 key:value
, 具體實現如下
/**
* 根據標準http協議,解析請求頭
*
* @param reader
* @param request
* @throws IOException
*/
private static void decodeRequestHeader(BufferedReader reader, Request request) throws IOException {
Map<String, String> headers = new HashMap<>(16);
String line = reader.readLine();
String[] kv;
while (!"".equals(line)) {
kv = StringUtils.split(line, ":");
assert kv.length == 2;
headers.put(kv[0].trim(), kv[1].trim());
line = reader.readLine();
}
request.setHeaders(headers);
}
最後就是正文的解析了,這一塊需要註意一點,正文可能為空,也可能有數據;有數據時,我們要如何把所有的數據都取出來呢?
先看具體實現如下
/**
* 根據標註http協議,解析正文
*
* @param reader
* @param request
* @throws IOException
*/
private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {
int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));
if (contentLen == 0) {
// 表示沒有message,直接返回
// 如get/options請求就沒有message
return;
}
char[] message = new char[contentLen];
reader.read(message);
request.setMessage(new String(message));
}
註意下上面我的使用姿勢,首先是根據請求頭中的Content-Type
的值,來獲得正文的數據大小,因此我們獲取的方式是創建一個這麽大的char[]
來讀取流中所有數據,如果我們的數組比實際的小,則讀不完;如果大,則數組中會有一些空的數據;
最後將上面的幾個解析封裝一下,完成request解析
/**
* http的請求可以分為三部分
*
* 第一行為請求行: 即 方法 + URI + 版本
* 第二部分到一個空行為止,表示請求頭
* 空行
* 第三部分為接下來所有的,表示發送的內容,message-body;其長度由請求頭中的 Content-Length 決定
*
* 幾個實例如下
*
* @param reqStream
* @return
*/
public static Request parse2request(InputStream reqStream) throws IOException {
BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));
Request httpRequest = new Request();
decodeRequestLine(httpReader, httpRequest);
decodeRequestHeader(httpReader, httpRequest);
decodeRequestMessage(httpReader, httpRequest);
return httpRequest;
}
b. 請求任務HttpTask
每個請求,單獨分配一個任務來幹這個事情,就是為了支持並發,對於ServerSocket而言,接收到了一個請求,那就創建一個HttpTask任務來實現http通信
那麽這個httptask幹啥呢?
- 從請求中撈數據
- 響應請求
- 封裝結果並返回
public class HttpTask implements Runnable {
private Socket socket;
public HttpTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
if (socket == null) {
throw new IllegalArgumentException("socket can‘t be null.");
}
try {
OutputStream outputStream = socket.getOutputStream();
PrintWriter out = new PrintWriter(outputStream);
HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream());
try {
// 根據請求結果進行響應,省略返回
String result = ...;
String httpRes = HttpMessageParser.buildResponse(httpRequest, result);
out.print(httpRes);
} catch (Exception e) {
String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString());
out.print(httpRes);
}
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
對於請求結果的封裝,給一個簡單的進行演示
@Data
public static class Response {
private String version;
private int code;
private String status;
private Map<String, String> headers;
private String message;
}
public static String buildResponse(Request request, String response) {
Response httpResponse = new Response();
httpResponse.setCode(200);
httpResponse.setStatus("ok");
httpResponse.setVersion(request.getVersion());
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Content-Length", String.valueOf(response.getBytes().length));
httpResponse.setHeaders(headers);
httpResponse.setMessage(response);
StringBuilder builder = new StringBuilder();
buildResponseLine(httpResponse, builder);
buildResponseHeaders(httpResponse, builder);
buildResponseMessage(httpResponse, builder);
return builder.toString();
}
private static void buildResponseLine(Response response, StringBuilder stringBuilder) {
stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ")
.append(response.getStatus()).append("\n");
}
private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {
for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {
stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
}
stringBuilder.append("\n");
}
private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {
stringBuilder.append(response.getMessage());
}
c. http服務搭建
前面的基本上把該幹的事情都幹了,剩下的就簡單了,創建ServerSocket
,綁定端口接收請求,我們在線程池中跑這個http服務
public class BasicHttpServer {
private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor();
private static ExecutorService taskExecutor;
private static int PORT = 8999;
static void startHttpServer() {
int nThreads = Runtime.getRuntime().availableProcessors();
taskExecutor =
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.DiscardPolicy());
while (true) {
try {
ServerSocket serverSocket = new ServerSocket(PORT);
bootstrapExecutor.submit(new ServerThread(serverSocket));
break;
} catch (Exception e) {
try {
//重試
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
bootstrapExecutor.shutdown();
}
private static class ServerThread implements Runnable {
private ServerSocket serverSocket;
public ServerThread(ServerSocket s) throws IOException {
this.serverSocket = s;
}
@Override
public void run() {
while (true) {
try {
Socket socket = this.serverSocket.accept();
HttpTask eventTask = new HttpTask(socket);
taskExecutor.submit(eventTask);
} catch (Exception e) {
e.printStackTrace();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
}
}
到這裏,一個基於socket實現的http服務器基本上就搭建完了,接下來就可以進行測試了
4. 測試
做這個服務器,主要是基於項目 quick-fix 產生的,這個項目主要是為了解決應用內部服務訪問與數據訂正,我們在這個項目的基礎上進行測試
一個完成的post請求如下
接下來我們看下打印出返回頭的情況
II. 其他
0. 項目源碼
- quick-fix
- 相關代碼:
com.git.hui.fix.core.endpoint.BasicHttpServer
com.git.hui.fix.core.endpoint.HttpMessageParser
com.git.hui.fix.core.endpoint.HttpTask
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 聲明
盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 掃描關註
一灰灰blog
知識星球
使用Java Socket手擼一個http服務器