Tomcat是怎麼工做的(2) -- 動手實現山寨版的簡單Web Server

      本文先講解一下Java web server都是怎麼工做的。web server也叫HTTP server——顧名思義它是用HTTP協議和客戶端交互的。客戶端通常就是各類各樣的瀏覽器了。相信全部朋友都清楚這個基本事實,不然你也不會看到這個系列文章了。html

      基於Java的web server必然用到兩個極其重要的類:java.net.Socket和java.net.ServerSocket,而後以HTTP消息進行交互。java

1. HTTP協議簡介(The Hypertext Transfer Protocol)web

      HTTP是用於web server和瀏覽器之間發送、接收數據的基礎核心協議——客戶端發起請求而後服務端進行響應。它使用應答式TCP鏈接,默認狀況下監聽在80端口上。初版協議是HTTP/0.9,而後又被HTTP/1.0重寫了,隨後HTTP/1.1又替換掉了HTTP/1.0——當前咱們使用的正是HTTP/1.1,它的協議文件叫RFC2616,有興趣的能夠去w3網站上下載回來研究一下,對你理解和掌握HTTP以及整個互聯網的核心有着無可替代的做用。接地氣的說法就是:明白了RFC2616,你就明白了易筋經和九陽神功,自此以後橫行天下無所顧忌。。。chrome

      HTTP裏,永遠都是客戶端主動發起請求,而後服務端纔有可能和它創建鏈接。web server永遠不會主動鏈接或者回調客戶端,可是兩邊均可以直接斷開鏈接。編程

      總結成一句話就是:服務端永遠處於絕對優點地位,客戶端你不連我我就絕對不會連你,只有你客戶端發起請求了,我服務端纔會和你鏈接,固然,心情很差時我也照樣能夠不對你的請求作出任何響應。像極了男人追女人的戀愛過程吧。。。瀏覽器

1.1 HTTP請求服務器

      它由如下部分組成:cookie

      第一部分:方式 — URI — 協議/版本號網絡

      第二部分:請求頭app

      第三部分:實體數據

      典型例如以下:

   1:  POST /baidu.com/小蘋果歌詞.txt HTTP/1.1
   2:   
   3:  Accept: text/plain; text/html 
   4:  Accept-Language: en-gb 
   5:  Connection: Keep-Alive 
   6:  Host: localhost 
   7:  User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98) 
   8:  Content-Length: 33 
   9:  Content-Type: application/x-www-form-urlencoded 
  10:  Accept-Encoding: gzip, deflate 
  11:   
  12:  lastName=Franks&firstName=Michael

 

     對應的第一部分就是這段請求信息的第一行,下面再詳細講解下:

     POST /baidu.com/小蘋果歌詞.txt HTTP/1.1

     這一行的「POST」是請求方式,「/baidu.com/小蘋果歌詞.txt」是對應的URI,而「HTTP/1.1」就是對應的協議/版本號了。

     HTTP協議定義了不少請求方式,每個HTTP請求均可以使用其中的一種。HTTP 1.1 支持7種請求類型:GET,POST,HEAD,OPTIONS,PUT,DELETE以及TRACE。通常狀況下,咱們只用到GET和POST就足夠了。

     URI完整的指定了一個網絡資源,通常狀況下它都是相對於服務器的根目錄進行資源定位,你看到的URI才常常以斜槓「/」開頭,固然,一般咱們只知道URL,URL實際上只是URI的一種而已(細節可研究RFC2396協議)。第一行的協議版本號,顧名思義就是當前使用的是哪版HTTP協議了。

     請求頭包含了一些關於客戶端環境和請求體的有用信息。例如,它能夠指示瀏覽器使用的語言、請求體的數據長度等等。每個請求頭和請求體之間都經過回車換行符(CRLF)分隔。

     請求頭和請求體之間的空白行(CRLF)是HTTP請求格式中不可或缺的一部分,它用於指明請求體數據開始的w位置。甚至在一些網絡編程書中,這個空白行(CRLF)直接被看成了HTTP請求標準格式的第四個組成部分。

     在上面那個例子中,請求體的實體數據只有簡單的一行,不過實際應用中實體數據每每比較多:

     lastName=Franks&firstName=Michael

     1.2 HTTP響應

     和HTTP請求類似,HTTP響應也由三部分組成:

     第一部分:協議 -- 狀態碼 --描述

     第二部分:響應頭

     第三部分:響應體

     舉個例子:

   1:  HTTP/1.1 200 OK 
   2:  Server: Microsoft-IIS/4.0 
   3:  Date: Mon, 5 Jan 2004 13:13:33 GMT 
   4:  Content-Type: text/html 
   5:  Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT 
   6:  Content-Length: 112 
   7:   
   8:  <html> 
   9:  <head> 
  10:      <title>HTTP Response Example</title> 
  11:  </head> 
  12:  <body> 
  13:       Welcome to Brainy Software 
  14:  </body> 
  15:  </html>
 

     響應頭第一行和請求頭極爲類似,它指示了當前使用的協議是HTTP/1.1版本,並且請求成功了(200=成功),一切順利。
