NanoHttpd源码分析

111 篇文章 1 订阅
57 篇文章 1 订阅

最近在GitHub上发现一个有趣的项目——NanoHttpd。

说它有趣,是因为他是一个只有一个Java文件构建而成,实现了部分http协议的http server。

GitHub地址:https://github.com/NanoHttpd/nanohttpd 

作者最近还有提交,看了下最新的代码,写篇源码分析贴上来,欢迎大家多给些建议。大笑

------------------------------------------

NanoHttpd源码分析

NanoHttpd仅由一个文件构建而成,按照作者的意思是可以用作一个嵌入式http server。

由于它使用的是Socket BIO(阻塞IO),一个客户端连接分发到一个线程的经典模型,而且具有良好的扩展性。所以可以算是一个学习Socket  BIO Server比较不错的案例,同时如果你需要编写一个Socket Server并且不需要使用到NIO技术,那么NanoHttpd中不少代码都可以参考复用。

NanoHTTPD.java中,启动服务器执行start()方法,停止服务器执行stop()方法。

主要逻辑都在start()方法中:

Java代码  收藏代码

  1. private ServerSocket myServerSocket;  
  2. private Thread myThread;  
  3. private AsyncRunner asyncRunner;  
  4. //...  
  5. public void start() throws IOException {  
  6.         myServerSocket = new ServerSocket();  
  7.         myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));  
  8.         myThread = new Thread(new Runnable() {  
  9.             @Override  
  10.             public void run() {  
  11.                 do {  
  12.                     try {  
  13.                         final Socket finalAccept = myServerSocket.accept();  
  14.                         InputStream inputStream = finalAccept.getInputStream();  
  15.                         OutputStream outputStream = finalAccept.getOutputStream();  
  16.                         TempFileManager tempFileManager = tempFileManagerFactory.create();  
  17.                         final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);  
  18.                         asyncRunner.exec(new Runnable() {  
  19.                             @Override  
  20.                             public void run() {  
  21.                                 session.run();  
  22.                                 try {  
  23.                                     finalAccept.close();  
  24.                                 } catch (IOException ignored) {  
  25.                                     ignored.printStackTrace();  
  26.                                 }  
  27.                             }  
  28.                         });  
  29.                     } catch (IOException e) {  
  30.                         e.printStackTrace();  
  31.                     }  
  32.                 } while (!myServerSocket.isClosed());  
  33.             }  
  34.         });  
  35.         myThread.setDaemon(true);  
  36.         myThread.setName("NanoHttpd Main Listener");  
  37.         myThread.start();  
  38. }  

首先,创建serversocket并绑定端口。然后开启一个线程守护线程myThread,用作监听客户端连接。守护线程作用是为其它线程提供服务,就是类似于后来静默执行的线程,当所有非守护线程执行完后,守护线程自动退出。

当myThread线程start后,执行该线程实现runnable接口的匿名内部类run方法:

run方法中do...while循环保证serversocket关闭前该线程一直处于监听状态。myServerSocket.accept()如果在没有客户端连接时会一直阻塞,只有客户端连接后才会继续执行下面的代码。

当客户端连接后,获取其input和output stream后,需要将每个客户端连接都需要分发到一个线程中,这部分逻辑在上文中的asyncRunner.exec()内。

Java代码  收藏代码

  1. public interface AsyncRunner {  
  2.      void exec(Runnable code);  
  3. }  
  4. public static class DefaultAsyncRunner implements AsyncRunner {  
  5.      private long requestCount;  
  6.      @Override  
  7.      public void exec(Runnable code) {  
  8.          ++requestCount;  
  9.          Thread t = new Thread(code);  
  10.          t.setDaemon(true);  
  11.          t.setName("NanoHttpd Request Processor (#" + requestCount + ")");  
  12.          System.out.println("NanoHttpd Request Processor (#" + requestCount + ")");  
  13.          t.start();  
  14.      }  
  15.  }  

DefaultAsyncRunner是NanoHTTPD的静态内部类,实现AsyncRunner接口,作用是对每个请求创建一个线程t。每个t线程start后,会执行asyncRunner.exec()中匿名内部类的run方法:

Java代码  收藏代码

  1. TempFileManager tempFileManager = tempFileManagerFactory.create();  
  2. final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);  
  3. asyncRunner.exec(new Runnable() {  
  4.           @Override  
  5.            public void run() {  
  6.                  session.run();  
  7.                  try {  
  8.                         finalAccept.close();  
  9.                  } catch (IOException ignored) {  
  10.                         ignored.printStackTrace();  
  11.                  }  
  12.            }  
  13. });  

