最近在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-- //结束分隔符