This article has been included in: github.com/Snailclimb/… Netty from start to action: Handwritten HTTP Server+RPC framework. Related projects: github.com/Snailclimb/… (a lightweight HTTP framework that mimics But differs from Spring Boot)

I am currently working on a lightweight HTTP framework called JsonCat for the built-in HTTP server, which I wrote myself based on Netty. All the core code adds up to just a few dozen lines. This is thanks to Netty’s various out-of-the-box components, which save us a lot of things.

In this article I will walk you through the implementation of a simple HTTP Server.

If there is any improvement or improvement in the article, please point it out in the comments section and make progress together!

Before we begin, in case you don’t know Netty, let’s introduce it briefly.

What is Netty?

Simply summarize Netty with 3 points.

  1. Netty is a NIO-based client-server framework that can be used to develop network applications quickly and easily.
  2. Netty greatly simplifies and optimizes network programming such as TCP and UDP socket servers, and in many ways improves performance and security.
  3. Netty supports multiple protocols such as FTP, SMTP, HTTP, and a variety of binary and text-based traditional protocols. Netty supports HTTP (Hypertext Transfer Protocol).

What are the application scenarios of Netty?

With their own understanding, say it briefly! In theory, everything NIO can do with Netty can be done better.

First, though, let’s be clear that Netty is primarily used for network communication.

  1. The network communication module of the framework: Netty can meet the network communication requirement of almost any scene, therefore, the network communication module of the framework can be done based on Netty. Take the RPC framework! In a distributed system, different service nodes often need to call each other, which requires RPC framework. How is communication between different service Pointers done? Then use Netty to do it! For example, if I call another node’s method, I should at least let the other party know which method in which class I am calling and the related parameters.
  2. Implement your own HTTP server: With Netty, you can easily implement a simple HTTP server with a small amount of code. Netty comes with its own codec and message aggregator, which saves a lot of work for our development!
  3. Implementing an INSTANT messaging system: Using Netty, we can implement an instant messaging system that can chat like wechat. There are many open source projects in this area, you can go to Github for a look.
  4. Implement message push system: there are a lot of message push systems on the market are based on Netty to do.
  5. .

Which open source projects use Netty?

Dubbo, RocketMQ, Elasticsearch, gRPC, Spring Cloud Gateway, and more all use Netty.

It’s safe to say that many open source projects use Netty, so knowing Netty will help you use these open source projects and give you the ability to re-develop them.

Netty. IO /wiki/relate… .

The prerequisites for implementing HTTP Server

Since we want to implement HTTP Server, we must first review the basic knowledge of HTTP protocol.

The HTTP protocol

HyperText Transfer Protocol (HTTP) is primarily designed for communication between Web browsers and Web servers.

When we use a browser to browse a web page, our web page is loaded through HTTP request, the whole process is shown in the figure below.

https://www.seobility.net/en/wiki/HTTP_headers

HTTP is based on TCP. Therefore, before sending an HTTP request, a TCP connection must be established, that is, three handshakes must be performed. Most of the HTTP protocols in use today are 1.1. In the 1.1 protocol, keep-alive is enabled by default so that connections can be reused across multiple requests.

Now that you know the HTTP protocol, let’s take a look at the content of HTTP packets. This part of the content is very important! (reference images from: iamgopikrishna.wordpress.com/2014/06/13/…).

HTTP request packet:

HTTP response packet:

Our HTTP server parses the CONTENT of THE HTTP request packet in the background, processes the packet content, and then returns the HTTP response packet to the client.

Netty codec

If we want to process HTTP requests through Netty, we need to encode and decode them first. Codec essentially translates HTTP requests and responses, such as HttpRequest and HttpContent, between ByteBuf and Netty, which Netty uses to transfer data.

Netty comes with four common codecs:

  1. HttpRequestEncoder(HTTP request encoder) : willHttpRequestHttpContentEncoded asByteBuf
  2. HttpRequestDecoder(HTTP request decoder) : willByteBufDecoding forHttpRequestHttpContent
  3. HttpResponsetEncoder(HTTP response encoder) : willHttpResponseHttpContentEncoded asByteBuf
  4. HttpResponseDecoder(HTTP response decoder) : willByteBufDecoding forHttpResponstHttpContent

Network traffic is ultimately carried by byte stream. ByteBuf is a byte container provided by Netty that contains an array of bytes. When we transfer data over Netty, we do it over ByteBuf.

The HTTP Server receives AN HTTP Request and then sends an HTTP Response. So we just need toHttpRequestDecoderHttpResponseEncoderCan.

