最近在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-- //结束分隔符
相关推荐
可以嵌入程序的web服务器,rmi共享类分发利器。
最后,`nanohttpd-2.3.1`版本的jar文件包含了NanoHttpd库的这个特定版本,可以直接在Android项目中作为依赖使用,无需构建源代码。在Android Studio中,可以通过添加以下依赖项到`build.gradle`文件来引入: ```...
这就涉及到如何利用NanoHttpd在Android上搭建一个微型的webservice。NanoHttpd是一个轻量级、易于集成的HTTP服务器库,非常适合在移动设备上使用。 首先,我们来深入理解`Android NanoHttpd`的核心概念。NanoHttpd...
nanohttpd-2.3.1.jar最新版java http服务器
在手机搭建一个html源码 运行之后,在浏览器中打开 http://127.0.0.1:8088 就可以打开一个html 支持,热点连接,, 当然大家可以通过android 自身的 webview加载 http://127.0.0.1:8088
- 或者,你可以直接将`NanoHTTPD`源码导入到项目中,这样无需网络也能使用。 2. **创建HTTP服务器** - 创建一个新的Java类,继承`NanoHTTPD`,重写`serve()`方法,这个方法会处理所有HTTP请求。例如: ```java ...
NanoHTTPD是一个免费、轻量级的(只有一个Java文件) HTTP服务器,可以很好地嵌入到Java程序中。支持 GET, POST, PUT, HEAD 和 DELETE 请求,支持文件上传,占用内存很小。可轻松定制临时文件使用和线程模型。
Android轻松搭建HTTP服务Jar包,找了很久,都需要很多积分,在这里少分分享给大家密码nanohttpd
首先,让我们了解一下NanoHTTPD的基本概念。NanoHTTPD是一个开源的、易于嵌入的HTTP服务器,它的设计目标是简单、快速和轻量级。它不包含复杂的特性,如模块化、多线程处理等,而是专注于提供基本的HTTP服务。 ...
NanoHTTPD是一个免费、轻量级的(只有一个Java文件) HTTP服务器,可以很好地嵌入到Java程序中。支持 GET, POST, PUT, HEAD 和 DELETE 请求,支持文件上传,占用内存很小。
在Java环境中,NanoHTTPD允许开发者在本地运行一个小型Web服务器,用于测试、调试或者提供简单的服务。 NanoHTTPD的设计理念是小巧且可扩展。它的源代码简洁,易于理解和修改,使得开发人员可以根据自己的需求对其...
在这个压缩包中,包含了一个经过修改的NanoHTTPD版本以及原版的源代码,供开发者在Android项目中使用。 NanoHTTPD是一个开源的HTTP服务器,它设计的目标是简单、快速和低内存占用。它的核心思想是将HTTP服务器的...
内含最新nanohttpd-all-2.3.1.jar,直接下载代码引入到eclipse就能用。HTML5播放使用的时候,需要把工程的movie.mp4拷贝到SD卡根目录下即可。文件上传路径为ip:port/uploadfiles?filename=aa.txt,最好使用jmeter...
nanohttpd-2.2.0.jar
nanohttpd是一个java开源项目,对http服务做了很好的封装,使用起来非常方便。只需一个文件即可实现httpserver; nanohttpd 2.3.0是最新的版本,只需将资源中的jar包引入到工程中,按照例程进行开发,很轻松就可以...
public NanoHTTPD int port throws IOException Java代码 收藏代码 { myTcpPort port; final ServerSocket ss new ServerSocket myTcpPort ; Thread t new Thread new Runnable { public void run {...
NanoHTTPD是一个优秀的嵌入式Web服务器,且用java语言编写的,可以应用于Android平台以及其它支持Java的平台,并且支持servlet,可以方便第三方开发人员在此基础上做开发。
NanoHTTPD是一个优秀的嵌入式Web服务器,且用java语言编写的,可以应用于Android平台以及其它支持Java的平台,并且支持servlet,可以方便第三方开发人员在此基础上做开发。说明:int port=8080;String wwwroot="/...