Chapter1 一个简单的Web服务器

Chapter1 一个简单的Web服务器

1.1 HTTP

HTTP
RFC 2616 - Hypertext Transfer Protocol — HTTP/1.1
版本: HTTP/1.1
TCP 连接
基于:“请求—响应”的协议

1.1.1 HTTP 请求

一个 HTTP 请求包含以下三部分
* 请求方法 —— URI(Uniform Resource Identifier, 统一资源标识符)
* 请求头
* 实体

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
Chapter1 一个简单的Web服务器_第1张图片
http .png

HTTP 支持的七种请求方法
1. GET
2. POST
3. HEAD
4. OPTIONS
5. PUT
6. DELET
7. TRACE

URI(Uniform Resource Identifier, 统一资源标识符) 制定 Internet 资源的完整路径。 URI 通常会被解释为相对于服务器根目录的相对路径,因此,它总是以 “/” 开头的。
URL(Uniform Resource Locator, 统一资源定位符) 实际上是 URI 的一种类型。

版本协议指明了当前请求使用的 HTTP 协议的版本。

1.1.2 HTTP 响应

一个 HTTP 响应包括三部分

  • 协议——状态码——描述
  • 响应头
  • 相应实体段
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Data: Mon, 5 Jan 2004 13:13:33 GMT
Content-Length: 112


    
        HTTP Response Example
    
    
        Welcome to Brainy Software
    

Chapter1 一个简单的Web服务器_第2张图片
http-respond.jpg

状态码 200 表示请求成功。
响应实体正文是一段 HTML 代码

1.2 Socket 类

Socket (套接字) 是网络连接的端点。Socket 使应用程序可以从网络中读取数据,可以向网络中写入数据。不同计算机上的两个应用程序可以通过连接发送或接收字节流,以此达到相互通信的目的。为了从一个应用程序向另一个应用程序发送消息,需要知道另一个应用程序中 Socket 的 IP 地址和端口号。

public class Socket
extends Object
implements Closeable
This class implements client sockets (also called just "sockets"). A socket is an endpoint for communication between two machines.
The actual work of the socket is performed by an instance of the SocketImpl class. An application, by changing the socket factory that creates the socket implementation, can configure itself to create sockets appropriate to the local firewall.

创建一个 Socket :

Socket() Creates an unconnected socket, with the system-default type of SocketImpl.
Socket(String host, int port) Creates a stream socket and connects it to the specified port number on the named host.
Socket(String host, int port, boolean stream) Deprecated. Use DatagramSocket instead for UDP transport.

host - the host name, or null for the loopback address.

port - the port number.

host:远程主机的名称或 IP 地址 (127.0.0.1 表示一个本地主机)
post:连接远程应用程序的端口号

使用 socket 实例发送/接收字节流:
socket.getOutputStream()

发送文本
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)

接收字节流:
socket.getInputStream()

Example: 创建一个Socket,用于与本地 HTTP 服务器进行通信


Socket socket = new Socket(host, portNumber);
OutputStream os = socket.getOutputStream();
boolean autoflush = true;
PrintWriter out = new PrintWriter(socket.getOutputStream(), autoflush);
String message = command.getText();
out.println(message);
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();
BufferedReader in = new BufferedReader(
        new InputStreamReader(socket.getInputStream()));
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);
}
    response.setText(sb.toString());
    socket.close();

ServerSocket 类

Socket 类:表示一个客户端套接字。
ServerSocket 类:服务器套接字。

public class ServerSocket
extends Object
implements Closeable
This class implements server sockets. A server socket waits for requests to come in over the network. It performs some operation based on that request, and then possibly returns a result to the requester.
The actual work of the server socket is performed by an instance of the SocketImpl class. An application can change the socket factory that creates the socket implementation to configure itself to create sockets appropriate to the local firewall.

服务器套接字要等待来自客户端的连接请求。当服务器套接字接收到了连接请求之后,它会创建一个 Socket 实例来处理与客户端的通信。

ServerSocket 的构造函数

Constructor Description
ServerSocket() Creates an unbound server socket.
ServerSocket(int port) Creates a server socket, bound to the specified port.
ServerSocket(int port, int backlog) Creates a server socket and binds it to the specified local port number, with the specified backlog.
ServerSocket(int port, int backlog,InetAddress bindAddr) Create a server with the specified port, listen backlog, and local IP address to bind to.

port - the port number, or 0 to use a port number that is automatically allocated.

backlog - requested maximum length of the queue of incoming connections.

bindAddr - the local InetAddress the server will bind to

backlog:服务端socket处理客户端socket连接是需要一定时间的。ServerSocket有一个队列,存放还没有来得及处理的客户端Socket,这个队列的容量就是backlog的含义。如果队列已经被客户端socket占满了,如果还有新的连接过来,那么ServerSocket会拒绝新的连接。也就是说backlog提供了容量限制功能,避免太多的客户端socket占用太多服务器资源。

