1. 程式人生 > >使用Java Socket手擼一個http服務器

使用Java Socket手擼一個http服務器

con body buffered run value nal uic news ble

原文連接:使用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服務器