一文读懂 NanoHttpd 微型服务器原理
NanoHttpd仅有一个Java文件的微型Http服务器实现。其方便嵌入式设备(例如:Android设备)中启动一个本地服务器,接收客户端本地部分请求;应用场景也非常广泛,例如:本地代理方式播放m3u8视频、本地代理方式加载一些加密秘钥等。
这里分三部分学习一个这个NanoHttpd:
- 了解官方对NanoHttpd描述定义
- 举例NanoHttpd在Android上的使用(Android本地代理,播放Sdcard中的m3u8视频)
- 分析源码实现
NanoHttpd GitHub地址:https://github.com/NanoHttpd/nanohttpd
一、NanoHttpd 官方描述
Tiny, easily embeddable HTTP server in Java.
微小的,轻量级适合嵌入式设备的Java Http服务器;NanoHTTPD is a light-weight HTTP server designed for embedding in other applications, released under a Modified BSD licence.
NanoHTTPD是一个轻量级的、为嵌入式设备应用设计的HTTP服务器,遵循修订后的BSD许可协议。
核心功能描述:
Only one Java file, providing HTTP 1.1 support.
仅一个Java文件,支持Http 1.1
No fixed config files, logging, authorization etc. (Implement by yourself if you need them. Errors are passed to java.util.logging, though.)
没有固定的配置文件、日志系统、授权等等(如果你有需要需自己实现。工程中的日志输出,通过java.util.logging实现的)
Support for HTTPS (SSL).
支持Https
Basic support for cookies.
支持cookies
Supports parameter parsing of GET and POST methods.
支持POST和GET 参数请求
Some built-in support for HEAD, POST and DELETE requests. You can easily implement/customize any HTTP method, though.
内置支持HEAD、POST、DELETE请求,你可以方便的实现或自定义任何HTTP方法请求。
Supports file upload. Uses memory for small uploads, temp files for large ones.
支持文件上传。小文件上传使用内存缓存,大文件使用临时文件缓存。
Never caches anything.
不缓存任何内容
Does not limit bandwidth, request time or simultaneous connections by default.- 默认不限制带宽、请求时间 和 最大请求量
All header names are converted to lower case so they don't vary between browsers/clients.
所有Header 名都被转换为小写,因此不会因客户端或浏览器的不同而有所差别
Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection.
支持一个socket连接服务多个长连接请求。
二、NanoHttpd在Android上的使用举例
为了学习NanoHttpd,做了一个简单Demo “Android 本地代理方式播放 Sdcard中的m3u8视频”:
Android 本地代理方式播放 Sdcard中的m3u8视频
https://github.com/AndroidAppCodeDemo/Android_LocalM3u8Server
实现效果图:

