原文参见:如何用Netty构建一个HTTP服务器?
1.概述
本文介绍如何用Netty构建一个简单的HTTP服务器,体验一下如何通过Netty进行Java网络应用开发。
2.服务器引导
真正开始之前,请先了解一下Netty的基本概念,诸如channel、handler、encoder和decoder等(译注:不在本文介绍)。
最先介绍的是服务器引导(Server Bootstraping)部分。这里的服务器另一篇文章中的Simple Protocol Server类似:
public class HttpServer {
private int port;
private static Logger logger = LoggerFactory.getLogger(HttpServer.class);
// constructor
// main method, same as simple protocol server
public void run() throws Exception {
...
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpRequestDecoder());
p.addLast(new HttpResponseEncoder());
p.addLast(new CustomHttpServerHandler());
}
});
...
}
}
只有childHandler中要实现的协议不同(此处为HTTP)。
代码为服务器的ChannelPipeline添加三个Handler:
-
Netty的HttpResponseEncoder –用于序列化
-
Netty的HttpRequestDecoder –用于反序列化
-
需自己实现的CustomHttpServerHandler –用于定义服务器的行为
最后一个Handler是实现HTTP服务的重点,来看一下它的实现。
3. CustomHttpServerHandler
我们这是实现的自定义Handler会用于处理入站数据并发送响应。分析、了解一下其工作原理。
3.1. Handler的结构
CustomHttpServerHandler扩展了Netty的抽象类SimpleChannelInboundHandler,实现了其生命周期方法:
public class CustomHttpServerHandler extends SimpleChannelInboundHandler {
private HttpRequest request;
StringBuilder responseData = new StringBuilder();
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
// implementation to follow
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
正如方法名称所示,处理完pipeline中的最后一条消息之后,channelReadComplete冲刷(flush)handlerContext,以便可处理下一条传入的消息。方法exceptionCaught用于可能发生的异常处理。
目前为止,所看到的还仅仅只是通用部分的代码。
下来进入最有意思的部分,即channelRead0的实现。
3.2. 读取channel的数据
样例代码很简单,服务器将请求正文和查询参数(如果有的话)转换为大写。在此提醒一下样例代码从请求数据产生响应消息的逻辑:样例代码的大写转化逻辑很简单,仅用于演示目的,用于了解如何使用Netty来实现HTTP服务器。
样例代码对收到的消息或请求,按照协议的建议设置其响应(所用到的RequestUtils稍后介绍):
if (msg instanceof HttpRequest) {
HttpRequest request = this.request = (HttpRequest) msg;
if (HttpUtil.is100ContinueExpected(request)) {
writeResponse(ctx);
}
responseData.setLength(0);
responseData.append(RequestUtils.formatParams(request));
}
responseData.append(RequestUtils.evaluateDecoderResult(request));
if (msg instanceof HttpContent) {
HttpContent httpContent = (HttpContent) msg;
responseData.append(RequestUtils.formatBody(httpContent));
responseData.append(RequestUtils.evaluateDecoderResult(request));
if (msg instanceof LastHttpContent) {
LastHttpContent trailer = (LastHttpContent) msg;
responseData.append(RequestUtils.prepareLastResponse(request, trailer));
writeResponse(ctx, trailer, responseData);
}
}
可以看到,从channel收到HttpRequest后,代码首先检查请求是否期望得到一个“100 Continue”,如果是这种情况,代码立即回写一个空响应,状态为CONTINUE:
private void writeResponse(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE,
Unpooled.EMPTY_BUFFER);
ctx.write(response);
}
此后,handler初始化用于响应的字符串,将请求的查询参数添加到该字符串并原样发回。
接下来,定义方法formatParams,置于RequestUtils帮助类中,此方法执行以下操作:
StringBuilder formatParams(HttpRequest request) {
StringBuilder responseData = new StringBuilder();
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri());
Map<String, List<String>> params = queryStringDecoder.parameters();
if (!params.isEmpty()) {
for (Entry<String, List<String>> p : params.entrySet()) {
String key = p.getKey();
List<String> vals = p.getValue();
for (String val : vals) {
responseData.append("Parameter: ").append(key.toUpperCase()).append(" = ")
.append(val.toUpperCase()).append("\r\n");
}
}
responseData.append("\r\n");
}
return responseData;
}
之后,接收到HttpContent后,代码获取请求正文,将其转换为大写:
StringBuilder formatBody(HttpContent httpContent) {
StringBuilder responseData = new StringBuilder();
ByteBuf content = httpContent.content();
if (content.isReadable()) {
responseData.append(content.toString(CharsetUtil.UTF_8).toUpperCase())
.append("\r\n");
}
return responseData;
}
另需注意,如果接收到的HttpContent是LastHttpContent,则添加"goodbye"消息和trailing headers(如果有的话):
StringBuilder prepareLastResponse(HttpRequest request, LastHttpContent trailer) {
StringBuilder responseData = new StringBuilder();
responseData.append("Good Bye!\r\n");
if (!trailer.trailingHeaders().isEmpty()) {
responseData.append("\r\n");
for (CharSequence name : trailer.trailingHeaders().names()) {
for (CharSequence value : trailer.trailingHeaders().getAll(name)) {
responseData.append("P.S. Trailing Header: ");
responseData.append(name).append(" = ").append(value).append("\r\n");
}
}
responseData.append("\r\n");
}
return responseData;
}
3.3. 写入响应数据
要发送的响应数据至此已经准备就绪,可将其写入ChannelHandlerContext:
private void writeResponse(ChannelHandlerContext ctx, LastHttpContent trailer,
StringBuilder responseData) {
boolean keepAlive = HttpUtil.isKeepAlive(request);
FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1,
((HttpObject) trailer).decoderResult().isSuccess() ? OK : BAD_REQUEST,
Unpooled.copiedBuffer(responseData.toString(), CharsetUtil.UTF_8));
httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
if (keepAlive) {
httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH,
httpResponse.content().readableBytes());
httpResponse.headers().set(HttpHeaderNames.CONNECTION,
HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(httpResponse);
if (!keepAlive) {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
}
此方法中创建了HTTP/1.1版本的FullHttpResponse,添加了上面准备好的响应数据。
如请求需保持活动状态,即保持连接不关闭,需将响应的header设置为keep-alive。不然的话连接将关闭。
4.测试服务器
为了测试我们实现的服务器,我们发送一些cURL命令来查看响应。
在此之前,需要先运行HttpServer类来启动服务器。
4.1.GET请求
发起一个服务器GET请求,此请求提供一个参数:
curl http://127.0.0.1:8080?param1=one
收到的响应为:
Parameter: PARAM1 = ONE
Good Bye!
可以使用任何浏览器来访问http://127.0.0.1:8080?param1=one,都将会得到相同的结果。
4.2.POST请求
第二个测试发送正文(body)内容为“sample content”的POST 请求:
curl -d "sample content" -X POST http://127.0.0.1:8080
响应为:
SAMPLE CONTENT Good Bye!
这次发送的请求包含了请求体(body),故服务器以大写形式将其内容返回。
5.最后
本文介绍了如何实现HTTP协议,即通过Netty来实现一个HTTP服务器。
Netty中的HTTP/2演示了HTTP/2协议的客户端-服务器实现。
与以往一样,源代码可以从GitHub上获得。