偶然返现NanoHttpd
,仅仅一个Java文件,可在嵌入式设备(例:Android手机)中启动一个本地服务器,接收客户端本地部分请求。
认真学习了其源码实现,这里按照我的学习顺序写了一篇简单的文章(算是学习笔记吧):
NanoHttpd GitHub地址:
https://github.com/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许可协议。
Core
Only one Java file, providing HTTP 1.1 support
.No fixed config files, logging, authorization etc. (Implement by yourself if you need them. Errors are passed to java.util.logging, though.
)Support for HTTPS (SSL)
.Basic support for cookies
.Supports parameter parsing of GET and POST methods
.Some built-in support for HEAD, POST and DELETE requests. You can easily implement/customize any HTTP method, though
.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
.Persistent connections (Connection "keep-alive") support allowing multiple requests to be served over a single socket connection
.为了学习NanoHttpd,做了一个简单Demo:Android 本地代理方式播放 Sdcard中的m3u8视频
Demo实现下载
Android 本地代理方式播放 Sdcard中的m3u8视频(使用的NanoHttpd 版本为 2.3.1
)
NanoHttpd 2.3.1版本下载
实现效果如下图所示:
NanoHttpd的使用,使 “本地代理方式播放Android Sdcard中的m3u8视频” Demo实现变得很简单,这里不做具体介绍,有兴趣的朋友可以自行下载了解。
下边来主要来跟踪学习NanoHttpd的源码…
注:基于 NanoHttpd 2.3.1版本
NanoHttpd 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)
ChunkedOutputStream.send(outputStream)
返回给客户端建议:对于Http request、response 数据组织形式不是很了解的同学,建议自己了解后再阅读NanoHTTPD源码。 也可参考我的另一篇文章:Http请求数据格式
从服务器启动开始学习…
/**
* 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
中。@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)
方法,进行相应的业务处理@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()
中会对客户端的请求进行解析
@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
NanoHttpd源码分析