最近在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-- //结束分隔符
如果是普通的表单提交的话,就循环读取post body直到结束(\r\n)为止。
另外,简单看了一下:decodeMultipartData作用是将post中上传文件的内容解析出来,decodeParms作用是将post中含有%的值使用URLDecoder.decode解码出来,这里就不贴代码了。
最后,除了处理POST请求外,还对PUT请求进行了处理。
else if (Method.PUT.equals(method)) {
files.put("content", saveTmpFile(fbuf, 0, fbuf.limit()));
}
其中,saveTmpFile方法是将body写入临时文件并返回其路径,limit为当前buffer中可用的位置(即内容):
private String saveTmpFile(ByteBuffer b, int offset, int len) {
String path = "";
if (len > 0) {
try {
TempFile tempFile = tempFileManager.createTempFile();
ByteBuffer src = b.duplicate();
FileChannel dest = new FileOutputStream(tempFile.getName()).getChannel();
src.position(offset).limit(offset + len);
dest.write(src.slice());
path = tempFile.getName();
} catch (Exception e) { // Catch exception if any
System.err.println("Error: " + e.getMessage());
}
}
return path;
}
现在,所有请求处理完成,下面构造响应并关闭流:
Response r = serve(uri, method, header, parms, files);
if (r == null) {
Response.error(outputStream, Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
throw new InterruptedException();
} else {
r.setRequestMethod(method);
r.send(outputStream);
}
in.close();
inputStream.close();
其中serve是个抽象方法,用于构造响应内容,需要用户在子类中实现(后面会给出例子)。
public abstract Response serve(String uri,Method method,Map<String, String> header,Map<String, String> parms,Map<String, String> files);
构造完响应内容,最后就是发送响应了:
private void send(OutputStream outputStream) {
String mime = mimeType;
SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
if (status == null) {
throw new Error("sendResponse(): Status can't be null.");
}
PrintWriter pw = new PrintWriter(outputStream);
pw.print("HTTP/1.0 " + status.getDescription() + " \r\n");
if (mime != null) {
pw.print("Content-Type: " + mime + "\r\n");
}
if (header == null || header.get("Date") == null) {
pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
}
if (header != null) {
for (String key : header.keySet()) {
String value = header.get(key);
pw.print(key + ": " + value + "\r\n");
}
}
pw.print("\r\n");
pw.flush();
if (requestMethod != Method.HEAD && data != null) {
int pending = data.available();
int BUFFER_SIZE = 16 * 1024;
byte[] buff = new byte[BUFFER_SIZE];
while (pending > 0) {
int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
if (read <= 0) {
break;
}
outputStream.write(buff, 0, read);
pending -= read;
}
}
outputStream.flush();
outputStream.close();
if (data != null)
data.close();
} catch (IOException ioe) {
// Couldn't write? No can do.
}
}
通过PrintWriter构造响应头,如果请求不为HEAD方法(没有响应body),则将用户构造的响应内容写入outputStream作为响应体。
下面给出一个使用案例(官方提供):
public class HelloServer extends NanoHTTPD {
public HelloServer() {
super(8080);
}
@Override
public Response serve(String uri, Method method, Map<String, String> header, Map<String, String> parms, Map<String, String> files) {
String msg = "<html><body><h1>Hello server</h1>\n";
if (parms.get("username") == null)
msg +=
"<form action='?' method='post'>\n" +
" <p>Your name: <input type='text' name='username'></p>\n" +
"</form>\n";
else
msg += "<p>Hello, " + parms.get("username") + "!</p>";
msg += "</body></html>\n";
return new NanoHTTPD.Response(msg);
}
//后面public static void main...就不贴了
}
由此可见,serve是上文中的抽象方法,由用户构造响应内容,此处给出的例子是一个html。
结束语:
至此,NanoHTTPD的源码基本就算分析完了。通过分析该源码,可以更深入的了解Socket BIO编程模型以及HTTP协议请求响应格式。希望能对看到的人有所帮助,同时欢迎大家多拍砖。