SpringBoot integrates Netty to realize message push, receive and automatic reconnection

Netty

background

I have always wanted to do a Netty project before, integrating functions such as message sending, message receiving, active push, identity verification, and automatic reconnection, which are convenient for direct use in actual project use. Because the time issue has been delayed until now o(╯□╰)o, just recently the company has related business functions, and it has been vigorously promoted by itself, so today it will be completed in one go while the iron is hot, and it can be used directly at that time.

introduce

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high-performance protocol servers and clients.

Netty is an NIO client-server framework that enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming, such as TCP and UDP socket servers.

Simple does not mean that the resulting application will suffer from maintainability or performance issues. Netty was carefully designed from experience gained from implementing many protocols such as FTP, SMTP, HTTP, and various legacy binary and text-based protocols. So Netty managed to find a way to achieve ease of development, performance, stability and flexibility without compromise.

characteristic

design

1. Unified interface for multiple transport types - blocking and non-blocking

2. Simple but more powerful threading model

3. True connectionless datagram socket support

4. Link logic supports multiplexing

performance

1. Better throughput and lower latency than the core Java API

2. Less resource consumption, thanks to shared pool and reuse

3. Reduce memory copy

Safety

1. Full SSL/TLS and StartTLS support

2. Run in a restricted environment such as Applet or OSGI

combat

First go to the spring official website to initialize and download a SpringBoot project, and add three submodules: commonclientserver

server: Netty service provider

client: Netty service connector

common: general package, tool class

Add Netty dependency information, version 5 is not recommended , because version 4 is no longer officially supported, so version 4 is selected

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

server

1. Write Netty service configuration

package com.whl.mq.server;

import com.whl.mq.handle.NettyServerChannelInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * @author hl.Wu
 *
 * @date 2022/07/17
 * @description: netty server info
 **/
@Slf4j
@Component
public class NioNettyServer {

    @Value("${nio.netty.server.port: 8099}")
    private int port;
    @Autowired
    private NettyServerChannelInitializer nettyServerChannelInitializer;

