Java 6 开始引入了 com.sun.net.httpserver.HttpServer,且 Java 15 还依旧保留,暂不用担心 com.sun 被废弃的问题。而且 Java 15 还将 com.sun.net.httpserver.* 代码开源了,相信在不久的将来,JDK Http 服务器将会转正。那为什么要使用 Java 自带的的 HttpServer 服务器呢?可以使服务减少外部依赖,启动 Http 服务更加简单方便,值得一提的是 Java 的 HttpServer 使用的是 NIO 技术,通过单 Reactor 多线程实现轻量级且高性能的服务器,不过,使用 HttpServer 作为服务器还是有一些需要特别注意的地方,下面将展开介绍。

启动服务

现在监听本地的 9099 端口,backlog 传参为 0,表示使用默认的 50,backlog 是什么?backlog 是请求连接的最大队列长度,代码如下。

  1. int backlog = 0; // 小于 1 表示使用默认值 50
  2. HttpServer server = HttpServer.create(new InetSocketAddress(9099), backlog);
  3. server.start();

如果请求的连接大于 backlog 规定的值则拒绝连接,请参考以下 Java Spec 说明。

  1. /**
  2. * Sets the maximum queue length for incoming connection indications
  3. * (a request to connect) to the {@code count} argument. If a
  4. * connection indication arrives when the queue is full, the
  5. * connection is refused.
  6. *
  7. * @param backlog the maximum length of the queue.
  8. * @throws IOException if an I/O error occurs when creating the queue.
  9. */
  10. protected abstract void listen(int backlog) throws IOException;

If a connection indication arrives when the queue is full, the connection is refused. 当队列已经到达满载时则该拒绝连接,注意这里的 Queue 是被 ServerSocket accept 之后移除。

默认情况下,HttpServer 使用的默认线程池是 DefaultExecutor,而这个是对每一个请求都会使用一条线程进行处理,单线程的服务并非我们想要的,所以,HttpServer 还提供了 setExecutor 方法设置我们想要的线程池。

int backlog = 0; // 小于 1 表示使用默认值 50
HttpServer server = HttpServer.create(new InetSocketAddress(9099), backlog);
ExecutorService executor = new ThreadPoolExecutor(
                   16,                // 核心处理线程数
                   32,                // 最大的处理线程数
                   15,                // 核心线程获取 work task 的等待时间(秒)
                   TimeUnit.SECONDS,  // 获取 work task 的等待时间单位(秒)
                   new LinkedBlockingDeque<Runnable>(300)); // work queue,容量 300
server.setExecutor(executor);
server.start();

路由器

启动服务后依然无法对外提供可用的访问 URL,这需要我们设置相应的路由,代码如下。

server.createContext("/demo/hello", new HttpHandler() {
    public void handle(HttpExchage exchange) throws IOException {
        ... ...
    }
});
// Lambda 方式
server.createContext("/demo/echo", exchange -> {});

路由器的匹配测试是大小写敏感的,如 /Demo/echo 则不能进行匹配,且按照最长前缀匹配策略选择相应的路由器,下面是路由规则及其匹配的例子。

Router Router Path
router1 “/“
router2 “/apps/“
router3 “/apps/foo”
Request URI Matches Router
http://foo.com/apps/foo/bar router3 (匹配 /apps/foo)
http://foo.com/apps/Foo/bar no match, wrong case
http://foo.com/apps/app1 router2 (匹配 /apps)
http://foo.com/foo router1 (匹配 /)

处理器

请求

1. 获取请求头(Http Request Headers)

server.createContext("/demo/router", exchange -> {
    Headers headers = exchange.getRequestHeaders();
    List<String> values = headers.get("header-key");
    String value = headers.getFirst("header-key");
});

按照 RFC2616 的标准,Header 的 Key 可以相同,所以,headers.get(“header-key”) 获取的是一个列表,这是为了实现 RFC2616 的规范,而大部分情况下 Key 都只有一个,因此 Headers 提供了 getFirst(KeyName) 的方法以便客户端获取单个 Header 的值。

2. 获取请求报文体(Http Request Body)

server.createContext("/demo/router", exchange -> {
    try (InputStream reqIn = exchange.getRequestBody()) {
        // ... 读请求的报文体 ...
    }
});

响应

开发过程中必须严格符合以下顺序,必须先定义响应头部,再定义响应状态码,最后再定义响应报文体。
1. 定义响应的头部信息

server.createContext("/demo/router", exchange -> {
    exchange.getReponseHeader().add("header1", "value1");
    exchange.getReponseHeader().add("header1", "value2");

    exchange.getReponseHeader().add("header2", "value");
    ...
});

注意到,其实存在多个 header1 但值不同,HTTP 响应报文将如下表示。

