现在作为一个开发人员,http server相关的内容已经是无论如何都要了解的知识了。用curl发一个请求,配置一下apache,部署一个web server对我们来说都不是很难,但要想搞清楚这些背后都发生了什么技术细节还真不是很简单的。所以新的系列将是分享我学习Http Server的过程。
NanoHttpd是Github上的一个开源项目,号称只用一个java文件就能创建一个http server,我将通过分析NanoHttpd的源码解析如何开发自己的HttpServer。Github 地址:https://github.com/NanoHttpd/nanohttpd
在开始前首先简单说明HttpServer的基本要素:
1.能接受HttpRequest并返回HttpResponse
2.满足一个Server的基本特征,能够长时间运行
关于Http协议一般HttpServer都会声明支持Http协议的哪些特性,nanohttpd作为一个轻量级的httpserver只实现了最简单、最常用的功能,不过我们依然可以从中学习很多。
首先看下NanoHttpd类的start函数
- publicvoidstart()throwsIOException{
- myServerSocket=newServerSocket();
- myServerSocket.bind((hostname!=null)?newInetSocketAddress(hostname,myPort):newInetSocketAddress(myPort));
- myThread=newThread(newRunnable(){
- @Override
- publicvoidrun(){
- do{
- try{
- finalSocketfinalAccept=myServerSocket.accept();
- registerConnection(finalAccept);
- finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
- finalInputStreaminputStream=finalAccept.getInputStream();
- asyncRunner.exec(newRunnable(){
- @Override
- publicvoidrun(){
- OutputStreamoutputStream=null;
- try{
- outputStream=finalAccept.getOutputStream();
- TempFileManagertempFileManager=tempFileManagerFactory.create();
- HTTPSessionsession=newHTTPSession(tempFileManager,inputStream,outputStream,finalAccept.getInetAddress());
- while(!finalAccept.isClosed()){
- session.execute();
- }
- }catch(Exceptione){
- //Whenthesocketisclosedbytheclient,wethrowourownSocketException
- //tobreakthe"keepalive"loopabove.
- if(!(einstanceofSocketException&&"NanoHttpdShutdown".equals(e.getMessage()))){
- e.printStackTrace();
- }
- }finally{
- safeClose(outputStream);
- safeClose(inputStream);
- safeClose(finalAccept);
- unRegisterConnection(finalAccept);
- }
- }
- });
- }catch(IOExceptione){
- }
- }while(!myServerSocket.isClosed());
- }
- });
- myThread.setDaemon(true);
- myThread.setName("NanoHttpdMainListener");
- myThread.start();
- }
1.创建ServerSocket,bind制定端口
2.创建主线程,主线程负责和client建立连接
3.建立连接后会生成一个runnable对象放入asyncRunner中,asyncRunner.exec会创建一个线程来处理新生成的连接。
4.新线程首先创建了一个HttpSession,然后while(true)的执行httpSession.exec。
这里介绍下HttpSession的概念,HttpSession是java里Session概念的实现,简单来说一个Session就是一次httpClient->httpServer的连接,当连接close后session就结束了,如果没结束则session会一直存在。这点从这里的代码也能看到:如果socket不close或者exec没有抛出异常(异常有可能是client段断开连接)session会一直执行exec方法。
一个HttpSession中存储了一次网络连接中server应该保存的信息,比如:URI,METHOD,PARAMS,HEADERS,COOKIES等。
5.这里accept一个client的socket就创建一个独立线程的server模型是ThreadServer模型,特点是一个connection就会创建一个thread,是比较简单、常见的socket server实现。缺点是在同时处理大量连接时线程切换需要消耗大量的资源,如果有兴趣可以了解更加高效的NIO实现方式。
当获得client的socket后自然要开始处理client发送的httprequest。
Http Request Header的parse:
- //Readthefirst8192bytes.
- //Thefullheadershouldfitinhere.
- //Apache‘sdefaultheaderlimitis8KB.
- //DoNOTassumethatasinglereadwillgettheentireheaderatonce!
- byte[]buf=newbyte[BUFSIZE];
- splitbyte=0;
- rlen=0;
- {
- intread=-1;
- try{
- read=inputStream.read(buf,0,BUFSIZE);
- }catch(Exceptione){
- safeClose(inputStream);
- safeClose(outputStream);
- thrownewSocketException("NanoHttpdShutdown");
- }
- if(read==-1){
- //socketwasbeenclosed
- safeClose(inputStream);
- safeClose(outputStream);
- thrownewSocketException("NanoHttpdShutdown");
- }
- while(read>0){
- rlen+=read;
- splitbyte=findHeaderEnd(buf,rlen);
- if(splitbyte>0)
- break;
- read=inputStream.read(buf,rlen,BUFSIZE-rlen);
- }
- }
1.读取socket数据流的前8192个字节,因为http协议中头部最长为8192
2.通过findHeaderEnd函数找到header数据的截止位置,并把位置保存到splitbyte内。
- if(splitbyte<rlen){
- inputStream.unread(buf,splitbyte,rlen-splitbyte);
- }
- parms=newHashMap<String,String>();
- if(null==headers){
- headers=newHashMap<String,String>();
- }
- //CreateaBufferedReaderforparsingtheheader.
- BufferedReaderhin=newBufferedReader(newInputStreamReader(newByteArrayInputStream(buf,0,rlen)));
- //Decodetheheaderintoparmsandheaderjavaproperties
- Map<String,String>pre=newHashMap<String,String>();
- decodeHeader(hin,pre,parms,headers);
1.使用unread函数将之前读出来的body pushback回去,这里使用了pushbackstream,用法比较巧妙,因为一旦读到了header的尾部就需要进入下面的逻辑来判断是否需要再读下去了,而不应该一直读,读到没有数据为止
2.decodeHeader,将byte的header转换为java对象
- privateintfindHeaderEnd(finalbyte[]buf,intrlen){
- intsplitbyte=0;
- while(splitbyte+3<rlen){
- if(buf[splitbyte]==‘\r‘&&buf[splitbyte+1]==‘\n‘&&buf[splitbyte+2]==‘\r‘&&buf[splitbyte+3]==‘\n‘){
- returnsplitbyte+4;
- }
- splitbyte++;
- }
- return0;
- }
1.http协议规定header和body之间使用两个回车换行分割
- privatevoiddecodeHeader(BufferedReaderin,Map<String,String>pre,Map<String,String>parms,Map<String,String>headers)
- throwsResponseException{
- try{
- //Readtherequestline
- StringinLine=in.readLine();
- if(inLine==null){
- return;
- }
- StringTokenizerst=newStringTokenizer(inLine);
- if(!st.hasMoreTokens()){
- thrownewResponseException(Response.Status.BAD_REQUEST,"BADREQUEST:Syntaxerror.Usage:GET/example/file.html");
- }
- pre.put("method",st.nextToken());
- if(!st.hasMoreTokens()){
- thrownewResponseException(Response.Status.BAD_REQUEST,"BADREQUEST:MissingURI.Usage:GET/example/file.html");
- }
- Stringuri=st.nextToken();
- //DecodeparametersfromtheURI
- intqmi=uri.indexOf(‘?‘);
- if(qmi>=0){
- decodeParms(uri.substring(qmi+1),parms);
- uri=decodePercent(uri.substring(0,qmi));
- }else{
- uri=decodePercent(uri);
- }
- //Ifthere‘sanothertoken,it‘sprotocolversion,
- //followedbyHTTPheaders.Ignoreversionbutparseheaders.
- //NOTE:thisnowforcesheadernameslowercasesincetheyare
- //caseinsensitiveandvarybyclient.
- if(st.hasMoreTokens()){
- Stringline=in.readLine();
- while(line!=null&&line.trim().length()>0){
- intp=line.indexOf(‘:‘);
- if(p>=0)
- headers.put(line.substring(0,p).trim().toLowerCase(Locale.US),line.substring(p+1).trim());
- line=in.readLine();
- }
- }
- pre.put("uri",uri);
- }catch(IOExceptionioe){
- thrownewResponseException(Response.Status.INTERNAL_ERROR,"SERVERINTERNALERROR:IOException:"+ioe.getMessage(),ioe);
- }
- }
1.Http协议第一行是Method URI HTTP_VERSION
2.后面每行都是KEY:VALUE格式的header
3.uri需要经过URIDecode处理后才能使用
4.uri中如果包含?则表示有param,httprequest的param一般表现为:/index.jsp?username=xiaoming&id=2
下面是处理cookie,不过这里cookie的实现较为简单,所以跳过。之后是serve方法,serve方法提供了用户自己实现httpserver具体逻辑的很好接口。在NanoHttpd中的serve方法实现了一个默认的简单处理功能。
- /**
- *Overridethistocustomizetheserver.
- *<p/>
- *<p/>
- *(Bydefault,thisdelegatestoserveFile()andallowsdirectorylisting.)
- *
- *@paramsessionTheHTTPsession
- *@returnHTTPresponse,seeclassResponsefordetails
- */
- publicResponseserve(IHTTPSessionsession){
- Map<String,String>files=newHashMap<String,String>();
- Methodmethod=session.getMethod();
- if(Method.PUT.equals(method)||Method.POST.equals(method)){
- try{
- session.parseBody(files);
- }catch(IOExceptionioe){
- returnnewResponse(Response.Status.INTERNAL_ERROR,MIME_PLAINTEXT,"SERVERINTERNALERROR:IOException:"+ioe.getMessage());
- }catch(ResponseExceptionre){
- returnnewResponse