    @Async
    public void start() {
        log.info("start to netty server port is {}", port);
        // 接收连接
        EventLoopGroup boss = new NioEventLoopGroup();
        // 处理信息
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 定义server
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 添加分组
            serverBootstrap.group(boss, worker)
                // 添加通道设置非阻塞
                .channel(NioServerSocketChannel.class)
                // 服务端可连接队列数量
                .option(ChannelOption.SO_BACKLOG, 128)
                // 开启长连接
                .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
                // 流程处理
                .childHandler(nettyServerChannelInitializer);
            // 绑定端口
            ChannelFuture cf = serverBootstrap.bind(port).sync();
            // 优雅关闭连接
            cf.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("connection error",e.getMessage(), e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

}

2. Write business processing flow NettyServerChannelInitializer

package com.whl.mq.handle;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Component
@Slf4j
public class NettyServerChannelInitializer extends ChannelInitializer {
    @Autowired
    private ClientMessageHandler clientMessageHandler;
    @Autowired
    private ClientUserHandler clientUserHandler;
    @Override
    protected void initChannel(Channel channel) {
        // 设置编码类型
        channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
        // 设置解码类型
        channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
        // 用户校验处理逻辑
        channel.pipeline().addLast("ClientTokenHandler", clientUserHandler);
        // 通过校验最终消息业务处理
        channel.pipeline().addLast("ClientMessageHandler",clientMessageHandler);
    }
}

3. Write user Token identity information verification logic

ChannelHandler.Sharable: If an annotation ChannelHandleris used @Sharable, only one instance can bootstrapbe created in it, and it can be added to one or more pipeline中without competition, which can reduce the sum handlerof the same class , save resources and improve efficiency.newGC

package com.whl.mq.handle;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.common.bo.ResponseBO;
import com.google.common.base.Preconditions;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Slf4j
@Component
@ChannelHandler.Sharable
public class ClientUserHandler extends SimpleChannelInboundHandler<String> {

    /**
     * k:token,v:userId
     */
    public static Map<String,String> userMap = new ConcurrentHashMap<>();

    /**
     * 通道信息 k:userId v:通道信息
     */
    public Map<String, Channel> channelMap = new ConcurrentHashMap<>();

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) {
        // 解析消息对象 校验token信息
        log.info("receive message info is {}", msg);
        String token = (String)JSONUtil.getByPath(JSONUtil.parse(msg), MessageBO.Fields.token);
        // 校验token是否失效
        // TODO 根据业务场景添加身份校验
        Preconditions.checkArgument(userMap.containsKey(token),"抱歉,Token已失效");
        if(!channelMap.containsKey(userMap.get(token))){
            channelMap.put(userMap.get(token), ctx.channel());
        }
        // 开启访问后面的处理逻辑
        ctx.fireChannelRead(msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Channel channel = ctx.channel();
        ResponseBO resp = ResponseBO.builder()
            .code(HttpStatus.FORBIDDEN.toString())
            .success(Boolean.FALSE)
            .build();
        // 返回错误code
        if(cause instanceof IllegalArgumentException){
            resp.setMessage(cause.getMessage());
            channel.writeAndFlush(JSON.toJSONString(resp));
            log.warn("Token 校验未通过,{}", channel.localAddress());
        }
    }
}

4. Write the final processing logic of the ClientMessageHandler message

package com.whl.mq.handle;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.common.bo.ResponseBO;
import io.netty.channel.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Component
@Slf4j
@ChannelHandler.Sharable
public class ClientMessageHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) {
        log.info("接收到的信息数据={}", msg);
        // 返回请求结果
        String requestUid = (String)JSONUtil.getByPath(JSONUtil.parse(msg), MessageBO.Fields.requestUid);
        ResponseBO resp = ResponseBO
            .builder()
            .requestUid(requestUid)
            .code(HttpStatus.OK.toString())
            .success(Boolean.TRUE)
            .message("请求成功")
            .build();
        Channel channel = ctx.channel();
        channel.writeAndFlush(JSONUtil.toJsonStr(resp));
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        Channel channel = ctx.channel();
        ResponseBO resp = ResponseBO.builder()
            .code(HttpStatus.INTERNAL_SERVER_ERROR.toString())
            .success(Boolean.FALSE)
            .build();
        // 返回错误code
        if(cause instanceof IllegalArgumentException){
            resp.setMessage(cause.getMessage());
            channel.writeAndFlush(JSON.toJSONString(resp));
            log.warn("业务异常请排查, ;{}", cause.getMessage(), cause);
        }
        log.error("error message {}",cause.getMessage(),cause);
    }

}

5. Write the interface control class that provides Token externally

package com.whl.mq.controller;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.whl.mq.handle.ClientUserHandler;
import io.netty.channel.Channel;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理")
public class UserController {
    @Autowired
    private ClientUserHandler clientUserHandler;

    @GetMapping("/token")
    @ApiOperation("获取token信息")
    public String getToken(){
        String token = IdUtil.fastSimpleUUID();
        ClientUserHandler.userMap.put(token,token);
        return token;
    }

    @PostMapping("/tips")
    @ApiOperation("发送提醒")
    public void sendToClient(@RequestParam("tips") String tips, @RequestParam("userId") String userId){
        Map<String, Channel> channelMap = clientUserHandler.channelMap;
        Channel channel = channelMap.get(userId);
        if(ObjectUtil.isNotNull(channel)){
            channel.writeAndFlush(tips);
        }
    }
}

6. Finally, the Netty method is automatically loaded and started in the startup service, and the asynchronous startup is set

package com.whl.mq;

import com.whl.mq.server.NioNettyServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
@Slf4j
public class NettyServerApplication implements CommandLineRunner {