bindAddr:必须是 java.net.InetAddress. 类的实例

创建 InetAddress 实例对象的一种简单方法是调用其静态方法 getByName(),传入包含主机名的字符串:

InetAddress.getByName("127.0.0.1");

创建了 ServerSocket 实例后,可以使其等待传入的连接请求,该请求会通过服务器套接字侦听的端口上的绑定地址传入,这些工作可以通过调用 ServerSocket 类的 accept 方法完成。只有收到连接请求后,该方法才会返回。

public Socket accept() throws IOException

A new Socket s is created and, if there is a security manager, the security manager's checkAccept method is called with s.getInetAddress().getHostAddress() and s.getPort() as its arguments to ensure the operation is allowed. This could result in a SecurityException.

Returns: the new Socket.

1.3 应用程序

本章的 Web 服务器包括三个类:

  • HttpServer
  • Request
  • Response
  1. 程序的入口点(静态 main()方法)在 HttpServer 类中。main()方法创建一个 HttpServer 实例,然后调用其 await() 方法。
  2. await()方法会在指定端口上等待 HTTP 请求,对其处理,然后发送响应信息回客户端。在接收到关闭命令前,它会保持等待状态。

该应用程序仅发送位于指定目录的静态资源请求,如 HTML 文件和图像文件,它将传入到的 HTTP 请求字节流现实到控制台上。但是它不发送任何 header (头信息) 到浏览器,如日期或 cookies 等。

1.3.1 HttpServer

package ch1;
import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;


// 处理对指定目录中静态资源的请求
public class HttpServer {

    // 指明目录
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

    //目录名为 'webroot'


    // shutdown command
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    // 关闭命令——/SHUTDOWN

    // the shutdown command received
    private boolean shutdown = false;

    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.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);
        }

        //Loop waiting for a request
        while (!shutdown) {
            Socket socket = null;
            InputStream input = null;
            OutputStream output = null;

            try {
                // accept()
                // Listens for a connection to be made to this socket
                // and accepts it. The method blocks until a connection is made.
                socket = serverSocket.accept();

                // getInputStream()
                // an input stream for reading bytes from this socket.
                input = socket.getInputStream();

                // getOutputStream()
                // an output stream for writing bytes to this socket.
                output = socket.getOutputStream();

                // creat Request object and parse
                Request request = new Request(input);
                request.parse();

                // creat Response object
                Response response = new Response(output);
                response.setRequest(request);
                response.sendStaticResource();

                // close the socket
                socket.close();

                // check if the prevous URI is a shutdown command
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
                // 判断一个输入元素与是否与已知元素相同

            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
}

这个 Web 服务器可以处理对指定目录中的静态资源的请求。该目录包括公有静态变量 final WEB_ROOT 指明的目录及其所有子目录。WEB_ROOT 的初始值为:

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

若要请求静态资源,可以在浏览器的地址栏或 URL 框中输入如下的 URL

http://machineName:port/staticResource

http://localhost:8080/source

若从另一台计算机上向该应用程序发出请求,则 machineName 是应用程序所在计算机的名称或 IP 地址;若在同一台机器上发出的请求,则可以将 machineName 替换为 localhost

此连接请求使用的端口为 8080。

staticResourse 是请求的文件的名字,该文件必须位于 WEB_ROOT 指向的目录下。

若要关闭服务器,可以通过 Web 浏览器的地址栏或 URI 框,在 URI 的 host:port 部分后面输入预先定义好的字符串,从 WEB 浏览器发送一条关闭命令,这样服务器就会收到关闭命令了。

例中的关闭命令定义在 HttpServer 类的 SHUTDOWN 静态 final 变量中:

private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

若要关闭服务器,输入

http://localhost:8080/SHUTDOWN

await() 方法而不是 wait() 方法,是因为 wait() 方法是 java.lang.Object 类中与使用线程相关的重要方法。

  1. await() 方法先创建一个 ServerSocket 实例,然后进入一个 while 循环,从 8080 端口接收到 Http 请求后, await() 方法会从 accept() 方法返回的 Socket 实例中,获取 java.io.InputStreamjava.io.OutputStream 对象:

    input = socket.getInputStream();
    output = socket.getOutputStream();
    
  2. 之后,await() 方法创建一个 Request 对象,并调用其 paser() 方法来解析 HTTP 请求的原始数据:

//create Request object and parse
Request request = new Request(input);
request.parse(); 
  1. 然后,await() 方法会创建一个 Response 对象,并分别调用其 setRequest() 方法和 sendStaticResource() 方法:

    // creat Response object
    Response response = new Response(output);
    response.setRequest(request);
    response.sendStaticResource();
    
  2. 最后,await() 方法关闭套接字,调用 Request 类的 getUri() 方法来测试 HTTP 请求的 URI 是否是关闭命令,若是,则将变量 shutdown 设置为 true,程序退出 while 循环。

    // close the socket
    socket.close();
    
    // check if the prevous URI is a shutdown command
    shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
    

1.3.2 Request

Request 类表示一个 HTTP 请求,可以传递 InputStream 对象(从通过处理与客户端通信的 Socket 对象中获取的),来创建 Request 对象。可以调用 InputStream 对象中的 read() 方法来读取 HTTP 请求的原始数据。

package ch1;

import java.io.IOException;
import java.io.InputStream;

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

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

    // 解析 HTTP 请求的原始数据
    public void parse() {
        // read a set of characters from the 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());
    }

    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;
    }

    public String getUri() {
        return uri;
    }
    
}