<Response Status Line>
header1=value1
header1=value2
header2=value
... ...

<message body>

2. 定义响应的状态码信息

server.createContext("/demo/router", exchange -> {
    ...
    exchange.sendResponseHeaders(200, messageBodyLength);
    ...
});

表示响应返回状态码为 200messageBodyLength 解释如下表所示。

messageBodyLength 值说明
-1 响应报文体为空
0 响应报文体为不定长报文(Chunk-Data)
N > 0 响应报文体为固定长度,且长度为 N 字节(N > 0)

3. 定义响应的报文体

server.createContext("/demo/router", exchange -> {
    ...
    try (OutputStream resp = exchange.getResponseBody()) {
        // ... 写响应的报文体 ...
    }
});

使用 exchange.getResponseBody().close() / exchange.close() 方法表示完成当前请求,但这样并不会关闭客户端到服务器的实际长连接。

不过需要注意的是,如果 exchange.getResponseBody().write 的数据长度与 exchange.sendResponseHeaders 定义的不一致,则会关闭当前连接,并抛出 IOException(“insufficient bytes written to stream”),完整代码样例如下。

server.createContext("/router/...", exchange -> {
    exchange.getReponseHeader().add("header1", "value1");
    exchange.getReponseHeader().add("header1", "value2");
    exchange.getReponseHeader().add("header2", "value");

    exchange.sendResponseHeaders(200, messageBodyLength);
    try (OutputStream resp = exchange.getResponseBody()) {
        // ... Response Payload Data ...
    }
});

IP 源

InetSocketAddress remoteAddr = exchange.getRemoteAddress();
String remoteIP = remoteAddr.getAddress().getHostAddress();
int port = remoteAddr.getPort();

这样我们就可以做一些 IP 授权的低端版安全校验,但 IP 授权是一个极不安全的行为,这是因为如果增加反向代理服务器,那么这个反向代理的服务器必须要透传 IP 才能使 IP 授权正式生效。很多时候,投产上线为了解决问题而信任了代理服务器的 IP,就在这一刻,所有的访问都是受信任的了。

过滤器

继承 com.sun.net.httpserver.Filter 并实现 doFilter / description 方法,最后配置到 HttpContext 里即可。

public class DemoFilter extends com.sun.net.httpserver.Filter {
    @Override
    public void doFilter(com.sun.net.httpserver.HttpExchange exchange,
                         Chain chain) throws IOException {
        // pre filter
        chain.doFilter(exchange);
        // post filter
    }
    @Override
    public String description() {
        return "Demo Filter";
    }
}

添加到 Filters 里。

context.getFilters().add(new DemoFilter());

或者在 SystemFilters 里添加,不过其优先级低于 Filters。

context.getSystemFilters().add(new DemoFilter());

使得 Filters / SystemFilters 生效的完整代码如下。

HttpContext context = server.createContext(router, exchange -> {
    // ... handle request ...
});
context.getFilters()            // 优先执行
       .add(new Filter(){...})
       .add(new Filter(){...});
context.getSystemFilters()      // 最后执行
       .add(new Filter(){...})
       .add(new Filter(){...});

完整例子

int backlog = 0; // 小于 1 表示使用默认值 50
// 监听本地 9099 端口作为 HTTP 服务端口
HttpServer server = HttpServer.create(new InetSocketAddress(9099), backlog);
ExecutorService executor = new ThreadPoolExecutor(
                   16,                // 核心处理线程数
                   32,                // 最大的处理线程数
                   15,                // 核心线程获取 work task 的等待时间(秒)
                   TimeUnit.SECONDS,  // 获取 work task 的等待时间单位(秒)
                   new LinkedBlockingDeque<Runnable>(300)); // work queue,容量 300
// 设置工作线程池
server.setExecutor(executor);

// 设置 HTTP 访问路由
server.createContext("/demo/router", exchange -> {
    exchange.getReponseHeader().add("header1", "value1");
    exchange.getReponseHeader().add("header1", "value2");
    exchange.getReponseHeader().add("header2", "value");

    exchange.sendResponseHeaders(200, messageBodyLength);
    try (OutputStream resp = exchange.getResponseBody()) {
        // ... Response Payload Data ...
    }
});

// 设置过滤器
context.getFilters().add(new DemoFilter());
// 或者优先级更低的过滤器
// context.getSystemFilters().add(new DemoFilter());

// 启动 HTTP 服务
server.start();
public class DemoFilter extends com.sun.net.httpserver.Filter {
    @Override
    public void doFilter(com.sun.net.httpserver.HttpExchange exchange,
                         Chain chain) throws IOException {
        // pre filter
        chain.doFilter(exchange);
        // post filter
    }
    @Override
    public String description() {
        return "Demo Filter";
    }
}