• 实战WEB 服务器(JAVA编写WEB服务器)


     

    实战WEB 服务器(JAVA编写WEB服务器) 

    标签: web服务服务器javawebsockethttp服务器
     分类:

      一、超文本传输协议 

        1.1 HTTP请求 

        1.2 HTTP应答 

      二、Socket类 

      三、ServerSocket类 

      四、Web服务器实例 

        4.1 HttpServer类 

        4.2 Request类 

        4.3 Response类 

      五、编译和运行 



      =================== 

      正文: 

      =================== 

      Web服务器与客户端的通信使用HTTP协议(超文本传输协议),所以也叫做HTTP服务器。用Java构造Web服务器主要用二个类,java.net.Socket和java.net.ServerSocket,来实现HTTP通信。因此,本文首先要讨论的是HTTP协议和这两个类,在此基础上实现一个简单但完整的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服务器不具备复杂的功能,但它足以作为一个不错的学习工具。 


    完整代码如下:

    1. package http;  
    2.   
    3. import java.net.Socket;  
    4. import java.net.ServerSocket;  
    5. import java.net.InetAddress;  
    6. import java.io.InputStream;  
    7. import java.io.OutputStream;  
    8. import java.io.IOException;  
    9. import java.io.File;  
    10.   
    11. public class HttpServer {  
    12.   
    13.   /** WEB_ROOT is the directory where our HTML and other files reside. 
    14.    *  For this package, WEB_ROOT is the "webroot" directory under the working 
    15.    *  directory. 
    16.    *  The working directory is the location in the file system 
    17.    *  from where the java command was invoked. 
    18.    */  
    19.   public static final String WEB_ROOT =  
    20.     System.getProperty("user.dir") + File.separator  + "webroot";  
    21.   
    22.   // shutdown command  
    23.   private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";  
    24.   
    25.   // the shutdown command received  
    26.   private boolean shutdown = false;  
    27.   
    28.   public static void main(String[] args) {  
    29.     HttpServer server = new HttpServer();  
    30.     server.await();  
    31.   }  
    32.   
    33.   public void await() {  
    34.     ServerSocket serverSocket = null;  
    35.     int port = 8080;  
    36.     try {  
    37.       serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));  
    38.     }  
    39.     catch (IOException e) {  
    40.       e.printStackTrace();  
    41.       System.exit(1);  
    42.     }  
    43.   
    44.     // Loop waiting for a request  
    45.     while (!shutdown) {  
    46.       Socket socket = null;  
    47.       InputStream input = null;  
    48.       OutputStream output = null;  
    49.       try {  
    50.         socket = serverSocket.accept();  
    51.         input = socket.getInputStream();  
    52.         output = socket.getOutputStream();  
    53.   
    54.         // create Request object and parse  
    55.         Request request = new Request(input);  
    56.         request.parse();  
    57.   
    58.         // create Response object  
    59.         Response response = new Response(output);  
    60.         response.setRequest(request);  
    61.         response.sendStaticResource();  
    62.   
    63.         // Close the socket  
    64.         socket.close();  
    65.   
    66.         //check if the previous URI is a shutdown command  
    67.         shutdown = request.getUri().equals(SHUTDOWN_COMMAND);  
    68.       }  
    69.       catch (Exception e) {  
    70.         e.printStackTrace();  
    71.         continue;  
    72.       }  
    73.     }  
    74.   }  
    75. }  
    1. package http;  
    2.   
    3. import java.io.InputStream;  
    4. import java.io.IOException;  
    5.   
    6. public class Request {  
    7.   
    8.   private InputStream input;  
    9.   private String uri;  
    10.   
    11.   public Request(InputStream input) {  
    12.     this.input = input;  
    13.   }  
    14.   
    15.   public void parse() {  
    16.     // Read a set of characters from the socket  
    17.     StringBuffer request = new StringBuffer(2048);  
    18.     int i;  
    19.     byte[] buffer = new byte[2048];  
    20.     try {  
    21.       i = input.read(buffer);  
    22.     }  
    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. }  
    1. package http;  
    2.   
    3. import java.io.OutputStream;  
    4. import java.io.IOException;  
    5. import java.io.FileInputStream;  
    6. import java.io.File;  
    7.   
    8. /* 
    9.   HTTP Response = Status-Line 
    10.     *(( general-header | response-header | entity-header ) CRLF) 
    11.     CRLF 
    12.     [ message-body ] 
    13.     Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF 
    14. */  
    15.   
    16. public class Response {  
    17.   
    18.   private static final int BUFFER_SIZE = 1024;  
    19.   Request request;  
    20.   OutputStream output;  
    21.   
    22.   public Response(OutputStream output) {  
    23.     this.output = output;  
    24.   }  
    25.   
    26.   public void setRequest(Request request) {  
    27.     this.request = request;  
    28.   }  
    29.   
    30.   public void sendStaticResource() throws IOException {  
    31.     byte[] bytes = new byte[BUFFER_SIZE];  
    32.     FileInputStream fis = null;  
    33.     try {  
    34.       File file = new File(HttpServer.WEB_ROOT, request.getUri());  
    35.       if (file.exists()) {  
    36.         fis = new FileInputStream(file);  
    37.         int ch = fis.read(bytes, 0, BUFFER_SIZE);  
    38.         while (ch!=-1) {  
    39.           output.write(bytes, 0, ch);  
    40.           ch = fis.read(bytes, 0, BUFFER_SIZE);  
    41.         }  
    42.       }  
    43.       else {  
    44.         // file not found  
    45.         String errorMessage = "HTTP/1.1 404 File Not Found/r/n" +  
    46.           "Content-Type: text/html/r/n" +  
    47.           "Content-Length: 23/r/n" +  
    48.           "/r/n" +  
    49.           "<h1>File Not Found</h1>";  
    50.         output.write(errorMessage.getBytes());  
    51.       }  
    52.     }  
    53.     catch (Exception e) {  
    54.       // thrown if cannot instantiate a File object  
    55.       System.out.println(e.toString() );  
    56.     }  
    57.     finally {  
    58.       if (fis!=null)  
    59.         fis.close();  
    60.     }  
    61.   }  
    62. }  
     
     
  • 相关阅读:
    JavaScript或jQuery模拟点击超链接和按钮
    web开发中目录路径问题的解决
    jQuery操作复选框的简单使用
    php中常用魔术方法的举例
    Code-Validator:验证经度、验证维度
    Code-Validator:验证身份证号
    Code-Validator:验证IPv6地址
    Code-Validator:验证IPv4地址
    Code-Validator:验证网址(可以匹配IPv4地址但没对IPv4地址进行格式验证;IPv6暂时没做匹配)
    Code-Validator:验证电子邮箱
  • 原文地址:https://www.cnblogs.com/qingchen1984/p/5059684.html
Copyright © 2020-2023  润新知