`

NanoHttpd源码分析

 
阅读更多

最近在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()方法中:

private ServerSocket myServerSocket;
private Thread myThread;
private AsyncRunner asyncRunner;
//...
public void start() throws IOException {
        myServerSocket = new ServerSocket();
        myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
        myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                do {
                    try {
                        final Socket finalAccept = myServerSocket.accept();
                        InputStream inputStream = finalAccept.getInputStream();
                        OutputStream outputStream = finalAccept.getOutputStream();
                        TempFileManager tempFileManager = tempFileManagerFactory.create();
                        final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);
                        asyncRunner.exec(new Runnable() {
                            @Override
                            public void run() {
                                session.run();
                                try {
                                    finalAccept.close();
                                } catch (IOException ignored) {
                                    ignored.printStackTrace();
                                }
                            }
                        });
                    } catch (IOException e) {
                    	e.printStackTrace();
                    }
                } while (!myServerSocket.isClosed());
            }
        });
        myThread.setDaemon(true);
        myThread.setName("NanoHttpd Main Listener");
        myThread.start();
}

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

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

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

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

   public interface AsyncRunner {
        void exec(Runnable code);
   }
   public static class DefaultAsyncRunner implements AsyncRunner {
        private long requestCount;
        @Override
        public void exec(Runnable code) {
            ++requestCount;
            Thread t = new Thread(code);
            t.setDaemon(true);
            t.setName("NanoHttpd Request Processor (#" + requestCount + ")");
            System.out.println("NanoHttpd Request Processor (#" + requestCount + ")");
            t.start();
        }
    }

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

TempFileManager tempFileManager = tempFileManagerFactory.create();
final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);
asyncRunner.exec(new Runnable() {
          @Override
           public void run() {
                 session.run();
                 try {
                        finalAccept.close();
                 } catch (IOException ignored) {
                        ignored.printStackTrace();
                 }
           }
});

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

public static final int BUFSIZE = 8192;
public void run() {
            try {
                if (inputStream == null) {
                    return;
                }
                byte[] buf = new byte[BUFSIZE];
                int splitbyte = 0;
                int rlen = 0;
                {
                    int read = inputStream.read(buf, 0, BUFSIZE);
                    while (read > 0) {
                        rlen += read;
                        splitbyte = findHeaderEnd(buf, rlen);
                        if (splitbyte > 0)
                            break;
                        read = inputStream.read(buf, rlen, BUFSIZE - rlen);
                    }
                }
                //...
}

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

private int findHeaderEnd(final byte[] buf, int rlen) {
            int splitbyte = 0;
            while (splitbyte + 3 < rlen) {
                if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
                    return splitbyte + 4;
                }
                splitbyte++;
            }
            return 0;
}

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

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

BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
Map<String, String> pre = new HashMap<String, String>();
Map<String, String> parms = new HashMap<String, String>();
Map<String, String> header = new HashMap<String, String>();
Map<String, String> files = new HashMap<String, String>();
decodeHeader(hin, pre, parms, header);

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

                String inLine = in.readLine();
                if (inLine == null) {
                    return;
                }
                StringTokenizer st = new StringTokenizer(inLine);
                if (!st.hasMoreTokens()) {
                    Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
                    throw new InterruptedException();
                }
                pre.put("method", st.nextToken());
                if (!st.hasMoreTokens()) {
                    Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
                    throw new InterruptedException();
                }
                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 (st.hasMoreTokens()) {
                    String line = in.readLine();
                    while (line != null && line.trim().length() > 0) {
                        int p = line.indexOf(':');
                        if (p >= 0)
                            header.put(line.substring(0, p).trim().toLowerCase(), line.substring(p + 1).trim());
                        line = in.readLine();
                    }
                }

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

从decodeHeader中解析出header后,

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

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

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

RandomAccessFile f = getTmpBucket();

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

private final TempFileManager tempFileManager;
private RandomAccessFile getTmpBucket() {
            try {
                TempFile tempFile = tempFileManager.createTempFile();
                return new RandomAccessFile(tempFile.getName(), "rw");
            } catch (Exception e) {
                System.err.println("Error: " + e.getMessage());
            }
            return null;
}

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

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

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

public interface TempFile {
        OutputStream open() throws Exception;
        void delete() throws Exception;
        String getName();
}
public static class DefaultTempFile implements TempFile {
        private File file;
        private OutputStream fstream;
        public DefaultTempFile(String tempdir) throws IOException {
            file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
            fstream = new FileOutputStream(file);
        }
        @Override
        public OutputStream open() throws Exception {
            return fstream;
        }
        @Override
        public void delete() throws Exception {
            file.delete();
        }
        @Override
        public String getName() {
            return file.getAbsolutePath();
        }
}
public static class DefaultTempFileManager implements TempFileManager {
        private final String tmpdir;
        private final List<TempFile> tempFiles;
        public DefaultTempFileManager() {
            tmpdir = System.getProperty("java.io.tmpdir");
            tempFiles = new ArrayList<TempFile>();
        }
        @Override
        public TempFile createTempFile() throws Exception {
            DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
            tempFiles.add(tempFile);
            return tempFile;
        }
        @Override
        public void clear() {
            for (TempFile file : tempFiles) {
                try {
                    file.delete();
                } catch (Exception ignored) {
                }
         }
         tempFiles.clear();
}

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

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

                if (splitbyte < rlen) {
                    f.write(buf, splitbyte, rlen - splitbyte);
                }

                if (splitbyte < rlen) {
                    size -= rlen - splitbyte + 1; 
                } else if (splitbyte == 0 || size == 0x7FFFFFFFFFFFFFFFl) {
                    size = 0;
                }

                // Now read all the body and write it to f
                buf = new byte[512];
                while (rlen >= 0 && size > 0) {  
                    rlen = inputStream.read(buf, 0, 512);
                    size -= rlen;
                    if (rlen > 0) {
                        f.write(buf, 0, rlen);
                    }
                }
                System.out.println("buf body:"+new String(buf));

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

// Get the raw body as a byte []
ByteBuffer fbuf = f.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, f.length());
f.seek(0);
// Create a BufferedReader for easily reading it as string.
InputStream bin = new FileInputStream(f.getFD());
BufferedReader in = new BufferedReader(new InputStreamReader(bin));
之后,如果请求是POST方法,则取出content-type,并对multipart/form-data(上传)和application/x-www-form-urlencoded(表单提交)分别进行了处理:
if (Method.POST.equals(method)) {
                    String contentType = "";
                    String contentTypeHeader = header.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()) {
                            Response.error(outputStream, Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
                            throw new InterruptedException();
                        }
                        String boundaryStartString = "boundary=";
                        int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
                        String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
                        if (boundary.startsWith("\"") && boundary.startsWith("\"")) {
                            boundary = boundary.substring(1, boundary.length() - 1);
                        }
                        decodeMultipartData(boundary, fbuf, in, parms, files);//
                    } else {
                        // Handle application/x-www-form-urlencoded
                        String postLine = "";
                        char pbuf[] = new char[512];
                        int read = in.read(pbuf);
                        while (read >= 0 && !postLine.endsWith("\r\n")) {
                            postLine += String.valueOf(pbuf, 0, read);
                            read = in.read(pbuf);
                        }
                        postLine = postLine.trim();
                        decodeParms(postLine, parms);//
                    }
} 

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

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

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

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

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

else if (Method.PUT.equals(method)) {
         files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
}
其中,saveTmpFile方法是将body写入临时文件并返回其路径,limit为当前buffer中可用的位置(即内容):
private String saveTmpFile(ByteBuffer  b, int offset, int len) {
            String path = "";
            if (len > 0) {
                try {
                    TempFile tempFile = tempFileManager.createTempFile();
                    ByteBuffer src = b.duplicate();
                    FileChannel dest = new FileOutputStream(tempFile.getName()).getChannel();
                    src.position(offset).limit(offset + len);
                    dest.write(src.slice());
                    path = tempFile.getName();
                } catch (Exception e) { // Catch exception if any
                    System.err.println("Error: " + e.getMessage());
                }
            }
            return path;
}

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

                Response r = serve(uri, method, header, parms, files);
                if (r == null) {
                    Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
                    throw new InterruptedException();
                } else {
                    r.setRequestMethod(method);
                    r.send(outputStream);
                }
                in.close();
                inputStream.close();
其中serve是个抽象方法,用于构造响应内容,需要用户在子类中实现(后面会给出例子)。
public abstract Response serve(String uri,Method method,Map<String, String> header,Map<String, String> parms,Map<String, String> files);
构造完响应内容,最后就是发送响应了:
private void send(OutputStream outputStream) {
            String mime = mimeType;
            SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
            gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
            try {
                if (status == null) {
                    throw new Error("sendResponse(): Status can't be null.");
                }
                PrintWriter pw = new PrintWriter(outputStream);
                pw.print("HTTP/1.0 " + status.getDescription() + " \r\n");
                if (mime != null) {
                    pw.print("Content-Type: " + mime + "\r\n");
                }
                if (header == null || header.get("Date") == null) {
                    pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
                }
                if (header != null) {
                    for (String key : header.keySet()) {
                        String value = header.get(key);
                        pw.print(key + ": " + value + "\r\n");
                    }
                }
                pw.print("\r\n");
                pw.flush();
                if (requestMethod != Method.HEAD && data != null) {
                    int pending = data.available();
                    int BUFFER_SIZE = 16 * 1024;
                    byte[] buff = new byte[BUFFER_SIZE];
                    while (pending > 0) {
                        int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
                        if (read <= 0) {
                            break;
                        }
                        outputStream.write(buff, 0, read);
                        pending -= read;
                    }
                }
                outputStream.flush();
                outputStream.close();
                if (data != null)
                    data.close();
            } catch (IOException ioe) {
                // Couldn't write? No can do.
            }
}

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

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

public class HelloServer extends NanoHTTPD {
    public HelloServer() {
        super(8080);
    }
    @Override
    public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) {
        String msg = "<html><body><h1>Hello server</h1>\n";
        if (parms.get("username") == null)
            msg +=
                    "<form action='?' method='post'>\n" +
                            "  <p>Your name: <input type='text' name='username'></p>\n" +
                            "</form>\n";
        else
            msg += "<p>Hello, " + parms.get("username") + "!</p>";
        msg += "</body></html>\n";
        return new NanoHTTPD.Response(msg);
    }
    //后面public static void main...就不贴了
}

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

结束语:

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

 

分享到:
评论
1 楼 zhongyuanceshi 2019-01-05  
非常厉害。看了下有了整体认识

相关推荐

Global site tag (gtag.js) - Google Analytics