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 是请求连接的最大队列长度,代码如下。
int backlog = 0; // 小于 1 表示使用默认值 50
HttpServer server = HttpServer.create(new InetSocketAddress(9099), backlog);
server.start();
如果请求的连接大于 backlog 规定的值则拒绝连接,请参考以下 Java Spec 说明。
/**
* Sets the maximum queue length for incoming connection indications
* (a request to connect) to the {@code count} argument. If a
* connection indication arrives when the queue is full, the
* connection is refused.
*
* @param backlog the maximum length of the queue.
* @throws IOException if an I/O error occurs when creating the queue.
*/
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);
...
});
表示响应返回状态码为 200,messageBodyLength 解释如下表所示。
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";
}
}