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


現在作為一個開發人員,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函數

 

[java]  view plain  copy
 
  1. public void start() throws IOException {  
  2.         myServerSocket = new ServerSocket();  
  3.         myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));  
  4.   
  5.         myThread = new Thread(new Runnable() {  
  6.             @Override  
  7.             public void run() {  
  8.                 do {  
  9.                     try {  
  10.                         final Socket finalAccept = myServerSocket.accept();  
  11.                         registerConnection(finalAccept);  
  12.                         finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);  
  13.                         final InputStream inputStream = finalAccept.getInputStream();  
  14.                         asyncRunner.exec(new Runnable() {  
  15.                             @Override  
  16.                             public void run() {  
  17.                                 OutputStream outputStream = null;  
  18.                                 try {  
  19.                                     outputStream = finalAccept.getOutputStream();  
  20.                                     TempFileManager tempFileManager = tempFileManagerFactory.create();  
  21.                                     HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());  
  22.                                     while (!finalAccept.isClosed()) {  
  23.                                         session.execute();  
  24.                                     }  
  25.                                 } catch (Exception e) {  
  26.                                     // When the socket is closed by the client, we throw our own SocketException  
  27.                                     // to break the  "keep alive" loop above.  
  28.                                     if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {  
  29.                                         e.printStackTrace();  
  30.                                     }  
  31.                                 } finally {  
  32.                                     safeClose(outputStream);  
  33.                                     safeClose(inputStream);  
  34.                                     safeClose(finalAccept);  
  35.                                     unRegisterConnection(finalAccept);  
  36.                                 }  
  37.                             }  
  38.                         });  
  39.                     } catch (IOException e) {  
  40.                     }  
  41.                 } while (!myServerSocket.isClosed());  
  42.             }  
  43.         });  
  44.         myThread.setDaemon(true);  
  45.         myThread.setName("NanoHttpd Main Listener");  
  46.         myThread.start();  
  47.     }  

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:

 

[plain]  view plain  copy
 
  1. // Read the first 8192 bytes.  
  2. // The full header should fit in here.  
  3.                 // Apache's default header limit is 8KB.  
  4.                 // Do NOT assume that a single read will get the entire header at once!  
  5.                 byte[] buf = new byte[BUFSIZE];  
  6.                 splitbyte = 0;  
  7.                 rlen = 0;  
  8.                 {  
  9.                     int read = -1;  
  10.                     try {  
  11.                         read = inputStream.read(buf, 0, BUFSIZE);  
  12.                     } catch (Exception e) {  
  13.                         safeClose(inputStream);  
  14.                         safeClose(outputStream);  
  15.                         throw new SocketException("NanoHttpd Shutdown");  
  16.                     }  
  17.                     if (read == -1) {  
  18.                         // socket was been closed  
  19.                         safeClose(inputStream);  
  20.                         safeClose(outputStream);  
  21.                         throw new SocketException("NanoHttpd Shutdown");  
  22.                     }  
  23.                     while (read > 0) {  
  24.                         rlen += read;  
  25.                         splitbyte = findHeaderEnd(buf, rlen);  
  26.                         if (splitbyte > 0)  
  27.                             break;  
  28.                         read = inputStream.read(buf, rlen, BUFSIZE - rlen);  
  29.                     }  
  30.                 }  

1.讀取socket數據流的前8192個字節,因為http協議中頭部最長為8192

 

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

 

[java]  view plain  copy
 
  1. if (splitbyte < rlen) {  
  2.                     inputStream.unread(buf, splitbyte, rlen - splitbyte);  
  3.                 }  
  4.   
  5.                 parms = new HashMap<String, String>();  
  6.                 if(null == headers) {  
  7.                     headers = new HashMap<String, String>();  
  8.                 }  
  9.   
  10.                 // Create a BufferedReader for parsing the header.  
  11.                 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));  
  12.   
  13.                 // Decode the header into parms and header java properties  
  14.                 Map<String, String> pre = new HashMap<String, String>();  
  15.                 decodeHeader(hin, pre, parms, headers);  

 

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

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

 

 

[java]  view plain  copy
 
  1. private int findHeaderEnd(final byte[] buf, int rlen) {  
  2.             int splitbyte = 0;  
  3.             while (splitbyte + 3 < rlen) {  
  4.                 if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {  
  5.                     return splitbyte + 4;  
  6.                 }  
  7.                 splitbyte++;  
  8.             }  
  9.             return 0;  
  10.         }  

1.http協議規定header和body之間使用兩個回車換行分割

 

 

 

