SpringBoot integrates Netty + Websocket to realize NIO communication

Keywords: Netty JSON SpringBoot codec

Long-connection services are needed in recent projects to specifically integrate Netty+Websocket. Our system needs to actively push order messages to users, and the function of forcing users to go offline also needs long connections to push messages.

I. Preparations

Here's Netty's introduction: https://www.jianshu.com/p/b9f3f6a16911
Some basic concepts must be understood, such as BIO, NIO, AIO, multiplexing, Channel (equivalent to a connection), pipeline and so on.

Environmental Science:

  • JDK8
  • SpringBoot - 2.1.5.RELEASE

Dependence:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.25.Final</version>
</dependency>

2. Upper Code

* It will contain some business code
Project structure:

WebSocketServer

@Component
@Slf4j
public class WebSocketServer {

    /**
     * The main thread group is responsible for receiving requests
     */
    private EventLoopGroup mainGroup;
    /**
     * The slave thread group is responsible for processing requests. The master-slave thread group here is a typical idea of multiplexing.
     */
    private EventLoopGroup subGroup;
    /**
     * starter
     */
    private ServerBootstrap server;
    /**
     * future will be notified when an operation is completed, whether or not it succeeds.
     */
    private ChannelFuture future;

    /**
     * Single WbSocket Server
     */
    private static class SingletonWsServer {
        static final WebSocketServer instance = new WebSocketServer();
    }

    public static WebSocketServer getInstance() {
        return SingletonWsServer.instance;
    }


    public WebSocketServer() {
        mainGroup = new NioEventLoopGroup();
        subGroup = new NioEventLoopGroup();
        server = new ServerBootstrap();
        server.group(mainGroup, subGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new WwbSocketServerInitialize());//Custom initialization class to register processors in the pipeline
    }

    public void start() {
        this.future = server.bind(8088);
        log.info("| Netty WebSocket Server After startup, listen on port: 8088 | ------------------------------------------------------ |");
    }
}

WwbSocketServerInitialize
Each request to a service connection is processed once by these registered handlers, similar to interceptors, which are equivalent to a product going through a pipeline and being processed by the workers on the pipeline.

public class WwbSocketServerInitialize extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
    	//Define Pipeline
        ChannelPipeline pipeline = socketChannel.pipeline();
        //Define the number of processors in the pipeline
        //HTTP codec processor HttpRequestDecoder, HttpResponseEncoder
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new ChunkedWriteHandler());
        // Aggregate httpMessage to Full HttpRequest or Full HttpResponse
        pipeline.addLast(new HttpObjectAggregator(1024 * 64));
        // Increase heartbeat support
        // For the client, if the read-write heartbeat (ALL) is not sent to the server at 1 minute, the active disconnection occurs.
        pipeline.addLast(new IdleStateHandler(60, 60, 60));
        pipeline.addLast(new HeartBeatHandler());//Custom Heart Processor

        // ====================== The following is support for HTTP Websocket======================
        /**
         * websocket A protocol for server processing that specifies a route for client connection access: / ws
         * For websocket s, frames are used for transmission, and the corresponding frames for different data types are also different.
         */
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        // Customized Business Processing handler
        pipeline.addLast(new NoMaybeHandler());
    }
}

HeartBeatHandler
Heart beat support, if the server does not receive the heartbeat of the client for a period of time, disconnect the connection actively to avoid wasting resources.

@Slf4j
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // Determine whether evt is an IdleStateEvent (used to trigger user events, including read/write/read/write idle)
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.info("Enter Reading Freedom...");
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("Enter Writing Freedom...");
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Turn off useless Channel,In case of waste of resources. Channel Id: {}", ctx.channel().id());
                Channel channel = ctx.channel();
                channel.close();
                UserChannelRelation.remove(channel);
                log.info("Channel After closing, client The number is:{}", NoMaybeHandler.clients.size());
            }
        }
    }
}

The most critical business processing Handler - NoMaybeHandler
According to their own business needs, the message requesting to the server is processed.

