最近在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. if (splitbyte < rlen) {
  5. size -= rlen - splitbyte + 1;
  6. else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl) {
  7. size = 0;
  8. }
  9. // Now read all the body and write it to f
  10. buf = new byte[512];
  11. while (rlen >= 0 && size > 0) {
  12. rlen = inputStream.read(buf, 0, 512);
  13. size -= rlen;
  14. if (rlen > 0) {
  15. f.write(buf, 0, rlen);
  16. }
  17. }
  18. 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协议请求响应格式。希望能对看到的人有所帮助,同时欢迎大家多拍砖。

NanoHttpd源码分析相关推荐

  1. Android作为HTTP服务器--NanoHTTPD源码分析

    欢迎访问个人开发论坛:http://jwzhangjie.com 专注盒子开发 @jwzhangjie NanoHTTPD源码以及解释 package com.jwzhangjie.shafa.mar ...

  2. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  3. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  4. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  5. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  6. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  7. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  8. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

  9. MyBatis原理分析之四:一次SQL查询的源码分析

    上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码   String res ...

最新文章

  1. 算法分析赛:从数据中挖掘价值,72万奖金,DCIC 2020 大数据赛道来了!
  2. CSS魔法堂:深入理解line-height和vertical-align
  3. Python练习:站队顺序输出
  4. java三元组的快速转置_矩阵压缩——三元组以及矩阵快速转置程序实现
  5. Javascript事件模型系列(一)事件及事件的三种模型
  6. windows上vscode 安装Fortran-language-server
  7. CROW-5 WEB APP引擎商业计划书(HTML5方向)-微信网页版微信公众平台登录-水仙谷...
  8. 博图安装msi失败_西门子软件WIN7系统安装须知
  9. Windows程式开发设计指南--Unicode简介
  10. springboot中.yml 用jasypt 密码加密
  11. Android编译时冲突报错的完美解决方案
  12. 两步完成druid数据库连接池的密文配置
  13. Fiddler移动端代理设置(移动端抓包设置,包教会)
  14. 激活DataGrip
  15. ubuntu中打开出现闪退_如何解决ubuntu软件中心闪退
  16. Pygame:播放声音和音效
  17. ProGuard使用说明
  18. Java集合篇:Map接口、Map接口的实现类、Collections集合工具类
  19. java 通过Qrcode生成二维码添加图片logo和文字描述
  20. JavaScript奇淫技巧:反调试

热门文章

  1. 【Android】getActionBar()为null的解决方法总结
  2. leetcode面试准备:Decode Ways
  3. coroSync packmarker
  4. JS 获取控件的绝对位置
  5. 最近安装了win2008R2,界面比win2003友好多了
  6. matlab强制数据类型转换
  7. Linux下ps -ef和ps aux的区别
  8. Windows10下使用darknet和YOLOV3训练自己的数据集
  9. 北邮 复习 软件工程_软件工程的一些基本概念总结(北邮版本)
  10. python3 32位_Python3.8.2安装教程