|------|-----------------------------------------------------------------------|
| 408: | Simple Web Server |
Python、Ruby、PHP、Erlang 和许多其他平台提供从命令行运行的开箱即用服务器。这种现有的替代方案表明了对此类工具的公认需求。
提供一个命令行工具来启动仅提供静态文件的最小web服务器。没有CGI或类似servlet的功能可用。该工具将用于原型设计、即席编码和测试目的,特别是在教育背景下。
Simple Web Server是一个用于服务单个目录层次结构的最小HTTP服务器。它基于自2006年以来JDK中包含的com.sun.net.httpserver包中的web服务器实现。该包得到了官方支持,我们用API对其进行了扩展,以简化服务器创建并增强请求处理。Simple Web Server可以通过专用命令行工具jwebserver使用,也可以通过其API以编程方式使用。
以下命令启动简单Web服务器
通过jwebserver运行
jwebserver

然后在提示serving的目录下放一张图片asd.jpg,然后请求结果如下

注意仅支持 HTTP/1.1。不支持 HTTPS。(但是测试了几次HTTP/2.0是可以访问到的)
命令的几个参数也很简单
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.
通过JSHELL运行
在Jshell中导入会报错 sun.net.httpserver.simpleserver.FileServerHandler
import sun.net.httpserver.simpleserver.FileServerHandler;
| 错误:
| 程序包 sun.net.httpserver.simpleserver 不可见
| (程序包 sun.net.httpserver.simpleserver 已在模块 jdk.httpserver 中声明, 但该模块未导出它)
| import sun.net.httpserver.simpleserver.FileServerHandler;
| ^-----------------------------^
所以可以自己复制一个一模一样的FileServerHandler
同样的sun.net.httpserver.simpleserver.ResourceBundleHelper也复制一个
ResourceBundleHelper
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
class ResourceBundleHelper {
static final ResourceBundle bundle;
static {
try {
bundle = ResourceBundle.getBundle("sun.net.httpserver.simpleserver.resources.simpleserver");
} catch (MissingResourceException e) {
throw new InternalError("Cannot find simpleserver resource bundle for locale " + Locale.getDefault());
}
}
static String getMessage(String key, Object... args) {
try {
return MessageFormat.format(bundle.getString(key), args);
} catch (MissingResourceException e) {
throw new InternalError("Missing message: " + key);
}
}
}
复制到Jshell执行