I’ve drawn a picture, so it should be easier to understand.

Netty’s abstraction of HTTP messages

To represent various HTTP messages, Netty designs and abstracts a complete HTTP message structure. The core inheritance relationship is shown in the following figure.

  1. HttpObject: the uppermost interface of the entire HTTP messaging architecture.HttpObjectThere’s another one under the interfaceHttpMessageHttpContentTwo core interfaces.
  2. HttpMessage: Defines the HTTP message, asHttpRequestandHttpResponseProvide common attributes
  3. HttpRequest : HttpRequestCorresponding to HTTP request. throughHttpRequestWe can access Query Parameters and cookies. Unlike the Servlet API, the query parameters are passedQueryStringEncoderandQueryStringDecoderTo construct and parse the query query parameters.
  4. HttpResponseHttpResponseCorresponding to HTTP Response. andHttpMessageCompared to theHttpResponseAdded status (corresponding status code) property and corresponding methods.
  5. HttpContent : Block transfer encoding(Chunked transfer encoding) is a data transfer mechanism in the Hypertext Transfer Protocol (HTTP) (only available in HTTP/1.1) that allows HTTP data sent from an application server to a client application (usually a Web browser) to be broken up into “chunks” (in the case of large amounts of data). We can putHttpContentThink of it as this piece of data.
  6. LastHttpContent: indicates the end of the HTTP request and containsHttpHeadersObject.
  7. FullHttpRequestFullHttpResponseHttpMessageHttpContentThe aggregated object.

HTTP message aggregator

HttpObjectAggregator is an HTTP message aggregator provided by Netty. It can be used to aggregate HttpMessage and HttpContent into FullHttpRequest or FullHttpResponse(depending on whether the request is handled or the response is handled).

HttpObjectAggregator can aggregate these messages into a complete HttpObjectAggregator.

Usage: Add HttpObjectAggregator to the ChannelPipeline, after HttpResponseEncoder if it is used to handle HTTP requests, or HttpResponseEncoder if it is used to handle HTTP requests. Place it after HttpResponseDecoder if used to handle HTTP Response.

The HTTP Server is used to receive HTTP requests.

ChannelPipeline p = ... ; p.addLast("decoder".new HttpRequestDecoder())
  .addLast("encoder".new HttpResponseEncoder())
  .addLast("aggregator".new HttpObjectAggregator(512 * 1024))
  .addLast("handler".new HttpServerHandler());
Copy the code

Implement an HTTP Server based on Netty

With Netty, you can easily build a lightweight HTTP Server that handles GET and POST requests correctly with a little code.

Source code address: github.com/Snailclimb/… .

Add the required dependencies to pom.xml

As a first step, we need to add the third party dependent coordinates necessary to implement HTTP Server to POM.xml.

<! --netty-->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.42. The Final</version>
</dependency>
<! -- log -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.25</version>
</dependency>
<! -- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>
<! --commons-codec-->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>
Copy the code

Creating a Server

@Slf4j
public class HttpServer {

    private static final int PORT = 8080;

    public void start(a) {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // TCP enables the Nagle algorithm by default. This algorithm is used to send large data as fast as possible and reduce network transmission. The TCP_NODELAY parameter controls whether the Nagle algorithm is enabled.
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    // Whether to enable the TCP underlying heartbeat mechanism
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    // Indicates the maximum length of the queue used by the system to temporarily store requests that have completed the three-way handshake
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast("decoder".new HttpRequestDecoder())
                                    .addLast("encoder".new HttpResponseEncoder())
                                    .addLast("aggregator".new HttpObjectAggregator(512 * 1024))
                                    .addLast("handler".newHttpServerHandler()); }}); Channel ch = b.bind(PORT).sync().channel(); log.info("Netty Http Server started on port {}.", PORT);
            ch.closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("occur exception when start server:", e);
        } finally {
            log.error("shutdown bossGroup and workerGroup"); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }}}Copy the code

Let’s take a look at how the server creation process works.

1. Create twoNioEventLoopGroupObject instance:bossGroupworkerGroup.

  • bossGroup: Used to process TCP connection requests from clients.
  • workerGroup: Is responsible for processing logic of read/write data of each connection. It is responsible for I/O read/write operations and sends them to the corresponding Handler.

For example, we regard the boss of the company as the bossGroup and the employees as the workerGroup. After the bossGroup picks up the work outside, it throws it to the workerGroup to deal with. Typically we specify the number of bossGroup threads to be 1 (for small concurrent connections) and the number of workGroup threads to be CPU cores *2. Also, according to the source code, the default value for setting the number of threads using the no-argument constructor of the NioEventLoopGroup class is number of CPU cores *2.

