跳到主要内容

JEP 408: 简单 Web 服务器

QWen Max 中英对照 JEP 408: Simple Web Server

总结

提供一个命令行工具,用于启动仅提供静态文件的最小 Web 服务器。没有 CGI 或类似 servlet 的功能可用。此工具对于原型设计、即兴编码和测试目的非常有用,尤其是在教育环境中。

目标

  • 提供一个开箱即用的静态 HTTP 文件服务器,设置简单且功能精简。

  • 降低开发者的学习门槛,使 JDK 更加平易近人。

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

非目标

  • 提供功能丰富或商业级服务器并不是目标。在服务器框架(例如 Jetty、Netty 和 Grizzly)和生产服务器(例如 Apache Tomcat、Apache httpd 和 NGINX)方面,已经有好得多的替代方案。这些功能齐全且性能优化的技术需要花费精力进行配置,而这正是我们想要避免的。

  • 提供诸如身份验证、访问控制或加密等安全功能也不是目标。该服务器仅用于测试、开发和调试。因此,它的设计明确保持极简,以避免与全功能服务器应用程序混淆。

动机

开发者的一个常见成年礼是通过网络提供文件服务,很可能是一个 “Hello, world!” HTML 文件。大多数计算机科学课程都会向学生介绍 Web 开发,其中通常会使用本地测试服务器。开发者通常还会学习系统管理与 Web 服务,这些领域中具备基本服务器功能的开发工具同样可以派上用场。像这样的教育性或非正式任务正是一个开箱即用的小型服务器的理想应用场景。其用途包括:

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

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

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

当然,在所有这些情况中,我们可以使用 Web 服务器框架,但这种方法的启动成本较高:我们必须寻找选项、挑选一个、下载、配置,并在处理第一个请求之前弄清楚如何使用它。这些步骤包含了大量的仪式性操作,这是一个缺点;在中途卡住可能会令人沮丧,甚至可能阻碍进一步使用 Java。通过命令行或几行代码启动的基本 Web 服务器可以让我们绕过这些繁琐的步骤,从而专注于手头的任务。

Python、Ruby、PHP、Erlang 以及许多其他平台都提供了开箱即用的服务器,可以直接从命令行运行。这种多样化的现有替代方案表明了对此类工具的广泛需求。

描述

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

命令行工具

以下命令启动 Simple Web Server:

java -jar sws.jar
bash
$ jwebserver
shell

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

$ jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /cwd and subdirectories on 127.0.0.1 port 8000
URL: http://127.0.0.1:8000/
shell

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

$ jwebserver -p 9000
shell

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

$ jwebserver -b 0.0.0.0
Serving /cwd and subdirectories on 0.0.0.0 (all interfaces) port 8000
URL: http://123.456.7.891:8000/
shell

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

仅提供幂等的 HEAD 和 GET 请求服务。任何其他请求都会收到 501 - Not Implemented405 - Not Allowed 响应。GET 请求会按如下方式映射到所服务的目录:

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

Simple Web Server 仅支持 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 的手册页。

Options:
-h or -? or --help
Prints the help message and exits.

-b addr or --bind-address addr
Specifies the address to bind to. Default: 127.0.0.1 or ::1 (loopback). For
all interfaces use -b 0.0.0.0 or -b ::.

-d dir or --directory dir
Specifies the directory to serve. Default: current directory.

-o level or --output level
Specifies the output format. none | info | verbose. Default: info.

-p port or --port port
Specifies the port to listen on. Default: 8000.

-version or --version
Prints the version information and exits.

To stop the server, press Ctrl + C.
shell

API

虽然命令行工具很有用,但如果有人想将 Simple Web Server(即服务器、处理器和过滤器)的组件与现有代码一起使用,或者进一步自定义处理器的行为该怎么办呢?虽然可以在命令行上进行一些配置,但一个简洁且直观的编程解决方案来创建和自定义会提升服务器组件的实用性。为了在命令行工具的简单性与当前 com.sun.net.httpserver API 的自行编写方法之间架起桥梁,我们为服务器创建和自定义请求处理定义了新的 API。

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

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

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) {...}
...
}
java

使用这个类,只需在 jshell 中编写几行代码,就可以启动一个极简但可定制的服务器:

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

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

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

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

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

最后两个示例通过 HttpServerHttpsServer 类中的新重载 create 方法实现:

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

增强的请求处理

Simple Web Server 的核心功能由其处理器提供。为了支持扩展此处理器以用于现有代码,我们引入了一个新的 HttpHandlers 类,该类包含两个用于处理器创建和自定义的静态方法,并在 Filter 类中新增了一个用于调整请求的方法:

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) {...}
{...}
}
java

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

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

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

替代方案

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

  • 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 测试。

风险与假设

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