JEP 408:简单的 Web 服务器
概括
提供一个命令行工具来启动仅提供静态文件的最小 Web 服务器。没有可用的 CGI 或类似 servlet 的功能。该工具对于原型设计、临时编码和测试目的非常有用,特别是在教育环境中。
目标
-
提供开箱即用的静态 HTTP 文件服务器,具有简单的设置和最少的功能。
-
降低开发者的激活能量,让JDK更加平易近人。
-
通过命令行提供默认实现以及用于编程创建和自定义的小型 API。
非目标
-
提供功能丰富或商业级服务器不是目标。更好的替代方案以服务器框架(例如 Jetty、Netty 和 Grizzly)和生产服务器(例如 Apache Tomcat、Apache httpd 和 NGINX)的形式存在。这些成熟且性能优化的技术需要花费精力来配置,这正是我们想要避免的。
-
提供身份验证、访问控制或加密等安全功能不是目标。该服务器仅用于测试、开发和调试。因此,其设计明确是最小化的,以避免与全功能服务器应用程序混淆。
动机
开发人员的常见仪式是在网络上提供文件,可能是“Hello,world!” HTML 文件。大多数计算机科学课程都会向学生介绍网络开发,其中通常使用本地测试服务器。开发人员通常还会了解系统管理和 Web 服务,以及具有基本服务器功能的开发工具可以派上用场的其他领域。诸如此类的教育和非正式任务是需要小型开箱即用服务器的地方。用例包括:
-
Web 开发测试,其中使用本地测试服务器来模拟客户端-服务器设置。
-
Web 服务或应用程序测试,其中静态文件用作镜像 RESTful URL 并包含虚拟数据的目录结构中的 API 存根。
-
跨系统非正式地浏览和共享文件,例如从本地计算机搜索远程服务器上的目录。
当然,在所有这些情况下,我们都可以使用 Web 服务器框架,但这种方法具有很高的激活能:我们必须寻找选项,选择一个,下载它,配置它,并弄清楚如何使用它。我们可以满足我们的第一个请求。这些步骤相当繁琐,这是一个缺点;在途中遇到困难可能会令人沮丧,甚至可能阻碍 Java 的进一步使用。从命令行或通过几行代码启动的基本 Web 服务器可以让我们绕过这个仪式,这样我们就可以专注于手头的任务。
Python、Ruby、PHP、Erlang 和许多其他平台提供从命令行运行的开箱即用服务器。现有的多种替代方案表明了对此类工具的公认需求。
描述
简单 Web 服务器是一个最小的 HTTP 服务器,用于为单个目录层次结构提供服务。它基于com.sun.net.httpserver
自 2006 年起包含在 JDK 中的包中的 Web 服务器实现。该包受到官方支持,我们使用 API 对其进行了扩展,以简化服务器创建并增强请求处理。简单 Web 服务器可以通过专用命令行工具使用jwebserver
,也可以通过其 API 以编程方式使用。
命令行工具
以下命令启动简单 Web 服务器:
$ jwebserver
如果启动成功,则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/
默认情况下,服务器在前台运行并绑定到环回地址和端口 8000。这可以使用-b
和-p
选项进行更改。例如,要在端口 9000 上运行服务器,请使用:
$ jwebserver -p 9000
例如,将服务器绑定到所有接口:
$ 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/
默认情况下,文件从当前目录提供。可以使用该选项指定不同的目录-d
。
仅提供幂等 HEAD 和 GET 请求。任何其他请求都会收到一个501 - Not Implemented
或一个405 - 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
。默认设置为info
。该verbose
设置还包括请求和响应标头以及所请求资源的绝对路 径。
一旦成功启动,服务器就会运行直到停止。在 Unix 平台上,可以通过向服务器发送 SIGINT 信号(Ctrl+C
在终端窗口中)来停止服务器。
该选项显示一条帮助消息,列出了所有选项,这些选项遵循JEP 293-h
中的指南。还提供了一个手册页。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.
应用程序编程接口
虽然命令行工具很有用,但如果想要将简单 Web 服务器的组件(即服务器、处理程序和过滤器)与现有代码一起使用,或者进一步自定义处理程序的行为,该怎么办?虽然可以在命令行上进行某些配置,但用于创建和定制的简洁直观的编程解决方案将提高服务器组件的实用性。为了弥补命令行工具的简单性和当前 API 的自行编写方法之间的差距com.sun.net.httpserver
,我们定义了用于服务器创建和自定义请求处理的新 API。
新类是SimpleFileServer
、HttpHandlers
和Request
,每个类都基于包中的现有类和接口构建com.sun.net.httpserver
:HttpServer
、HttpHandler
、Filter
和HttpExchange
。
该类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) {...}
...
}
使用此类,可以通过以下几行代码启动最小但定制的服务器jshell
:
jshell> var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
...> Path.of("/some/path"), OutputLevel.VERBOSE);
jshell> server.start()
可以将自定义的文件服务器处理程序添加到现有服务器中:
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();
可以在创建过程中将自定义输出过滤器添加到服务器:
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();
最后两个示例是通过和类create
中的新重载方法启用的:HttpServer``HttpsServer
public static HttpServer create(InetSocketAddress addr,
int backlog,
String root,
HttpHandler handler,
Filter... filters) throws IOException {...}
增强的请求处理
简单 Web 服务器的核心功能由其处理程序提供。为了支持扩展此处理程序以与现有代码一起使用,我们引入了一个新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) {...}
{...}
}
handleOrElse
用另一个处理程序补充条件处理程序,而工厂方法of
允许您创建具有预设响应状态的处理程序。从 获得的预处理过滤器adaptRequest
可用于在处理请求之前检查和调整请求的某些属性。这些方法的用例包 括根据请求方法委托交换、创建始终返回特定响应的“预设响应”处理程序,或向所有传入请求添加标头。
现有的 API 将 HTTP 请求捕获为由类实例表示的请求-响应对的一部分HttpExchange
,该类实例描述了交换的完整且可变的状态。并非所有此状态对于处理程序定制和适应都有意义。因此,我们引入了一个更简单的Request
接口来提供不可变请求状态的有限视图:
public interface Request {
URI getRequestURI();
String getRequestMethod();
Headers getRequestHeaders();
default Request with(String headerName, List<String> headerValues)
{...}
}
这使得可以直接定制现有的处理程序,例如:
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
另一方面,该类使用 outboarding 模式,其中类的静态帮助器方法或工厂捆绑在新类中。几乎相同的名称可以很容易地找到类,方便新的API点的理解和使用,并且隐藏了委托的实现细节。 -
HttpHandler
作为服务 - 转变HttpHandler
为服务并提供内部文件服务器处理程序实现。开发人员可以提供自定义处理程序或使用默认提供程序。这种方法的缺点是,对于我们想要提供的一小部分功能来说,它更难以使用并且相当复杂。 -
Filter
而不是HttpHandler
— 仅使用过滤器而不是处理程序来处理请求。过滤器通常是预处理或后处理,这意味着它们在调用处理程序之前或之后访问请求,例如用于身份验证或日志记录。然而,它们并不是为了完全取代处理程序而设计的。以这种方式使用它们是违反直觉的,而且方法也更难找到。
测试
命令行工具的核心功能是由API提供的,因此我们的大部分测试工作将集中在API上。 API 点可以与单元测试和现有测试框架分开进行测试。我们将特别关注文件系统访问和 URI 清理。我们将通过命令行工具的命令和健全性测试来补充 API 测试。
风险和假设
这个简单的服务器仅用于测试、开发和调试目的。在此范围内,服务器的一般安全问题适用,并将通过遵循安全最佳实践和彻底的测试来解决。