《how tomcat works》第一章 构建一个简单的web服务器

本文翻译自:《how tomcat works》第一章, 个人学习之用,有错误、歧义,敬请告知。


这一章解释一个web服务器是怎么工作的。web服务器也叫http服务器,因为它是通过http协议与客户端连接的,这些客户端通常是浏览器。基于java的web服务器通常有两个重要的类:java.net.socket 和 java.net.ServerSocket,并且通过http协议传输信息。所以我们这一章会从讨论http协议和这两个类开始,然后通过一个小程序来回顾这一章的内容。


http协议(Hypertext Transfer Protocol)

http是一个规定web服务器和浏览器通过网络收发消息的协议。是一个请求、响应的协议——客户端发出请求浏览器进行回应。http使用可靠的TCP连接,默认使用80端口。第一版的Http是http/0.9,不久被http/1.0所替代,后来http/1.1(RFC标准,大多数的网络协议标准都通过RFC定义)替换掉了http/1.0。

注意:本章所讲解的http 1.1 协议旨在帮助理解web服务器所发送的信息,如果你对细节感兴趣,可以阅读RFC2616.

在http中,总是客户端发起一个业务流程。首先客户端和服务器建立连接,然后客户端发出一个(request)请求。服务器无条件的建立连接并给予反馈。无论是客户端还是服务器都可以提前终止一个连接。比如:客户端点击取消按钮 ,或者浏览器断开http连接。


http请求(request)

一个http请求包含三个部分:

  • 方法——资源统一定为符——协议/版本
  • 请求头信息
  • 消息体

下面是一个http请求的例子:

POST  /examples/default.jsp  HTTP/1.1

Accept :  text/plain;text/html
Accept-language :  en-gb
Connection :  Keep-Alive
Host :  localhost
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  

lastName=Franks&firstName=Michael 
  • 方法——资源统一定为符——协议/版本 出现在第一行
POST  /examples/default.jsp  HTTP/1.1

POST 即方法,/examples/default.jsp 即资源统一定位符,HTTP/1.1即协议/版本

每个http请求可以使用http标准所指定的多种方法中的一种,http/1.1支持7种类型的请求:GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE。GET和POST是最常用的。

URI代表一个网络资源,URI通常被理解为指向服务器根目录,因此,它应该总是以”/”开头,URL独立资源定位器时URI的一种【查看说明】。协议版本代表正在使用的http版本。

http请求头包含有关客户端环境的信息和请求的消息体。比如:它可以包含浏览器所设定的语言,实体的长度等等。每一个头信息通过一个回车分离。

请求头信息和消息体通过一个空白行分割,这样有利于信息的格式化。空白行可以告诉服务器从什么地方开始是消息体。在有一些编程书籍上,空白行被当做是http协议的一个组成部分。

上面的http请求的消息体是很简单:

lastName=Franks&firstName=Michael 

但是在http协议中消息体可以很容易变得很长。


http响应(response)

和http请求类似,http响应也包含三个部分:

  • 协议——状态码——描述
  • 响应头信息
  • 消息体

下面是一个http响应的例子:

HTTP 200 OK
Server : Microsoft-IIS/4.0
Date : Mon, 5 Jan 2004 13:13:33 GMT
Content-Type : text/html
Last-Modified : Mon, 5 Jan 2004 13:13:33 GMT 
Content-Length : 112

<html>
<head>
<title>http响应信息例子</title>
</head>
<body>
你好!
</body>
</html>

第一行和http请求的第一行很相似。Http标示使用的协议,200标示请求成功,OK标示无异常。

返回头信息http请求头信息相似,包含一些有用的信息,消息体是一个html,消息头和消息体使用空白行隔开。


Socket类

一个Socket是一个网络连接的一个端点。Socket可以让应用程序可以通过网络进行读和写。在不同地方的两台计算机上的应用程序可以通过发送和接受字节流的形式进行交流。当你通过应用程序给另一个台计算机上的应用程序发送信息时,需要知道对方的ip地址和程序所使用的端口号(port),在java中,java.net.Socket类实现了Socket。

你可以通过java.net.Socket类的很多构造方法创建socket。比如其一:

public Socket(java.lang.String host , int port)

host是远程主机的名称或者IP地址,port是应用程序所使用的端口号。比如连接yahoo.com的80端口,可以这么写:

new Socket("yahoo.com",80)

一但Socket创建成功,你就可以用它来发送和接受字节流信息了。要发送字节流,首先你要调用Socket类的getOutputStream 方法获得一个 java.io.OutputStream对象。想要发送一段文本给远程应用,你通常想要通过OutputStream构造一个 java.io.PrintWriter对象。想要从连接的另一端收到字节流,需要调用Socket类的getInputStream方法获得一个java.io.InputStream对象。

下面的一小段代码创建了一个和本地http服务器连接的Socket。发送了一个http请求,并且接受了来自服务器的响应信息。这里创建了一个StringBuffer接受响应信息并将其答应到控制台。

