简单的静态 HTTP 服务器

我们经常会使用 HTTP 服务。也许你没听过 HTTP,但是你一定浏览过网页,而浏览器与服务器之间就是使用 HTTP 协议来通信的。这篇笔记记录了实现一个简单的静态 HTTP 服务器的过程。

HTTP

HTTP(Hyper Text Transfer Protocol) 是一种用于 Web 服务器和客户端之前交换数据的传送协议,它使用可靠的 TCP/IP 通信协议来传递数据。

一般情况下,总是由客户端发送 HTTP 请求,Web 服务器处理请求之后返回一个 HTTP 响应。

HTTP 请求

一个 HTTP 请求包含以下三个部分:

  • 请求方法 统一资源标识符 协议/版本
  • 请求头
  • 请求实体

例如:

GET / HTTP/1.1
Host: 127.0.0.1:8000
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html
Accept-Encoding: gzip, deflate, br

username=xuewen&password=pwd123

请求行

请求的第一行包含了三个参数,分别表示请求方法、统一资源标识符、协议/版本:

GET / HTTP/1.1

其中请求方法为 GET,统一资源标识符为 /,协议/版本为 HTTP/1.1

每个 HTTP 请求都可以使用 HTTP 标准指定的请求方法之一,HTTP 1.1 支持的7中请求协议包括:

  • GET
  • POST
  • HEAD
  • PUT
  • DELETE
  • OPTION
  • TRACE

请求资源标识符 (URI) 是 Internet 资源的完整路径。URI 通常是相对于服务器根目录的路径,通常以 / 开始。

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

请求头

请求头指定了 HTTP 请求的一些参数,如:

Host: 127.0.0.1:8000
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html
Accept-Encoding: gzip, deflate, br

Host 指出了本次请求的 Web 服务器的主机名

Cache-Control 用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。

User-Agent 包含了一个特征字符串,用来让 Web 服务器识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。

Accept 用来告知(服务器)客户端可以处理的内容类型。

Accept-Encoding 用来说明客户端接能处理的内容编码方式,一般是压缩算法。

请求实体

在请求头的后面是一行空行,这个空行用来分隔请求头和请求体,即在空行的后面是本次请求的内容。

username=xuewen&password=pwd123

HTTP 响应

HTTP 响应与 HTTP 请求类似,也有三个组成部分:

  • 协议/版本 状态码描述
  • 响应头
  • 响应体

例如:

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.6.8
Date: Wed, 23 Oct 2019 12:42:06 GMT
Content-type: text/html
Content-Length: 6
Last-Modified: Wed, 23 Oct 2019 12:42:00 GMT

hello

响应的第一行与请求类似,指明了 HTTP 协议的版本,响应的状态码 (200 表示请求成功)。

接下来的是响应头,与请求头类似,包含了许多参数。
可以点击查看HTTP 头部信息

发送 HTTP 请求

一般情况下我们都是使用浏览器来浏览网页,浏览器会帮助我们完成发送 HTTP 请求和解析服务器返回的数据。

我们也可以尝试自己发送 HTTP 请求,这样可以更好的理解 HTTP 协议。

在发送请求时,可以使用 Java 中的 Socket 来连接服务器。

下面给出完整的代码:

折叠/展开代码
import java.io.*;
import java.net.Socket;