響應頭包含的有用信息相似於請求頭的。響應體是一段HTML內容,響應頭和響應體之間以CRLF分隔。


2. Socket類
      socket是網絡鏈接的一個端點,它賦予應用程序讀寫網絡流的能力。兩臺電腦經過發送和接收基於鏈接的字節流來進行交流溝通。若要發消息給另外一個程序,你須要知道這個程序socket的ip地址和端口號。在java裏,socket指的是java.net.Socket類。


      你可使用Socket類的諸多構造器中任意一個來建立socket,下面這個構造器接收主機名和端口號做爲參數:
      public Socket (java.lang.String host, int port)


      在此,host能夠是主機名或者ip地址,端口號就是對應的程序佔用的端口。例如,要想鏈接80端口上的yahoo.com,你須要以下構造方式:
      new Socket("yahoo.com", 80);


      一旦成功建立Socket實例,你就能夠用它來發送接收字節流了。要發送字節流,你必須首先調用Socket類的getOutputStream方法獲取java.io.OutputStream對象,要發送純文本的話,咱們一般構造一個OutputStream對象返回的java.io.PrintWriter對象。要接收字節流,你就應該調用Socket類的getInputStream方法來獲取 java.io.InputStream。


下面就是代碼展現了,各位看官請好:

   1:  Socket socket = new Socket("127.0.0.1", "8080"); 
   2:  OutputStream os = socket.getOutputStream(); 
   3:  boolean autoflush = true; 
   4:  PrintWriter out = new PrintWriter( socket.getOutputStream(), autoflush); 
   5:  BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputstream() )); 
   6:   
   7:  // 向web server發送HTTP請求
   8:  out.println("GET /index.jsp HTTP/1.1"); 
   9:  out.println("Host: localhost:8080");
  10:  out.println("Connection: Close"); 
  11:  out.println(); 
  12:   
  13:  // 讀取響應
  14:  boolean loop = true; 
  15:  StringBuffer sb = new StringBuffer(8096); 
  16:  while (loop) { 
  17:      if ( in.ready() ) { 
  18:          int i=0;
  19:          while (i!=-1) { 
  20:              i = in.read(); 
  21:              sb.append((char) i); 
  22:          } 
  23:          loop = false; 
  24:      } 
  25:      Thread.currentThread().sleep(50); 
  26:  } 
  27:   
  28:  // 輸出響應內容 
  29:  System.out.println(sb.toString()); 
  30:  socket.close();

 