2. Create a server boot/helper class:ServerBootstrapThis class will guide us through the server startup process.

Through 3..group()Method to the bootstrap classServerBootstrapConfigure two large thread groups, determine the thread model.

Through 4.channel()Method to the bootstrap classServerBootstrapIO model is specified asNIO

  • NioServerSocketChannel: Specifies the IO model of the server as NIO, and BIO programming modelServerSocketThe corresponding
  • NioSocketChannel: Specifies the CLIENT IO model as NIO, and BIO programming modelSocketThe corresponding

Through 5..childHandler()Create one for the bootstrap classChannelInitializer, and then specifies the business processing logic for the server message, which is customChannelHandlerobject

6. Call the bind() method of the ServerBootstrap class to bind the port.

// Bind () is asynchronous, but you can make it synchronous with the sync() method.
ChannelFuture f = b.bind(port).sync();
Copy the code

Custom server ChannelHandler processes HTTP requests

We inherit SimpleChannelInboundHandler, and rewrite the following three methods:

  1. channelRead(): a method invoked by the server to receive and process HTTP requests sent by the client.
  2. exceptionCaught(): This parameter is invoked when an exception occurs in the HTTP request sent by the client.
  3. channelReadComplete(): method invoked by the server after consuming the HTTP request sent by the client.

In addition, the client HTTP request parameter type is FullHttpRequest. We can think of the FullHttpRequest object as a Java object representation of an HTTP request message.

@Slf4j
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private static final String FAVICON_ICO = "/favicon.ico";
    private static final AsciiString CONNECTION = AsciiString.cached("Connection");
    private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
    private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
    private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) {
        log.info("Handle http request:{}", fullHttpRequest);
        String uri = fullHttpRequest.uri();
        if (uri.equals(FAVICON_ICO)) {
            return;
        }
        RequestHandler requestHandler = RequestHandlerFactory.create(fullHttpRequest.method());
        Object result;
        FullHttpResponse response;
        try {
            result = requestHandler.handle(fullHttpRequest);
            String responseHtml = "<html><body>" + result + "</body></html>";
            byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
            response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes));
            response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
            response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            String responseHtml = "<html><body>" + e.toString() + "</body></html>";
            byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
            response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, Unpooled.wrappedBuffer(responseBytes));
            response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
        }
        boolean keepAlive = HttpUtil.isKeepAlive(fullHttpRequest);
        if(! keepAlive) { ctx.write(response).addListener(ChannelFutureListener.CLOSE); }else{ response.headers().set(CONNECTION, KEEP_ALIVE); ctx.write(response); }}@Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); }}Copy the code

The body of the message we return to the client is the FullHttpResponse object. Through the FullHttpResponse object, we can set the HTTP protocol version of the HTTP response message and the specific content of the response.

We can think of the FullHttpResponse object as a Representation of a Java object for an HTTP response message.

FullHttpResponse response;

String responseHtml = "<html><body>" + result + "</body></html>";
byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
// Initialize FullHttpResponse and set the HTTP protocol, response status code, and response content
response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes));
Copy the code

We get HttpHeaders using the headers() method of FullHttpResponse, where HttpHeaders corresponds to the header of the HTTP response packet. Using the HttpHeaders object, we can set the Content of the HTTP response header such as Content-TYp.

response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
Copy the code

In this case, to disguise the fact that we set the content-type to text/ HTML, we return data in HTML format to the client.

Common the content-type

Content-Type explain
text/html HTML format
text/plain Plain text format
text/css CSS format
text/javascript Js format
application/json Json format (commonly used for front and back separated projects)
image/gif GIF image format
image/jpeg JPG image format
image/png PNG image format

The concrete processing logic implementation of the request

Because there are POST requests and GET requests. So we need to first define an interface to handle HTTP requests.

public interface RequestHandler {
    Object handle(FullHttpRequest fullHttpRequest);
}
Copy the code

HTTP methods include not only GET and POST, but also PUT, DELETE, and PATCH. However, the HTTP Server implemented in this case only considers GET and POST.

  • GET: Requests to GET a specific resource from the server. Here’s an example:GET /classes(Get all classes)
  • POST: Creates a new resource on the server. Here’s an example:POST /classes(Create class)
  • PUT: Updates the resource on the server (the client provides the updated entire resource). Here’s an example:PUT /classes/12(Update class 12)
  • DELETE: Deletes a specific resource from the server. Here’s an example:DELETE /classes/12(delete class no. 12)
  • PATCH: Updates resources on the server (the client provides the changed properties, which can be regarded as partial updates). It is rarely used, so there are no examples here.

