java18学习笔记-Simple Web Server

|------|-----------------------------------------------------------------------|
| 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) '&'  , "&amp;"   ,
            (int) '<'  , "&lt;"    ,
            (int) '>'  , "&gt;"    ,
            (int) '"'  , "&quot;"  ,
            (int) '\'' , "&#x27;"  ,
            (int) '/'  , "&#x2F;"  );

    // 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