Tomcat是怎么工作的(2) -- 动手实现山寨版的简

系统 1726 0

      本文先讲解一下Java web server都是怎么工作的。web server也叫HTTP server——顾名思义它是用HTTP协议和客户端交互的。客户端一般就是各种各样的浏览器了。相信所有朋友都清楚这个基本事实,否则你也不会看到这个系列文章了。

      基于Java的web server必然用到两个极其重要的类:java.net.Socket和java.net.ServerSocket,然后以HTTP消息进行交互。

1. HTTP协议简介(The Hypertext Transfer Protocol)

      HTTP是用于web server和浏览器之间发送、接收数据的基础核心协议——客户端发起请求然后服务端进行响应。它使用应答式TCP连接,默认情况下监听在80端口上。第一版协议是HTTP/0.9,然后又被HTTP/1.0重写了,随后HTTP/1.1又替换掉了HTTP/1.0——当前我们使用的正是HTTP/1.1,它的协议文件叫RFC2616,有兴趣的可以去w3网站上下载回来研究一下,对你理解和掌握HTTP以及整个互联网的核心有着无可替代的作用。接地气的说法就是:明白了RFC2616,你就明白了易筋经和九阳神功,自此之后横行天下无所顾忌。。。

      HTTP里,永远都是客户端主动发起请求,然后服务端才有可能和它建立连接。web server永远不会主动连接或者回调客户端,但是两边都可以直接断开连接。

      总结成一句话就是:服务端永远处于绝对优势地位,客户端你不连我我就绝对不会连你,只有你客户端发起请求了,我服务端才会和你连接,当然,心情不好时我也照样可以不对你的请求做出任何响应。像极了男人追女人的恋爱过程吧。。。

1.1 HTTP请求

      它由以下部分组成:

      第一部分:方式 — URI — 协议/版本号

      第二部分:请求头

      第三部分:实体数据

      典型例如如下:

      
           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了。

 

文档信息

Tomcat是怎么工作的(2) -- 动手实现山寨版的简单Web Server


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论