最近在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-- //结束分隔符
相关推荐
了解了nanohttpd的基本架构后,我们可以通过分析源码来学习如何实现一个简单的HTTP服务器。这包括解析HTTP请求报文、构建响应报文、处理文件I/O操作等关键环节。此外,nanohttpd的源码也提供了一个很好的学习HTTP...
通常,这些文件会包括Java源码、工程配置文件(如build.gradle或project.properties)、资源文件(如图片、音频、布局XML)、测试代码以及其他必要的构建和运行文件。 通过这个项目,我们可以学习到以下知识点: 1...
源码分析可以帮助我们了解如何集成网络编程、文件操作和UI设计。 至于 "工具" 标签,这表明该程序是一个实用工具,可能包含一些便利的功能,如一键分享、文件搜索等。 在压缩包中,文件 "MyHttpServer.apk" 是...
【LPSO-BP分类】基于改进莱维飞行和混沌映射的粒子群优化算法优化BP神经网络分类研究(Matlab代码实现)
皮带线滚筒线sw20可编辑_三维3D设计图纸_三维3D设计图纸.zip
基于单片机protues仿真的双路压力监测报警系统设计(仿真图、源代码) 这个基于C51单片机的双路压力监测报警系统是做的程设计,已通过验收,没有bug,功能更完善,所附包含proteus仿真文件,源代码,使用说明,可以做一个不错的参考。 所完成功能为模拟电路输入测量两路压力值,量程范围0~100MPa,带有LED报警功能,可以自行设置上限,压力测量值分时显示。 使用说明: 1.开始仿真,压力上限初始值为99MPa; 2.点击按钮button(要使数码管全部清零),在键盘处输入上限值; 3.上限值修改成功,重新开始显示,显示第一路时左侧带有上限值标识,显示第二路时左侧没有标识; 4.当测量数值超过了所设定上限,二极管闪烁报警; 5.结束仿真。
本书旨在通过21天的时间,带领读者从零基础开始学习C#语言。第一天介绍C#的基础知识,包括C#是什么、如何准备编程环境、程序开发周期以及创建和编译C#程序。随后,作者逐步引导读者深入理解C#程序的结构、关键字、数据类型、值的操作和程序流程控制。进入第二周,读者将学习类和对象、方法和成员函数、结构、枚举器和数组等面向对象编程的核心概念。第三周的内容包括继承、接口、委托、事件、索引器、运算符重载以及.NET基类的使用、Windows窗体和应用程序的创建、数据库操作以及远程过程调用和Web应用程序的创建。书末附有C#关键字列表、编译器标志、数字系统理解、SharpDevelop安装使用指南以及完整的索引,为读者提供全面的参考资料。
本文探讨了广告与文化之间的关系及其对品牌塑造的影响。随着科技的进步,文化和广告的互动变得更加复杂,文化变得碎片化和快速变化,人们渴望通过广告获得归属感和共同体验。研究表明,广告不仅能塑造文化,还能通过传达价值观和建立社区来增强消费者的归属感。不同代际的人群对广告和品牌的期望有所不同,年轻一代更希望广告能激发创造力并提供沉浸式的互动体验。此外,品牌应积极参与文化对话,创造有意义的内容,避免单纯追求表面的流量和曝光率。
基于单片机protues仿真的智能烟雾温度监测预警系统设计(仿真图、源代码) 1、该电路方案使用了外部ADC、烟雾浓度监测、温度监测、数据显示、预警值设置及其超标预警、还有掉电数据保存EEPROM。 2、使用说明: 1:开机自动进入如上图界面 2:点击设置按钮,进入预警值设置界面,自动进入浓度预警值设置状态,点击 “加” 或者 “减” 修改数值。每次+5或-5。可软件修改。 3:再次点击设置按钮,进入温度预警值设置状态,点击 “加” 或者 “减” 修改数值。每次+1或-1。可软件修改。 4:再次点击设置按钮,返回主界面。
内容概要:本文详细介绍了基于三菱FX3U PLC和组态王软件的三层电梯控制系统的设计与实现。首先,文章阐述了PLC端子的定义和IO分配表,明确了各个输入输出端口的功能。接着,深入讲解了梯形图逻辑,如楼层判断、方向控制、门机控制等关键部分的实现方法,并提供了具体的梯形图代码示例。此外,还讨论了组态王的画面设计技巧,包括电梯井道动画、按钮交互效果以及楼层显示等。最后,分享了一些调试经验和常见问题的解决方案,如平层信号处理、防抖动措施等。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对PLC编程和组态软件有一定基础的人群。 使用场景及目标:适用于需要理解和掌握三菱PLC和组态王联合使用的场合,旨在帮助读者构建完整的三层电梯控制系统,提高系统稳定性和可靠性。 其他说明:文中不仅涵盖了硬件接线和软件编程的知识,还包括了许多实践经验,有助于读者在实际项目中避免常见的错误和陷阱。
内容概要:本文详细对比了递归与迭代两种编程方法,解释了它们在不同应用场景下的优缺点。文中通过多个选择题解析了递归基、递归参数等概念,强调了递归基的重要性,指出它是递归函数停止调用自身的条件。同时,通过实例展示了如何使用递归和迭代实现阶乘、斐波那契数列、数组求和等功能,并讨论了递归与迭代在内存占用、执行效率等方面的差异。此外,文章还探讨了递归和迭代在搜索算法(如广度优先搜索、深度优先搜索)、排序算法(如快速排序、归并排序)中的应用,指出了递归在处理树形结构和分治问题时的优势,以及迭代在处理有明确循环次数的任务时的高效性。 适合人群:具备一定编程基础的学习者,尤其是对递归和迭代概念存在疑惑的程序员。 使用场景及目标:①理解递归和迭代的基本概念及其在实际编程中的应用;②掌握递归基的作用,避免无限递归;③学会根据问题特点选择合适的算法实现方式,优化程序性能。 其他说明:本文不仅提供了理论知识,还通过具体的代码示例加深理解,建议读者在学习过程中动手实践,尝试将递归算法转换为迭代算法,以更好地掌握两者之间的转换技巧。
开关自动组装线体总图sw20可编辑_三维3D设计图纸_三维3D设计图纸.zip
内容概要:本文详细介绍了如何使用Python的SALib库进行Sobol全局灵敏度分析。首先解释了Sobol方法的基本概念,然后逐步展示了从定义参数空间、生成Sobol序列样本、构建目标函数到最后计算灵敏度指数的具体步骤。文中还讨论了参数范围设定、采样点选择以及二阶交互关闭等技巧,强调了这些因素对分析结果的影响。此外,提供了多个实例,包括线性组合、带噪声的函数、非线性函数等,展示了Sobol方法在不同场景下的应用效果。 适合人群:具有一定数学建模和编程基础的研究人员和技术开发者,尤其是从事数据分析、机器学习等领域的人士。 使用场景及目标:适用于需要评估多个输入参数对系统输出影响程度的场合,如工程设计、金融风险评估、环境科学等领域的模型优化和参数筛选。主要目标是帮助用户快速定位关键参数,减少不必要的试验次数,提高模型的可靠性和准确性。 其他说明:文章不仅提供了详细的代码示例,还分享了许多实用的经验和注意事项,如参数范围的合理设定、采样点数量的选择等,有助于读者更好地理解和应用Sobol方法。同时,文中提及的一些高级技巧(如关闭二阶交互以节省计算资源)也为处理大规模参数问题提供了有效的解决方案。
仿生青蛙sw20可编辑_三维3D设计图纸.zip
内容概要:本文详细介绍了六种不同类型的操作系统:批处理、分时、实时、网络、分布式和嵌入式操作系统。批处理操作系统以最大化吞吐量为目标,通过自动转接特性实现无人干预的连续作业处理;分时操作系统通过时间片轮换允许多个用户同时交互使用计算机资源;实时操作系统专注于严格的时间约束,确保任务在规定时间内完成,适用于工业控制、航空等领域;网络操作系统主要用于网络环境,提供网络通信和资源管理功能;分布式操作系统通过透明地提供网络资源给用户,让用户感觉像在使用单个计算机;嵌入式操作系统则针对资源受限的设备,如智能手机和物联网设备,具备小型化和专用性的特点。 适合人群:计算机科学专业学生、软件开发者以及对操作系统理论感兴趣的读者。 使用场景及目标:帮助读者理解各种操作系统的核心概念和应用场景,为选择合适的操作系统类型提供理论依据。具体目标包括:①掌握批处理系统如何提高处理效率;②理解分时系统如何实现多用户交互;③学习实时系统的任务调度和时间约束管理;④了解网络操作系统在网络资源管理中的作用;⑤认识分布式系统的资源透明共享机制;⑥熟悉嵌入式系统在特定设备中的应用。 其他说明:文中通过选择题的形式加深对各操作系统特性的理解,并提供了具体的实例和应用场景,有助于读者更好地掌握相关知识点。
11ZJ401 楼梯栏杆建筑土木工程.doc
内容概要:本文详细介绍了如何使用Matlab、YALMIP和CPLEX求解带储能的微电网优化调度问题。首先,构建了一个包含光伏、风电、柴油机和储能电池在内的微电网系统模型,目标是最小化总运行成本并满足负荷需求。文中展示了如何定义决策变量、处理关键约束(如功率平衡、储能SOC限制、充放电互斥等),以及设定目标函数。此外,还提供了具体的求解步骤和调试技巧,强调了时间耦合约束的重要性及其正确处理方法。最后,通过实例验证了所提出方法的有效性和高效性。 适合人群:对电力系统优化调度感兴趣的科研人员、工程师和技术爱好者。 使用场景及目标:适用于希望深入了解微电网优化调度机制的人群,尤其是那些想要掌握如何利用现代优化工具解决复杂电力系统问题的技术人员。目标是帮助读者理解并能够独立搭建类似的优化模型。 其他说明:文中不仅提供了详细的理论解释,还有丰富的代码示例,便于读者理解和实践。同时指出了一些常见的错误和注意事项,有助于提高模型的成功率和准确性。
锁螺丝螺母设备sw23_三维3D设计图纸_三维3D设计图纸.zip
内容概要:本文详细对比了传输层协议TCP和UDP的特点及其应用场景。TCP是面向连接的协议,提供可靠的数据传输服务,通过确认应答、重传机制、拥塞控制等技术确保数据的完整性和顺序性。UDP则是无连接的协议,提供尽力而为的服务,不保证数据的可靠性,但具有更高的传输效率。文章还介绍了TCP的三次握手、滑动窗口、拥塞控制机制,以及UDP的报文结构、端口号使用、错误检测机制等。此外,文中通过选择题的形式探讨了两者在不同场景下的适用性,如TCP适用于文件传输、网页浏览等需要高可靠性的场景,而UDP则适用于实时音视频传输等对延迟敏感的场景。 适合人群:计算机网络相关专业的学生、网络工程师以及对传输层协议感兴趣的IT从业者。 使用场景及目标:①帮助读者理解TCP和UDP的工作原理及差异;②指导读者根据实际需求选择合适的传输层协议;③为网络编程和系统设计提供理论依据。 其他说明:本文以问答形式呈现知识点,便于记忆和理解。同时,文中涉及的选择题不仅考察了基本概念,还涵盖了协议的具体实现细节,有助于加深读者对传输层协议的理解。
电磁领域系列仿真模拟教程,每个包10几个教程,从基础到精通,案例多多。