如何開發自己的HttpServer-NanoHttpd源碼解讀

來源:互聯網
上載者:User

標籤:http協議   源碼   伺服器   

現在作為一個開發人員,http server相關的內容已經是無論如何都要瞭解的知識了。用curl發一個請求,配置一下apache,部署一個web server對我們來說都不是很難,但要想搞清楚這些背後都發生了什麼技術細節還真不是很簡單的。所以新的系列將是分享我學習Http Server的過程。


NanoHttpd是Github上的一個開源項目,號稱只用一個java檔案就能建立一個http server,我將通過分析NanoHttpd的源碼解析如何開發自己的HttpServer。Github 地址:https://github.com/NanoHttpd/nanohttpd


在開始前首先簡單說明HttpServer的基本要素:

1.能接受HttpRequest並返回HttpResponse

2.滿足一個Server的基本特徵,能夠長時間運行


關於Http協議一般HttpServer都會聲明支援Http協議的哪些特性,nanohttpd作為一個輕量級的httpserver只實現了最簡單、最常用的功能,不過我們依然可以從中學習很多。


首先看下NanoHttpd類的start函數

public void start() throws IOException {        myServerSocket = new ServerSocket();        myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));        myThread = new Thread(new Runnable() {            @Override            public void run() {                do {                    try {                        final Socket finalAccept = myServerSocket.accept();                        registerConnection(finalAccept);                        finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);                        final InputStream inputStream = finalAccept.getInputStream();                        asyncRunner.exec(new Runnable() {                            @Override                            public void run() {                                OutputStream outputStream = null;                                try {                                    outputStream = finalAccept.getOutputStream();                                    TempFileManager tempFileManager = tempFileManagerFactory.create();                                    HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());                                    while (!finalAccept.isClosed()) {                                        session.execute();                                    }                                } catch (Exception e) {                                    // When the socket is closed by the client, we throw our own SocketException                                    // to break the  "keep alive" loop above.                                    if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {                                        e.printStackTrace();                                    }                                } finally {                                    safeClose(outputStream);                                    safeClose(inputStream);                                    safeClose(finalAccept);                                    unRegisterConnection(finalAccept);                                }                            }                        });                    } catch (IOException e) {                    }                } while (!myServerSocket.isClosed());            }        });        myThread.setDaemon(true);        myThread.setName("NanoHttpd Main Listener");        myThread.start();    }
1.建立ServerSocket,bind制定連接埠

2.建立主線程,主線程負責和client建立串連

3.建立串連後會產生一個runnable對象放入asyncRunner中,asyncRunner.exec會建立一個線程來處理新產生的串連。

4.新線程首先建立了一個HttpSession,然後while(true)的執行httpSession.exec。

這裡介紹下HttpSession的概念,HttpSession是java裡Session概念的實現,簡單來說一個Session就是一次httpClient->httpServer的串連,當串連close後session就結束了,如果沒結束則session會一直存在。這點從這裡的代碼也能看到:如果socket不close或者exec沒有拋出異常(異常有可能是client段中斷連線)session會一直執行exec方法。

一個HttpSession中儲存了一次網路連接中server應該儲存的資訊,比如:URI,METHOD,PARAMS,HEADERS,COOKIES等。

5.這裡accept一個client的socket就建立一個獨立線程的server模型是ThreadServer模型,特點是一個connection就會建立一個thread,是比較簡單、常見的socket server實現。缺點是在同時處理大量串連時線程切換需要消耗大量的資源,如果有興趣可以瞭解更加高效的NIO實現方式。

當獲得client的socket後自然要開始處理client發送的httprequest。


Http Request Header的parse:

// Read the first 8192 bytes.// The full header should fit in here.                // Apache's default header limit is 8KB.                // Do NOT assume that a single read will get the entire header at once!                byte[] buf = new byte[BUFSIZE];                splitbyte = 0;                rlen = 0;                {                    int read = -1;                    try {                        read = inputStream.read(buf, 0, BUFSIZE);                    } catch (Exception e) {                        safeClose(inputStream);                        safeClose(outputStream);                        throw new SocketException("NanoHttpd Shutdown");                    }                    if (read == -1) {                        // socket was been closed                        safeClose(inputStream);                        safeClose(outputStream);                        throw new SocketException("NanoHttpd Shutdown");                    }                    while (read > 0) {                        rlen += read;                        splitbyte = findHeaderEnd(buf, rlen);                        if (splitbyte > 0)                            break;                        read = inputStream.read(buf, rlen, BUFSIZE - rlen);                    }                }
1.讀取socket資料流的前8192個位元組,因為http協議中頭部最長為8192

2.通過findHeaderEnd函數找到header資料的截止位置,並把位置儲存到splitbyte內。

if (splitbyte < rlen) {                    inputStream.unread(buf, splitbyte, rlen - splitbyte);                }                parms = new HashMap<String, String>();                if(null == headers) {                    headers = new HashMap<String, String>();                }                // Create a BufferedReader for parsing the header.                BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));                // Decode the header into parms and header java properties                Map<String, String> pre = new HashMap<String, String>();                decodeHeader(hin, pre, parms, headers);

1.使用unread函數將之前讀出來的body pushback回去,這裡使用了pushbackstream,用法比較巧妙,因為一旦讀到了header的尾部就需要進入下面的邏輯來判斷是否需要再讀下去了,而不應該一直讀,讀到沒有資料為止

2.decodeHeader,將byte的header轉換為java對象


private int findHeaderEnd(final byte[] buf, int rlen) {            int splitbyte = 0;            while (splitbyte + 3 < rlen) {                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {                    return splitbyte + 4;                }                splitbyte++;            }            return 0;        }
1.http協議規定header和body之間使用兩個斷行符號換行分割


private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)            throws ResponseException {            try {                // Read the request line                String inLine = in.readLine();                if (inLine == null) {                    return;                }                StringTokenizer st = new StringTokenizer(inLine);                if (!st.hasMoreTokens()) {                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");                }                pre.put("method", st.nextToken());                if (!st.hasMoreTokens()) {                    throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");                }                String uri = st.nextToken();                // Decode parameters from the URI                int qmi = uri.indexOf('?');                if (qmi >= 0) {                    decodeParms(uri.substring(qmi + 1), parms);                    uri = decodePercent(uri.substring(0, qmi));                } else {                    uri = decodePercent(uri);                }                // If there's another token, it's protocol version,                // followed by HTTP headers. Ignore version but parse headers.                // NOTE: this now forces header names lowercase since they are                // case insensitive and vary by client.                if (st.hasMoreTokens()) {                    String line = in.readLine();                    while (line != null && line.trim().length() > 0) {                        int p = line.indexOf(':');                        if (p >= 0)                            headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());                        line = in.readLine();                    }                }                pre.put("uri", uri);            } catch (IOException ioe) {                throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);            }        }
1.Http協議第一行是Method URI HTTP_VERSION

2.後面每行都是KEY:VALUE格式的header

3.uri需要經過URIDecode處理後才能使用

4.uri中如果包含?則表示有param,httprequest的param一般表現為:/index.jsp?username=xiaoming&id=2


下面是處理cookie,不過這裡cookie的實現較為簡單,所以跳過。之後是serve方法,serve方法提供了使用者自己實現httpserver具體邏輯的很好介面。在NanoHttpd中的serve方法實現了一個預設的簡單處理功能。

/**     * Override this to customize the server.     * <p/>     * <p/>     * (By default, this delegates to serveFile() and allows directory listing.)     *     * @param session The HTTP session     * @return HTTP response, see class Response for details     */    public Response serve(IHTTPSession session) {        Map<String, String> files = new HashMap<String, String>();        Method method = session.getMethod();        if (Method.PUT.equals(method) || Method.POST.equals(method)) {            try {                session.parseBody(files);            } catch (IOException ioe) {                return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());            } catch (ResponseException re) {                return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());            }        }        Map<String, String> parms = session.getParms();        parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());        return serve(session.getUri(), method, session.getHeaders(), parms, files);    }
這個預設的方法處理了PUT和POST方法,如果不是就返回預設的返回值。

parseBody方法中使用了tmpFile的方法儲存httpRequest的content資訊,然後處理,具體邏輯就不細說了,不是一個典型的實現。


最後看一下發response的邏輯:

/**         * Sends given response to the socket.         */        protected void send(OutputStream outputStream) {            String mime = mimeType;            SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);            gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));            try {                if (status == null) {                    throw new Error("sendResponse(): Status can't be null.");                }                PrintWriter pw = new PrintWriter(outputStream);                pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");                if (mime != null) {                    pw.print("Content-Type: " + mime + "\r\n");                }                if (header == null || header.get("Date") == null) {                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");                }                if (header != null) {                    for (String key : header.keySet()) {                        String value = header.get(key);                        pw.print(key + ": " + value + "\r\n");                    }                }                sendConnectionHeaderIfNotAlreadyPresent(pw, header);                if (requestMethod != Method.HEAD && chunkedTransfer) {                    sendAsChunked(outputStream, pw);                } else {                    int pending = data != null ? data.available() : 0;                    sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);                    pw.print("\r\n");                    pw.flush();                    sendAsFixedLength(outputStream, pending);                }                outputStream.flush();                safeClose(data);            } catch (IOException ioe) {                // Couldn't write? No can do.            }        }
發送response的步驟如下:

1.設定mimeType和Time等內容。

2.建立一個PrintWriter,按照HTTP協議依次開始寫入內容

3.第一行是HTTP的返回碼

4.然後是content-Type

5.然後是Date時間

6.之後是其他的HTTP Header

7.設定Keep-Alive的Header,Keep-Alive是Http1.1的新特性,作用是讓用戶端和伺服器端之間保持一個長連結。

8.如果用戶端指定了ChunkedEncoding則分塊發送response,Chunked Encoding是Http1.1的又一新特性。一般在response的body比較大的時候使用,server端會首先發送response的HEADER,然後分塊發送response的body,每個分塊都由chunk length\r\n和chunk data\r\n組成,最後由一個0\r\n結束。

private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {            pw.print("Transfer-Encoding: chunked\r\n");            pw.print("\r\n");            pw.flush();            int BUFFER_SIZE = 16 * 1024;            byte[] CRLF = "\r\n".getBytes();            byte[] buff = new byte[BUFFER_SIZE];            int read;            while ((read = data.read(buff)) > 0) {                outputStream.write(String.format("%x\r\n", read).getBytes());                outputStream.write(buff, 0, read);                outputStream.write(CRLF);            }            outputStream.write(String.format("0\r\n\r\n").getBytes());        }

9.如果沒指定ChunkedEncoding則需要指定Content-Length來讓用戶端指定response的body的size,然後再一直寫body直到寫完為止。

private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {            if (requestMethod != Method.HEAD && data != null) {                int BUFFER_SIZE = 16 * 1024;                byte[] buff = new byte[BUFFER_SIZE];                while (pending > 0) {                    int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));                    if (read <= 0) {                        break;                    }                    outputStream.write(buff, 0, read);                    pending -= read;                }            }        }


最後總結下實現HttpServer最重要的幾個部分:

1.能夠accept tcp串連並從socket中讀取request資料

2.把request的位元流轉換成request對象中的對象資料

3.根據http協議的規範處理http request

4.產生http response再寫回到socket中傳給client。




如何開發自己的HttpServer-NanoHttpd源碼解讀

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.