標籤: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源碼解讀