NanoHTTPD源码解析

2017/5/4 posted in  Android

前言

在微信 、UC 等 APP 中,无法通过scheme 或者 AppLiks 调起我们的 APP,所以想在APP 内部建立起一个 LocalHttpServer,监听某个端口,web 通过访问本地的这个端口,完成通信,调用起一些服务

在 github 上发现了 NanoHttpd 项目,它仅仅使用一个类,完成了 server 的搭建。它使用的是Socket BIO(阻塞IO),一个客户端连接分发到一个线程的经典模型,而且具有良好的扩展性.

使用方法

使用比较简单。继承 'NanoHTTPD' , 复写'serve()' , 在该函数内处理。调用 start() 启动服务 。stop() 关闭服务

源码解析

从调用的入口 start() 开始分析

    /**
     * Start the server.
     *
     * @throws IOException
     *             if the socket is in use.
     */
    public void start() throws IOException {
        //建立 ServerSocket ,并且 bind 对应的 host 和端口号
        myServerSocket = new ServerSocket();
        myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort)
                : new InetSocketAddress(myPort));

        //开启一个线程监听这个端口
        myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int requestCount = 0;
                do {
                    try {
                        //阻塞方法,直到有 socket 链接
                        final Socket finalAccept = myServerSocket.accept();
                        ++requestCount;
                        //register ,存入一个 set ,方便统一管理 。
                        registerConnection(finalAccept);
                        finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
                        //从 socket 中拿到InputStream,客户端发送的数据都在这里
                        final InputStream inputStream = finalAccept.getInputStream();
                        final int finalRequestCount = requestCount;
                        //再开启一个线程单独处理这个 socket 
                        asyncRunner.exec(new Runnable() {
                            @Override
                            public void run() {
                                Thread.currentThread().setName("NanoHttpd Request Processor (#" + finalRequestCount + ")");
                                OutputStream outputStream = null;
                                try {
                                    outputStream = finalAccept.getOutputStream();
                                    TempFileManager tempFileManager = tempFileManagerFactory.create();
                                    //通过 inputStream ,outputStream 最终得到一个 HTTPSession
                                    HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream,
                                            finalAccept.getInetAddress());
                                    while (!finalAccept.isClosed()) {
                                        //最终调用session.execute 来处理
                                        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 (Throwable t) {
                    }
                } while (!myServerSocket.isClosed());
            }
        });
        myThread.setDaemon(true);
        myThread.setName("NanoHttpd Main Listener");
        myThread.start();
    }

可以看到所有数据的处理都在HTTPSession,excute()中 ,接着看下 HTTPSession 数据结构。他是NanoHTTPD 的内部类

    protected class HTTPSession implements IHTTPSession {
        public static final int BUFSIZE = 8192;
        private final TempFileManager tempFileManager;
        private final OutputStream outputStream;
        private PushbackInputStream inputStream;
        private int splitbyte;
        private int rlen;
        private String uri;
        private Method method;
        private Map<String, String> parms;
        private Map<String, String> headers;
        private CookieHandler cookies;
        private String queryParameterString;
        ....
        
        
        

实现了 IHTTPSession 这样一个借口

    /**
     * Handles one session, i.e. parses the HTTP request and returns the
     * response.
     */
    public interface IHTTPSession {
        void execute() throws IOException;

        Map<String, String> getParms();

        Map<String, String> getHeaders();

        /**
         * @return the path part of the URL.
         */
        String getUri();

        String getQueryParameterString();

        Method getMethod();

        InputStream getInputStream();

        CookieHandler getCookies();

        /**
         * Adds the files in the request body to the files map.
         *
         * @arg files - map to modify
         */
        void parseBody(Map<String, String> files) throws IOException, ResponseException;
    }

类似于getMethod() ,getInputStream(),getParms(),都提供了。
具体看下 execute方法

@Override
public void execute() throws IOException {
  try {
      // 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!
      //header 最大为8k,所以一次读取8k 的大小
      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;
              //找到 head 结束,也就是两个换行的位置,存入splitbyte ,后面解析的时候用到
              splitbyte = findHeaderEnd(buf, rlen);
              if (splitbyte > 0)
                  break;
              read = inputStream.read(buf, rlen, BUFSIZE - rlen);
          }
      }

    //为了避免 inputStream 被破坏,将读取的数据在回退到 inputStream 中
      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>();
      //把 header 中的参数 等解析出来
      decodeHeader(hin, pre, parms, headers);

      method = Method.lookup(pre.get("method"));
      if (method == null) {
          throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
      }

      uri = pre.get("uri");

      cookies = new CookieHandler(headers);

      // Ok, now do the serve()
      //这里是回调,HTTPSession 本身实现了 IHTTPSession 接口 
      Response r = serve(this);
      if (r == null) {
          throw new ResponseException(Response.Status.INTERNAL_ERROR,
                  "SERVER INTERNAL ERROR: Serve() returned a null response.");
      } else {
          cookies.unloadQueue(r);
          r.setRequestMethod(method);
          r.send(outputStream);
      }
  } catch (SocketException e) {
      // throw it out to close socket object (finalAccept)
      throw e;
  } catch (SocketTimeoutException ste) {
      throw ste;
  } catch (IOException ioe) {
      Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT,
              "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
      r.send(outputStream);
      safeClose(outputStream);
  } catch (ResponseException re) {:
      Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
      r.send(outputStream);
      safeClose(outputStream);
  } finally {
      tempFileManager.clear();
  }
}
//解析 header
/**
* Decodes the sent headers and loads the data into Key/value pairs
*/
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 将首行安装空格分割
      StringTokenizer st = new StringTokenizer(inLine);
      if (!st.hasMoreTokens()) {
          throw new ResponseException(Response.Status.BAD_REQUEST,
                  "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
      }

    //根据下图的 http 结构可以看出。首先读取的是 methos
      pre.put("method", st.nextToken());

      if (!st.hasMoreTokens()) {
          throw new ResponseException(Response.Status.BAD_REQUEST,
                  "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
      }

    //其次是 url
      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);
  }
}

