zoukankan      html  css  js  c++  java
  • 用java写一个web服务器



    一、超文本传输协议

    Web服务器和浏览器通过HTTP协议在Internet上发送和接收消息。HTTP协议是一种请求-应答式的协议——客户端发送一个请求,服务器返回该请求的应答。HTTP协议使用可靠的TCP连接,默认端口是80。HTTP的第一个版本是HTTP/0.9,后来发展到了HTTP/1.0,现在最新的版本是HTTP/1.1。HTTP/1.1由 RFC 2616 定义(pdf格式)。

    本文只简要介绍HTTP 1.1的相关知识,但应该足以让你理解Web服务器和浏览器发送的消息。如果你要了解更多的细节,请参考RFC 2616。

    在HTTP中,客户端/服务器之间的会话总是由客户端通过建立连接和发送HTTP请求的方式初始化,服务器不会主动联系客户端或要求与客户端建立连接。浏览器和服务器都可以随时中断连接,例如,在浏览网页时你可以随时点击“停止”按钮中断当前的文件下载过程,关闭与Web服务器的HTTP连接。

    1.1 HTTP请求

    HTTP请求由三个部分构成,分别是:方法-URI-协议/版本,请求头,请求正文。下面是一个HTTP请求的例子:

    GET /servlet/default.jsp HTTP/1.1
    Accept: text/plain; text/html 
    Accept-Language: en-gb 
    Connection: Keep-Alive 
    Host: localhost 
    Referer: http://localhost/ch8/SendDetails.htm 
    User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98) 
    Content-Length: 33 
    Content-Type: application/x-www-form-urlencoded 
    Accept-Encoding: gzip, deflate 
    
    userName=JavaJava&userID=javaID



    请求的第一行是“方法-URI-协议/版本”,其中GET就是请求方法,/servlet/default.jsp表示URI,HTTP/1.1是协议和协议的版本。根据HTTP标准,HTTP请求可以使用多种请求方法。例如,HTTP 1.1支持七种请求方法:GET,POST,HEAD,OPTIONS,PUT,DELETE,和TRACE。在Internet应用中,最常用的请求方法是GET和POST。

    URI完整地指定了要访问的网络资源,通常认为它相对于服务器的根目录而言,因此总是以“/”开头。URL实际上是URI 一种类型。最后,协议版本声明了通信过程中使用的HTTP协议的版本。

    请求头包含许多有关客户端环境和请求正文的有用信息。例如,请求头可以声明浏览器所用的语言,请求正文的长度,等等,它们之间用一个回车换行符号(CRLF)分隔。

    请求头和请求正文之间是一个空行(只有CRLF符号的行),这个行非常重要,它表示请求头已经结束,接下来的是请求的正文。一些介绍Internet编程的书籍把这个CRLF视为HTTP请求的第四个组成部分。

    在前面的HTTP请求中,请求的正文只有一行内容。当然,在实际应用中,HTTP请求正文可以包含更多的内容。

    1.2 HTTP应答

    和HTTP请求相似,HTTP应答也由三个部分构成,分别是:协议-状态代码-描述,应答头,应答正文。下面是一个HTTP应答的例子:

    HTTP/1.1 200 OK
    Server: Microsoft-IIS/4.0
    Date: Mon, 3 Jan 1998 13:13:33 GMT
    Content-Type: text/html
    Last-Modified: Mon, 11 Jan 1998 13:23:42 GMT
    Content-Length: 112
    
    <html>
    <head>
    <title>HTTP应答示例</title></head><body>
    Hello HTTP!
    </body>
    </html>



    HTTP应答的第一行类似于HTTP请求的第一行,它表示通信所用的协议是HTTP 1.1,服务器已经成功地处理了客户端发出的请求(200表示成功),一切顺利。

    应答头也和请求头一样包含许多有用的信息,例如服务器类型、日期时间、内容类型和长度等。应答的正文就是服务器返回的HTML页面。应答头和正文之间也用CRLF分隔。

    二、Socket类

    Socket代表着网络连接的一个端点,应用程序通过该端点向网络发送或从网络读取数据。位于两台不同机器上的应用软件通过网络连接发送和接收字节流,从而实现通信。要把消息发送给另一个应用,首先要知道对方的IP地址以及其通信端点的端口号。在Java中,通信端点由java.net.Socket类表示。

    Socket类有许多构造函数,其中一个构造函数的参数是主机名称和端口号:

    public Socket(String host, int port)



    host是远程机器的名字或IP地址,port是远程应用的端口号。例如,如果要连接到yahoo.com的80端口,我们可以用“new Socket("yahoo.com", 80);”语句构造一个Socket。

    成功创建了Socket类的实例之后,我们就可以用它来发送和接收字节流形式的数据。要发送字节流,首先要调用Socket类的getOutputStream方法获得一个java.io.OutputStream对象;为了向远程应用发送文本数据,我们经常要从返回的OutputStream对象构造一个java.io.PrintWriter对象。要从连接的另一端接收字节流,首先要调用Socket类的getInputStream方法获得一个java.io.InputStream对象。

    例如,下面的代码片断创建一个与本地HTTP服务器(127.0.0.1代表本地主机的IP地址)通信的Socket,发送一个HTTP请求,准备接收服务器的应答。它创建了一个StringBuffer对象来保存应答,然后把应答输出到控制台。

    Socket socket    = new Socket("127.0.0.1", "8080");
    OutputStream os   = socket.getOutputStream();
    boolean autoflush = true;
    PrintWriter out   = new PrintWriter( socket.getOutputStream(), autoflush );
    BufferedReader in = new BufferedReader( 
        new InputStreamReader( socket.getInputStream() ));
    
    // 向Web服务器发送一个HTTP请求
    out.println("GET /index.jsp HTTP/1.1");
    out.println("Host: localhost:8080");
    out.println("Connection: Close");
    out.println();
    
    // 读取服务器的应答
    boolean loop    = true;
    StringBuffer sb = new StringBuffer(8096);
    
    while (loop) {
        if ( in.ready() ) {
            int i=0;
            while (i!=-1) {
                i = in.read();
                sb.append((char) i);
            }
            loop = false;
        }
        Thread.currentThread().sleep(50);
    }
    
    // 把应答显示到控制台
    System.out.println(sb.toString());
    socket.close();



    注意,为了保证Web服务器能够返回正确的应答,客户端发送的HTTP请求应该遵从双方约定的HTTP协议版本。

    三、ServerSocket类

    Socket类代表的是“客户”通信端点,它是一个连接远程服务器应用时临时创建的端点。对于服务器应用,例如HTTP服务器或FTP服务器,我们需要另一种端点,因为我们不知道客户端应用什么时候会试图连接服务器,服务器必须一直处于等待连接的状态。

    因此,对于服务器端的通信端点,我们要使用java.net.ServerSocker类。ServerSocket等待来自客户端的连接请求;一旦接收到请求,ServerSocket创建一个Socket实例来处理与该客户端的通信。

    ServerSocket提供了四个构造函数。创建ServerSocket的实例时,我们必须指定监听客户端消息的IP地址(称为“绑定地址”,Binding Address)和端口。通常情况下,这个IP地址总是127.0.0.1,也就是说服务器端点将在本地机器上监听。服务器端点的另一个重要属性是它的backlog值,这是保存客户端连接请求的最大队列长度,一旦超越这个长度,服务器端点开始拒绝客户端的连接请求。

    下面是ServerSocket类构造函数的其中一种形式:

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



    这个构造函数要求绑定地址必须是一个java.net.InetAddress的实例。要构造一个InetAddress对象,一种简单的办法是调用它的静态getByName方法,传入一个表示主机名称/地址的String。例如,下面的代码构造了一个在本地机器的8080端口监听的ServerSocket,它的backlog值是1:

    new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));



    创建好ServerSocket实例之后,调用它的accept方法,要求它等待传入的连接请求。只有出现了连接请求时,accept方法才会返回,它的返回值是一个Socket类的实例。随后,这个Socket对象就可以用来与客户端应用通信。

    四、Web服务器实例

    本文的Web服务器由三个类构成,分别是:HttpServer,Request ,Response。

    应用的入口点(static main方法)在HttpServer类。main方法创建一个HttpServer实例,然后调用await方法。从await方法的名字也可以看出,它的功能是在指定的端口上等待HTTP请求,然后处理请求,把处理的结果返回给客户端。除非收到了关闭服务器的命令,否则await将一直保持等待客户端请求的状态。(之所以用await而不是wait作为方法名,是因为wait是System.Object类中一个用来操作线程的重要方法)。

    本文的Web服务器只能发送指定目录下的静态资源,例如HTML和图形文件。它不支持头信息(例如日期时间、Cookie等)。

    4.1 HttpServer类

    HttpServer类代表一个Web服务器,提供由WEB_ROOT变量指定的目录及其子目录下的静态资源。WEB_ROOT用下面的语句初始化:

    public static final String WEB_ROOT =
        System.getProperty("user.dir") + File.separator  + "webroot";



    本文最后的下载代码包中有一个webroot目录,它里面有一些静态Web页面,可用来测试本文的服务器。要打开webroot目录下的静态页面,在浏览器的地址栏输入URL:http://machineName:port/staticResource。

    如果运行Web服务器的机器和浏览器所在的机器不同,machineName必须是Web服务器所在机器的IP地址或名称;如果浏览器和Web服务器在同一台机器上运行,machineName也可以是localhost。port是8080,staticResource是要请求的资源(页面)文件名称。

    例如,假设我们在同一台机器上运行Web服务器和浏览器,如果要求HttpServer返回index.html文件,则URL是:

    http://localhost:8080/index.html



    要关闭Web服务器,在浏览器的地址栏输入一个预定义的关闭命令,即在URL的“主机名称:端口”之后,加上SHUTDOWN_COMMAND变量定义的字符。假设SHUTDOWN_COMMAND变量的值是“/SHUTDOWN”,我们可以在浏览器地址栏输入“http://localhost:8080/SHUTDOWN”关闭Web服务器。

    下面我们来看看await方法的代码,代码的说明随后给出。

    【HttpServer类的await方法】
    
    public void await() {
        ServerSocket serverSocket = null;
        int port = 8080;
        try {
            serverSocket =  new ServerSocket(port, 1,
            InetAddress.getByName("127.0.0.1"));
        }
        catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    
        // 循环,等待客户端发来的请求
        while (!shutdown) {
            Socket socket = null;
            InputStream input = null;
            OutputStream output = null;
            try {
                socket = serverSocket.accept();
                input = socket.getInputStream();
                output = socket.getOutputStream();
                // 创建Request对象并予以解析
                Request request = new Request(input);
                request.parse();
                // 创建Response对象
                Response response = new Response(output);
                response.setRequest(request);
                response.sendStaticResource();
                // 关闭Socket
                socket.close();
                // 检查该URI是否为关闭服务器的命令
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            }
            catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }



    await方法首先创建一个ServerSocket实例,然后进入while循环等待来自客户端的请求。while循环里面的代码会在执行ServerSocket的accept方法时等待,直到8080端口收到一个HTTP请求。然后,从accept返回的Socket获得一个java.io.InputStream和一个java.io.OutputStream。接下来,await方法创建一个Request对象,调用parse方法解析原始的HTTP请求。接着,await方法又创建一个Response对象,把前面创建的Request对象传递给它,调用它的sendStaticRessource方法。

    最后,await方法关闭Socket,调用Request方法的getUri方法,查检该HTTP请求的URI是否为一个关闭服务器的命令。如果是,把shutdown变量的值设置为true,while循环结束。

    4.2 Request类

    Request类代表一个HTTP请求。创建Request类的实例时要传入一个从负责与客户端通信的Socket获得的InputStream对象。调用InputStream对象的其中一个read方法可获得HTTP请求的原始数据。

    Request类有两个公用方法parse和getUri。parse方法解析HTTP请求中的原始数据,其实它的功能并不多——它唯一提取的信息是HTTP请求的URI,通过调用私有的parseUri方法获得。parseUri把Uri保存在uri变量中。调用公用的getUri方法可返回HTTP请求的URI。

    要理解parse和parseUri的工作原理,首先要理解HTTP请求的结构,参见本文前面内容以及RFC 2616。如前所述,HTTP请求包含三个部分,现在我们感兴趣的是第一部分,即所谓的“请求行”,包括请求方法、URI和协议版本,最后是一个CRLF字符。请求行里面的各个部分由空格分隔,例如,用GET方法请求index.html文件的请求行是:

    GET /index.html HTTP/1.1



    parse方法读取传递给Request对象的InputStream的整个字节流,把字节数据保存到缓冲区,然后利用buffer字节数组中的内容填写一个称为request的StringBuffer对象,把该StringBuffer的String描述传递给parseUri方法。parse方法的代码如下所示:

    【Request类的parse方法】
    
    public void parse() {
        // 从Socket读取一组数据
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
    
        try {
            i = input.read(buffer);
        }
        catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
    
        for (int j=0; j<i; j++) {
            request.append((char) buffer[j]);
        }
    
        System.out.print(request.toString());
        uri   = parseUri(request.toString());
    }



    parseUri从请求行获得URI,下面给出了parseUri方法的代码。parseUri方法搜索请求中的第一、二两个空格字符,提取出URI。

    【Request类的parseUri方法】
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
    
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
               return requestString.substring(index1 + 1, index2);
        }
        return null;
    }



    4.3 Response类

    Response类代表一个HTTP应答。它的构造函数要求指定一个OutputStream对象,例如:

    public Response(OutputStream output) {
        this.output = output;
    }



    Response类有两个公用方法:setRequest和sendStaticResource。setRequest方法用来把Request对象传递给Response对象,很简单,如下所示:

    【Response类的setRequest方法】
    
    public void setRequest(Request request) {
        this.request = request;
    }



    sendStaticResource方法用来发送静态资源,如HTML文件等。它的实现如下所示:

    【Response类的sendStaticResource方法】
    
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
    
        try {
            File file  = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
    
                while (ch != -1) {
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            }
            else {
                // 找不到文件
                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>";
                output.write(errorMessage.getBytes());
            }
        }
        catch (Exception e) {
            // 如不能实例化File对象,抛出异常。
            System.out.println(e.toString() );
        }
        finally {
            if (fis != null)
                fis.close();
        }
    }



    sendStaticResource方法首先创建一个java.io.File类的实例,在调用File类构造函数时指定了Web服务器的根目录和请求的目标URI。然后,sendStaticResource 检查用户请求的文件是否存在,如存在,它在该File对象的基础上创建一个java.io.FileInputStream对象,然后调用FileInputStream的read方法,把读取的字节数组写入到OutputStream输出。如果用户请求的文件不存在,sendStaticResource方法向浏览器发送一个错误信息。

    五、编译和运行

    下载本文后面提供的zip文件,解开压缩。解开压缩时你指定的目标目录称为“工作目录”。工作目录下有二个子目录:src,webroot。webroot目录下包含一些示例页面。在工作目录下执行下面的命令编译Web服务器:

    javac -d . src/*.java



    “-d .”选项表示把编译结果保存到当前目录(即工作目录),而不是保存到src目录。执行java HttpServer就可以启动Web服务器。

    假设浏览器和Web服务器运行在同一台机器上,打开浏览器,输入URL:http://localhost:8080/index.html。浏览器显示出图一所示的页面。


    结束语:本文通过开发一个简单的JavaWeb服务器,介绍了Web服务器的基本工作原理。虽然本文开发的Web服务器不具备复杂的功能,但它足以作为一个不错的学习工具。


    完整代码如下:

    package http; 

     

    import java.net.Socket; 

    import java.net.ServerSocket; 

    import java.net.InetAddress; 

    import java.io.InputStream; 

    import java.io.OutputStream; 

    import java.io.IOException; 

    import java.io.File; 

     

    publicclass HttpServer { 

     

      /** WEB_ROOT is the directory where our HTML and other files reside.

       *  For this package, WEB_ROOT is the "webroot" directory under the working

       *  directory.

       *  The working directory is the location in the file system

       *  from where the java command was invoked.

       */ 

      publicstatic final String WEB_ROOT = 

        System.getProperty("user.dir") + File.separator  + "webroot"

     

      // shutdown command 

      privatestatic final String SHUTDOWN_COMMAND = "/SHUTDOWN"

     

      // the shutdown command received 

      private boolean shutdown = false

     

      publicstaticvoid main(String[] args) { 

        HttpServer server = new HttpServer(); 

        server.await(); 

      } 

     

      publicvoid await() { 

        ServerSocket serverSocket = null

        int port = 8080; 

        try

          serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); 

        } 

        catch (IOException e) { 

          e.printStackTrace(); 

          System.exit(1); 

        } 

     

        // Loop waiting for a request 

        while (!shutdown) { 

          Socket socket = null

          InputStream input = null

          OutputStream output = null

          try

            socket = serverSocket.accept(); 

            input = socket.getInputStream(); 

            output = socket.getOutputStream(); 

     

            // create Request object and parse 

            Request request = new Request(input); 

            request.parse(); 

     

            // create Response object 

            Response response = new Response(output); 

            response.setRequest(request); 

            response.sendStaticResource(); 

     

            // Close the socket 

            socket.close(); 

     

            //check if the previous URI is a shutdown command 

            shutdown = request.getUri().equals(SHUTDOWN_COMMAND); 

          } 

          catch (Exception e) { 

            e.printStackTrace(); 

            continue

          } 

        } 

      } 

    package http; 

     

    import java.io.InputStream; 

    import java.io.IOException; 

     

    publicclass Request { 

     

      private InputStream input; 

      private String uri; 

     

      public Request(InputStream input) { 

        this.input = input; 

      } 

     

      publicvoid parse() { 

        // Read a set of characters from the socket 

        StringBuffer request = new StringBuffer(2048); 

        int i; 

        byte[] buffer = newbyte[2048]; 

        try

          i = input.read(buffer); 

        } 

        catch (IOException e) { 

          e.printStackTrace(); 

          i = -1; 

        } 

        for (int j=0; j<i; j++) { 

          request.append((char) buffer[j]); 

        } 

        System.out.print(request.toString()); 

        uri = parseUri(request.toString()); 

      } 

     

      private String parseUri(String requestString) { 

        int index1, index2; 

        index1 = requestString.indexOf(' '); 

        if (index1 != -1) { 

          index2 = requestString.indexOf(' ', index1 + 1); 

          if (index2 > index1) 

            return requestString.substring(index1 + 1, index2); 

        } 

        returnnull

      } 

     

      public String getUri() { 

        return uri; 

      } 

     

    package http; 

     

    import java.io.OutputStream; 

    import java.io.IOException; 

    import java.io.FileInputStream; 

    import java.io.File; 

     

    /*

      HTTP Response = Status-Line

        *(( general-header | response-header | entity-header ) CRLF)

        CRLF

        [ message-body ]

        Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

    */ 

     

    publicclass Response { 

     

      privatestatic final int BUFFER_SIZE = 1024; 

      Request request; 

      OutputStream output; 

     

      public Response(OutputStream output) { 

        this.output = output; 

      } 

     

      publicvoid setRequest(Request request) { 

        this.request = request; 

      } 

     

      publicvoid sendStaticResource() throws IOException { 

        byte[] bytes = newbyte[BUFFER_SIZE]; 

        FileInputStream fis = null

        try

          File file = new File(HttpServer.WEB_ROOT, request.getUri()); 

          if (file.exists()) { 

            fis = new FileInputStream(file); 

            int ch = fis.read(bytes, 0, BUFFER_SIZE); 

            while (ch!=-1) { 

              output.write(bytes, 0, ch); 

              ch = fis.read(bytes, 0, BUFFER_SIZE); 

            } 

          } 

          else

            // file not found 

            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>"

            output.write(errorMessage.getBytes()); 

          } 

        } 

        catch (Exception e) { 

          // thrown if cannot instantiate a File object 

          System.out.println(e.toString() ); 

        } 

        finally

          if (fis!=null

            fis.close(); 

        } 

      } 

    }

  • 相关阅读:
    POJ 3041 Asteroids 最小点覆盖 == 二分图的最大匹配
    POJ 3083 Children of the Candy Corn bfs和dfs
    POJ 2049 Finding Nemo bfs 建图很难。。
    POJ 2513 Colored Sticks 字典树、并查集、欧拉通路
    POJ 1013 Counterfeit Dollar 集合上的位运算
    POJ 2965 The Pilots Brothers' refrigerator 位运算枚举
    无聊拿socket写的100以内的加法考试。。。
    POJ 1753 Flip Game
    初学socket,c语言写的简单局域网聊天
    汇编语言 复习 第十一章 标志寄存器
  • 原文地址:https://www.cnblogs.com/javawebsoa/p/3237146.html
Copyright © 2011-2022 走看看