3. ServerSocket類
      Socket類表明的是客戶端Socket,例如IE瀏覽器、chrome、火狐、safari等發起的鏈接。若是你想實現一個服務器應用程序,像HTTP server或者FTP server的話,你就必須使用不一樣的方法了。這是由於服務端根本不知道客戶端會發起請求創建鏈接,它必須永不停歇的等待客戶端請求。爲此,你必須使用java.net.ServerSocket類,它是服務端socket的實現。


      ServerSocket不一樣於Socket,服務端的ServerSocket必須一直等着客戶端請求的到來。一旦server socket接到鏈接請求,它必須建立一個Socket實例來處理和客戶端的交互。


      要建立server socket,你得用ServerSocket類提供的四個構造器之一。它須要你指明IP地址和server socket要監聽的端口號。經典的127.0.0.1意味着server socket將監聽本機。server socket監聽的IP地址一般也叫綁定地址。另外一個重要的屬性是backlog,它意味着接入的鏈接請求超過此數值以後server socket就會拒絕後續請求。


      public ServerSocket(int port, int backlog, InetAddress bindingAddress);


      值得注意的是,這個構造器的綁定地址必須是java.net.InetAddress類的實例。構造InetAddress對象的簡易方法就是調用它的靜態方法getByname,並傳一個主機名字符創參數,以下所示:
      InetAddress.getByName("127.0.0.1");


      下面這行代碼構造了一個ServerSocket,監聽本機8080端口,同時backlog爲1:
      new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));


      一旦ServerSocket實例構造完成,它就能夠一直監聽在綁定地址的對應端口上等待請求的到來,你須要作的就是調用ServerSocket類的accept方法來啓動這個監聽過程。這個方法只返回什麼時候產生了鏈接請求而且返回值是一個Sokcet類的實例。而後經過這個Socket能夠發送和接收字節流。


4. 動手實現本身的山寨版web server
      這個山寨web server由三個類組成:HttpServer、Request、Response。
      HttpServer的main方法建立一個HttpServer實例並調用它的await方法,顧名思義,這個await方法一直等着請求到來,而後處理請求、發送響應信息到客戶端。它會一直等,直到程序終止或停機。
      這個山寨版的server目前只能發送靜態資源,它會在控制檯顯示HTTP請求的字節流,但不能發送任何響應頭,好比data、cookie之類的。


4.1 HTTPServer.java

 

   1:  import java.io.File;
   2:  import java.io.IOException;
   3:  import java.io.InputStream;
   4:  import java.io.OutputStream;
   5:  import java.net.InetAddress;
   6:  import java.net.ServerSocket;
   7:  import java.net.Socket;
   8:   
   9:  public class HttpServer {
  10:   
  11:      /**
  12:       * WEB_ROOT is the directory where our HTML and other files reside. For this
  13:       * package, WEB_ROOT is the "webroot" directory under the working directory.
  14:       * The working directory is the location in the file system from where the
  15:       * java command was invoked.
  16:       */
  17:      public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
  18:   
  19:      // 關機命令
  20:      private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
  21:   
  22:      // the shutdown command received
  23:      private boolean shutdown = false;
  24:   
  25:      public static void main(String[] args) {
  26:          HttpServer server = new HttpServer();
  27:          server.await();
  28:      }
  29:   
  30:      public void await() {
  31:          ServerSocket serverSocket = null;
  32:          int port = 8080;
  33:          try {
  34:              serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
  35:          } catch(IOException e) {
  36:              e.printStackTrace();
  37:              System.exit(1);
  38:          }
  39:          // 輪詢是否有請求進來
  40:          while(!shutdown) {
  41:              Socket socket = null;
  42:              InputStream input = null;
  43:              OutputStream output = null;
  44:              try {
  45:                  socket = serverSocket.accept();
  46:                  input = socket.getInputStream();
  47:                  output = socket.getOutputStream();
  48:                  // create Request object and parse
  49:                  Request request = new Request(input);
  50:                  request.parse();
  51:                  // create Response object
  52:                  Response response = new Response(output);
  53:                  response.setRequest(request);
  54:                  response.sendStaticResource();
  55:                  // Close the socket
  56:                  socket.close();
  57:                  // check if the previous URI is a shutdown command
  58:                  shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
  59:              } catch(Exception e) {
  60:                  e.printStackTrace();
  61:                  continue;
  62:              }
  63:          }
  64:      }
  65:  }