	public static void main(String[] args) {
		ConfigurableApplicationContext context = SpringApplication.run(NettyServerApplication.class, args);
		// start to netty server
		context.getBean(NioNettyServer.class).start();
	}

	@Override
	public void run(String... args) {
		log.info("========================server start success========================");
	}

}

Client

1. Write client connection Netty service configuration

PostConstruct: This method will be called after the service starts, and cannot contain any parameters.
PreDestroy: This method will be called before the service is closed, and cannot contain any parameters.

package com.whl.client.client;

import com.whl.client.handle.NettyClientChannelInitializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;

/**
 * @author hl.Wu
 * @date 2021/11/9
 *
 * @description: netty client info
 **/
@Slf4j
@Component
public class NioNettyClient {

    @Value("${netty.server.host:localhost}")
    private String host;

    @Value("${netty.server.port:8099}")
    private int port;

    private SocketChannel socketChannel;

    /**
     * work 线程组用于数据处理
     */
    private EventLoopGroup work = new NioEventLoopGroup();

    @Autowired
    private NettyClientChannelInitializer nettyClientChannelInitializer;

    /**
     * 发送消息
     *
     * @param msg
     */
    public void sendMsg(String msg) {
        if(!socketChannel.isActive()){
            // 如果失去连接,重新创建新的连接
            log.info("****************服务失去连接,开始创建新的连接****************");
            start();
        }
        // 发送消息
        socketChannel.writeAndFlush(msg);
    }

    @PostConstruct
    public void start() {
        work = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(work)
                // 设置通道
                .channel(NioSocketChannel.class)
                // 日志处理格式
                .handler(new LoggingHandler(LogLevel.INFO))
                // 禁用nagle算法
                .option(ChannelOption.TCP_NODELAY, true)
                // 保持长连接
                .option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
                // 流程处理
                .handler(nettyClientChannelInitializer);
            // start to channel
            ChannelFuture future = bootstrap.connect(host, port).sync();
            future.addListener((ChannelFutureListener) future1 -> {
                if (future1.isSuccess()) {
                    log.info("**********************服务连接成功**********************");
                } else {
                    log.warn("**********************服务连接失败,20s后开始重新连接服务器**********************");
                    // 20s后重新连接
                    future1.channel().eventLoop().schedule(() -> this.start(), 20, TimeUnit.SECONDS);
                }
            });
            socketChannel = (SocketChannel) future.channel();
        } catch (Exception e) {
            log.error("connection error", e.getMessage(), e);
        }
    }

    @PreDestroy
    private void close() {
        if(socketChannel != null){
            socketChannel.close();
        }
        work.shutdownGracefully();
    }
}

2. Write NettyClientChannelInitializer to receive server response message business processing flow

Note:

In this example, I did not use the annotation method, because the heartbeat processing logic will refer to the bean of the reconnection service. If the annotation method is used, a circular dependency error will occur

package com.whl.client.handle;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Component
@Slf4j
public class NettyClientChannelInitializer extends ChannelInitializer {

    private final ResponseChannelHandler responseChannelHandler;

    private final ClientHeartbeatHandler clientChannelHandler;

    public NettyClientChannelInitializer(ResponseChannelHandler responseChannelHandler,
        ClientHeartbeatHandler clientChannelHandler) {
        this.responseChannelHandler = responseChannelHandler;
        this.clientChannelHandler = clientChannelHandler;
    }

    @Override
    protected void initChannel(Channel channel) {
        channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
        channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
        channel.pipeline().addLast("responseChannelHandler",responseChannelHandler);
        channel.pipeline().addLast("clientChannelHandler",clientChannelHandler);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("error message {}",cause.getMessage(),cause);
        super.exceptionCaught(ctx, cause);
    }
}

3. Write heartbeat processing logic ClientHeartbeatHandler

package com.whl.client.handle;

import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.client.client.NioNettyClient;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 心跳监测
 *
 * @author hl.Wu
 * @date 2022/7/15
 **/