Socket socket = new Socket("127.0.0.1",8080);
//发送工具
PrintWriter out = new PrintWriter(
    socket.getOutputStream(),
    autoflush
);
//接收工具
BufferedReader in = new BufferedReader(
    new InputStreamReader(
        socket.getInputStream()
    )
);

//发送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();

需要注意的是,想要正确的得到http服务器的响应,发送的请求必须要复核http规范。如果你读了上面关于http的小结,你应该可以理解上面的代码。

提示:你可以使用本书所带的 com.brainysoftware.pyrmont.util.HttpSniffer来发送请求和接受并打印响应。注意,使用这个程序你必须要连接网络,如果你有防火墙,可能也会导致失败。


ServletSocket类

Socket类代表客户端Socket,也就是说,当你想要连接远程服务器的时候,你就需要实例化一个Socket。现在如果你想实现一个服务器程序,比如http服务器或者ftp服务器,你需要一个不一样的方法,因为你的服务器必须无时无刻不保持接受状态,因为它不知道什么时候会有客户端请求连接。为了让你的服务器无时无刻处在等待连接状态,你需要使用java.net.ServetSocket类。这是一个服务器Socket的实现。

ServerSocket不同于Socket,Server Socket会一直等待客户端连接,一旦收到一条连接请求,它就会创建一个Socket示例来处理这个客户端连接。

要创建一个Socket,你可以使用ServerSocket提供的4个构造函数。你需要指定服务器监听的IP地址和端口号,很典型的,IP地址是1227.0.0.1,这就表示监听本机。Server Socket的监听地址取决于其绑定的IP地址。另外一个重要属性是,Server Socket会有积压,当达到它的最大连接数时将拒绝连接。

下面是ServerSocket的一个构造函数:

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

需要注意的是,bindAdress比如是java.net.InetAdress的一个实例,一个很简单的获得实例的方法是调用它的静态方法getByName(String host)方法。

InetAdress.getByName("127.0.0.1");

下面的代码构建了一个监听本机8080端口,允许一个积压的的ServerSocket实例:

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

一旦你得到一个ServerSocket实例,你就可以让它监听它所绑定的地址,端口所收到的连接。你可以通过调用ServerSocket的accept方法来启动监听。这个方法只有在它收到一个请求的时候才会返回,并且返回的是一个Socket实例。这个Socket实例可以被用来发送给和接受客户端数据,Socket的具体解释请看上一节。差不多,accept方法是唯一一个用在下面例子程序的方法。


例子程序

我们的服务器程序在ex01.pyrmont包里,由下面的三个Class文件组成:

  • HttpServer
  • Request
  • Response

这个程序的入口在HttpServer中,main方法创建了一个HttpServer实例,并且调用了它的await方法,await方法顾名思义,即在特定端口等待http连接,处理,然后发送响应给客户端。它会一直保持活跃,直到收到终止命令。

这个程序除了能发送预先准备好的数据,比如在指定文件夹里面的HTML文件,images文件。还可以在控制台显示收到的http请求字节流。但是,它不会发送任何头信息,比如:日期或者cookies给浏览器。


HttpServer class

HttpServer代表一个web服务器,如清单1.1.注意,await方法在清单1.2中,为了节约空间,省略了清单1.1中的内容。

清单1.1(省略了import部分)

package ex01.pyrmont

public HttpServer{

    /**
    *WEB_ROOT 是用来存放html和其他文件的地方,对于这个包来说,
    *WEB_ROOT是工作目录下的webroot文件夹
    *工作目录是调用java方法时所在的文件夹。
    */
    public static final String WEB_ROOT = 
        "System.getProperty("user.dir")" + File.separator + "webroot";

    //shutdown 命令
    private boolean shutdown = false;

    public static void main(String[] args){
        HttpServer server = new HttpServer();
        server.await();
    }

    public void await(){
        //内容见清单1.2
    }

}

清单1.2 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.printStackTrance();
        System.exit(-1);
    }
    //循环等待请求
    while(!shutdown){
        try{
            Socket socket = serverSocket.accept();
            InputStream input = socket.getInputStream();
            OutputStream out = socket.getOutputStream();

            //创建一个request对象并解析
            Request request = new Request(input);
            request.parse();

            //创建一个response对象
            Response response = new Response(out);
            response.setRequest(request);
            response.sendStaticResource();

            //关闭socket
            socket.close();
            //检查上一个uri是不是shutdown命令
            shutdown = request.getUri().equals("SHUTDOWN_COMMAND");
        }catch(Exception e){
            e.printStackTrance();
            continue;
        }
    }
}

web服务器可以返回WEB_ROOT及子目录下的静态资源,WEB_ROOT通过下面的代码初始化:

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

源文件里面包含一个叫webroot的文件夹,文件夹里面有可以用来测试程序的静态资源。你在这个文件夹还可以看到几个Servlet资源,这些事下一章节用来测试的资源。

想要请求这些静态资源,可以在浏览器中输入下面的url:

http://ipAddress:port/staticResource