该线程执行时,直接调用HTTPSession的run,执行完后关闭client连接。HTTPSession同样是NanoHTTPD的内部类,虽然实现了Runnable接口,但是并没有启动线程的代码,而是run方法直接被调用。下面主要看一下HTTPSession类中的run方法,有点长,分段解析:

Java代码  收藏代码

  1. public static final int BUFSIZE = 8192;  
  2. public void run() {  
  3.             try {  
  4.                 if (inputStream == null) {  
  5.                     return;  
  6.                 }  
  7.                 byte[] buf = new byte[BUFSIZE];  
  8.                 int splitbyte = 0;  
  9.                 int rlen = 0;  
  10.                 {  
  11.                     int read = inputStream.read(buf, 0, BUFSIZE);  
  12.                     while (read > 0) {  
  13.                         rlen += read;  
  14.                         splitbyte = findHeaderEnd(buf, rlen);  
  15.                         if (splitbyte > 0)  
  16.                             break;  
  17.                         read = inputStream.read(buf, rlen, BUFSIZE - rlen);  
  18.                     }  
  19.                 }  
  20.                 //...  
  21. }  

首先从inputstream中读取8k个字节(apache默认最大header为8k),通过findHeaderEnd找到http header和body是位于哪个字节分割的--splitbyte。由于不会一次从stream中读出8k个字节,所以找到splitbyte就直接跳出。如果没找到,就从上次循环读取的字节处继续读取一部分字节。下面看一下findHeaderEnd是怎么划分http header和body的:

Java代码  收藏代码

  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. }  

其实很简单,http header的结束一定是两个连续的空行(\r\n)。

回到HTTPSession类的run方法中,读取到splitbyte后,解析http header:

Java代码  收藏代码

  1. BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));  
  2. Map<String, String> pre = new HashMap<String, String>();  
  3. Map<String, String> parms = new HashMap<String, String>();  
  4. Map<String, String> header = new HashMap<String, String>();  
  5. Map<String, String> files = new HashMap<String, String>();  
  6. decodeHeader(hin, pre, parms, header);  

主要看decodeHeader方法,也比较长,简单说一下:

Java代码  收藏代码

  1. String inLine = in.readLine();  
  2. if (inLine == null) {  
  3.     return;  
  4. }  
  5. StringTokenizer st = new StringTokenizer(inLine);  
  6. if (!st.hasMoreTokens()) {  
  7.     Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");  
  8.     throw new InterruptedException();  
  9. }  
  10. pre.put("method", st.nextToken());  
  11. if (!st.hasMoreTokens()) {  
  12.     Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");  
  13.     throw new InterruptedException();  
  14. }  
  15. String uri = st.nextToken();  
  16. // Decode parameters from the URI  
  17. int qmi = uri.indexOf('?');//分割参数  
  18. if (qmi >= 0) {  
  19.     decodeParms(uri.substring(qmi + 1), parms);  
  20.     uri = decodePercent(uri.substring(0, qmi));  
  21. else {  
  22.     uri = decodePercent(uri);  
  23. }  
  24. if (st.hasMoreTokens()) {  
  25.     String line = in.readLine();  
  26.     while (line != null && line.trim().length() > 0) {  
  27.         int p = line.indexOf(':');  
  28.         if (p >= 0)  
  29.             header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());  
  30.         line = in.readLine();  
  31.     }  
  32. }  

读取第一行,按空格分隔,解析出method和uri。最后循环解析出header内各属性(以:分隔)。

从decodeHeader中解析出header后,

Java代码  收藏代码

  1. Method method = Method.lookup(pre.get("method"));  
  2. if (method == null) {  
  3.            Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");  
  4.            throw new InterruptedException();  
  5. }  
  6. String uri = pre.get("uri");  
  7. long size = extractContentLength(header);//获取content-length  

获取content-length的值,代码就不贴了,就是从header中取出content-length属性。

处理完header,然后开始处理body,首先创建一个临时文件:

Java代码  收藏代码

  1. RandomAccessFile f = getTmpBucket();  

NanoHTTPD中将创建临时文件进行了封装(稍微有点复杂吐舌头),如下:

Java代码  收藏代码

  1. private final TempFileManager tempFileManager;  
  2. private RandomAccessFile getTmpBucket() {  
  3.             try {  
  4.                 TempFile tempFile = tempFileManager.createTempFile();  
  5.                 return new RandomAccessFile(tempFile.getName(), "rw");  
  6.             } catch (Exception e) {  
  7.                 System.err.println("Error: " + e.getMessage());  
  8.             }  
  9.             return null;  
  10. }  

其中tempFileManager是在上文start方法中初始化传入httpsession构造方法:

Java代码  收藏代码

  1. TempFileManager tempFileManager = tempFileManagerFactory.create();  
  2. final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);  

实际的临时文件类定义如下:

Java代码  收藏代码

  1. public interface TempFile {  
  2.         OutputStream open() throws Exception;  
  3.         void delete() throws Exception;  
  4.         String getName();  
  5. }  
  6. public static class DefaultTempFile implements TempFile {  
  7.         private File file;  
  8.         private OutputStream fstream;  
  9.         public DefaultTempFile(String tempdir) throws IOException {  
  10.             file = File.createTempFile("NanoHTTPD-"""new File(tempdir));  
  11.             fstream = new FileOutputStream(file);  
  12.         }  
  13.         @Override  
  14.         public OutputStream open() throws Exception {  
  15.             return fstream;  
  16.         }  
  17.         @Override  
  18.         public void delete() throws Exception {  
  19.             file.delete();  
  20.         }  
  21.         @Override  
  22.         public String getName() {  
  23.             return file.getAbsolutePath();  
  24.         }  
  25. }  
  26. public static class DefaultTempFileManager implements TempFileManager {  
  27.         private final String tmpdir;  
  28.         private final List<TempFile> tempFiles;  
  29.         public DefaultTempFileManager() {  
  30.             tmpdir = System.getProperty("java.io.tmpdir");  
  31.             tempFiles = new ArrayList<TempFile>();  
  32.         }  
  33.         @Override  
  34.         public TempFile createTempFile() throws Exception {  
  35.             DefaultTempFile tempFile = new DefaultTempFile(tmpdir);  
  36.             tempFiles.add(tempFile);  
  37.             return tempFile;  
  38.         }  
  39.         @Override  
  40.         public void clear() {  
  41.             for (TempFile file : tempFiles) {  
  42.                 try {  
  43.                     file.delete();  
  44.                 } catch (Exception ignored) {  
  45.                 }  
  46.          }  
  47.          tempFiles.clear();  
  48. }  

可以看到,临时文件的创建使用的是File.createTempFile方法,临时文件存放目录在java.io.tmpdir所定义的系统属性下,临时文件的类型是RandomAccessFile,该类支持对文件任意位置的读取和写入。

继续回到HttpSession的run方法内,从上文中解析出的splitbyte处将body读出并写入刚才创建的临时文件:

Java代码  收藏代码

  1. if (splitbyte < rlen) {  
  2.     f.write(buf, splitbyte, rlen - splitbyte);  
  3. }  
  4.   
  5. if (splitbyte < rlen) {  
  6.     size -= rlen - splitbyte + 1;   
  7. else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl) {  
  8.     size = 0;  
  9. }  
  10.   
  11. // Now read all the body and write it to f  
  12. buf = new byte[512];  
  13. while (rlen >= 0 && size > 0) {    
  14.     rlen = inputStream.read(buf, 0512);  
  15.     size -= rlen;  
  16.     if (rlen > 0) {  
  17.         f.write(buf, 0, rlen);  
  18.     }  
  19. }  
  20. System.out.println("buf body:"+new String(buf));  

然后,创建一个bufferedreader以方便读取该文件。注意,此处对文件的访问使用的是NIO内存映射,seek(0)表示将文件指针指向文件头。

Java代码  收藏代码

  1. // Get the raw body as a byte []  
  2. ByteBuffer fbuf = f.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, f.length());  
  3. f.seek(0);  
  4. // Create a BufferedReader for easily reading it as string.  
  5. InputStream bin = new FileInputStream(f.getFD());  
  6. BufferedReader in = new BufferedReader(new InputStreamReader(bin));  

之后,如果请求是POST方法,则取出content-type,并对multipart/form-data(上传)和application/x-www-form-urlencoded(表单提交)分别进行了处理:

Java代码  收藏代码

  1. if (Method.POST.equals(method)) {  
  2.                     String contentType = "";  
  3.                     String contentTypeHeader = header.get("content-type");  
  4.                     StringTokenizer st = null;  
  5.                     if (contentTypeHeader != null) {  
  6.                         st = new StringTokenizer(contentTypeHeader, ",; ");  
  7.                         if (st.hasMoreTokens()) {  
  8.                             contentType = st.nextToken();  
  9.                         }  
  10.                     }  
  11.                     if ("multipart/form-data".equalsIgnoreCase(contentType)) {  
  12.                         // Handle multipart/form-data  
  13.                         if (!st.hasMoreTokens()) {  
  14.                             Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");  
  15.                             throw new InterruptedException();  
  16.                         }  
  17.                         String boundaryStartString = "boundary=";  
  18.                         int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();  
  19.                         String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());  
  20.                         if (boundary.startsWith("\"") && boundary.startsWith("\"")) {  
  21.                             boundary = boundary.substring(1, boundary.length() - 1);  
  22.                         }  
  23.                         decodeMultipartData(boundary, fbuf, in, parms, files);//  
  24.                     } else {  
  25.                         // Handle application/x-www-form-urlencoded  
  26.                         String postLine = "";  
  27.                         char pbuf[] = new char[512];  
  28.                         int read = in.read(pbuf);  
  29.                         while (read >= 0 && !postLine.endsWith("\r\n")) {  
  30.                             postLine += String.valueOf(pbuf, 0, read);  
  31.                             read = in.read(pbuf);  
  32.                         }  
  33.                         postLine = postLine.trim();  
  34.                         decodeParms(postLine, parms);//  
  35.                     }  
  36. }   

