1. Overview
1.概述
In this tutorial, we’re going to implement a simple upper-casing server over HTTP with Netty, an asynchronous framework that gives us the flexibility to develop network applications in Java.
在本教程中,我们将通过HTTP与Netty实现一个简单的大写字母服务器,Netty是一个异步框架,使我们可以在Java中灵活地开发网络应用。
2. Server Bootstrapping
2.服务器启动
Before we start, we should be aware of the basics concepts of Netty, such as channel, handler, encoder, and decoder.
在开始之前,我们应该了解Netty的基本概念,例如通道、处理程序、编码器和解码器。
Here we’ll jump straight into bootstrapping the server, which is mostly the same as a 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());
}
});
...
}
}
So, here only the childHandler differs as per the protocol we want to implement, which is HTTP for us.
因此,这里只有childHandler与我们想要实现的协议不同,对我们来说是HTTP。
We’re adding three handlers to the server’s pipeline:
我们要在服务器的管道中添加三个处理程序。
- Netty’s HttpResponseEncoder – for serialization
- Netty’s HttpRequestDecoder – for deserialization
- Our own CustomHttpServerHandler – for defining our server’s behavior
Let’s look at the last handler in detail next.
接下来让我们详细了解一下最后一个处理程序。
3. CustomHttpServerHandler
3、CustomHttpServerHandler
Our custom handler’s job is to process inbound data and send a response.
我们的自定义处理程序的工作是处理入站数据并发送一个响应。
Let’s break it down to understand its working.
让我们把它分解,以了解它的工作。
3.1. Structure of the Handler
3.1.处理程序的结构
CustomHttpServerHandler extends Netty’s abstract SimpleChannelInboundHandler and implements its lifecycle methods:
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();
}
}
As the method name suggests, channelReadComplete flushes the handler context after the last message in the channel has been consumed so that it’s available for the next incoming message. The method exceptionCaught is for handling exceptions if any.
正如该方法的名字所示,channelReadComplete在通道中的最后一条消息被消耗后刷新处理程序上下文,这样它就可以用来处理下一条传入的消息。方法exceptionCaught是用来处理异常的。
So far, all we’ve seen is the boilerplate code.
到目前为止,我们所看到的都是模板代码。
Now let’s get on with the interesting stuff, the implementation of channelRead0.
现在让我们继续讨论有趣的东西,即channelRead0的实现。
3.2. Reading the Channel
3.2.读取通道
Our use case is simple, the server will simply transform the request body and query parameters, if any, to uppercase. A word of caution here on reflecting request data in the response – we are doing this only for demonstration purposes, to understand how we can use Netty to implement an HTTP server.
我们的用例很简单,服务器将简单地把请求正文和查询参数(如果有的话)转换成大写字母。关于在响应中反映请求数据的问题,这里需要注意的是,我们这样做只是为了演示,以了解我们如何使用Netty来实现一个HTTP服务器。
Here, we’ll consume the message or request, and set up its response as recommended by the protocol (note that RequestUtils is something we’ll write in just a moment):
在这里,我们将消费消息或请求,并将其响应设置为协议所推荐的(注意,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);
}
}
As we can see, when our channel receives an HttpRequest, it first checks if the request expects a 100 Continue status. In that case, we immediately write back with an empty response with a status of CONTINUE:
我们可以看到,当我们的通道收到一个HttpRequest时,它首先检查该请求是否期望有100 Continue状态。在这种情况下,我们立即写回一个状态为CONTINUE的空响应。
private void writeResponse(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE,
Unpooled.EMPTY_BUFFER);
ctx.write(response);
}
After that, the handler initializes a string to be sent as a response and adds the request’s query parameters to it to be sent back as-is.
之后,处理程序初始化一个字符串,作为响应发送,并将请求的查询参数加入其中,按原样发回。
Let’s now define the method formatParams and place it in a RequestUtils helper class to do that:
现在让我们定义方法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;
}
Next, on receiving an HttpContent, we take the request body and convert it to upper case:
接下来,在收到一个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;
}
Also, if the received HttpContent is a LastHttpContent, we add a goodbye message and trailing headers, if any:
另外,如果收到的HttpContent是一个LastHttpContent,我们会添加一个告别信息和尾部头信息,如果有的话。
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. Writing the Response
3.3.撰写答复
Now that our data to be sent is ready, we can write the response to the ChannelHandlerContext:
现在我们要发送的数据已经准备好了,我们可以向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);
}
}
In this method, we created a FullHttpResponse with HTTP/1.1 version, adding the data we’d prepared earlier.
在这个方法中,我们创建了一个HTTP/1.1版本的FullHttpResponse,添加了我们之前准备的数据。
If a request is to be kept-alive, or in other words, if the connection is not to be closed, we set the response’s connection header as keep-alive. Otherwise, we close the connection.
如果一个请求是保持在线的,或者换句话说,如果连接不被关闭,我们将响应的connection头设置为keep-alive。否则,我们将关闭连接。
4. Testing the Server
4.测试服务器
To test our server, let’s send some cURL commands and look at the responses.
为了测试我们的服务器,让我们发送一些cURL命令并查看响应。
Of course, we need to start the server by running the class HttpServer before this.
当然,我们需要在这之前通过运行HttpServer类来启动服务器。
4.1. GET Request
4.1 GET请求
Let’s first invoke the server, providing a cookie with the request:
让我们首先调用服务器,在请求中提供一个cookie。
curl http://127.0.0.1:8080?param1=one
As a response, we get:
作为回应,我们得到了。
Parameter: PARAM1 = ONE
Good Bye!
We can also hit http://127.0.0.1:8080?param1=one from any browser to see the same result.
我们也可以从任何浏览器中点击http://127.0.0.1:8080?param1=one,看到同样的结果。
4.2. POST Request
4.2.POST请求
As our second test, let’s send a POST with body sample content:
作为我们的第二个测试,让我们发送一个正文为sample content的POST。
curl -d "sample content" -X POST http://127.0.0.1:8080
Here’s the response:
以下是答复。
SAMPLE CONTENT
Good Bye!
This time, since our request contained a body, the server sent it back in uppercase.
这一次,由于我们的请求包含一个主体,服务器以大写字母发回了它。
5. Conclusion
5.总结
In this tutorial, we saw how to implement the HTTP protocol, particularly an HTTP server using Netty.
在本教程中,我们看到了如何实现HTTP协议,特别是使用Netty的HTTP服务器。
HTTP/2 in Netty demonstrates a client-server implementation of the HTTP/2 protocol.
Netty中的HTTP/2演示了HTTP/2协议的客户-服务器实现。
As always, source code is available over on GitHub.
一如既往,源代码可在GitHub上获取。