最近在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代码
- 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()内。
Java代码
- 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方法:
Java代码
- 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方法,有点长,分段解析:
Java代码
- 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的:
Java代码
- 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:
Java代码
- 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方法,也比较长,简单说一下:
Java代码
- 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后,
Java代码
- 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,首先创建一个临时文件:
Java代码
- RandomAccessFile f = getTmpBucket();
NanoHTTPD中将创建临时文件进行了封装(稍微有点复杂),如下:
Java代码
- 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构造方法:
Java代码
- TempFileManager tempFileManager = tempFileManagerFactory.create();
- final HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream);
实际的临时文件类定义如下:
Java代码
- 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读出并写入刚才创建的临时文件:
Java代码
- 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)表示将文件指针指向文件头。
Java代码
- // 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(表单提交)分别进行了处理:
Java代码
- 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就是分隔符:
Request代码
- --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请求进行了处理。
Java代码
- else if (Method.PUT.equals(method)) {
- files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
- }
其中,saveTmpFile方法是将body写入临时文件并返回其路径,limit为当前buffer中可用的位置(即内容):
Java代码
- 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;
- }
现在,所有请求处理完成,下面构造响应并关闭流:
Java代码
- 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是个抽象方法,用于构造响应内容,需要用户在子类中实现(后面会给出例子)。
Java代码
- public abstract Response serve(String uri,Method method,Map<String, String> header,Map<String, String> parms,Map<String, String> files);
构造完响应内容,最后就是发送响应了:
Java代码
- 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作为响应体。
下面给出一个使用案例(官方提供):
Java代码
- 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协议请求响应格式。希望能对看到的人有所帮助,同时欢迎大家多拍砖。