注:
- Android 本地代理方式播放 Sdcard中的m3u8视频(
使用的NanoHttpd 版本为 2.3.1)
NanoHttpd 2.3.1版本下载
https://github.com/NanoHttpd/nanohttpd/releases/tag/nanohttpd-project-2.3.1
- NanoHttpd的使用,使 “本地代理方式播放Android Sdcard中的m3u8视频” Demo实现变得很简单,这里不做具体介绍,有兴趣的朋友可以自行下载了解。
下边来主要来跟踪学习NanoHttpd的源码...
三、NanoHttpd源码跟踪学习
注:基于 NanoHttpd 2.3.1版本
NanoHttpd 2.3.1版本下载
https://github.com/NanoHttpd/nanohttpd/releases/tag/nanohttpd-project-2.3.1
NanoHTTPD大概的处理流程是:
- 开启一个服务端线程,绑定对应的端口,调用
ServerSocket.accept()方法进入等待状态 - 每个客户端连接均开启一个线程,执行
ClientHandler.run()方法 - 客户端线程中,创建一个
HTTPSession会话。执行HTTPSession.execute() HTTPSession.execute()中会完成uri, method, headers, parms, files的解析,并调用如下方法:
// 自定义服务器时,亦需要重载该方法
// 该方法传入参数中,已解析出客户端请求的所有数据,重载该方法进行相应的业务处理
HTTPSession.serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files)
- 组织Response数据,并调用
ChunkedOutputStream.send(outputStream)返回给客户端
*建议:
对于Http request、response 数据组织形式不是很了解的同学,建议自己了解后再阅读NanoHTTPD源码。* 也可参考我的另一篇文章:
HTTP 协议详解
https://xiaxl.blog.csdn.net/article/details/104541274
3.1、NanoHTTPD.start
从服务器启动开始学习...
/**
* Start the server. 启动服务器
*
* @param timeout timeout to use for socket connections. 超时时间
* @param daemon start the thread daemon or not. 守护线程
* @throws IOException if the socket is in use.
*/
public void start(final int timeout, boolean daemon) throws IOException {
// 创建一个ServerSocket
this.myServerSocket = this.getServerSocketFactory().create();
this.myServerSocket.setReuseAddress(true);
// 创建 ServerRunnable
ServerRunnable serverRunnable = createServerRunnable(timeout);
// 启动一个线程监听客户端请求
this.myThread = new Thread(serverRunnable);
this.myThread.setDaemon(daemon);
this.myThread.setName("NanoHttpd Main Listener");
this.myThread.start();
//
while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
try {
Thread.sleep(10L);
} catch (Throwable e) {
// on android this may not be allowed, that's why we
// catch throwable the wait should be very short because we are
// just waiting for the bind of the socket
}
}
if (serverRunnable.bindException != null) {
throw serverRunnable.bindException;
}
}
从以上代码中,可以看到:
- 代码前两行,创建一个
ServerSocket - 开启一个线程,执行
ServerRunnable。这里其实就是服务端启动一个线程,用来监听客户端的请求,具体代码在ServerRunnable中。
3.2、ServerRunnable.run()
@Override
public void run() {
Log.e(TAG, "---run---");
try {
// bind
myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
hasBinded = true;
} catch (IOException e) {
this.bindException = e;
return;
}
Log.e(TAG, "bind ok");
do {
try {
Log.e(TAG, "before accept");
// 等待客户端连接
final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
// 设置超时时间
if (this.timeout > 0) {
finalAccept.setSoTimeout(this.timeout);
}
// 服务端:输入流
final InputStream inputStream = finalAccept.getInputStream();
Log.e(TAG, "asyncRunner.exec");
// 执行客户端 ClientHandler
NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
} catch (IOException e) {
NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
}
} while (!NanoHTTPD.this.myServerSocket.isClosed());
}
ServerRunnable的run()方法:
- 调用
ServerSocket.bind方法,绑定对应的端口 - 调用
ServerSocket.accept()线程进入阻塞等待状态 - 客户端连接后,会执行
createClientHandler(finalAccept, inputStream)创建一个ClientHandler,并开启一个线程,执行其对应的ClientHandler.run()方法 - 自定义服务器时,重载
Response HTTPSession.serve(uri, method, headers, parms, files)方法,对客户端的请求数据做出处理
3.3、ClientHandler.run()
@Override
public void run() {
Log.e(TAG, "---run---");
// 服务端 输出流
OutputStream outputStream = null;
try {
// 服务端的输出流
outputStream = this.acceptSocket.getOutputStream();
// 创建临时文件
TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
// session 会话
HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
// 执行会话
while (!this.acceptSocket.isClosed()) {
session.execute();
}
} catch (Exception e) {
// When the socket is closed by the client,
// we throw our own SocketException
// to break the "keep alive" loop above. If
// the exception was anything other
// than the expected SocketException OR a
// SocketTimeoutException, print the
// stacktrace
if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
NanoHTTPD.LOG.log(Level.SEVERE, "Communication with the client broken, or an bug in the handler code", e);
}
} finally {
safeClose(outputStream);
safeClose(this.inputStream);
safeClose(this.acceptSocket);
NanoHTTPD.this.asyncRunner.closed(this);
}
}
TempFileManager临时文件是为了缓存客户端Post请求的请求Body数据(如果数据较小,内存缓存;文件较大,缓存到文件中)- 创建一个
HTTPSession会话,并执行其对应的HTTPSession.execute()方法 HTTPSession.execute()中会对客户端的请求进行解析
3.4、HTTPSession.execute()
@Override
public void execute() throws IOException {
Log.e(TAG, "---execute---");
Response r = null;
try {
// Read the first 8192 bytes.
// The full header should fit in here.
// Apache's default header limit is 8KB.
// Do NOT assume that a single read will get the entire header
// at once!
// Apache默认header限制8k
byte[] buf = new byte[HTTPSession.BUFSIZE];
this.splitbyte = 0;
this.rlen = 0;
// 客户端输入流
int read = -1;
this.inputStream.mark(HTTPSession.BUFSIZE);
// 读取8k的数据
try {
read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
} catch (SSLException e) {
throw e;
} catch (IOException e) {
safeClose(this.inputStream);
safeClose(this.outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
if (read == -1) {
// socket was been closed
safeClose(this.inputStream);
safeClose(this.outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
// 分割header数据
while (read > 0) {
this.rlen += read;
// header
this.splitbyte = findHeaderEnd(buf, this.rlen);
// 找到header
if (this.splitbyte > 0) {
break;
}
// 8k中剩余数据
read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
}
// header数据不足8k,跳过header数据
if (this.splitbyte < this.rlen) {
this.inputStream.reset();
this.inputStream.skip(this.splitbyte);
}
//
this.parms = new HashMap<String, List<String>>();
// 清空header列表
if (null == this.headers) {
this.headers = new HashMap<String, String>();
} else {
this.headers.clear();
}
// 解析 客户端请求
// Create a BufferedReader for parsing the header.
BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
// Decode the header into parms and header java properties
Map<String, String> pre = new HashMap<String, String>();
decodeHeader(hin, pre, this.parms, this.headers);
//
if (null != this.remoteIp) {
this.headers.put("remote-addr", this.remoteIp);
this.headers.put("http-client-ip", this.remoteIp);
}
Log.e(TAG, "headers: " + headers);
this.method = Method.lookup(pre.get("method"));
if (this.method == null) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled.");
}
Log.e(TAG, "method: " + method);
this.uri = pre.get("uri");
Log.e(TAG, "uri: " + uri);
this.cookies = new CookieHandler(this.headers);
Log.e(TAG, "cookies: " + this.cookies.cookies);
String connection = this.headers.get("connection");
Log.e(TAG, "connection: " + connection);
boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*"));
Log.e(TAG, "keepAlive: " + keepAlive);
// Ok, now do the serve()
// TODO: long body_size = getBodySize();
// TODO: long pos_before_serve = this.inputStream.totalRead()
// (requires implementation for totalRead())
// 构造一个response
r = serve(HTTPSession.this);
// TODO: this.inputStream.skip(body_size -
// (this.inputStream.totalRead() - pos_before_serve))
if (r == null) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
} else {
String acceptEncoding = this.headers.get("accept-encoding");
this.cookies.unloadQueue(r);
// method
r.setRequestMethod(this.method);
r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip"));
r.setKeepAlive(keepAlive);
// 发送response
r.send(this.outputStream);
}
if (!keepAlive || r.isCloseConnection()) {
throw new SocketException("NanoHttpd Shutdown");
}
} catch (SocketException e) {
// throw it out to close socket object (finalAccept)
throw e;
} catch (SocketTimeoutException ste) {
// treat socket timeouts the same way we treat socket exceptions
// i.e. close the stream & finalAccept object by throwing the
// exception up the call stack.
throw ste;
} catch (SSLException ssle) {
Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} catch (IOException ioe) {
Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} catch (ResponseException re) {
Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} finally {
safeClose(r);
this.tempFileManager.clear();
}
}
HTTPSession.execute()完成了uri, method, headers, parms, files的解析- 完成解析后,调用
Response serve(IHTTPSession session)方法,创建了一个Response - 完成
Response数据组织后,这里会调用ChunkedOutputStream.send(outputStream)方法将数据发出去。
到这里,主要流程结束,其他细节需大家自己去用心研读源码了。我的Demo中增加了很多中文注释,可以帮助大家省下一部分力气,就这样了
相关参考
NanoHttpd GitHub
https://github.com/NanoHttpd/nanohttpd
NanoHttpd源码分析
https://www.iteye.com/blog/shensy-1880381
= THE END =
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,可关注我的公众号。
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,可关注我的公众号。
文章首发于公众号”CODING技术小馆“,如果文章对您有帮助,可关注我的公众号。