4.2 Request.java

   1:  package ex01.pyrmont;
   2:   
   3:  import java.io.IOException;
   4:  import java.io.InputStream;
   5:   
   6:  public class Request {
   7:   
   8:      private InputStream input;
   9:   
  10:      private String uri;
  11:   
  12:      public Request(InputStream input) {
  13:          this.input = input;
  14:      }
  15:   
  16:      public void parse() {
  17:          // Read a set of characters from the socket
  18:          StringBuffer request = new StringBuffer(2048);
  19:          int i;
  20:          byte[] buffer = new byte[2048];
  21:          try {
  22:              i = input.read(buffer);
  23:          } catch(IOException e) {
  24:              e.printStackTrace();
  25:              i = -1;
  26:          }
  27:          for(int j = 0; j < i; j++) {
  28:              request.append((char)buffer[j]);
  29:          }
  30:          System.out.print(request.toString());
  31:          uri = parseUri(request.toString());
  32:      }
  33:   
  34:      private String parseUri(String requestString) {
  35:          int index1, index2;
  36:          index1 = requestString.indexOf(' ');
  37:          if(index1 != -1) {
  38:              index2 = requestString.indexOf(' ', index1 + 1);
  39:              if(index2 > index1)
  40:                  return requestString.substring(index1 + 1, index2);
  41:          }
  42:          return null;
  43:      }
  44:   
  45:      public String getUri() {
  46:          return uri;
  47:      }
  48:  }
  49:   

4.3 Response.java

   1:  package ex01.pyrmont;
   2:   
   3:  import java.io.File;
   4:  import java.io.FileInputStream;
   5:  import java.io.IOException;
   6:  import java.io.OutputStream;
   7:   
   8:  /*
   9:  * HTTP Response = Status-Line (( general-header | response-header |
  10:  * entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP
  11:  * Status-Code SP Reason-Phrase CRLF
  12:  */
  13:  public class Response {
  14:   
  15:      private static final int BUFFER_SIZE = 1024;
  16:   
  17:      Request request;
  18:   
  19:      OutputStream output;
  20:   
  21:      public Response(OutputStream output) {
  22:          this.output = output;
  23:      }
  24:   
  25:      public void setRequest(Request request) {
  26:          this.request = request;
  27:      }
  28:   
  29:      public void sendStaticResource() throws IOException {
  30:          byte[] bytes = new byte[BUFFER_SIZE];
  31:          FileInputStream fis = null;
  32:          try {
  33:              File file = new File(HttpServer.WEB_ROOT, request.getUri());
  34:              if(file.exists()) {
  35:                  fis = new FileInputStream(file);
  36:                  int ch = fis.read(bytes, 0, BUFFER_SIZE);
  37:                  while(ch != -1) {
  38:                      output.write(bytes, 0, ch);
  39:                      ch = fis.read(bytes, 0, BUFFER_SIZE);
  40:                  }
  41:              } else {
  42:                  // file not found
  43:                  String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>";
  44:                  output.write(errorMessage.getBytes());
  45:              }
  46:          } catch(Exception e) {
  47:              // thrown if cannot instantiate a File object
  48:              System.out.println(e.toString());
  49:          } finally {
  50:              if(fis != null)
  51:                  fis.close();
  52:          }
  53:      }
  54:  }
  55:   


5. 總結

     本文講解了web server的基本原理,同時代碼貼出來了一個粗糙山寨的web server。它只有三個類構成,固然不是全功能的,不過呢,畢竟剛開始,咱們會不斷的逐步完善這個web server,到本系列結束時,基本上就有一個完整的web server了。

 

文檔信息

相關文章
相關標籤/搜索