Implementation of a simple server supporting http and webSocket protocol based on netty (including source code analysis of XXL job communication module)

background

Last time I looked at XXL job, I found that his communication mechanism was to implement an http server based on Netty. Then I found that I didn't understand it very well, so I planned to implement a simple server supporting http protocol and webSocket protocol to help me understand it

rely on

		<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.13</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>5.0.0.Alpha1</version>
        </dependency>

Here, I mainly use 5.x for my netty version. I recommend 4.x for netty, because netty 5.x seems to have been abandoned by the author of netty. 5.x and 4.x versions may have different APIs

Package structure

Implement WebSocketServer

  • WebSocketServer.java
@Slf4j
public class WebSocketServer {

    // /Users/weihu/Desktop/sofe/java/netty-student/netty-websocket/src/main/resources/WebSocketServer.html
    public static void main(String[] args) throws Exception{
        int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080;
        new WebSocketServer().run(port);
    }

    public void run(int port) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("http-codec", new HttpServerCodec()) // http codec processor
                                    // http multiple message parts are combined into a complete http message
                                    .addLast("aggregator", new HttpObjectAggregator(65536))
                                    // It supports sending html5 messages to the client. It is mainly used to support websocket communication between the browser and the server. If it is only an http service, the processor is not required
                                    .addLast("http-chunked", new ChunkedWriteHandler())
                                    // Core business logic processor
                                    .addLast("handler", new WebSocketServerHandler());
                        }
                    });
            Channel channel = bootstrap.bind(port).sync().channel();
            log.info("Web socket or http server started at port: {}", port);
            log.info("open your browser and navigate to http://localhost:{}/",port);
            channel.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

}

The code here is actually a fixed set of templates. Of course, if you want to optimize some network related parameters, you can see that the core business logic of receiving and processing is in the WebSocketServerHandler class

Business handler WebSocketServerHandler

  • WebSocketServerHandler.java
@Slf4j
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {

    private WebSocketServerHandshaker handshaker;

    @Override
    public void messageReceived(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // Traditional HTTP access
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        }
        // WebSocket access
        else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    private void handleHttpRequest(ChannelHandlerContext ctx,
                                   FullHttpRequest req) throws Exception {
        log.info("Deal with it here http request");
        // If http decoding fails, an error is returned
        if (!req.getDecoderResult().isSuccess()) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.BAD_REQUEST));
            return;
        }

        // If it is websocket handshake
        if (("websocket".equals(req.headers().get("Upgrade")))) {
            WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                    "ws://localhost:8080/websocket", null, false);
            handshaker = wsFactory.newHandshaker(req);
            if (handshaker == null) {
                WebSocketServerHandshakerFactory
                        .sendUnsupportedWebSocketVersionResponse(ctx.channel());
            } else {
                handshaker.handshake(ctx.channel(), req);
            }
            return;
        }
        // http request
        String uri = req.getUri();
        Map<String,String> resMap = new HashMap<>();
        resMap.put("method",req.getMethod().name());
        resMap.put("uri",uri);
        String msg = "<html><head><title>test</title></head><body>Your request is:" + JSON.toJSONString(resMap) +"</body></html>";
        // Create http response
        FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
        // Set header information
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8");
        // Write HTML to client
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);


    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx,
                                      WebSocketFrame frame) {

        // Determine whether it is an instruction to close the link
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(),
                    (CloseWebSocketFrame) frame.retain());
            return;
        }
        // Determine whether it is a Ping message
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(
                    new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // This routine only supports text messages, not binary messages
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(String.format(
                    "%s frame types not supported", frame.getClass().getName()));
        }

        // Return reply message
        String request = ((TextWebSocketFrame) frame).text();
        log.info("{} receiver {}", ctx.channel(), request);
        ctx.channel().write(
                new TextWebSocketFrame(request
                        + " , Welcome Netty WebSocket Service, now:"
                        + DateUtil.now()));
    }

    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req, FullHttpResponse res) {
        // Return reply to client
        if (res.getStatus().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(),
                    CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpHeaders.setContentLength(res, res.content().readableBytes());
        }

        // If it is not keep alive, close the connection
        ChannelFuture f = ctx.channel().writeAndFlush(res);
        if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

}

Here, in order to facilitate the testing of websock, a simple html page is written here

  • WebSocketServer.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    Netty WebSocket time server
</head>
<br>
<body>
<br>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket)
    {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8080/websocket");
        socket.onmessage = function(event) {
            var ta = document.getElementById('responseText');
            ta.value="";
            ta.value = event.data
        };
        socket.onopen = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = "open WebSocket The service is normal and the browser supports it WebSocket!";
        };
        socket.onclose = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = "";
            ta.value = "WebSocket close!";
        };
    }
    else
    {
        alert("Sorry, your browser doesn't support it WebSocket agreement!");
    }

    function send(message) {
        if (!window.WebSocket) { return; }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        }
        else
        {
            alert("WebSocket The connection was not established successfully!");
        }
    }
</script>
<form onsubmit="return false;">
    <input type="text" name="message" value="Netty Best practices"/>
    <br><br>
    <input type="button" value="send out WebSocket Request message" onclick="send(this.form.message.value)"/>
    <hr color="blue"/>
    <h3>Reply message returned by the server</h3>
    <textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>

test

We directly run the main method of WebSocketServer without passing in the port number. The default is 8080

We first test the processing of http requests and access them directly
http://localhost:8080/index?query=1

You can see that the processing is successful
Then let's try the WebSocket test
We directly enter the absolute path of WebSocketServer.html in the browser

You can see that the WebSocket connection is normal. Next, let's try sending messages

You can see that the client successfully received the data returned by the server
Let's look at the server's log

You can see that the message sent by the client has also been successfully received

http based on netty in XXL job source code

Above, we simply implement a demo of http and WebSocket. Let's take a brief look at how it is implemented in the XXL job source code

The core entry is in the EmbedServer class. Let's analyze it briefly

You can see the first two standard eventloopgroups

Then you can see that the added handler is similar to the demo we implemented above. The difference is that it only supports http, so it doesn't
ChunkedWriteHandler is a handler, but it has an IdleStateHandler. Netty's IdleStateHandler is mainly used for heartbeat mechanism to detect whether the remote end is alive. If it is not alive or active, it will process idle Socket connections to avoid waste of resources

Here, the core implementation of his http request is placed in the EmbedHttpServerHandler class. Let's take a look at this class

EmbedHttpServerHandler is a static internal class of EmbedServer. Similar to the WebSocketServerHandler we implemented, the difference is that firstly, it inherits the SimpleChannelInboundHandler and specifies the generic type as FullHttpRequest to only process http. Secondly, because the network version used by XXL job is 4.x, the abstract method it needs to implement is also changed to

        protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
}

Let's look at the implementation of the channelRead0 method

We can see that it is no different from our implementation. The difference is that a thread pool is opened for processing requests, and the core processing logic is in process

You can see that it is also very simple. If the post request is not directly supported, then some token verification is added, and then the request data is converted into java classes for some business logic processing, and then returned

So far, the communication source code of XXL job has been roughly analyzed

summary

It can be seen that if we do not need to customize the protocol, the overall out of the box implementation based on netty is very convenient. Let's focus more on the processing of business logic. If we want to customize the message body, add some codec, semi packet processing, etc., it is still more troublesome, and it is easier to implement simple http requests

reference resources

  • Netty authoritative guide
  • XXL job source code

Tags: Netty http websocket xxl-job

Posted on Wed, 06 Oct 2021 21:04:43 -0400 by OopyBoo