NanoHttpd源码分析

来源:互联网 发布:手机淘宝用什么付款 编辑:程序博客网 时间:2024/06/05 18:21

最近在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协议请求响应格式。希望能对看到的人有所帮助,同时欢迎大家多拍砖。大笑

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 爸妈吵架妈妈走了爸爸哭了该怎么办 总担心旅馆被拍视频传上网怎么办 微博买了猜冠军现在停了怎么办 脸上毛孔大有黑头怎么办小窍门去 进去精神病院出来真的疯了怎么办 房子已过户新业主不交物业费怎么办 村委会欠百姓征地补偿款不给怎么办 因为近亲人人都不看好的婚姻怎么办 碰到工作中特别积极的同事怎么办 丈夫车祸死亡妻子和孩子以后怎么办 丈夫死后妻子改嫁儿子不同意怎么办 满了60岁社保没满15年怎么办 捷豹的dpf灯亮了怎么办 朋友如新直销产品是你该怎么办 传福音接受了却被家人拦阻该怎么办 奶奶出钱由孙子抓奖中奖后怎么办 我不想学车了驾校不同意退学怎么办 2017年大学挂科面临退学怎么办 微信重新登录后东西全没了怎么办 宝宝吃鸡蛋过敏全身起红疹怎么办 180在产蛋鸡因断鸡减产怎么办 住友39熔接机熔接损耗大怎么办 支付宝实名认证刷脸失败怎么办 支付宝注册刷脸不是本人怎么办 小学科学只考88分中学怎么办 收银机关机时才上传数据是怎么办 刚做的系统玩cf卡屏怎么办 办健康证的资料掉了怎么办 刚刚办得的健康证掉了怎么办 房子都过户了银行贷不了款怎么办 我要办大病迁出应该怎么办啊? 遗产继承后户口没地迁出怎么办 安徽蒙城怎么办去韩国的签证的 夜间有人私自收停车费应该怎么办 上次摸不到环尾丝这次摸到了怎么办 法院判决书下来后对方不给钱怎么办 法院判决书下来了钱还保全么怎么办 深圳路边泊车不知道泊车编号怎么办 当事人进拘留所了我的工资怎么办 昆明公租房住满5年后怎么办 昆明公租房房子到期缴纳金怎么办