Skip to content

JEP 408: Simple Web Server | 简单 Web 服务器

摘要

提供一个命令行工具,用于启动一个仅提供静态文件服务的最小化 Web 服务器。该服务器不支持 CGI 或类似 Servlet 的功能。此工具对于原型设计、即席编码和测试目的特别有用,特别是在教育环境中。

目标

  • 提供一个易于设置且功能最少化的现成静态 HTTP 文件服务器。

  • 降低开发者的激活能量,使 JDK 更加易于接近。

  • 通过命令行提供默认实现,并附带一个小型 API 用于编程创建和自定义。

非目标

  • 不提供功能丰富或商业级别的服务器。服务器框架(如 Jetty、Netty 和 Grizzly)和生产服务器(如 Apache Tomcat、Apache httpd 和 NGINX)等更好的替代品已经存在。这些功能完备且性能优化的技术需要花费精力进行配置,而这正是我们想要避免的。

  • 不提供安全功能,如身份验证、访问控制或加密。该服务器仅用于测试、开发和调试。因此,其设计明确保持最小化,以避免与功能全面的服务器应用程序混淆。

动机

开发者常见的必经之路之一是在网络上提供服务文件,很可能是 “Hello, world!” HTML 文件。大多数计算机科学课程都会向学生介绍网络开发,其中 本地测试服务器 是常用的工具。开发者通常还会学习系统管理和网络服务等其他领域,在这些领域中,具有 基本服务器功能 的开发工具会很有用。在这些教育和非正式任务中,需要一个小型现成的服务器。用例包括:

  • 网络开发测试,其中使用本地测试服务器来模拟客户端 - 服务器设置。

  • Web 服务或应用程序测试,其中在反映 RESTful URL 的目录结构中,使用静态文件作为 API 存根,并包含虚拟数据。

  • 非正式地跨系统浏览和共享文件,例如,从本地机器搜索远程服务器上的目录。

在所有这些情况下,我们当然可以使用 Web 服务器框架,但这种方法的激活能量很高:我们需要寻找选项、选择一个、下载它、配置它,并了解如何使用它,然后才能处理我们的第一个请求。这些步骤相当繁琐,是一个缺点;中途遇到障碍可能会令人沮丧,甚至可能阻碍 Java 的进一步使用。通过命令行或几行代码启动的基本 Web 服务器可以让我们绕过这些繁琐的步骤,以便我们能够专注于手头的任务。

Python、Ruby、PHP、Erlang 以及许多其他平台都提供了 从命令行运行的现成服务器。这种现有替代方案的多样性表明了对这种类型工具的公认需求。

描述

简单 Web 服务器是一个最小化的 HTTP 服务器,用于服务单个目录层次结构。它基于自 2006 年以来已包含在 JDK 中的 com.sun.net.httpserver 包中的 Web 服务器实现。该包获得官方支持,我们通过扩展其 API 来简化服务器的创建并增强请求处理。简单 Web 服务器可以通过专用的命令行工具 jwebserver 使用,也可以通过其 API 以编程方式使用。

命令行工具

以下命令启动简单 Web 服务器:

shell
$ jwebserver

如果启动成功,则 jwebserver 会在 System.out 上打印一条消息,列出本地地址和被服务目录的绝对路径。例如:

shell
$ jwebserver
默认绑定到回环地址。要为所有接口使用,请使用"-b 0.0.0.0""-b ::"
在127.0.0.1端口8000上服务/cwd及其子目录
URL: http://127.0.0.1:8000/

默认情况下,服务器在前台运行,并绑定到回环地址和端口 8000。这可以通过 -b-p 选项进行更改。例如,要在端口 9000 上运行服务器,请使用:

shell
$ jwebserver -p 9000

例如,要将服务器绑定到所有接口:

shell
$ jwebserver -b 0.0.0.0
在0.0.0.0(所有接口)端口8000上服务/cwd及其子目录
URL: http://123.456.7.891:8000/

默认情况下,从当前目录提供服务文件。可以使用 -d 选项指定不同的目录。

