menu
more_vert

【转】如何开发自己的HttpServer-NanoHttpd源码解读

作者:dayu

  • 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:

    [plain]view plaincopy


    1. //Readthefirst8192bytes.
    2. //Thefullheadershouldfitinhere.
    3. //Apache‘sdefaultheaderlimitis8KB.
    4. //DoNOTassumethatasinglereadwillgettheentireheaderatonce!
    5. byte[]buf=newbyte[BUFSIZE];
    6. splitbyte=0;
    7. rlen=0;
    8. {
    9. intread=-1;
    10. try{
    11. read=inputStream.read(buf,0,BUFSIZE);
    12. }catch(Exceptione){
    13. safeClose(inputStream);
    14. safeClose(outputStream);
    15. thrownewSocketException("NanoHttpdShutdown");
    16. }
    17. if(read==-1){
    18. //socketwasbeenclosed
    19. safeClose(inputStream);
    20. safeClose(outputStream);
    21. thrownewSocketException("NanoHttpdShutdown");
    22. }
    23. while(read>0){
    24. rlen+=read;
    25. splitbyte=findHeaderEnd(buf,rlen);
    26. if(splitbyte>0)
    27. break;
    28. read=inputStream.read(buf,rlen,BUFSIZE-rlen);
    29. }
    30. }

    1.读取socket数据流的前8192个字节,因为http协议中头部最长为8192

    2.通过findHeaderEnd函数找到header数据的截止位置,并把位置保存到splitbyte内。

    [java]view plaincopy


    1. if(splitbyte<rlen){
    2. inputStream.unread(buf,splitbyte,rlen-splitbyte);
    3. }
    4. parms=newHashMap<String,String>();
    5. if(null==headers){
    6. headers=newHashMap<String,String>();
    7. }
    8. //CreateaBufferedReaderforparsingtheheader.
    9. BufferedReaderhin=newBufferedReader(newInputStreamReader(newByteArrayInputStream(buf,0,rlen)));
    10. //Decodetheheaderintoparmsandheaderjavaproperties
    11. Map<String,String>pre=newHashMap<String,String>();
    12. decodeHeader(hin,pre,parms,headers);

    1.使用unread函数将之前读出来的body pushback回去,这里使用了pushbackstream,用法比较巧妙,因为一旦读到了header的尾部就需要进入下面的逻辑来判断是否需要再读下去了,而不应该一直读,读到没有数据为止

    2.decodeHeader,将byte的header转换为java对象

    [java]view plaincopy


    1. privateintfindHeaderEnd(finalbyte[]buf,intrlen){
    2. intsplitbyte=0;
    3. while(splitbyte+3<rlen){
    4. if(buf[splitbyte]==‘\r‘&&buf[splitbyte+1]==‘\n‘&&buf[splitbyte+2]==‘\r‘&&buf[splitbyte+3]==‘\n‘){
    5. returnsplitbyte+4;
    6. }
    7. splitbyte++;
    8. }
    9. return0;
    10. }

    1.http协议规定header和body之间使用两个回车换行分割

    [java]view plaincopy


    1. privatevoiddecodeHeader(BufferedReaderin,Map<String,String>pre,Map<String,String>parms,Map<String,String>headers)
    2. throwsResponseException{
    3. try{
    4. //Readtherequestline
    5. StringinLine=in.readLine();
    6. if(inLine==null){
    7. return;
    8. }
    9. StringTokenizerst=newStringTokenizer(inLine);
    10. if(!st.hasMoreTokens()){
    11. thrownewResponseException(Response.Status.BAD_REQUEST,"BADREQUEST:Syntaxerror.Usage:GET/example/file.html");
    12. }
    13. pre.put("method",st.nextToken());
    14. if(!st.hasMoreTokens()){
    15. thrownewResponseException(Response.Status.BAD_REQUEST,"BADREQUEST:MissingURI.Usage:GET/example/file.html");
    16. }
    17. Stringuri=st.nextToken();
    18. //DecodeparametersfromtheURI
    19. intqmi=uri.indexOf(‘?‘);
    20. if(qmi>=0){
    21. decodeParms(uri.substring(qmi+1),parms);
    22. uri=decodePercent(uri.substring(0,qmi));
    23. }else{
    24. uri=decodePercent(uri);
    25. }
    26. //Ifthere‘sanothertoken,it‘sprotocolversion,
    27. //followedbyHTTPheaders.Ignoreversionbutparseheaders.
    28. //NOTE:thisnowforcesheadernameslowercasesincetheyare
    29. //caseinsensitiveandvarybyclient.
    30. if(st.hasMoreTokens()){
    31. Stringline=in.readLine();
    32. while(line!=null&&line.trim().length()>0){
    33. intp=line.indexOf(‘:‘);
    34. if(p>=0)
    35. headers.put(line.substring(0,p).trim().toLowerCase(Locale.US),line.substring(p+1).trim());
    36. line=in.readLine();
    37. }
    38. }
    39. pre.put("uri",uri);
    40. }catch(IOExceptionioe){
    41. thrownewResponseException(Response.Status.INTERNAL_ERROR,"SERVERINTERNALERROR:IOException:"+ioe.getMessage(),ioe);
    42. }
    43. }

    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方法实现了一个默认的简单处理功能。

    [java]view plaincopy


    1. /**
    2. *Overridethistocustomizetheserver.
    3. *<p/>
    4. *<p/>
    5. *(Bydefault,thisdelegatestoserveFile()andallowsdirectorylisting.)
    6. *
    7. *@paramsessionTheHTTPsession
    8. *@returnHTTPresponse,seeclassResponsefordetails
    9. */
    10. publicResponseserve(IHTTPSessionsession){
    11. Map<String,String>files=newHashMap<String,String>();
    12. Methodmethod=session.getMethod();
    13. if(Method.PUT.equals(method)||Method.POST.equals(method)){
    14. try{
    15. session.parseBody(files);
    16. }catch(IOExceptionioe){
    17. returnnewResponse(Response.Status.INTERNAL_ERROR,MIME_PLAINTEXT,"SERVERINTERNALERROR:IOException:"+ioe.getMessage());
    18. }catch(ResponseExceptionre){
    19. returnnewResponse(re.getStatus(),MIME_PLAINTEXT,re.getMessage());
    20. }
    21. }
    22. Map<String,String>parms=session.getParms();
    23. parms.put(QUERY_STRING_PARAMETER,session.getQueryParameterString());
    24. returnserve(session.getUri(),method,session.getHeaders(),parms,files);
    25. }

    这个默认的方法处理了PUT和POST方法,如果不是就返回默认的返回值。

    parseBody方法中使用了tmpFile的方法保存httpRequest的content信息,然后处理,具体逻辑就不细说了,不是一个典型的实现。

    最后看一下发response的逻辑:

    [java]view plaincopy


    1. /**
    2. *Sendsgivenresponsetothesocket.
    3. */
    4. protectedvoidsend(OutputStreamoutputStream){
    5. Stringmime=mimeType;
    6. SimpleDateFormatgmtFrmt=newSimpleDateFormat("E,dMMMyyyyHH:mm:ss‘GMT‘",Locale.US);
    7. gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
    8. try{
    9. if(status==null){
    10. thrownewError("sendResponse():Statuscan‘tbenull.");
    11. }
    12. PrintWriterpw=newPrintWriter(outputStream);
    13. pw.print("HTTP/1.1"+status.getDescription()+"\r\n");
    14. if(mime!=null){
    15. pw.print("Content-Type:"+mime+"\r\n");
    16. }
    17. if(header==null||header.get("Date")==null){
    18. pw.print("Date:"+gmtFrmt.format(newDate())+"\r\n");
    19. }
    20. if(header!=null){
    21. for(Stringkey:header.keySet()){
    22. Stringvalue=header.get(key);
    23. pw.print(key+":"+value+"\r\n");
    24. }
    25. }
    26. sendConnectionHeaderIfNotAlreadyPresent(pw,header);
    27. if(requestMethod!=Method.HEAD&&chunkedTransfer){
    28. sendAsChunked(outputStream,pw);
    29. }else{
    30. intpending=data!=null?data.available():0;
    31. sendContentLengthHeaderIfNotAlreadyPresent(pw,header,pending);
    32. pw.print("\r\n");
    33. pw.flush();
    34. sendAsFixedLength(outputStream,pending);
    35. }
    36. outputStream.flush();
    37. safeClose(data);
    38. }catch(IOExceptionioe){
    39. //Couldn‘twrite?Nocando.
    40. }
    41. }

    发送response的步骤如下:

    1.设置mimeType和Time等内容。

    2.创建一个PrintWriter,按照HTTP协议依次开始写入内容

    3.第一行是HTTP的返回码

    4.然后是content-Type

    5.然后是Date时间

    6.之后是其他的HTTP Header

    7.设置Keep-Alive的Header,Keep-Alive是Http1.1的新特性,作用是让客户端和服务器端之间保持一个长链接。

    8.如果客户端指定了ChunkedEncoding则分块发送response,Chunked Encoding是Http1.1的又一新特性。一般在response的body比较大的时候使用,server端会首先发送response的HEADER,然后分块发送response的body,每个分块都由chunk length\r\n和chunk data\r\n组成,最后由一个0\r\n结束。

    [java]view plaincopy


    1. privatevoidsendAsChunked(OutputStreamoutputStream,PrintWriterpw)throwsIOException{
    2. pw.print("Transfer-Encoding:chunked\r\n");
    3. pw.print("\r\n");
    4. pw.flush();
    5. intBUFFER_SIZE=16*1024;
    6. byte[]CRLF="\r\n".getBytes();
    7. byte[]buff=newbyte[BUFFER_SIZE];
    8. intread;
    9. while((read=data.read(buff))>0){
    10. outputStream.write(String.format("%x\r\n",read).getBytes());
    11. outputStream.write(buff,0,read);
    12. outputStream.write(CRLF);
    13. }
    14. outputStream.write(String.format("0\r\n\r\n").getBytes());
    15. }

    9.如果没指定ChunkedEncoding则需要指定Content-Length来让客户端指定response的body的size,然后再一直写body直到写完为止。

    [java]view plaincopy


    1. privatevoidsendAsFixedLength(OutputStreamoutputStream,intpending)throwsIOException{
    2. if(requestMethod!=Method.HEAD&&data!=null){
    3. intBUFFER_SIZE=16*1024;
    4. byte[]buff=newbyte[BUFFER_SIZE];
    5. while(pending>0){
    6. intread=data.read(buff,0,((pending>BUFFER_SIZE)?BUFFER_SIZE:pending));
    7. if(read<=0){
    8. break;
    9. }
    10. outputStream.write(buff,0,read);
    11. pending-=read;
    12. }
    13. }
    14. }

    最后总结下实现HttpServer最重要的几个部分:

    1.能够accept tcp连接并从socket中读取request数据

    2.把request的比特流转换成request对象中的对象数据

    3.根据http协议的规范处理http request

    4.产生http response再写回到socket中传给client。

    【转】如何开发自己的HttpServer-NanoHttpd源码解读

    原文地址:http://www.cnblogs.com/exmyth/p/7524432.html