最近在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()方法中:
Java代碼??
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()內。
Java代碼??
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方法:
Java代碼??
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方法,有點長,分段解析:
Java代碼??
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的:
Java代碼??
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:
Java代碼??
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方法,也比較長,簡單說一下:
Java代碼??
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后,
Java代碼??
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,首先創建一個臨時文件:
Java代碼??
RandomAccessFile?f?=?getTmpBucket();??
NanoHTTPD中將創建臨時文件進行了封裝(稍微有點復雜),如下:
Java代碼??
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構造方法:
Java代碼??
TempFileManager?tempFileManager?=?tempFileManagerFactory.create();??final?HTTPSession?session?=?new?HTTPSession(tempFileManager,?inputStream,?outputStream);??
實際的臨時文件類定義如下:
Java代碼??
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讀出并寫入剛才創建的臨時文件:
Java代碼??
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)表示將文件指針指向文件頭。
Java代碼??
//?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(表單提交)分別進行了處理:
Java代碼??
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就是分隔符:
Request代碼??
--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請求進行了處理。
Java代碼??
else?if?(Method.PUT.equals(method))?{???????????files.put("content",?saveTmpFile(fbuf,?0,?fbuf.limit()));??}??
其中,saveTmpFile方法是將body寫入臨時文件并返回其路徑,limit為當前buffer中可用的位置(即內容):
Java代碼??
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;??}??
現在,所有請求處理完成,下面構造響應并關閉流:
Java代碼??
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是個抽象方法,用于構造響應內容,需要用戶在子類中實現(后面會給出例子)。
Java代碼??
public?abstract?Response?serve(String?uri,Method?method,Map<String,?String>?header,Map<String,?String>?parms,Map<String,?String>?files);??
構造完響應內容,最后就是發送響應了:
Java代碼??
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作為響應體。
下面給出一個使用案例(官方提供):
Java代碼??
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協議請求響應格式。希望能對看到的人有所幫助,同時歡迎大家多拍磚。
總結
以上是生活随笔為你收集整理的NanoHttpd源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。