session.parseBody(files) ,是解析出请求数据用的 。

       @Override
        public void parseBody(Map<String, String> files) throws IOException, ResponseException {
            RandomAccessFile randomAccessFile = null;
            BufferedReader in = null;
            try {
            //通过 tempFileManager 得到一个 RandomAccessFile
                randomAccessFile = getTmpBucket();

                long size;
                //如果 header 中有content-length ,那么 size 用这个。否则用之前找到 header 结束的位置来算
                if (headers.containsKey("content-length")) {
                    size = Integer.parseInt(headers.get("content-length"));
                } else if (splitbyte < rlen) {
                    size = rlen - splitbyte;
                } else {
                    size = 0;
                }

                // Now read all the body and write it to f
                //把 body 写入 tempfile 中
                byte[] buf = new byte[512];
                while (rlen >= 0 && size > 0) {
                    rlen = inputStream.read(buf, 0, (int) Math.min(size, 512));
                    size -= rlen;
                    if (rlen > 0) {
                        randomAccessFile.write(buf, 0, rlen);
                    }
                }

                // Get the raw body as a byte []
                ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0,
                        randomAccessFile.length());
                randomAccessFile.seek(0);

                // Create a BufferedReader for easily reading it as string.
                InputStream bin = new FileInputStream(randomAccessFile.getFD());
                in = new BufferedReader(new InputStreamReader(bin));

                // If the method is POST, there may be parameters
                // in data section, too, read it:
                if (Method.POST.equals(method)) {
                    String contentType = "";
                    String contentTypeHeader = headers.get("content-type");

                    StringTokenizer st = null;
                    if (contentTypeHeader != null) {
                        st = new StringTokenizer(contentTypeHeader, ",; ");
                        if (st.hasMoreTokens()) {
                            contentType = st.nextToken();
                        }
                    }

                    if ("multipart/form-data".equalsIgnoreCase(contentType)) {
                        // Handle multipart/form-data
                        if (!st.hasMoreTokens()) {
                            throw new ResponseException(Response.Status.BAD_REQUEST,
                                    "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
                        }

                        String boundaryStartString = "boundary=";
                        int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString)
                                + boundaryStartString.length();
                        String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
                        if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
                            boundary = boundary.substring(1, boundary.length() - 1);
                        }

                        decodeMultipartData(boundary, fbuf, in, parms, files);
                    } else {
                        String postLine = "";
                        StringBuilder postLineBuffer = new StringBuilder();
                        char pbuf[] = new char[512];
                        int read = in.read(pbuf);
                        while (read >= 0 && !postLine.endsWith("\r\n")) {
                            postLine = String.valueOf(pbuf, 0, read);
                            postLineBuffer.append(postLine);
                            read = in.read(pbuf);
                        }
                        postLine = postLineBuffer.toString().trim();
                        // Handle application/x-www-form-urlencoded
                        if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
                            decodeParms(postLine, parms);
                        } else if (postLine.length() != 0) {
                            // Special case for raw POST data => create a
                            // special files entry "postData" with raw content
                            // data
                            files.put("postData", postLine);
                        }
                    }
                } else if (Method.PUT.equals(method)) {
                    //最终 file 放在 这个 map 里
                    files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
                }
            } finally {
                safeClose(randomAccessFile);
                safeClose(in);
            }
        }

参考

http://shensy.iteye.com/blog/1880381
http://www.voidcn.com/blog/mrtitan/article/p-3280792.html