这里需要注意的是,如果是文件上传的请求,根据HTTP协议就不能再使用a=b的方式了,而是使用分隔符的方式。例如:Content-Type:multipart/form-data; boundary=--AaB03x中boundary=后面的这个--AaB03x就是分隔符:

Request代码  收藏代码

  1. --AaB03x  
  2. Content-Disposition: form-data; name="submit-name"  //表单域名-submit-name  
  3. shensy  //表单域值  
  4. --AaB03x  
  5. Content-Disposition: form-data; name="file"; filename="a.exe" //上传文件  
  6. Content-Type: application/octet-stream  
  7. a.exe文件的二进制数据  
  8. --AaB03x--  //结束分隔符  

如果是普通的表单提交的话,就循环读取post body直到结束(\r\n)为止。

另外,简单看了一下:decodeMultipartData作用是将post中上传文件的内容解析出来,decodeParms作用是将post中含有%的值使用URLDecoder.decode解码出来,这里就不贴代码了。

最后,除了处理POST请求外,还对PUT请求进行了处理。

Java代码  收藏代码

  1. else if (Method.PUT.equals(method)) {  
  2.          files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));  
  3. }  

其中,saveTmpFile方法是将body写入临时文件并返回其路径,limit为当前buffer中可用的位置(即内容):

Java代码  收藏代码

  1. private String saveTmpFile(ByteBuffer  b, int offset, int len) {  
  2.             String path = "";  
  3.             if (len > 0) {  
  4.                 try {  
  5.                     TempFile tempFile = tempFileManager.createTempFile();  
  6.                     ByteBuffer src = b.duplicate();  
  7.                     FileChannel dest = new FileOutputStream(tempFile.getName()).getChannel();  
  8.                     src.position(offset).limit(offset + len);  
  9.                     dest.write(src.slice());  
  10.                     path = tempFile.getName();  
  11.                 } catch (Exception e) { // Catch exception if any  
  12.                     System.err.println("Error: " + e.getMessage());  
  13.                 }  
  14.             }  
  15.             return path;  
  16. }  

现在,所有请求处理完成,下面构造响应并关闭流:

Java代码  收藏代码

  1. Response r = serve(uri, method, header, parms, files);  
  2. if (r == null) {  
  3.     Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");  
  4.     throw new InterruptedException();  
  5. else {  
  6.     r.setRequestMethod(method);  
  7.     r.send(outputStream);  
  8. }  
  9. in.close();  
  10. inputStream.close();  

其中serve是个抽象方法,用于构造响应内容,需要用户在子类中实现(后面会给出例子)。

Java代码  收藏代码

  1. public abstract Response serve(String uri,Method method,Map<String, String> header,Map<String, String> parms,Map<String, String> files);  

构造完响应内容,最后就是发送响应了:

Java代码  收藏代码

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

通过PrintWriter构造响应头,如果请求不为HEAD方法(没有响应body),则将用户构造的响应内容写入outputStream作为响应体。

下面给出一个使用案例(官方提供):

Java代码  收藏代码

  1. public class HelloServer extends NanoHTTPD {  
  2.     public HelloServer() {  
  3.         super(8080);  
  4.     }  
  5.     @Override  
  6.     public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) {  
  7.         String msg = "<html><body><h1>Hello server</h1>\n";  
  8.         if (parms.get("username") == null)  
  9.             msg +=  
  10.                     "<form action='?' method='post'>\n" +  
  11.                             "  <p>Your name: <input type='text' name='username'></p>\n" +  
  12.                             "</form>\n";  
  13.         else  
  14.             msg += "<p>Hello, " + parms.get("username") + "!</p>";  
  15.         msg += "</body></html>\n";  
  16.         return new NanoHTTPD.Response(msg);  
  17.     }  
  18.     //后面public static void main...就不贴了  
  19. }  

由此可见,serve是上文中的抽象方法,由用户构造响应内容,此处给出的例子是一个html。

结束语:

至此,NanoHTTPD的源码基本就算分析完了。通过分析该源码,可以更深入的了解Socket BIO编程模型以及HTTP协议请求响应格式。希望能对看到的人有所帮助,同时欢迎大家多拍砖。大笑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值