public class SendRequest {
    public static void main(String[] args) {
        try {
            // 创建 Socket 对象
            Socket socket = new Socket("127.0.0.1", 8080);
            PrintWriter printWriter = new PrintWriter(
                    socket.getOutputStream(), true
            );

            // 向服务器发送 HTTP 请求,请求的格式遵守 HTTP 协议
            printWriter.println("GET / HTTP/1.1");
            printWriter.println("Host: 127.0.0.1");
            printWriter.println("Connection: close");
            // 按照 HTTP 协议,此处空一行
            printWriter.println();
            printWriter.flush();

            // 读取服务器返回的数据
            BufferedReader bufferedReader = new BufferedReader(
                    new InputStreamReader(
                            socket.getInputStream()
                    )
            );
            StringBuilder stringBuilder = new StringBuilder(8096);

            boolean loop = true;
            while (loop) {
                if (bufferedReader.ready()) {
                    int i = 0;
                    while (i != -1) {
                        i = bufferedReader.read();
                        stringBuilder.append((char) i);
                    }
                    loop = false;
                }
                Thread.sleep(10);
            }

            // 打印服务器返回的数据
            System.out.println(stringBuilder.toString());
            socket.close();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

为了运行上面的程序,可以使用 Python 来开启一个 HTTP 服务器:

# 创建一个 HTML 文件
echo 'The Server received your request!' > index.html
# 开启服务器
python3 -m http.server 8000

此时运行程序,可以在控制台得到以下输出,可以看到服务器返回的数据符合 HTTP 协议:

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.6.8
Date: Tue, 29 Oct 2019 11:39:59 GMT
Content-type: text/html
Content-Length: 34
Last-Modified: Tue, 29 Oct 2019 11:38:52 GMT

The Server received your request!

创建 HTTP 服务器

这个简单的 HTTP 服务器将由三个类组成:

  • HttpServer
  • Request
  • Response

HttpServer 类

HttpServer 表示一个 HTTP 服务器,具体代码如下:

折叠/展开代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpServer {
    private static int port;
    public static boolean shutdown = false;

    private static String webRoot = "/servlet-learn/web-root";
    private static String response404 = "404.html";
    public static String shutdownCommand = "/shutdown";

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

    public HttpServer(int port) {
        this.port = port;
    }

    public void await() {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(this.port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        System.out.println("服务器已建立, http://localhost:" + port);
        // 等待用户连接
        while (!shutdown) {
            Socket socket;
            InputStream inputStream;
            OutputStream outputStream;

            try {
                // 创建用户连接
                socket = serverSocket.accept();
                inputStream = socket.getInputStream();
                outputStream = socket.getOutputStream();

                System.out.println("--------------------------------");
                // 创建并解析请求对象
                Request request = new Request(inputStream);
                request.parse();

                // 创建响应对象
                Response response = new Response(outputStream);
                response.setRequest(request);
                response.sendStaticResource();

                inputStream.close();
                outputStream.flush();
                outputStream.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static String getWebRoot() {
        return webRoot;
    }

    public static String getResponse404() {
        return response404;
    }
}

这个 Web 服务器可以处理客户端对指定目录中静态资源的请求,默认资源目录为:

private static String webRoot = "/servlet-learn/web-root";

例如用户访问 http://localhost:8080/hello.html 时,程序会在 webRoot 目录下寻找 hello.html 文件并将文件返回给客户端。

若要关闭服务器,可以访问 http://localhost:8080/shutdown,该命令在程序中定义:

public static String shutdownCommand = "/shutdown";

当服务器建立后会调用 await() 方法监听用户请求。

httpServer.await();

await() 方法中,调用了 socket.accept() 来等待用户连接,并根据 socket 对象来获取用户的输入输出流。

socket = serverSocket.accept();
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();

然后在根据输入输出流来创建 requestresponse 对象。

// 创建并解析请求对象
Request request = new Request(inputStream);
request.parse();

// 创建响应对象
Response response = new Response(outputStream);
response.setRequest(request);
// 发送请求的文件资源
response.sendStaticResource();

最后服务器关闭连接:

inputStream.close();
outputStream.flush();
outputStream.close();
socket.close();

Request 类

Request 类用于处理用户的请求,主要代码如下:

折叠/展开代码
import java.io.*;

public class Request {
    private InputStream inputStream;
    private String uri;
    private String method;
    private String version;

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

    public void parse() throws IOException {
        parseHeader();
    }

    public void parseHeader() throws IOException {
        // 代码省略,见下文
        // 代码省略,见下文
        // 代码省略,见下文
    }

    public String getUri() {
        return uri;
    }

    public String getMethod() {
        return method;
    }

    public String getVersion() {
        return version;
    }
}

Request 类中保存了用户的输入流。在 parse() 方法中解析输入流来构建完整的 request 对象。

public void parse() throws IOException {
    parseHeader();
}

public void parseHeader() throws IOException {
    BufferedReader bufferedReader = new BufferedReader(
        new InputStreamReader(
            inputStream
        )
    );

    String line = bufferedReader.readLine();
    String[] ags = line.split(" ");
    this.method = ags[0];
    this.uri = ags[1];
    this.version = ags[2];
    System.out.println(String.format("请求头:%s, %s, %s", method, uri, version));
}

Response 类

Response 类用于处理给用户的响应。主要的代码为根据 Request 对象来查找文件并写入到 Socket 的输出流中去,即发送给用户。

折叠/展开代码
public void sendStaticResource() {
        int BUFFER_SIZE = 1024 * 4;
        String uri = this.request.getUri();
        String webRoot = HttpServer.getWebRoot();

        if (uri.equals("/")) {
            uri = "/index.html";
        } else if (HttpServer.shutdownCommand.equals(uri)) {
            System.out.println("收到关闭指令,正在关闭服务器!");
            HttpServer.shutdown = true;
            uri = HttpServer.shutdownCommand + ".html";
        }

        File file = new File(webRoot, uri);
        if (!file.exists()) {
            file = new File(HttpServer.getWebRoot(), HttpServer.getResponse404());
        }

        FileInputStream fileInputStream;
        try {
            fileInputStream = new FileInputStream(file);
            byte[] buffer = new byte[BUFFER_SIZE];
            int count = fileInputStream.read(buffer, 0, BUFFER_SIZE);
            while (count != -1) {
                this.outputStream.write(buffer, 0, count);
                count = fileInputStream.read(buffer, 0, BUFFER_SIZE);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在处理 Request 的时候,可以根据 URI 进行判断:

  • 如果 URI 为 /,则返回 index.html 文件
  • 如果 URI 为 /shutdown,则关闭服务器,并返回 shutdown.html 文件

测试客户端和服务器

在终端运行 HttpServer,使用 SendRequest 进行请求,可以得到以下结果:

服务端:

服务器已建立, http://localhost:8080
--------------------------------
请求头:GET, /, HTTP/1.1

客户端:

HTTP/1.1 200 OK
Connection: close

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <body>
    <p>Index File.</p>
  </body>
</html>

总结

在这个实验中学到了一个简单的 Web 服务器是如何工作的。虽然功能很简单,但是有助于理解客户端和服务器之间使用 HTTP 通信的细节。