原文鏈接:使用Java Socket手擼一個http服務器java
做爲一個java後端,提供http服務能夠說是基本技能之一了,可是你真的瞭解http協議麼?你知道知道如何手擼一個http服務器麼?tomcat的底層是怎麼支持http服務的呢?大名鼎鼎的Servlet又是什麼東西呢,該怎麼使用呢?git
在初學java時,socket編程是逃不掉的一章;雖然在實際業務項目中,使用這個的可能性基本爲0,本篇博文將主要介紹如何使用socket來實現一個簡單的http服務器功能,提供常見的get/post請求支持,並再此過程當中瞭解下http協議github
既然咱們的目標是藉助socket來搭建http服務器,那麼咱們首先須要確認兩點,一是如何使用socket;另外一個則是http協議如何,怎麼解析數據;下面分別進行說明編程
咱們這裏主要是利用ServerSocket來綁定端口,提供tcp服務,基本使用姿式也比較簡單,通常套路以下json
對應的僞代碼以下:bootstrap
ServerSocket serverSocket = new ServerSocket(port, ip) serverSocket.accept(); // 接收請求數據 socket.getInputStream(); // 返回數據給請求方 out = socket.getOutputStream() out.print(xxx) out.flush();; // 關閉鏈接 socket.close()
咱們上面的ServerSocket走的是TCP協議,HTTP協議自己是在TCP協議之上的一層,對於咱們建立http服務器而言,最須要關注的無非兩點後端
因此咱們須要知道數據格式的規範了數組
請求消息tomcat
響應消息服務器
上面兩張圖,先有個直觀映象,接下來開始抓重點
不論是請求消息仍是相應消息,均可以劃分爲三部分,這就爲咱們後面的處理簡化了不少
接下來開始進入正題,基於socket建立一個http服務器,使用socket基本沒啥太大的問題,咱們須要額外關注如下幾點
咱們從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; }
每一個請求,單獨分配一個任務來幹這個事情,就是爲了支持併發,對於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()); }
前面的基本上把該乾的事情都幹了,剩下的就簡單了,建立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服務器基本上就搭建完了,接下來就能夠進行測試了
作這個服務器,主要是基於項目 quick-fix 產生的,這個項目主要是爲了解決應用內部服務訪問與數據訂正,咱們在這個項目的基礎上進行測試
一個完成的post請求以下
接下來咱們看下打印出返回頭的狀況
com.git.hui.fix.core.endpoint.BasicHttpServer
com.git.hui.fix.core.endpoint.HttpMessageParser
com.git.hui.fix.core.endpoint.HttpTask
一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛
盡信書則不如,已上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
一灰灰blog
知識星球