仅处理幂等的 HEAD 和 GET 请求。其他任何请求都会收到 501 - Not Implemented405 - Not Allowed 响应。GET 请求映射到被服务的目录,如下所示:

  • 如果请求的资源是文件,则提供其内容。
  • 如果请求的资源是包含索引文件的目录,则提供索引文件的内容。
  • 否则,列出该目录中所有文件和子目录的名称。不列出或提供服务符号链接和隐藏文件。

简单 Web 服务器仅支持 HTTP/1.1。不支持 HTTPS。

MIME 类型会自动配置。例如,.html 文件作为 text/html 提供服务,而 .java 文件则作为 text/plain 提供服务。

默认情况下,每个请求都会在控制台上记录。输出看起来像这样:

127.0.0.1 - - [10/Feb/2021:14:34:11 +0000] "GET /some/subdirectory/ HTTP/1.1" 200 -

可以使用 -o 选项更改日志输出。默认设置为 infoverbose 设置除了包括请求和响应头之外,还包括请求资源的绝对路径。

一旦成功启动,服务器将一直运行,直到被停止。在 Unix 平台上,可以通过向服务器发送 SIGINT 信号(在终端窗口中按 Ctrl+C)来停止服务器。

-h 选项会显示一个帮助消息,列出所有选项,这些选项遵循 JEP 293 中的准则。此外,还提供了 jwebserver 的手册页。

选项:
       -h 或 -? 或 --help
              打印帮助消息并退出。

       -b addr 或 --bind-address addr
              指定要绑定的地址。默认:127.0.0.1或::1(回环)。对于所有接口,请使用-b 0.0.0.0或-b ::。

       -d dir 或 --directory dir
              指定要服务的目录。默认:当前目录。

       -o level 或 --output level
              指定输出格式。none | info | verbose。默认:info。

       -p port 或 --port port
              指定要监听的端口。默认:8000。

       -version 或 --version
              打印版本信息并退出。

       要停止服务器,请按Ctrl + C。

API

尽管命令行工具非常有用,但如果用户希望将简单 Web 服务器的组件(即服务器、处理器和过滤器)与现有代码一起使用,或进一步自定义处理器的行为,该怎么办?尽管可以在命令行上进行一些配置,但一种简洁且直观的程序化创建和自定义解决方案将提高服务器组件的实用性。为了弥合命令行工具的简洁性与当前 com.sun.net.httpserver API 的自行编写方法之间的差距,我们为服务器创建和自定义请求处理定义了新的 API。

新类包括 SimpleFileServerHttpHandlersRequest,它们均基于 com.sun.net.httpserver 包中的现有类和接口构建:HttpServerHttpHandlerFilterHttpExchange

SimpleFileServer 类支持文件服务器的创建、文件服务器处理器和输出过滤器的创建:

java
package com.sun.net.httpserver;

public final class SimpleFileServer {
    public static HttpServer createFileServer(InetSocketAddress addr,
                                              Path rootDirectory,
                                              OutputLevel outputLevel) {...}
    public static HttpHandler createFileHandler(Path rootDirectory) {...}
    public static Filter createOutputFilter(OutputStream out,
                                            OutputLevel outputLevel) {...}
    ...
}

使用此类,可以在 jshell 中以几行代码启动一个最小但已自定义的服务器:

java
jshell> var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
   ...> Path.of("/some/path"), OutputLevel.VERBOSE);
jshell> server.start()

可以将自定义的文件服务器处理器添加到现有服务器中:

java
jshell> var server = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/store/", new SomePutHandler());
jshell> var handler = SimpleFileServer.createFileHandler(Path.of("/some/path"));
jshell> server.createContext("/browse/", handler);
jshell> server.start();

在创建服务器时,可以添加自定义的输出过滤器:

java
jshell> var filter = SimpleFileServer.createOutputFilter(System.out,
   ...> OutputLevel.INFO);
jshell> var server = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/store/", new SomePutHandler(), filter);
jshell> server.start();

HttpServerHttpsServer 类中的新重载 create 方法使最后两个示例成为可能:

java
public static HttpServer create(InetSocketAddress addr,
                                int backlog,
                                String root,
                                HttpHandler handler,
                                Filter... filters) throws IOException {...}

增强的请求处理

