Original blog address: pjmike’s blog

preface

This article introduces how to build an HTTP/HTTPS application using Netty, using a HelloWorld Demo

Introduction to SSL/TLS protocols

Since we are building HTTPS applications at the same time, we need to protect Netty applications by using SSL/TLS. Here is a brief introduction to the SSL/TLS protocol.

SSL and TLS are both transport layer security protocols. Their history is as follows:

  • 1995: SSL 2.0, proposed by Netscape, this version was not secure due to design flaws and was soon found to have serious vulnerabilities and has been scrapped
  • 1996: SSL 3.0, written as RFC, became popular and is now (as of 2015) insecure and must be disabled
  • 1999: TLS1.0 the Internet standardization organization (ISOC) replaces NetScape with TLS1.0, an updated version of SSL
  • 2006: TLS 1.1. Released as RFC 4346. Fixed CBC mode related bugs such as BEAST attacks
  • 2008: TLS 1.2. Released as RFC 5246. Improve security. The current (2015) version that should be deployed primarily, make sure you use this version
  • After 2015: TLS 1.3, still under development, supports 0-RTT, greatly improves security, and cuts encryption methods outside AEAD

Since both versions of SSL have been retired, SSL is now commonly referred to as TLS

The schematic diagram of SSL/TLS security protocols is as follows:

SSL/TLS protocol is an optional layer between HTTP layer and TCP layer. It provides the following services:

  • Authenticate users and servers to ensure that data is sent to the correct clients and servers
  • Encrypt data to prevent it from being stolen
  • Maintain data integrity and ensure that data is not changed during transmission

A more detailed description of the SSL/TLS protocol can be found in the relevant materials, but I won’t go into details here.

JDK javax.net.ssl package VS Netty OpenSSL/SSLEngine

To support SSL/TLS, Java provides the Javax.net.ssl package, whose SSLContext and SSLEngine classes make decryption and encryption fairly simple and efficient. SSLContext is the context for SSL links, and SSLEngine is primarily used for outbound and inbound byte stream operations.

Netty also provides an SSLEngine implementation using the OpenSSL toolkit, which provides better performance than the SSLEngine implementation provided by the JDK

Netty implements encryption and decryption through a ChannelHandler called SslHandler, where SslHandler uses SSLEngine internally to do the actual work. SSLEngine can be implemented using either the JDK’s SSLEngine or Netty’s OpenSslEngine. Netty’s OpenSslEngine is recommended because of its better performance. Decryption and encryption via SslHandler is shown below (from Netty In Action) :

In most cases, SslHandler will be the first ChannelHandler in a ChannelPipeline. This ensures that encryption takes place only after all the other ChannelHandlers have applied their logic to the data.

HTTP request and response components

HTTP is based on the request/response model: the client sends an HTTP request to the server, and the server returns an HTTP response. Netty provides a variety of encoders and decoders to simplify the use of this protocol.

The components of an HTTP request are shown below:

The components of an HTTP response are shown below:

As shown in the two figures above, an HTTP request/response may consist of multiple data parts, and it always ends with a LastHttpContent part. FullHttpRequest and FullHttpResponse messages are special subtypes that represent the complete request and response, respectively.

All types of HTTP messages implement the HttpObject interface

HTTP decoders, encoders and codecs

Netty provides encoders and decoders for HTTP messages:

  • HttpRequestEncoder: encoder, used by the client to send requests to the server
  • HttpResponseEecoder: encoder, used by the server to send responses to the client
  • HttpRequestDecoder: decoder, used by the server to receive requests from the client
  • HttpResponseDecoder: decoder used by the client to receive requests from the server

Codec:

  • HttpClientCodec: codec for the client, equivalent toHttpRequestEncoderandHttpResponseDecoderThe combination of
  • HttpServerCodec: codec for the server, equivalent toHttpRequsetDecoderandHttpResponseEncoderThe combination of

Take HttpServerCodec as an example, its class inheritance structure is shown as follows:

HttpServerCodec implements both ChannelInboundHandler and ChannelOutboundHandler interfaces to achieve both encoding and decoding capabilities.

Aggregator:

  • HttpObjectAggregator: aggregator that can combine multiple message parts intoFullHttpRequestorFullHttpResponseThe message. The reason for using this aggregator is that the HTTP decoder generates multiple message objects per HTTP message, such asHttpRequest/HttpResponse,HttpContent,LastHttpContent, using aggregators to aggregate them into a complete message content, so you don’t have to worry about message fragments.