[java]  view plain  copy
 
  1. private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)  
  2.             throws ResponseException {  
  3.             try {  
  4.                 // Read the request line  
  5.                 String inLine = in.readLine();  
  6.                 if (inLine == null) {  
  7.                     return;  
  8.                 }  
  9.   
  10.                 StringTokenizer st = new StringTokenizer(inLine);  
  11.                 if (!st.hasMoreTokens()) {  
  12.                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");  
  13.                 }  
  14.   
  15.                 pre.put("method", st.nextToken());  
  16.   
  17.                 if (!st.hasMoreTokens()) {  
  18.                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");  
  19.                 }  
  20.   
  21.                 String uri = st.nextToken();  
  22.   
  23.                 // Decode parameters from the URI  
  24.                 int qmi = uri.indexOf('?');  
  25.                 if (qmi >= 0) {  
  26.                     decodeParms(uri.substring(qmi + 1), parms);  
  27.                     uri = decodePercent(uri.substring(0, qmi));  
  28.                 } else {  
  29.                     uri = decodePercent(uri);  
  30.                 }  
  31.   
  32.                 // If there's another token, it's protocol version,  
  33.                 // followed by HTTP headers. Ignore version but parse headers.  
  34.                 // NOTE: this now forces header names lowercase since they are  
  35.                 // case insensitive and vary by client.  
  36.                 if (st.hasMoreTokens()) {  
  37.                     String line = in.readLine();  
  38.                     while (line != null && line.trim().length() > 0) {  
  39.                         int p = line.indexOf(':');  
  40.                         if (p >= 0)  
  41.                             headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());  
  42.                         line = in.readLine();  
  43.                     }  
  44.                 }  
  45.   
  46.                 pre.put("uri", uri);  
  47.             } catch (IOException ioe) {  
  48.                 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);  
  49.             }  
  50.         }  

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方法實現了一個默認的簡單處理功能。

 

[java]  view plain  copy
 
  1. /** 
  2.      * Override this to customize the server. 
  3.      * <p/> 
  4.      * <p/> 
  5.      * (By default, this delegates to serveFile() and allows directory listing.) 
  6.      * 
  7.      * @param session The HTTP session 
  8.      * @return HTTP response, see class Response for details 
  9.      */  
  10.     public Response serve(IHTTPSession session) {  
  11.         Map<String, String> files = new HashMap<String, String>();  
  12.         Method method = session.getMethod();  
  13.         if (Method.PUT.equals(method) || Method.POST.equals(method)) {  
  14.             try {  
  15.                 session.parseBody(files);  
  16.             } catch (IOException ioe) {  
  17.                 return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());  
  18.             } catch (ResponseException re) {  
  19.                 return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());  
  20.             }  
  21.         }  
  22.   
  23.         Map<String, String> parms = session.getParms();  
  24.         parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());  
  25.         return serve(session.getUri(), method, session.getHeaders(), parms, files);  
  26.     }  

這個默認的方法處理了PUT和POST方法,如果不是就返回默認的返回值。

 

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

 

最后看一下發response的邏輯:

 

[java]  view plain  copy
 
  1. /** 
  2.          * Sends given response to the socket. 
  3.          */  
  4.         protected void send(OutputStream outputStream) {  
  5.             String mime = mimeType;  
  6.             SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);  
  7.             gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));  
  8.   
  9.             try {  
  10.                 if (status == null) {  
  11.                     throw new Error("sendResponse(): Status can't be null.");  
  12.                 }  
  13.                 PrintWriter pw = new PrintWriter(outputStream);  
  14.                 pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");  
  15.   
  16.                 if (mime != null) {  
  17.                     pw.print("Content-Type: " + mime + "\r\n");  
  18.                 }  
  19.   
  20.                 if (header == null || header.get("Date") == null) {  
  21.                     pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");  
  22.                 }  
  23.   
  24.                 if (header != null) {  
  25.                     for (String key : header.keySet()) {  
  26.                         String value = header.get(key);  
  27.                         pw.print(key + ": " + value + "\r\n");  
  28.                     }  
  29.                 }  
  30.   
  31.                 sendConnectionHeaderIfNotAlreadyPresent(pw, header);  
  32.   
  33.                 if (requestMethod != Method.HEAD && chunkedTransfer) {  
  34.                     sendAsChunked(outputStream, pw);  
  35.                 } else {  
  36.                     int pending = data != null ? data.available() : 0;  
  37.                     sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);  
  38.                     pw.print("\r\n");  
  39.                     pw.flush();  
  40.                     sendAsFixedLength(outputStream, pending);  
  41.                 }  
  42.                 outputStream.flush();  
  43.                 safeClose(data);  
  44.             } catch (IOException ioe) {  
  45.                 // Couldn't write? No can do.  
  46.             }  
  47.         }  

發送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結束。

 

[java]  view plain  copy
 
  1. private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {  
  2.             pw.print("Transfer-Encoding: chunked\r\n");  
  3.             pw.print("\r\n");  
  4.             pw.flush();  
  5.             int BUFFER_SIZE = 16 * 1024;  
  6.             byte[] CRLF = "\r\n".getBytes();  
  7.             byte[] buff = new byte[BUFFER_SIZE];  
  8.             int read;  
  9.             while ((read = data.read(buff)) > 0) {  
  10.                 outputStream.write(String.format("%x\r\n", read).getBytes());  
  11.                 outputStream.write(buff, 0, read);  
  12.                 outputStream.write(CRLF);  
  13.             }  
  14.             outputStream.write(String.format("0\r\n\r\n").getBytes());  
  15.         }  

 

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

 

[java]  view plain  copy
 
  1. private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {  
  2.             if (requestMethod != Method.HEAD && data != null) {  
  3.                 int BUFFER_SIZE = 16 * 1024;  
  4.                 byte[] buff = new byte[BUFFER_SIZE];  
  5.                 while (pending > 0) {  
  6.                     int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));  
  7.                     if (read <= 0) {  
  8.                         break;  
  9.                     }  
  10.                     outputStream.write(buff, 0, read);  
  11.                     pending -= read;  
  12.                 }  
  13.             }  
  14.         }  

 

 

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

1.能夠accept tcp連接並從socket中讀取request數據

2.把request的比特流轉換成request對象中的對象數據

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

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM