zoukankan      html  css  js  c++  java
  • 【转】如何开发自己的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] == ' ' && buf[splitbyte + 1] == ' ' && buf[splitbyte + 2] == ' ' && buf[splitbyte + 3] == ' ') {  
    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() + "  ");  
    15.   
    16.                 if (mime != null) {  
    17.                     pw.print("Content-Type: " + mime + " ");  
    18.                 }  
    19.   
    20.                 if (header == null || header.get("Date") == null) {  
    21.                     pw.print("Date: " + gmtFrmt.format(new Date()) + " ");  
    22.                 }  
    23.   
    24.                 if (header != null) {  
    25.                     for (String key : header.keySet()) {  
    26.                         String value = header.get(key);  
    27.                         pw.print(key + ": " + value + " ");  
    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(" ");  
    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 和chunk data 组成,最后由一个0 结束。

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

  • 相关阅读:
    centos7中Apache,MySQL,php安装和项目
    centos7中Apache,MySQL,php安装
    tp5中index.php的隐藏
    php打印
    绝对与相对的区别
    tinkphp框架中config.php中数组中参数的意义
    tinkphp框架开启调试
    获取唯一随机数
    如何在magento中建立自定义页面
    获取用户邮寄地址
  • 原文地址:https://www.cnblogs.com/exmyth/p/7524432.html
Copyright © 2011-2022 走看看