简单 Web 服务器的核心功能由其处理器提供。为了支持扩展此处理器以与现有代码一起使用,我们引入了一个新的 HttpHandlers 类,该类包含两个用于处理器创建和自定义的静态方法,以及在 Filter 类中用于适应请求的新方法:

java
package com.sun.net.httpserver;

public final class HttpHandlers {
    public static HttpHandler handleOrElse(Predicate<Request> handlerTest,
                                           HttpHandler handler,
                                           HttpHandler fallbackHandler) {...}
    public static HttpHandler of(int statusCode, Headers headers, String body) {...}
    {...}
}

public abstract class Filter {
    public static Filter adaptRequest(String description,
                                      UnaryOperator<Request> requestOperator) {...}
    {...}
}

handleOrElse 方法通过另一个处理器补充条件处理器,而工厂方法 of 允许您创建具有预设响应状态的处理器。从 adaptRequest 获得的预处理过滤器可用于在处理请求之前检查和适应请求的某些属性。这些方法的用例包括根据请求方法委托交换、创建一个始终返回特定响应的“预设响应”处理器,或向所有传入请求添加标头。

现有 API 将 HTTP 请求作为 HttpExchange 类实例表示的请求 - 响应对的一部分捕获,该类描述了交换的完整且可变状态。但是,并非所有这些状态对于处理器的自定义和适应都有意义。因此,我们引入了一个更简单的 Request 接口,以提供对不可变请求状态的有限视图:

java
public interface Request {
    URI getRequestURI();
    String getRequestMethod();
    Headers getRequestHeaders();
    default Request with(String headerName, List<String> headerValues)
    {...}
}

这允许对现有处理器进行直接的自定义,例如:

java
jshell> var h = HttpHandlers.handleOrElse(r -> r.getRequestMethod().equals("PUT"),
   ...> new SomePutHandler(), new SomeHandler());
jshell> var f = Filter.adaptRequest("Add Foo header", r -> r.with("Foo", List.of("Bar")));
jshell> var s = HttpServer.create(new InetSocketAddress(8080),
   ...> 10, "/", h, f);
jshell> s.start();

替代方案

我们考虑了命令行工具的替代方案:

  • java -m jdk.httpserver:最初,简单 Web 服务器是通过命令 java -m jdk.httpserver 而不是专用的命令行工具来运行的。虽然这仍然可行(事实上 jwebserver 在内部使用了 java -m ... 命令),但我们决定引入一个专用工具以提高便利性和易接近性。

在原型设计过程中,我们考虑了几个 API 替代方案:

  • 一个新的 DelegatingHandler 类 —— 将自定义方法捆绑在一个实现 HttpHandler 接口的单独类中。我们放弃了这个选项,因为它以引入新类型而不增加更多功能的代价出现。这个新类型也很难被发现。另一方面,HttpHandlers 类使用了外包模式,其中类的静态辅助方法或工厂被捆绑到一个新类中。几乎相同的名称使得很容易找到这个类,促进了新 API 点的理解和使用,并隐藏了委托的实现细节。

  • HttpHandler 作为服务 —— 将 HttpHandler 转变为服务,并提供内部文件服务器处理器实现。开发人员可以提供自定义处理器或使用默认提供程序。这种方法的缺点是对于我们要提供的一小套功能来说,它更难使用且相当复杂。

  • 使用 Filter 代替 HttpHandler —— 仅使用过滤器而不是处理器来处理请求。过滤器通常是预处理或后处理,意味着它们在处理器被调用之前或之后访问请求,例如用于身份验证或日志记录。但是,它们并非设计用于完全替代处理器。以这种方式使用它们会违反直觉,并且更难找到相关方法。

测试

命令行工具的核心功能由 API 提供,因此我们的测试工作将主要集中在 API 上。可以使用单元测试和现有的测试框架对 API 点进行隔离测试。我们将特别关注文件系统访问和 URI 清理。我们将通过命令行工具的命令和健全性测试来补充 API 测试。

风险和假设

这个简单的服务器仅用于测试、开发和调试目的。在此范围内,服务器的一般安全问题适用,并将通过遵循安全最佳实践和彻底测试来解决。