想要关闭服务器,使用下面的URL:

http://ipAddress:port/SHUTDOWN_COMMAND

现在来看清单1.2

使用await的原因是因为wait方法是java.lang.Object对象的一个有关线程的很重要的方法。
首先构建一个ServerSocket实例,然后进入while方法:

while中的代码,代码在serverSocket.accept()方法出阻塞,等待8080端口请求的到来。在收到一个request请求的时候,accept()方法返回一个socket,然后通过socket获得通向客户端的输入流InputStream,输出流OutputStream.

input = socket.getInputStream();
out = socket.getOutStream();

然后,await方法创建一个ex01.pyrmont.Request对象,然后调用它的parse()方法解析http请求数据。

Request request = new Request(input);
request.parse();

再然后,await创建一个ex01.pyrmont.Response对象,然后将request对象赋值给它的属性,并且调用sendStaticResource()方法。

Response response = new Response(out);
response.setRequest(request);
response.sendStaticResource();

最后,await方法关闭socket并且调用request的getUri()方法,并且检查Uri是不是 SHUTDOWN_COMMAND,如果是,shutdown被设置为true,然后在再次循环时程序退出。

//关闭socket
socket.close();
//检查上一个uri是不是shutdown命令
shutdown = request.getUri().equals("SHUTDOWN_COMMAND");

Request class

ex01.pyrmont.Request class文件表示一个Http请求,通过accept()方法返回的socket对象获得InputStream对象,来构造一个Request对象。你可以通过调用InputStream的read()方法来读取http请求中的数据。

清单1.3展示的是Request文件,清单1.4和清单1.5分别展示了Request的两个公用方法parse()和parseUri();

清单1.3(省略了import语句)

package ex01.pyrmont

public class Request{
    private InputStream input;
    private String uri;

    public Request(InputStream input){
        this.input = input;
    }

    public void parse(){
        //详情见清单1.4
    }

    private String parseUri(String requestString){
        //详情见清单1.5
    }

    public String getUri(){
        return uri;
    }

}

清单1.4 parse方法

public void parse(){
    StringBuffer request = new StringBuffer(2048);
    int i;
    byte[] buff = new byte[2048];
    try{
        i = input.read(buff);
    }catch(IOException e){
        e.printStackTrance();
        i = -1;
    }
    for(int j=0;j<i;j++){
        requset.append((char)buff[j]);
    }
    System.out.println(request.toString());
    parseUri(request.toString());
}

清单1.5 parseUri()方法:

private String parseUri(String requestString){
    int index1,index2;
    index1 = requestString.indexOf(" ");
    if(index1!=-1){
        index2 = requestString.indexOf(" ",index1+1);
    }
    if(index2!=-1){
        return requestString.substring(index1+1,index2);
    }
    return null;
}

parse()方法解析http请求中的信息为String,然后调用parseUri()方法得到Uri。

注意:
1.这里只关注http请求的第一行。可以参看本章给出的http请求结构。
2.IE浏览器的Uri最大长度为2038位,所以这里设置byte为2048足够。
3.英文字母在uri编码中为1个byte,但在java中标示一个英文字母用16位,也就是2byte = char.


Response class

ex01.pyrmont.Response class文件标示一个http响应。如清单1.6

清单1.6(省略import)

package ex01.pyrmont

/** Http Response 数据结构
*  协议 状态码 描述   例如:(HTTP 200 OK)
* *(( general-header | response-header | entity-header ) CRLF) 
* CRLF 
* [ message-body ]
* /

public Response(){
    private static final int BUFFER_SIZE = 1024;
    Request request ;
    OutputStream output;

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

    public void setRequest(Request request){
        this.request = request;
    }

    public void sendStaticResource(){
        byte[] buff = 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 i = fis.read(buff,0,BUFFER_SIZE);
                String header = "HTTP/1.1 200 OK\r\n" +
                        "Content-Type : text/html\r\n\r\n";
                output.write(header.getBytes());
                while(i!=-1){
                    output.write(buff,0,i);
                    i = fis.read(buff,0,BUFFER_SIZE);
                }
            }else{
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n"+
                        "Content-Type:text/html\r\n\r\n"+
                        "<h1>File Not Found</h1>";
                output.write(errorMessage.getBytes());
            }
        }catch(IOException e){
            e.printStackTrance();
        }finally{
            if(fis!=null){
                fis.close();
            }
        }
    }
}

首先,构造函数通过接受一个OutputStream对象实例化一个Response对象。
这个OutputStream对象来自accept()所返回的socket,这也是服务器端向客户端发送数据的通道,然后通过HttpServer.WEB_ROOT即根目录 + request.getUri()即资源的相对根目录的相对路径,得到请求资源的File对象,然后判定文件是否存在,如果存在,则先输出文件头,告诉浏览器所采用的通行协议、通行状态、状态描述(HTTP/1.1 200 OK),返回文件的类型(Content-Type : text/html),最后输出文件内容。如果不存在,则返回File Not Found.

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页