最近在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" 是...
内容概要:本文详细介绍了基于多目标粒子群(MOPSO)算法的冷热电联供(CCHP)综合能源系统运行优化的方法和技术细节。文章首先构建了一个涵盖冷、热、电负荷的优化调度模型,该模型不仅考虑了多种能源资源如燃气轮机、电制冷机、锅炉和风光机组,还包括与上级电网的购售电交易。随后,文章展示了MOPSO算法的具体实现步骤,包括粒子初始化、迭代更新、惯性权重调整、非支配排序和拥挤度计算等关键技术环节。此外,文中还讨论了MATLAB仿真平台的优势及其在处理多时间尺度耦合、风光出力波动等方面的应用。最终,通过Pareto前沿分析,揭示了系统在不同条件下的最优运行模式。 适用人群:适用于从事能源系统优化的研究人员、工程师以及对多目标优化算法有兴趣的学习者。 使用场景及目标:①帮助研究人员理解和应用MOPSO算法进行CCHP系统的优化;②为工程师提供具体的代码实现和仿真工具,以便更好地设计和管理实际的能源系统;③促进学术交流和技术进步,推动可持续能源的发展。 其他说明:文章提供了详细的MATLAB代码片段,便于读者理解和复现实验结果。同时,强调了多目标优化在解决复杂能源系统问题中的重要性和优越性。
原始数据集
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 编译闪电般迅速,并发性能卓越,部署轻松简单!Go 语言以极简设计理念和出色工程性能,成为云原生时代的首选编程语言。从 Docker 到 Kubernetes,全球顶尖科技企业都在采用 Go。点击了解 Go 语言的核心优势、实战窍门和未来走向,开启高效编程的全新体验!
数据库
内容概要:本文详细介绍了基于Matlab 2021a的异步电机矢量控制系统中电流滞环控制的实现过程。首先,文章解释了电流环的整体结构,包括定子电流的坐标变换、转矩分量和励磁分量的分离以及旋转变压器模块的应用。接着,展示了电流滞环控制的核心代码,强调了带积分修正的滞环控制机制,并讨论了SVPWM模块的实现技巧。此外,文章探讨了速度环PI参数的自整定设计、谐波分析、磁链观测器的改进方案以及仿真加速技巧。最后,分享了一些实用的调试经验和仿真优化方法,如参数自适应调整、变步长求解器的选择和数据存储格式的优化。 适合人群:从事电机控制领域的研究人员和技术人员,尤其是对异步电机矢量控制和电流滞环控制感兴趣的读者。 使用场景及目标:适用于希望深入了解异步电机矢量控制系统中电流滞环控制实现细节的研究人员和技术人员。目标是帮助读者掌握电流滞环控制的关键技术和调试技巧,提高仿真实践能力。 其他说明:文中提供了丰富的代码片段和调试经验,有助于读者更好地理解和应用所介绍的技术。同时,报告中还包括详细的故障分析和解决方案,确保读者能够避免常见陷阱并顺利进行仿真。
内容概要:本文详细介绍了如何在LabVIEW 2019环境中实现与三菱PLC的通信及其多线程交互。首先探讨了使用OPC和MC两种通讯协议与三菱PLC建立连接的方法,接着讲述了SQLite数据库用于数据存储的具体步骤,然后阐述了JKI状态机框架的应用,最后讲解了通过数组队列实现多线程交互的技术细节。文中不仅提供了具体的代码示例,还分享了许多实用的经验技巧,如异常处理、性能优化等。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是那些正在使用或计划使用LabVIEW进行PLC通信和数据处理的人群。 使用场景及目标:适用于需要构建高效稳定的工业控制系统的企业和个人开发者。主要目的是帮助读者掌握如何利用LabVIEW平台完成复杂的PLC通信任务,提高系统的可靠性和效率。 其他说明:作者强调了在实际应用过程中需要注意的问题,例如硬件兼容性、网络稳定性、数据安全性等方面的内容,并给出了针对性的解决方案。此外,还提到了一些常见的误区和潜在的风险点,为后续的工作提供了宝贵的参考资料。
计算机视觉_深度学习_图像处理_目标检测_OpenCV_TensorFlow_PyTorch_基于YOLOv5改进算法的高精度实时多目标检测与跟踪系统_用于智能监控_自动驾驶_工业
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 编译闪电般迅速,并发性能卓越,部署轻松简单!Go 语言以极简设计理念和出色工程性能,成为云原生时代的首选编程语言。从 Docker 到 Kubernetes,全球顶尖科技企业都在采用 Go。点击了解 Go 语言的核心优势、实战窍门和未来走向,开启高效编程的全新体验!
内容概要:本文详细介绍了威纶通触摸屏与施耐德ATV12变频器之间的Modbus通讯方法,涵盖硬件接线、参数设置、控制程序编写以及调试技巧。首先,文章讲解了正确的硬件连接方式,强调了接线规范和注意事项,如使用带屏蔽的双绞线并确保正确接地。接着,针对ATV12变频器的具体参数设置进行了详尽说明,包括通信模式的选择、波特率、校验位等重要参数的配置。随后,文章展示了如何在威纶通触摸屏上创建Modbus RTU设备,并提供了具体的配置参数和控制命令示例。此外,文中还分享了一些常见的调试问题及其解决办法,如通讯超时、频率设定异常等。最后,给出了实用的调试建议,如使用串口助手抓包分析和加入通讯心跳检测等功能。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是那些负责PLC编程、HMI界面开发以及设备集成工作的专业人员。 使用场景及目标:适用于需要将威纶通触摸屏与施耐德ATV12变频器进行Modbus通讯连接的实际工程项目中,帮助技术人员顺利完成设备间的通讯配置,确保系统稳定可靠运行。 其他说明:本文不仅提供了详细的理论指导,还结合了丰富的实践经验,能够有效地提高读者在实际工作中解决问题的能力。同时提醒读者,在进行相关操作前务必仔细阅读官方文档,避免因误操作造成不必要的损失。
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 Rust 以内存安全、零成本抽象和并发高效的特性,重塑编程体验。无需垃圾回收,却能通过所有权与借用检查机制杜绝空指针、数据竞争等隐患。从底层系统开发到 Web 服务构建,从物联网设备到高性能区块链,它凭借出色的性能和可靠性,成为开发者的全能利器。拥抱 Rust,解锁高效、安全编程新境界!
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 编译闪电般迅速,并发性能卓越,部署轻松简单!Go 语言以极简设计理念和出色工程性能,成为云原生时代的首选编程语言。从 Docker 到 Kubernetes,全球顶尖科技企业都在采用 Go。点击了解 Go 语言的核心优势、实战窍门和未来走向,开启高效编程的全新体验!
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 Rust 以内存安全、零成本抽象和并发高效的特性,重塑编程体验。无需垃圾回收,却能通过所有权与借用检查机制杜绝空指针、数据竞争等隐患。从底层系统开发到 Web 服务构建,从物联网设备到高性能区块链,它凭借出色的性能和可靠性,成为开发者的全能利器。拥抱 Rust,解锁高效、安全编程新境界!
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 Rust 以内存安全、零成本抽象和并发高效的特性,重塑编程体验。无需垃圾回收,却能通过所有权与借用检查机制杜绝空指针、数据竞争等隐患。从底层系统开发到 Web 服务构建,从物联网设备到高性能区块链,它凭借出色的性能和可靠性,成为开发者的全能利器。拥抱 Rust,解锁高效、安全编程新境界!
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 Rust 以内存安全、零成本抽象和并发高效的特性,重塑编程体验。无需垃圾回收,却能通过所有权与借用检查机制杜绝空指针、数据竞争等隐患。从底层系统开发到 Web 服务构建,从物联网设备到高性能区块链,它凭借出色的性能和可靠性,成为开发者的全能利器。拥抱 Rust,解锁高效、安全编程新境界!
文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 编译闪电般迅速,并发性能卓越,部署轻松简单!Go 语言以极简设计理念和出色工程性能,成为云原生时代的首选编程语言。从 Docker 到 Kubernetes,全球顶尖科技企业都在采用 Go。点击了解 Go 语言的核心优势、实战窍门和未来走向,开启高效编程的全新体验!
内容概要:本文详细介绍了直流无感无刷电机的方波控制方法及其初始位置检测方案。主要内容涵盖ADC和比较器结合用于初始位置检测的技术细节,包括代码示例;多种控制方式如开环控制、速度环控制和双闭环控制的具体实现;通信部分采用串口进行数据交换;多重保护机制确保系统的安全可靠;以及启动方式的选择和优化。此外,还讨论了一些硬件特色,如休眠电路和防打火电路的设计。 适合人群:从事电机控制系统设计的研发工程师和技术爱好者,尤其是对直流无感无刷电机有研究兴趣的专业人士。 使用场景及目标:适用于需要深入了解直流无感无刷电机控制原理的研究人员,帮助他们掌握具体的实现技术和优化技巧,从而应用于实际项目中,提高电机控制系统的性能和可靠性。 其他说明:文中提供了大量实用的代码片段和实践经验,强调了实际应用中的注意事项和调试技巧,对于解决常见问题非常有帮助。