Request

parse():解析 HTTP 请求中的原始数据。通过调用 private 方法 parseUri() 来解析 HTTP 请求的 URI

parseUri(String requestString):将 URI 存储在变量 uri 中。

getUri():返回 HTTP 请求的 URI

  1. parse() 方法从传入到 Request 对象中的套接字的 InputStream 对象中读取整个字节流,并将字节数据存储在缓冲区中。然后使用缓冲区字节数组中的数组填充 StringBuffer 对象 request ,并将 StringBufferString表示 传递给 parseUri() 方法。

    public void parse() {
            // read a set of characters from the 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());
        }
    
  2. parseURI() 方法从请求行中获取 URI

    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;
        }
    

1.3.3 Response

Response 类表示 HTTP 响应。

package ch1;

import java.io.*;

public class Response {
    private static final int BUFFER_SIZE = 2048;
    Request request;
    OutputStream output;
    
    public Response(OutputStream output) {
        this.output = output;
    }

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

    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);

                String msg = "HTTP/1.1 404 File Not Found\r\n" +
                        "Content-Type: text/html\r\n" +
                        "\r\n";
                output.write(msg.getBytes());

                while (ch != -1){
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            }
            else {
                // find 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" +
                        "

File Not Found

"; 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(); } } } }
  1. Response 类的构造函数接受一个 java.io.OutputStream 对象,:

        public Response(OutputStream output) {
            this.output = output;
        }
    
  2. Response 对象在 HttpServer 类的 await() 方法中,通过传入从套接字中获取的 OutputStream 来创建。

  3. Response 内有两个 public 方法:setRequest()sendStaticResourse()setRequest() 方法会接受一个 Request 对象作为参数。

  4. sendStaticResource() 方法用于发丝路过一个静态资源到浏览器,如 HTML 文件。

    1. 它首先会通过传入父路径和子路径到 File 类的构造函数中来实例化 java.io.File 类:

      File file = new File(HttpServer.WEB_ROOT, request.getUri());
      
    2. 然后,它检查该文件是否存在。

    3. 若存在,sedStaticResource() 方法会使用 File 对象创建 java.io.InputStream 。然后它调用 FileInputStream 类的 read() 方法,并将字节数据写入到 OutputStream 输出中。(这种情况下,静态资源的内容作为原始数据发送到浏览器的):

      if (file.exists()){
          fis = new FileInputStream(file);
          int ch = fis.read(bytes, 0, BUFFER_SIZE);
          
          // mac 防止出错
          String msg = "HTTP/1.1 404 File Not Found\r\n" +
                  "Content-Type: text/html\r\n" +
                  "\r\n";
          output.write(msg.getBytes());
          
          while (ch != -1){
              output.write(bytes, 0, ch);
              ch = fis.read(bytes, 0, BUFFER_SIZE);
          }
      }
      
    4. 若不存在,staticResource() 会发送错误信息到浏览器:

       else {
          // find 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" +
                  "

      File Not Found

      "; output.write(errorMessage.getBytes()); }

1.3.5 运行应用程序

  1. 执行 ch1 包

  2. 在地址栏或 URL 框输入:

    http://localhost:8080/source.html

Result

GET /source.html HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Cookie: __utma=111872281.722673633.1521287843.1521287843.1521287843.1; __utmz=111872281.1521287843.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); Idea-7e6930cd=8b83b26b-bd8b-480c-832b-3c0b6e5d4c39
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Connection: keep-alive

GET /favicon.ico HTTP/1.1
Host: localhost:8080
Accept: */*
Connection: keep-alive
Cookie: __utma=111872281.722673633.1521287843.1521287843.1521287843.1; __utmz=111872281.1521287843.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); Idea-7e6930cd=8b83b26b-bd8b-480c-832b-3c0b6e5d4c39
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6
Accept-Language: zh-cn
Referer: http://localhost:8080/source.html
Accept-Encoding: gzip, deflate
Chapter1 一个简单的Web服务器_第3张图片
ch1_result.png

你可能感兴趣的:(Chapter1 一个简单的Web服务器)