Application code

Netty-based HTTP/HTTPS application build source code from the Netty official provided demo, I have made some changes, the original address is: https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld

The source code:

public class HttpHelloWorldServer {


    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", SSL ? "8443" : "8080"));

    public static void main(String[] args) throws Exception {
        final SslContext sslContext;
        // Check whether SSL is true, which means HTTPS connection is used; otherwise, HTTP is used
        if (SSL) {
            // Use the Netty certificate tool to generate a digital certificate
            SelfSignedCertificate certificate = new SelfSignedCertificate();
            sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build();
        } else {
            sslContext = null;
        }
        EventLoopGroup boss = new NioEventLoopGroup(1);
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            if(sslContext ! =null) {
                                pipeline.addLast(sslContext.newHandler(ch.alloc()));
                            }
                            // Add an HTTP codec
                            pipeline.addLast(new HttpServerCodec());
                            // Add the HTTP message aggregator
                            pipeline.addLast(new HttpObjectAggregator(64 * 1024));
                            // Add a custom server Handler
                            pipeline.addLast(newHttpHelloWorldServerHandler()); }}); ChannelFuture future = bootstrap.bind(PORT).sync(); System.err.println("Open your web browser and navigate to " +
                    (SSL? "https" : "http") + ": / / 127.0.0.1." + PORT + '/');

            future.channel().closeFuture().sync();
        } finally{ boss.shutdownGracefully().sync(); worker.shutdownGracefully().sync(); }}}Copy the code

Code reading

First check whether the system attribute SSL exists. If it does, it indicates that a secure connection is used; otherwise, a normal HTTP connection is used.

  final SslContext sslContext;
        if (SSL) {
            SelfSignedCertificate certificate = new SelfSignedCertificate();
            sslContext = SslContextBuilder.forServer(certificate.certificate(), certificate.privateKey()).build();
        } else {
            sslContext = null;
        }
Copy the code

As shown in the preceding code, when SSL is true, use the signature certificate tool provided by Netty to customize the digital certificate sent by the server to the client.

The ServerBootstrap startup class is created, the NioEventLoopGroup thread pool is set and bound, the server Channel is created, and the ChannelHandler is added. It is worth noting that the channelHandlers added are ALL HTTP-related handlers.

HttpHelloWorldServerHandler

The custom Handler code is as follows:

public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
    private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");
    private static final AsciiString CONNECTION = AsciiString.cached("Connection");
    private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest req = (HttpRequest) msg;
            System.out.println("Browser request mode:"+req.method().name());
            String content = "";
            if ("/hello".equals(req.uri())) {
                content = "hello world";
                response2Client(ctx,req,content);
            } else {
                content = "Connect the Server"; response2Client(ctx,req,content); }}}private void response2Client(ChannelHandlerContext ctx, HttpRequest req, String content) {
        boolean keepAlive = HttpUtil.isKeepAlive(req);
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content.getBytes()));
        response.headers().set(CONTENT_TYPE, "text/plain");
        response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
        if(! keepAlive) { ctx.write(response).addListener(ChannelFutureListener.CLOSE); }else{ response.headers().set(CONNECTION, KEEP_ALIVE); ctx.write(response); }}@Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}Copy the code

The incoming data stream is handled in this Handler, but this code only handles GET requests, not POST requests. So when the browser sends a GET request, this Handler defines an HTTP response body FullHttpResponse, sets some response headers, · Content-Type, Connection, content-Length, etc., set the response Content, and then write the HTTP message through ctx.write method

AsciiString

We use AsciiString when setting the response header. Since Netty 4.1, we have provided AsciiString that implements the CharSequence interface, which is the parent class of String. AsciiString contains only 1 byte of characters, which can save space when dealing with US-ASCII or ISO-8859-1 strings. For example, HTTP codecs use AsciiString to process header names, because there is no conversion cost to encoding AsciiString into ByteBuf. The internal implementation uses byte, whereas for strings, Internal holds char[]. Using String requires converting char to byte, so AsciiString has better performance than String.

test

Client tests:

Server logs:

summary

This summarizes how to build a simple HTTP/HTTPS application using Netty. Of course, the above procedure reference is Netty official Demo, Netty official also provides a lot of other examples, for entry learning is also good, detailed address is: https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example

References & acknowledgements

  • Overview of the SSL/TLS protocol operation mechanism
  • TLS protocol analysis and modern encrypted communication protocol design
  • Netty In Action