FileServerHandler
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.System.Logger;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpHandlers;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* A basic HTTP file server handler for static content.
*
* <p> Must be given an absolute pathname to the directory to be served.
* Supports only HEAD and GET requests. Directory listings and files can be
* served, content types are supported on a best-guess basis.
*/
public final class FileServerHandler implements HttpHandler {
private static final List<String> SUPPORTED_METHODS = List.of("HEAD", "GET");
private static final List<String> UNSUPPORTED_METHODS =
List.of("CONNECT", "DELETE", "OPTIONS", "PATCH", "POST", "PUT", "TRACE");
private final Path root;
private final UnaryOperator<String> mimeTable;
private final Logger logger;
private FileServerHandler(Path root, UnaryOperator<String> mimeTable) {
root = root.normalize();
@SuppressWarnings("removal")
var securityManager = System.getSecurityManager();
if (securityManager != null)
securityManager.checkRead(pathForSecurityCheck(root.toString()));
if (!Files.exists(root))
throw new IllegalArgumentException("Path does not exist: " + root);
if (!root.isAbsolute())
throw new IllegalArgumentException("Path is not absolute: " + root);
if (!Files.isDirectory(root))
throw new IllegalArgumentException("Path is not a directory: " + root);
if (!Files.isReadable(root))
throw new IllegalArgumentException("Path is not readable: " + root);
this.root = root;
this.mimeTable = mimeTable;
this.logger = System.getLogger("com.sun.net.httpserver");
}
private static String pathForSecurityCheck(String path) {
var separator = String.valueOf(File.separatorChar);
return path.endsWith(separator) ? (path + "-") : (path + separator + "-");
}
private static final HttpHandler NOT_IMPLEMENTED_HANDLER =
HttpHandlers.of(501, Headers.of(), "");
private static final HttpHandler METHOD_NOT_ALLOWED_HANDLER =
HttpHandlers.of(405, Headers.of("Allow", "HEAD, GET"), "");
public static HttpHandler create(Path root, UnaryOperator<String> mimeTable) {
var fallbackHandler = HttpHandlers.handleOrElse(
r -> UNSUPPORTED_METHODS.contains(r.getRequestMethod()),
METHOD_NOT_ALLOWED_HANDLER,
NOT_IMPLEMENTED_HANDLER);
return HttpHandlers.handleOrElse(
r -> SUPPORTED_METHODS.contains(r.getRequestMethod()),
new FileServerHandler(root, mimeTable), fallbackHandler);
}
private void handleHEAD(HttpExchange exchange, Path path) throws IOException {
handleSupportedMethod(exchange, path, false);
}
private void handleGET(HttpExchange exchange, Path path) throws IOException {
handleSupportedMethod(exchange, path, true);
}
private void handleSupportedMethod(HttpExchange exchange, Path path, boolean writeBody)
throws IOException {
if (Files.isDirectory(path)) {
if (missingSlash(exchange)) {
handleMovedPermanently(exchange);
return;
}
if (indexFile(path) != null) {
serveFile(exchange, indexFile(path), writeBody);
} else {
listFiles(exchange, path, writeBody);
}
} else {
serveFile(exchange, path, writeBody);
}
}
private void handleMovedPermanently(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders().set("Location", getRedirectURI(exchange.getRequestURI()));
exchange.sendResponseHeaders(301, -1);
}
private void handleForbidden(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(403, -1);
}
private void handleNotFound(HttpExchange exchange) throws IOException {
String fileNotFound = ResourceBundleHelper.getMessage("html.not.found");
var bytes = (openHTML
+ "<h1>" + fileNotFound + "</h1>\n"
+ "<p>" + sanitize.apply(exchange.getRequestURI().getPath()) + "</p>\n"
+ closeHTML).getBytes(UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
if (exchange.getRequestMethod().equals("HEAD")) {
exchange.getResponseHeaders().set("Content-Length", Integer.toString(bytes.length));
exchange.sendResponseHeaders(404, -1);
} else {
exchange.sendResponseHeaders(404, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
private static void discardRequestBody(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
}
private String getRedirectURI(URI uri) {
String query = uri.getRawQuery();
String redirectPath = uri.getRawPath() + "/";
return query == null ? redirectPath : redirectPath + "?" + query;
}
private static boolean missingSlash(HttpExchange exchange) {
return !exchange.getRequestURI().getPath().endsWith("/");
}
private static String contextPath(HttpExchange exchange) {
String context = exchange.getHttpContext().getPath();
if (!context.startsWith("/")) {
throw new IllegalArgumentException("Context path invalid: " + context);
}
return context;
}
private static String requestPath(HttpExchange exchange) {
String request = exchange.getRequestURI().getPath();
if (!request.startsWith("/")) {
throw new IllegalArgumentException("Request path invalid: " + request);
}
return request;
}
// Checks that the request does not escape context.
private static void checkRequestWithinContext(String requestPath,
String contextPath) {
if (requestPath.equals(contextPath)) {
return; // context path requested, e.g. context /foo, request /foo
}
String contextPathWithTrailingSlash = contextPath.endsWith("/")
? contextPath : contextPath + "/";
if (!requestPath.startsWith(contextPathWithTrailingSlash)) {
throw new IllegalArgumentException("Request not in context: " + contextPath);
}
}
// Checks that path is, or is within, the root.
private static Path checkPathWithinRoot(Path path, Path root) {
if (!path.startsWith(root)) {
throw new IllegalArgumentException("Request not in root");
}
return path;
}
// Returns the request URI path relative to the context.
private static String relativeRequestPath(HttpExchange exchange) {
String context = contextPath(exchange);
String request = requestPath(exchange);
checkRequestWithinContext(request, context);
return request.substring(context.length());
}
private Path mapToPath(HttpExchange exchange, Path root) {
try {
assert root.isAbsolute() && Files.isDirectory(root); // checked during creation
String uriPath = relativeRequestPath(exchange);
String[] pathSegment = uriPath.split("/");
// resolve each path segment against the root
Path path = root;
for (var segment : pathSegment) {
path = path.resolve(segment);
if (!Files.isReadable(path) || isHiddenOrSymLink(path)) {
return null; // stop resolution, null results in 404 response
}
}
path = path.normalize();
return checkPathWithinRoot(path, root);
} catch (Exception e) {
logger.log(System.Logger.Level.TRACE,
"FileServerHandler: request URI path resolution failed", e);
return null; // could not resolve request URI path
}
}
private static Path indexFile(Path path) {
Path html = path.resolve("index.html");
Path htm = path.resolve("index.htm");
return Files.exists(html) ? html : Files.exists(htm) ? htm : null;
}
private void serveFile(HttpExchange exchange, Path path, boolean writeBody)
throws IOException
{
var respHdrs = exchange.getResponseHeaders();
respHdrs.set("Content-Type", mediaType(path.toString()));
respHdrs.set("Last-Modified", getLastModified(path));
if (writeBody) {
exchange.sendResponseHeaders(200, Files.size(path));
try (InputStream fis = Files.newInputStream(path);
OutputStream os = exchange.getResponseBody()) {
fis.transferTo(os);
}
} else {
respHdrs.set("Content-Length", Long.toString(Files.size(path)));
exchange.sendResponseHeaders(200, -1);
}
}
private void listFiles(HttpExchange exchange, Path path, boolean writeBody)
throws IOException
{
var respHdrs = exchange.getResponseHeaders();
respHdrs.set("Content-Type", "text/html; charset=UTF-8");
respHdrs.set("Last-Modified", getLastModified(path));
var bodyBytes = dirListing(exchange, path).getBytes(UTF_8);
if (writeBody) {
exchange.sendResponseHeaders(200, bodyBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bodyBytes);
}
} else {
respHdrs.set("Content-Length", Integer.toString(bodyBytes.length));
exchange.sendResponseHeaders(200, -1);
}
}
private static final String openHTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
""";
private static final String closeHTML = """
</body>
</html>
""";
private static final String hrefListItemTemplate = """
<li><a href="%s">%s</a></li>
""";
private static String hrefListItemFor(URI uri) {
return hrefListItemTemplate.formatted(uri.toASCIIString(), sanitize.apply(uri.getPath()));
}
private static String dirListing(HttpExchange exchange, Path path) throws IOException {
String dirListing = ResourceBundleHelper.getMessage("html.dir.list");
var sb = new StringBuilder(openHTML
+ "<h1>" + dirListing + " "
+ sanitize.apply(exchange.getRequestURI().getPath())
+ "</h1>\n"
+ "<ul>\n");
try (var paths = Files.list(path)) {
paths.filter(p -> Files.isReadable(p) && !isHiddenOrSymLink(p))
.map(p -> path.toUri().relativize(p.toUri()))
.forEach(uri -> sb.append(hrefListItemFor(uri)));
}
sb.append("</ul>\n");
sb.append(closeHTML);
return sb.toString();
}
private static String getLastModified(Path path) throws IOException {
var fileTime = Files.getLastModifiedTime(path);
return fileTime.toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
private static boolean isHiddenOrSymLink(Path path) {
try {
return Files.isHidden(path) || Files.isSymbolicLink(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// Default for unknown content types, as per RFC 2046
private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
private String mediaType(String file) {
String type = mimeTable.apply(file);
return type != null ? type : DEFAULT_CONTENT_TYPE;
}
// A non-exhaustive map of reserved-HTML and special characters to their
// equivalent entity.
private static final Map<Integer,String> RESERVED_CHARS = Map.of(
(int) '&' , "&" ,
(int) '<' , "<" ,
(int) '>' , ">" ,
(int) '"' , """ ,
(int) '\'' , "'" ,
(int) '/' , "/" );
// A function that takes a string and returns a sanitized version of that
// string with the reserved-HTML and special characters replaced with their
// equivalent entity.
private static final UnaryOperator<String> sanitize =
file -> file.chars().collect(StringBuilder::new,
(sb, c) -> sb.append(RESERVED_CHARS.getOrDefault(c, Character.toString(c))),
StringBuilder::append).toString();
@Override
public void handle(HttpExchange exchange) throws IOException {
assert List.of("GET", "HEAD").contains(exchange.getRequestMethod());
try (exchange) {
discardRequestBody(exchange);
Path path = mapToPath(exchange, root);
if (path != null) {
exchange.setAttribute("request-path", path.toString()); // store for OutputFilter
if (!Files.exists(path) || !Files.isReadable(path) || isHiddenOrSymLink(path)) {
handleNotFound(exchange);
} else if (exchange.getRequestMethod().equals("HEAD")) {
handleHEAD(exchange, path);
} else {
handleGET(exchange, path);
}
} else {
exchange.setAttribute("request-path", "could not resolve request URI path");
handleNotFound(exchange);
}
}
}
}
复制到Jshell执行

创建简单的服务
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.util.function.UnaryOperator;
UnaryOperator<String> identity = UnaryOperator.identity();
var server = HttpServer.create(new InetSocketAddress(9000), 10, "/img/",
FileServerHandler.create(Path.of("D:\\Program Files"), identity));
server.start();
由于在idea中执行放在jshell中执行之后报端口被占用异常,关了idea中的就好了
另外jshell中运行的需要手动自己去找服务停止。
参数解释
9000 端口号
10 最大并发数量 <1的话默认会设置成50
/img/ 访问链接前缀
D:\\Program Files 代理到的目标文件,此文件夹下的文件都可以通过http://127.0.0.1:+端口9000+访问前缀/img/ +文件夹下的文件名(带后缀)如下
http://127.0.0.1:9000/img/asd.jpg