@Slf4j
public class NoMaybeHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    /**
     * Manage channel channels for all clients
     */
    public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        //Get the message from the client
        String content = textWebSocketFrame.text();
        Channel currentChannel = channelHandlerContext.channel();
        try {
            //Converting messages into POJOs
            WsDataContent wsDataContent = JacksonUtils.stringToObject(content, WsDataContent.class);
            if (wsDataContent == null) {
                throw new RuntimeException("Connection request parameter error!");
            }
            Integer action = wsDataContent.getAction();
            String msgId = wsDataContent.getMsgId();
            //Judge the message type and handle different business according to different types
            if (action.equals(MsgActionEnum.CONNECT.type)) {
                //When Websocket is first established, initialize Channel and associate Channel with userId
                UserWebsocketSalt userWebsocketSalt = wsDataContent.getSalt();
                if (userWebsocketSalt == null || userWebsocketSalt.getUserId() == null) {
                    //Active disconnection
                    writeAndFlushResponse(MsgActionEnum.BREAK_OFF.type, msgId, createKickMsgBody(), currentChannel);
                    //currentChannel.close();
                    return;
                }
                String userId = userWebsocketSalt.getUserId();
                //We use loginLabel tag and long connection message to do single sign-on, kick device offline, and ignore the middle business code. Here we mainly deal with binding userId to Channel, which exists in Map - "UserChannelRelation. put (userId, current Channel)
                String loginLabel = userWebsocketSalt.getLoginLabel();
                Channel existChannel = UserChannelRelation.get(userId);
                if (existChannel != null) {
                    //There is a connection for the current user to verify the login tag
                    LinkUserService linkUserService = (LinkUserService) SpringUtil.getBean("linkUserServiceImpl");
                    if (linkUserService.checkUserLoginLabel(userId, loginLabel)) {
                        //It's the same login tag, add a new connection, close the old connection
                        UserChannelRelation.put(userId, currentChannel);
                        writeAndFlushResponse(MsgActionEnum.BREAK_OFF.type, null, createKickMsgBody(), existChannel);
                        writeAndFlushResponse(MsgActionEnum.MESSAGE_SIGN.type, msgId, null, currentChannel);
                        //existChannel.close();
                    } else {
                        //Not the same login label, refuse to connect
                        writeAndFlushResponse(MsgActionEnum.BREAK_OFF.type, null, createKickMsgBody(), currentChannel);
                        //currentChannel.close();
                    }
                } else {
                    UserChannelRelation.put(userId, currentChannel);
                    writeAndFlushResponse(MsgActionEnum.MESSAGE_SIGN.type, msgId, null, currentChannel);
                }
            } else if (action.equals(MsgActionEnum.KEEPALIVE.type)) {
                //Heartbeat type message
                log.info("Received from Channel by{}Pericardium......", currentChannel);
                writeAndFlushResponse(MsgActionEnum.MESSAGE_SIGN.type, msgId, null, currentChannel);
            } else {
                throw new RuntimeException("Connection request parameter error!");
            }
        } catch (Exception e) {
            log.debug("Current connection error! Close the current Channel!");
            closeAndRemoveChannel(currentChannel);
        }
    }

    /**
     * Response Client
     */
    public static void writeAndFlushResponse(Integer action, String msgId, Object data, Channel channel) {
        WsDataContent wsDataContent = new WsDataContent();
        wsDataContent.setAction(action);
        wsDataContent.setMsgId(msgId);
        wsDataContent.setData(data);
        channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(wsDataContent)));
    }

    /**
     * Constructing Forced Offline Message Body
     *
     * @return
     */
    public static PushMessageData createKickMsgBody() {
        PushMessageData pushMessageData = new PushMessageData();
        pushMessageData.setMsgType(MessageEnums.MsgTp.ClientMsgTp.getId());
        pushMessageData.setMsgVariety(MessageEnums.ClientMsgTp.FORCED_OFFLINE.getCode());
        pushMessageData.setTime(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli());
        pushMessageData.setMsgBody(null);
        return pushMessageData;
    }

    /**
     * Construct the dispatch singleton message body
     *
     * @return
     */
    public static PushMessageData createDistributeOrderMsgBody(String orderId) {
        PushMessageData pushMessageData = new PushMessageData();
        pushMessageData.setMsgType(MessageEnums.MsgTp.OrderMsgTp.getId());
        pushMessageData.setMsgVariety(MessageEnums.OrderMsgTp.PUSH_CODE_ORDER_ROB.getCode());
        pushMessageData.setTime(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli());
        MsgBodyVO msgBodyVO = new MsgBodyVO(orderId);
        pushMessageData.setMsgBody(msgBodyVO);
        return pushMessageData;
    }

    /**
     * When the client connects to the server (open the connection)
     * Get the channel of the client and put it in Channel Group for management
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("The client establishes the connection. Channel Id by:{}", ctx.channel().id().asShortText());
        clients.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //When handler Remove is triggered, ChannelGroup automatically removes the channel from the corresponding client.
        Channel channel = ctx.channel();
        clients.remove(channel);
        UserChannelRelation.remove(channel);
        log.info("The client disconnects. Channel Id by:{}", channel.id().asShortText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //Close the connection (close channel) after an exception occurs, and then remove it from ChannelGroup
        Channel channel = ctx.channel();
        cause.printStackTrace();
        channel.close();
        clients.remove(channel);
        UserChannelRelation.remove(channel);
        log.info("The connection is abnormal. Channel Id by:{}", channel.id().asShortText());
    }

    /**
     * Close Channel
     *
     * @param channel
     */
    public static void closeAndRemoveChannel(Channel channel) {
        channel.close();
        clients.remove(channel);
    }
}