Processing of GET requests

@Slf4j
public class GetRequestHandler implements RequestHandler {
    @Override
    public Object handle(FullHttpRequest fullHttpRequest) {
        String requestUri = fullHttpRequest.uri();
        Map<String, String> queryParameterMappings = this.getQueryParams(requestUri);
        return queryParameterMappings.toString();
    }

    private Map<String, String> getQueryParams(String uri) {
        QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8));
        Map<String, List<String>> parameters = queryDecoder.parameters();
        Map<String, String> queryParams = new HashMap<>();
        for (Map.Entry<String, List<String>> attr : parameters.entrySet()) {
            for(String attrVal : attr.getValue()) { queryParams.put(attr.getKey(), attrVal); }}returnqueryParams; }}Copy the code

I simply return the URI query parameters directly to the client.

In fact, given the mapping of query parameters to urIs, and the knowledge of reflection and annotations, it’s easy to implement @RequestParam annotations similar to Spring Boot.

If you want to learn, you can do it yourself. If you don’t know how to do this, you can refer to my open source lightweight HTTP framework jSONCat (a lightweight HTTP framework that mimics But differs from Spring Boot).

Processing of POST requests

@Slf4j
public class PostRequestHandler implements RequestHandler {

    @Override
    public Object handle(FullHttpRequest fullHttpRequest) {
        String requestUri = fullHttpRequest.uri();
        log.info("request uri :[{}]", requestUri);
        String contentType = this.getContentType(fullHttpRequest.headers());
        if (contentType.equals("application/json")) {
            return fullHttpRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
        } else {
            throw new IllegalArgumentException("only receive application/json type data"); }}private String getContentType(HttpHeaders headers) {
        String typeStr = headers.get("Content-Type");
        String[] list = typeStr.split(";");
        return list[0]; }}Copy the code

For POST requests, we only accept data whose Content-type is Application/JSON. If the POST request does not send application/ JSON data, we directly throw an exception.

In fact, once we have the JSON-formatted data from the client, we can easily implement a Spring Boot-like @RequestBody annotation with our knowledge of reflection and annotations.

If you want to learn, you can do it yourself. If you don’t know how to do this, you can refer to my open source lightweight HTTP framework jSONCat (a lightweight HTTP framework that mimics But differs from Spring Boot).

Request processing factory class

public class RequestHandlerFactory {
    public static final Map<HttpMethod, RequestHandler> REQUEST_HANDLERS = new HashMap<>();

    static {
        REQUEST_HANDLERS.put(HttpMethod.GET, new GetRequestHandler());
        REQUEST_HANDLERS.put(HttpMethod.POST, new PostRequestHandler());
    }

    public static RequestHandler create(HttpMethod httpMethod) {
        returnREQUEST_HANDLERS.get(httpMethod); }}Copy the code

I’m using factory mode here. When we’re working with the new HTTP Method, we just implement the RequestHandler interface and add the implementation class to the RequestHandlerFactory.

Start the class

public class HttpServerApplication {
    public static void main(String[] args) {
        HttpServer httpServer = newHttpServer(); httpServer.start(); }}Copy the code

The effect

Run the main() method of HttpServerApplication, and the console prints:

[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a] REGISTERED [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a] BIND: [nioEventLoopGroup 0.0.0.0/0.0.0.0:8080-2-1] INFO io.net. Ty handler. Logging. LoggingHandler - [id: 0x9bb1012a, L:/0:0:0:0:0:0:0:0:8080] ACTIVE [main] INFO server.HttpServer - Netty Http Server started on port 8080.Copy the code

A GET request

A POST request

reference

  1. Netty Learning Notes – HTTP Objects

My open source project recommendation

  1. JavaGuide: “Java Learning + Interview Guide” covers the core knowledge that most Java programmers need to master. Prepare for a Java interview, preferably JavaGuide!
  2. Guide xml-rpc – framework: Implemented by Netty+Kyro+Zookeeper A Custom RPC Framework Implemented by Netty+Kyro+Zookeeper
  3. Jsoncat: a lightweight HTTP framework that mimics but differs from Spring Boot
  4. Advancement: Good Habits for Programmers + Essentials for Interviews
  5. Springboot-guide: Not only SpringBoot but also important knowledge of Spring
  6. Awesome – Java: Collection of Awesome Java Project on Github