基於httpserver類實現的簡易http服務器

一、前言

利用Java的com.sun.net包下的httpserver類,實現的簡易http服務器。全部代碼已上傳至github,鏈接在文末。

主要功能爲:
1、只處理GET方式和POST方式的請求,其他的返回 501 Not Implemented
2、GET:1)如果請求路徑爲一個存在的html文件,返回200 OK和該html文件的全部內容。(對子目錄下的文件同樣也要能返回);2)如果請求路徑爲一個文件夾,且該文件夾內有index.html文件,返回200 OK和index.html文件的全部內容;3)如果請求的文件不存在,或者請求的路徑下無index.html文件,返回404 Not Found
3、POST:1)構造一個HTTP請求,包含兩個鍵值對:Name:姓名 和ID:學號,提交給/Post_show。服務器收到請求後返回200 OK且輸出以POST方式發送的Name和ID;2)其他情況如請求路徑不爲Post_show或鍵的名稱不爲Name和ID,返回404 Not Found
4、使用多線程處理,具體爲線程池方式實現
5、支持長選項參數:1)–ip 設置服務器端的IP地址;2)–port 設置服務器端監聽的端口號;3)–number-thread 線程數目。

二、功能展示

這裏只貼windows環境下測試的結果了。

1、Idea上運行

Run->Edit Configurations 設置參數

image-20200428144026839

也可以不設置,默認值就是上圖中的數

點擊運行

image-20200428144144719

這個“當前路徑”設置的是:

private static final String WEB_ROOT = System.getProperty("user.dir")
            + File.separator + "webroot";

後文有解釋。在webroot文件夾內存放了一些子文件夾和html文件等。

1.1、瀏覽器輸入請求

瀏覽器中輸入

127.0.0.1:8888/sub/02.jpg

便可請求到圖片D:\Study\Java\httpserver\webroot\sub\02.jpg

image-20200428144426501

image-20200428144632869

1.2、curl命令請求

curl現在是win10自帶的了吧,不用安裝直接能用。不過需要注意的是windows和linux下的curl使用存在區別。

image-20200428145850223

1.3、Postman請求

Postman是個很棒的工具!

image-20200428150629364

2、命令行上java -jar運行

參考IDEA打包jar包詳盡流程打成jar包,放在E盤

image-20200428151144321

此時將webroot和httpserver.jar放在同一目錄下

image-20200428151542441

請求方式和1中介紹過的類似

image-20200428151657616

三、具體實現

1、支持長選項參數(long options)

主要用到的是org.apache.commons.cli包,需要下載。