@Component
@Slf4j
@ChannelHandler.Sharable
public class ClientHeartbeatHandler extends ChannelInboundHandlerAdapter {

    private final NioNettyClient nioNettyClient;

    public ClientHeartbeatHandler(NioNettyClient nioNettyClient) {
        this.nioNettyClient = nioNettyClient;
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
            if (idleStateEvent.state() == IdleState.WRITER_IDLE) {
                log.info("已经10s没有发送消息给服务端");
                //发送心跳消息,并在发送失败时关闭该连接
                MessageBO message = new MessageBO();
                message.setHeartbeat(Boolean.TRUE);
                ctx.writeAndFlush(JSON.toJSONString(message)).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //如果运行过程中服务端挂了,执行重连机制
        log.info("start to reconnect netty server");
        ctx.channel().eventLoop().schedule(() -> nioNettyClient.start(), 3L, TimeUnit.SECONDS);
        super.channelInactive(ctx);
    }

}

4. Write the final processing logic ResponseChannelHandler for receiving the server response message

package com.whl.client.handle;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author hl.Wu
 * @date 2022/7/15
 **/
@Component
@Slf4j
@ChannelHandler.Sharable
public class ResponseChannelHandler extends SimpleChannelInboundHandler<String> {
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        log.info("response info is {}", msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}

5. Finally write the sending message interface

package com.whl.client.controller;

import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.client.client.NioNettyClient;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@RestController
@RequestMapping("/client")
@Api(tags = "客户端管理")
@Slf4j
public class ClientController {
    @Autowired
    private NioNettyClient nioNettyClient;

    @PostMapping("/send")
    public void send(@RequestBody MessageBO message) {
        log.info(JSON.toJSONString(message));
        nioNettyClient.sendMsg(JSON.toJSONString(message));
    }
}

common

1. Write public entity objects

package com.whl.common.bo;

import java.io.Serializable;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
public abstract class BaseBO implements Serializable {

    protected String toLog() {
        return null;
    }
}
package com.whl.common.bo;

import lombok.Data;
import lombok.experimental.FieldNameConstants;

import java.util.List;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Data
@FieldNameConstants
public class MessageBO extends BaseBO{

    /**
     * 请求唯一标识id
     */
    private String requestUid;

    private String token;

    private Boolean heartbeat;

    private List<VerificationEmsBO> data;
}
package com.whl.common.bo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ResponseBO extends BaseBO{

    private String requestUid;
    /**
     * 响应code
     */
    private String code;

    /**
     * 响应提示信息
     */
    private String message;

    /**
     * 是否成功
     */
    private Boolean success;
}
package com.whl.common.bo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 验证码短信
 *
 * @author hl.Wu
 * @date 2022/7/14
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerificationEmsBO extends BaseBO{

    /**
     * 手机号码
     */
    private String mobile;

    /**
     * 短信内容
     */
    private String message;
}

 test

1. Start the server service

Access server service, http://localhost:8081/doc.html

2. Start the Client service

Access the client service, http://localhost:8082/doc.html

3. Request server token interface 

4. Assemble the returned token information to the client sending interface

5. View the background receiving log

client

 server

 6. Test failure reconnection mechanism

Operation: Restart the server, and after the service starts successfully, request the client to send the interface again to achieve reconnection

 7. The test server actively pushes messages to the client

Operation: Call the server to send reminder interface, userId uses the token when the client access is successful

client log

Summarize

You're done, tested all the features and everything works perfectly.

1. Authentication and heartbeat mechanisms are just simple processing, and Redis can be used for optimization in actual projects.

2. User information storage is too simple, no database is used

Ha ha! A little bit of knowledge every day, have you learned it today?

reference address

Netty: Home

Project source code

https://gitee.com/mackjie/whl-netty

GitHub - mackjie/whl-netty: SpringBoot integrates Netty to realize message push, receive and automatic reconnection

Branch: develop-nomq

Guess you like

Origin blog.csdn.net/qq_31150503/article/details/125829994