UserChannelRelation
Map stores the corresponding relationship between userId and Channel

public class UserChannelRelation {

    private static Logger logger = LoggerFactory.getLogger(UserChannelRelation.class);

    private static HashMap<String, Channel> manager = new HashMap<>();

    public static void put(String userId, Channel channel) {
        manager.put(userId, channel);
    }

    public static Channel get(String userId) {
        return manager.get(userId);
    }

    public static void remove(String userId) {
        manager.remove(userId);
    }

    public static void output() {
        for (HashMap.Entry<String, Channel> entry : manager.entrySet()) {
            logger.info("UserId:{},ChannelId{}", entry.getKey(), entry.getValue().id().asLongText());
        }
    }

    /**
     * Remove Channel
     *
     * @param channel
     */
    public static void remove(Channel channel) {
        for (Map.Entry<String, Channel> entry : manager.entrySet()) {
            if (entry.getValue().equals(channel)) {
                manager.remove(entry.getKey());
            }
        }
    }
}

Message Type Enumeration MsgAction Enum

public enum MsgActionEnum {

    /**
     * Websocket Message type, WsDataContent.action
     */
    CONNECT(1, "Client Initialization to Establish Connections"),
    KEEPALIVE(2, "Client keeps heartbeat"),
    MESSAGE_SIGN(3, "Client Connection Request-Server-side response-Message signing"),
    BREAK_OFF(4, "Active disconnection of server"),
    BUSINESS(5, "Server Actively Pushes Business Messages"),
    SEND_TO_SOMEONE(9, "Send a message to sb.(Used for communication testing)");

    public final Integer type;
    public final String content;

    MsgActionEnum(Integer type, String content) {
        this.type = type;
        this.content = content;
    }

    public Integer getType() {
        return type;
    }
}

Message Body WsDataContent

@Data
public class WsDataContent implements Serializable {

    private static final long serialVersionUID = 5128306466491454779L;

    /**
     * Message type
     */
    private Integer action;
    /**
     * msgId
     */
    private String msgId;
    /**
     * Parameters required to initiate a connection
     */
    private UserWebsocketSalt salt;
    /**
     * data
     */
    private Object data;
}

UserWebsocketSalt
Client resume connection is the required parameter, userId

@Data
public class UserWebsocketSalt {

    /**
     * userId
     */
    private String userId;

    /**
     * loginLabel Current login tag
     */
    private String loginLabel;
}

Each request will be processed by the channelRead0 method, and the message from the front end will be sent back - here is the agreed Json string, which will be converted into the corresponding entity class, and then the business operation will be carried out.

  • For each connection request or message communication of the client, the server must respond. So in WsDataContent, a msgId is defined. When receiving a message, it must respond to the message signature, return a unified msgId, or respond actively to disconnect.
  • Client initiates the connection. We correspond the Channel of the connection to userId, which exists in Map (Custom User Channel Relation class). If we want to send a message to a user, we only need to get the corresponding Channel according to userId, and then through channel. writeAndFlush (new TextWebSocket Frame ("message-Json string"). Method, you can send a message to the user.
  • The heartbeat connection of the client accepts the heartbeat request of the client, does not do any operation, just responds to it, and the server receives the heartbeat, which is similar to shaking hands. The server should also actively detect the heartbeat and actively shut down the Channel beyond the specified time. It's pipeline. addLast (new IdleStateHandler (60, 60, 60) configured in WwbSocket Server Initialize); heartbeat time.
  • Client business type messages are processed in combination with business scenarios.

Finally, let the Websocket service start NettyNIOServer with the application

@Component
public class NettyNIOServer implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {

            try {
                WebSocketServer.getInstance().start();
            } catch (Exception e) {
                e.printStackTrace();
            }

    }
}

Some of the business code is not pasted up, it does not affect.

Posted by WakeAngel on Sat, 07 Sep 2019 03:38:53 -0700