private static void HandleOption(String[] args) {
        CommandLineParser parser = new BasicParser();
        Options options = new Options();
        options.addOption("i", "ip", true, "Specify the server IP address");
        options.addOption("p", "port", true, "Selects which port the HTTP server listens on for incoming connections");
        options.addOption("n", "number-thread", true, "The number of threads");
        try {
            CommandLine commandLine = parser.parse(options, args);
            if (commandLine.hasOption('i')) {
                IP = commandLine.getOptionValue('i');
            }
            if (commandLine.hasOption('p')) {
                PORT = Integer.parseInt(commandLine.getOptionValue('p'));
            }
            if (commandLine.hasOption('n')) {
                ThreadNum = Integer.parseInt(commandLine.getOptionValue('n'));
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

如參數給出--ip 127.0.0.1 --port 8888 --number-thread 2,則將全局變量IP設爲"127.0.0.1",PORT設爲8888,ThreadNum設爲2。這些值也是默認值。

2、構建httpserver

public static void main(String[] args) {
        //System.out.println(WEB_ROOT);
        HandleOption(args);
        HttpServerProvider provider = HttpServerProvider.provider();
        HttpServer httpserver = null;
        try {
            httpserver = provider.createHttpServer(new InetSocketAddress(IP, PORT), ThreadNum);//監聽端口PORT,能同時接受ThreadNum個請求
            httpserver.setExecutor(Executors.newFixedThreadPool(ThreadNum));
        } catch (Exception e) {
            e.printStackTrace();
        }

        httpserver.createContext("/", new MyHandler());
        httpserver.createContext("/Post_show", new PostShowHandler());
        httpserver.start();
        System.out.println("當前路徑:"+WEB_ROOT);
        System.out.println("Http Server is running...");
    }

接收完命令行參數後,監聽地址IP的端口PORT,設能同時接收ThreadNum個請求。然後設置服務器的線程池對象,這裏用的是定長線程池newFixedThreadPool,最大線程數設爲ThreadNum,也可以使用其他的線程池對象。

然後創建服務器監聽的上下文,這裏創建了兩個。其實創建一個就好了,這裏爲了方便就創建了兩個。第二個表示只要匹配到/Post_show,就會調用PostShowHandler()處理對應的請求。第一個本可以表示只要匹配到/,就會調用MyHandle()處理對應的請求,因爲又設置了/Post_show的上下文監聽器,所以若http請求爲127.0.0.1:8888/Post_show的話,只會調用PostShowHandler()而不會調用MyHandler()了。

然後開啓服務。

WEB_ROOT爲:

private static final String WEB_ROOT = System.getProperty("user.dir") + ...

其中,System.getProperty(“user.dir”)爲獲得程序當前路徑。

3、處理Post請求

3.1、解析URI參數

static class PostShowHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange httpExchange) throws IOException { 
        String query = httpExchange.getRequestURI().getQuery();
        String[] keyValues = query.split("&");
        if (keyValues.length != 2) {
            System.out.println(keyValues.length);
            try {
                NotFoundResponse(httpExchange);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return;
        }

        String[] NamePara = keyValues[0].split("=");
        String[] IDPara = keyValues[1].split("=");
        if (!NamePara[0].equals("Name") || !IDPara[0].equals("ID") || NamePara.length != 2 || IDPara.length != 2) {
            try {
                NotFoundResponse(httpExchange);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return;
        }
        try {
            HandlePostRequest(httpExchange, NamePara[1], IDPara[1]);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

要處理的POST請求形如:127.0.0.1:8888/Post_show?Name=jsglyzcq&ID=40889820

首先利用getQuery()函數得到Query部分Name=jsglyzcq&ID=40889820,然後利用split函數分割字符串,以&隔開得到鍵值對Name=jsglyzcqID=40889820,再利用split函數以=分割得到具體的值Name和ID。

如果參數不是兩個,或者參數的名稱不是NameID等,都返回404,調用函數NotFoundResponse,該函數實現見3.2。

3.2、不滿足條件

private static void NotFoundResponse(HttpExchange httpExchange) throws Exception {
    Headers responseHeaders = httpExchange.getResponseHeaders();
    responseHeaders.set("Content-Type", "text/html;charset=utf-8");
    OutputStream responseBody = httpExchange.getResponseBody();
    OutputStreamWriter writer = new OutputStreamWriter(responseBody, "UTF-8");
    String response = "<html><title>404 Not Found</title><body bgcolor=ffffff>" + CRLF +
            "Not Found" + CRLF +
            "<hr><em>HTTP Web Server</em>" + CRLF +
            "</body></html>";
    httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, response.getBytes("UTF-8").length);
    writer.write(response);
    writer.close();
    responseBody.close();
}

比較簡單,看函數名稱就知道在做啥。測試如下:

image-20200428134248810

預覽:

image-20200428134316511

3.3、滿足條件

private static void HandlePostRequest(HttpExchange httpExchange, String Name, String ID) throws Exception {
        Headers responseHeaders = httpExchange.getResponseHeaders();
        responseHeaders.set("Content-Type", "text/html; charset=utf-8");
        OutputStream responseBody = httpExchange.getResponseBody();
        OutputStreamWriter writer = new OutputStreamWriter(responseBody, "UTF-8");
        String response = "<html><title>" + httpExchange.getRequestMethod() + "</title><body bgcolor=ffffff>" + CRLF +
                "Your Name: " + Name + CRLF +
                "ID: " + ID + CRLF +
                "<hr><em>HTTP Web Server</em>" + CRLF +
                "</body></html>";
        httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.getBytes("UTF-8").length);
        writer.write(response);
        writer.close();
        responseBody.close();
    }

和3.2差不多,就是返回內容有所更改

image-20200428133923463

預覽:

image-20200428134019690

4、處理Get請求

4.1、判斷何種方式請求

static class MyHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange httpExchange) throws IOException {
            String requestMethord = httpExchange.getRequestMethod();
            try {
                if (is404NotFound(httpExchange)) {
                    System.out.println(httpExchange.getRequestURI() + " does not exist!");
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (requestMethord.equalsIgnoreCase("GET")) {
                HandleGetRequest(httpExchange);
            } else if (requestMethord.equalsIgnoreCase("POST")) {
                //
            } else {
                Headers responseHeaders = httpExchange.getResponseHeaders();
                responseHeaders.set("Content-Type", "text/html;charset=utf-8");
                OutputStream responseBody = httpExchange.getResponseBody();
                OutputStreamWriter writer = new OutputStreamWriter(responseBody, "UTF-8");
                String response = "<html><title>501 Not Implemented</title><body bgcolor=ffffff>" + CRLF +
                        "Not Implemented" + CRLF +
                        "<p>Does not implement this methord: " + requestMethord + CRLF +
                        "<hr><em>HTTP Web Server</em>" + CRLF +
                        "</body></html>";
                httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_IMPLEMENTED, response.getBytes("UTF-8").length);
                writer.write(response);
                writer.close();
                responseBody.close();
            }
        }
    }

利用getRequestMethod()函數得到是何種方式的請求。判斷訪問路徑是否存在見4.2。

如果訪問路徑存在,並且訪問方式爲GET,則調用函數HandleGetRequest,見4.3;若訪問方式爲POST,也可以調用相應的函數,但我在之前已經實現過了,這裏就不填了;若訪問方式爲除了GETPOST以外的方式,則返回501 Not IMPLEMENTED:

image-20200428135640254

預覽:

image-20200428135707110

4.2、判斷訪問路徑是否存在

private static boolean is404NotFound(HttpExchange httpExchange) throws Exception {
        String requestURI = String.valueOf(httpExchange.getRequestURI());
        File file = new File(WEB_ROOT, requestURI);
        if (file.exists()) return false;
        else {
            System.out.println(file.getPath() + "does not exist!" + CRLF);
            NotFoundResponse(httpExchange);
            return true;
        }
    }

前文提到,WEB_ROOT爲獲取的當前程序正在運行的路徑,加上requestURI得到請求的文件,利用exists()函數判斷是否存在。若存在則返回false,表示不是404;若不存在則調用3.2中提過的函數NotFoundResponse()。

4.3、滿足條件

private static void HandleGetRequest(HttpExchange httpExchange) {
        String requestURI = String.valueOf(httpExchange.getRequestURI());
        File file = new File(WEB_ROOT, requestURI);
        File filetoSend = null;
        boolean hasIndexHtml = false;
        if (file.isDirectory()) {//若爲文件夾
            String[] files = file.list();
            for (String filename : files) {
                if (filename.equals("index.html")) {
                    filetoSend = new File(file.getPath() + File.separator + filename);
                    hasIndexHtml = true;
                    break;
                }
            }
            if (!hasIndexHtml) {
                NotFoundResponse(httpExchange);//這裏要處理異常,爲了減少篇幅刪去了
            } else {//這裏要處理異常,爲了減少篇幅刪去了
                FileSendResponse(httpExchange, filetoSend, "text/html; charset=utf-8");
            }
        } else {
            if (file.getPath().endsWith(".jpg")) {
                //這裏要處理異常,爲了減少篇幅刪去了
                FileSendResponse(httpExchange, file, "image/jpeg");
            }else if(file.getPath().endsWith(".html")){
                //這裏要處理異常,爲了減少篇幅刪去了
                FileSendResponse(httpExchange, file, "text/html; charset=utf-8");
            }
        }
    }

調用這個函數時說明訪問路徑是存在的。然後進一步判斷訪問的是一個文件夾還是文件,如果訪問的是文件夾,那麼查找該文件夾內是否有文件名爲index.html,若有則調用FileSendResponse()函數發送文件,見4.4,否則報404;如果訪問的是一個文件,就根據文件後綴名,設置相應的媒體類型mime。

4.4、發送文件

private static void FileSendResponse(HttpExchange httpExchange, File file, String mime) throws Exception {
        Headers responseHeaders = httpExchange.getResponseHeaders();
        responseHeaders.set("Content-Type", mime);
        OutputStream responseBody = httpExchange.getResponseBody();
        httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, file.length());
        if (file.exists()) {
            //System.out.println(file.length());
            System.out.println(file.getAbsolutePath() + " Exist");
            byte[] buffer = new byte[10240];
            FileInputStream fis = new FileInputStream(file);
            int readLength;
            while ((readLength = fis.read(buffer, 0, 10240)) > 0) {
                responseBody.write(buffer, 0, readLength);
            }
        }
        responseBody.close();
    }

將文件以字節流的形式發送出去。這裏的緩衝區大小爲10240。

例:

image-20200428141322487

四、後記

這是筆者對大三下選修的雲計算實驗二基礎版本的實現,因爲時間關係,直接選用了httpserver來實現。畢竟是兩天內完成的,若文中有詞不達意、錯漏之處,敬請指正🐟

代碼鏈接:待上傳

五、主要參考資料

1、java解析命令行參數——Iteye(org.apache.commons.cli包的使用)
2、Java服務端使用HttpServer處理Http請求——CSDN
3、Java項目——簡單的WebServer(一)——CSDN
4、使用Java實現一個最簡單的Web Server——CSDN

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章