亲测!超详细的使用netty进行tcp数据通信的流程

Netty 是一个强大的网络框架,能够高效处理 TCP 数据通讯,适用于各种高并发、高性能的网络应用。

使用 TCP 通信的优点

可靠性:TCP 是面向连接的协议,保证数据包的顺序到达和数据的完整性。
流量控制:TCP 提供了流量控制和拥塞控制机制,能够适应不同的网络条件。
广泛应用:适用于各种需要高可靠性的数据传输场景,如文件传输、电子邮件、Web 服务等。
双向通信:TCP 支持全双工通信,允许客户端和服务器同时发送和接收数据。

1. 确认通讯数据帧格式(十六进制)

格式帧头帧长校验码循环码通讯渠道设备id码数据方向设备类型时间命令数据帧尾
字节数2byte2byte2byte4byte1byte12byte1byte1byte12byte2byte+nbyte2byte
注释通知对方本帧数据包开始传输表示本帧数据包(不含帧头)长度为0x0042CRC16的校验码,0x54为高字节,0x36为低字节表示是开机以来第0x00000005次帧包发送表示是通过以太网渠道设备ID码(3个WORD)是0x01020304,0x05060708,0x090a0b0c数据方向是设备上传到平台表示是行人过街设备表示2024年1月8日,9时12分36秒见指令概述部分通知对方本帧数据包结束传输

之前讨论过似乎这种数据帧的格式不合理,指出帧长应该放在帧尾处…具体情况具体分析,该通讯数据帧应该软硬件协商一致后进行代码开发。

2. 建立服务端对象

2.1 导入netty依赖

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

2.2 配置文件指定测试端口和正式端口

netty:
  port: xxxxx

2.3 创建netty服务端

在 Spring Boot 应用启动的同时,异步启动一个 Netty 服务器

@SpringBootApplication
public class SentryServiceApplication implements CommandLineRunner {
	@Value("${netty.port}")
    private int nettyPort;
    
    @Override
    public void run(String... args) throws Exception {
        new Thread(() -> {
            try{
                new NettyServer(nettyPort).run();
            } catch (Exception e){
                e.printStackTrace();
            }
        }).start();
    }
}

NettyServer 类的主要职责是启动和配置 Netty 服务器,包括创建线程组、配置服务器引导、绑定端口等。该类封装了 Netty 服务器的启动逻辑和生命周期管理。

/**
 * netty服务器,主要用于与客户端通讯
 */
public class NettyServer {
    private final int port; //监听端口

    //连接map
    public  static Map<String, ChannelHandlerContext> map = new HashMap<String, ChannelHandlerContext>();


    public NettyServer(int port) {
        this.port = port;
    }

    //编写run方法,处理客户端的请求
    public void run() throws Exception {

        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop

        try {
            ServerBootstrap b = new ServerBootstrap();
            /*将线程组传入*/
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)/*指定使用NIO进行网络传输*/
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置线程的连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    /*服务端每接收到一个连接请求,就会新启一个socket通信,也就是channel,
                   所以下面这段代码的作用就是为这个子channel增加handle*/
                    .childHandler(new NettyServerInitializer());
            System.out.println("netty服务器启动成功(port:" + port + ")......");
            /*异步绑定到服务器,sync()会阻塞直到完成*/
            ChannelFuture channelFuture = b.bind(port).sync();
            //监听关闭 /*阻塞直到服务器的channel关闭*/
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            /**关闭线程组*/
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}

3.管理Netty会话

NettySessionNettySessionManager类用于管理客户端会话

NettySession

public class NettySession {
    /**
     *channel   id
     */
    private String channelId;

    /**
     * 业务关联的key
     */
    private String bizKey;

    /**
     * 真正的连接对象
     */
    private Channel channel = null;

    /**
     * 连接上下文
     */
    private ChannelHandlerContext ctx = null;

    private LocalDateTime createTime;

    private LocalDateTime lastTime;

    /**
     * 消息流水号 word(2字节) 按发送顺序从 0 开始循环累加
     */
    private int currentFlowId = 0;


    public static NettySession build(String bizKey, ChannelHandlerContext ctx) {
        NettySession session = new NettySession();
        Channel channel = ctx.channel();
        session.bizKey = bizKey;
        session.channelId = channel.id().asLongText();
        session.ctx = ctx;
        session.channel = channel;
        session.createTime = LocalDateTime.now();
        session.lastTime = session.createTime;
        return session;
    }
    
    public boolean isActive() {
        if (ctx == null) {
            return false;
        }

        if (channel == null) {
            return false;
        }

        return channel.isActive();
    }

    public boolean isWritable() {
        if (ctx == null) {
            return false;
        }

        if (channel == null) {
            return false;
        }

        return channel.isWritable();
    }
    public boolean writeAndFlush(byte[] bytes) {
        if (bytes == null) {
            return false;
        }

        if (bytes.length == 0) {
            return false;
        }

        if (!isActive()) {
            return false;
        }

        if (!isWritable()) {
            return false;
        }

        ByteBuf retBuf = channel.alloc().buffer(bytes.length);
        retBuf.writeBytes(bytes);
        channel.writeAndFlush(retBuf);

        return true;
    }

    public boolean writeAndFlush(String msg){
        if(StrUtil.isBlank(msg)) {
            return false;
        }

        if (msg.length() == 0) {
            return false;
        }

        if (!isActive()) {
            return false;
        }

        if (!isWritable()) {
            return false;
        }
        channel.writeAndFlush(msg);
        return true;
    }

    /**
     * 关闭连接
     */
    public void close() {
        try {
            ctx.close();
        } catch (Exception ex) {
            log.error(ex.getLocalizedMessage(), ex);
        }
    }
}
NettySessionManager
@Slf4j
public class NettySessionManager {

    private static Map<String, NettySession> sessionMap = new ConcurrentHashMap<>();

    private static Map<String, Set<String>> bizMap = new ConcurrentHashMap<>();

    public static void addSession(NettySession session) {
        if (session == null) {
            return;
        }

        String bizKey = session.getBizKey();
        if (StrUtil.isBlank(bizKey)) {
            return;
        }

        String sessionId = session.getChannelId();
        if (StrUtil.isBlank(sessionId)) {
            return;
        }

        sessionMap.put(sessionId, session);
        log.info("-------------新增客户端连接【addSession】,sessionId:{},session:{}----------------", sessionId, session);

        bizMap.compute(bizKey, (key, sessionIdSet) -> {
            if (sessionIdSet == null) {
                sessionIdSet = ConcurrentHashMap.newKeySet();
            }
            sessionIdSet.add(sessionId);
            log.info("-------------新增客户端连接【addSession】,bizMap:{},bizKey:{},sessionIdSet:{}----------------", bizMap, key, sessionIdSet);
            return sessionIdSet;
        });
    }

    public static void removeSession(NettySession session) {
        if (session == null) {
            return;
        }

        removeSession(session.getChannel());
    }

    public static void removeSession(ChannelHandlerContext ctx) {
        if (ctx == null) {
            return;
        }
        log.info("Removing session for ChannelHandlerContext: {}", ctx);
        removeSession(ctx.channel());
    }

    public static void removeSession(Channel channel) {
        if (channel == null) {
            return;
        }

        String channelId = channel.id()
                .asLongText();
        log.info("Removing session for Channel: {}", channelId);
        removeSession(channelId);
    }

    public static void removeSession(String channelId) {
        if (StrUtil.isEmpty(channelId)) {
            return;
        }
        String trimmedChannelId = channelId.trim();
        log.info("Removing session for Channel ID: {}", trimmedChannelId);

        NettySession session = sessionMap.remove(trimmedChannelId);
        if (session == null) {
            log.info("No session found for Channel ID: {}", trimmedChannelId);
            return;
        }

        String bizKey = session.getBizKey();
        if (StrUtil.isBlank(bizKey)) {
            log.info("No bizKey found for Channel ID: {}", trimmedChannelId);
            return;
        }
        log.info("Removing bizKey: {} for Channel ID: {}", bizKey, trimmedChannelId);
        bizMap.computeIfPresent(bizKey, (key, sessionIdSet) -> {
            sessionIdSet.remove(trimmedChannelId);
            log.info("Removed session ID from bizKey: {}. Remaining session IDs: {}", bizKey, sessionIdSet);
            return sessionIdSet.isEmpty() ? null : sessionIdSet;
        });

        // Ensure channel is fully closed
        Channel channel = session.getChannel();
        if (channel != null && channel.isActive()) {
            log.info("Closing channel associated with session: {}", trimmedChannelId);
            channel.close();
        }
    }

    public static boolean isExistChannelId(ChannelHandlerContext ctx ){
        if (ctx == null) {
            return false;
        }
        return isExistChannelId(ctx.channel());
    }
    public static boolean isExistChannelId(Channel channel){
        if (channel == null) {
            return false;
        }
        String channelId=channel.id().asLongText();
        return isExistChannelId(channelId);
    }
    public static boolean isExistChannelId(String channelId){
        return sessionMap.containsKey(channelId);
    }

    public static Optional<NettySession> getSession(ChannelHandlerContext ctx) {
        if (ctx == null) {
            return Optional.empty();
        }

        return getSession(ctx.channel());
    }

    public static Optional<NettySession> getSession(Channel channel) {
        if (channel == null) {
            return Optional.empty();
        }

        String channelId = channel.id()
                .asLongText();

        return getSession(channelId);
    }

    public static Optional<NettySession> getSession(String channelId) {
        if (StrUtil.isEmpty(channelId)) {
            return Optional.empty();
        }

        if (!sessionMap.containsKey(channelId)) {
            return Optional.empty();
        }

        NettySession nettySession = sessionMap.get(channelId);
        if (nettySession == null) {
            return Optional.empty();
        }

        return Optional.of(nettySession);
    }

    /**
     * 根据BizKey获取Session列表,可能为null
     * @param bizKey
     * @return
     */
    public static List<NettySession> getSessionByBizKey(String bizKey) {
        if (StrUtil.isBlank(bizKey)) {
            return Collections.emptyList();
        }

        if (!bizMap.containsKey(bizKey)) {
            return Collections.emptyList();
        }

        Set<String> idList = bizMap.get(bizKey);
        if (idList == null || idList.isEmpty()) {
            bizMap.remove(bizKey);
            return Collections.emptyList();
        }

        List<NettySession> list = new ArrayList<>();
        for (String id : idList) {
            Optional<NettySession> op = getSession(id);
            op.ifPresent(list::add);
        }
        return list;
    }

    public static boolean isHaveSessionId(String bizKey){
        Set<String> idList = bizMap.get(bizKey);
        if (idList == null || idList.isEmpty()) {
            return true;
        }
        return false;
    }
}

管理客户端标识ID与Netty Channel对象之间的映射关系

public class ChannelMap {
    /**
     * 存放客户端标识ID(消息ID)与channel的对应关系
     */
    private static volatile ConcurrentHashMap<String, Channel> channelMap = null;

    private ChannelMap() {
    }

    public static ConcurrentHashMap<String, Channel> getChannelMap() {
        if (null == channelMap) {
            synchronized (ChannelMap.class) {
                if (null == channelMap) {
                    channelMap = new ConcurrentHashMap<>();
                }
            }
        }
        return channelMap;
    }

    public static Channel getChannel(String id) {
        return getChannelMap().get(id);
    }
}

4. 配置消息编解码

4.1 自定义初始化,用于自定义的编解码

@Component
@ChannelHandler.Sharable
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new LineBasedFrameDecoder(1024,false,true));
        // 限制消息读写时间,如果超过指定时间未进行数据通讯,则认为客户端闲置离线
        pipeline.addLast(new IdleStateHandler(0,0,180, TimeUnit.SECONDS));
        // 字符串解码 和 编码
        pipeline.addLast(new MyDecoder());
        pipeline.addLast(new MyEncoder());
        // 自己的逻辑Handler
        pipeline.addLast("handler", new NettyServerHandler());
    }
}


4.2 解码

@Slf4j
public class MyDecoder extends ByteToMessageDecoder {

    private static StringBuffer MsgBuffer = new StringBuffer();

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
        //创建字节数组,buffer.readableBytes可读字节长度
        byte[] b = new byte[buffer.readableBytes()];
        //复制内容到字节数组b
        buffer.readBytes(b);
        String msg = toHexString1(b);
        msgHandle(msg,out);
    }

    public void msgHandle(String msg,List<Object> out){
        log.info("-----------------【MyDecoder.msgHandle】待处理的数据帧:{}", msg);
        // 判断是否需要拼接
        if (msg.indexOf("0a") > 0 && !(msg.indexOf("fefd") >= 0 && (msg.length() - msg.lastIndexOf("0d0a") == 4))) {
            MsgBuffer.append(msg);
            String msgBuf = MsgBuffer.toString();
            if (msgBuf.indexOf("fefd") >= 0 && (msgBuf.length() - msgBuf.lastIndexOf("0d0a") == 4)) {
                // 校验和处理拼接后的完整消息
                if (processMessage(msgBuf, out)) {
                    MsgBuffer.delete(0, MsgBuffer.length());
                } else {
                    MsgBuffer.delete(0, MsgBuffer.length());
                }
            }
            return
        }else {
            processMessage(msg, out);
        }
    }

    // 处理完整的消息,返回是否校验成功
    private boolean processMessage(String msg, List<Object> out) {
        // 检查帧头和帧尾
        if (!msg.startsWith("fefd") || !msg.endsWith("0d0a")) {
            return false;
        }

        // 提取校验码
        String checkCode = msg.substring(8, 12);

        // 计算 CRC 校验码
        byte[] crcbyte = MyEncoder.hexString2Bytes(msg.substring(12));
        int crc = CRCUtil.calcCrc16(crcbyte);
        crc = CRCUtil.revert(crc);
        String crcCode = String.format("%04x", crc);

        // 比较校验码
        if (!checkCode.equals(crcCode)) {
            log.error("--------------【processMessage】CRC校验失败!服务端的校验码{},设备端的校验码{}", crcCode, checkCode);
            return false;
        }

        // 校验通过,添加消息到输出列表
        out.add(msg);
        return true;
    }

    public String bytesToHexString(byte[] bArray) {
        StringBuffer sb = new StringBuffer(bArray.length);
        String sTemp;
        for (int i = 0; i < bArray.length; i++) {
            sTemp = Integer.toHexString(0xFF & bArray[i])+" ";
            if (sTemp.length() < 3){
                sb.append(0);
            }
            sb.append(sTemp.toUpperCase());
        }
        return sb.toString();
    }

    public static String toHexString1(byte[] b) {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < b.length; ++i) {
            buffer.append(toHexString1(b[i]));
        }
        return buffer.toString();
    }

    public static String toHexString1(byte b) {
        String s = Integer.toHexString(b & 0xFF);
        if (s.length() == 1) {
            return "0" + s;
        } else {
            return s;
        }
    }

    /**
     * 十六进制字符串转字符串
     *
     * @param hexStr 原16进制字符串
     * @return 字符串
     * */
    public static String decodeHex(String hexStr) {
        // 定义字符数组,用于保存字符串字符,长度为16进制字符串的一半
        byte[] strs = new byte[hexStr.length() / 2];
        // 遍历赋值
        for (int i = 0; i < strs.length; i++) {
            // 截取高位,使用Integer.parseInt进行转换
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            // 截取低位,使用Integer.parseInt进行转换
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            // 拼接赋值
            strs[i] = (byte)(high * 16 + low);
        }
        // 将字符数组转换为字符串,返回结果
        return new String(strs);
    }

    /**
     * 字符串转换成为16进制(无需Unicode编码)
     * @param str
     * @return
     */
    public static String str2HexStr(String str) {
        char[] chars = "0123456789ABCDEF".toCharArray();
        StringBuilder sb = new StringBuilder("");
        byte[] bs = str.getBytes();
        int bit;
        for (int i = 0; i < bs.length; i++) {
            bit = (bs[i] & 0x0f0) >> 4;
            sb.append(chars[bit]);
            bit = bs[i] & 0x0f;
            sb.append(chars[bit]);
            // sb.append(' ');
        }
        return sb.toString().trim();
    }

  
    /**
     * 16进制高低位转换
     * @param hex
     * @return java.lang.String
     * @author wangguangle
     * @date: 2021/4/8 19:33
     */
    public static String reverseHex(String hex) {
        char[] charArray = hex.toCharArray();
        int length = charArray.length;
        int times = length / 2;
        for (int c1i = 0; c1i < times; c1i += 2) {
            int c2i = c1i + 1;
            char c1 = charArray[c1i];
            char c2 = charArray[c2i];
            int c3i = length - c1i - 2;
            int c4i = length - c1i - 1;
            charArray[c1i] = charArray[c3i];
            charArray[c2i] = charArray[c4i];
            charArray[c3i] = c1;
            charArray[c4i] = c2;
        }
        return new String(charArray);
    }

    /**
     * 十六进制转ASCII码
     * @other > Integer.toHexString(int) -> 10 to 16
     * @param hex
     * @return
     */
    public static String convertHexToString(String hex) {

        StringBuilder sb = new StringBuilder();
        StringBuilder temp = new StringBuilder();

        for (int i = 0; i < hex.length() - 1; i += 2) {

            // grab the hex in pairs
            String output = hex.substring(i, (i + 2));
            // convert hex to decimal
            int decimal = Integer.parseInt(output, 16);
            // convert the decimal to character
            sb.append((char) decimal);

            temp.append(decimal);
        }
        return sb.toString();
    }

    /**
     * 十六进制转ASCII码对应值
     * @other
     * @param hexString
     * @return
     */
    public static String hexStringToAscii(String hexString) {

        int len = hexString.length();
        byte[] bytes = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
                    + Character.digit(hexString.charAt(i+1), 16));
        }
        String asciiString = new String(bytes);
        return asciiString;
    }

    /**
     * 十六进制转10进制 按位计算,位值乘权重
     * @Author @zzh
     * @Description // 十六进制转10进制
     * @Date 14:59 2023/5/4
     * @param hex
     * @return int
     **/
    public static int hexToDecimal(String hex) {
        int decimal = 0;
        String digits = "0123456789abcdef";
        for (int i = 0; i < hex.length(); i++) {
            char c = hex.charAt(i);
            int d = digits.indexOf(c);
            decimal = decimal * 16 + d;
        }
        return decimal;
    }

    /**
     * 汉字转GB2312
     *
     * @param chinese 汉字
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String StringToGb(String chinese) throws UnsupportedEncodingException {
        // 先把字符串按gb2312转成byte数组
        byte[] bytes = chinese.getBytes("GB2312");
        StringBuilder gbString = new StringBuilder();
        // 遍历数组
        for (byte b : bytes){
            // 再用Integer中的方法,把每个byte转换成16进制输出
            String temp = Integer.toHexString(b);
            // 截取
            if (temp.length() > 2){
                temp = temp.substring(6, 8);
            }else if (temp.length() == 2){
                // 为数字,数字的区为A3
                gbString.append("A3");
                temp = "B" + temp.substring(1);
            }
            gbString.append(temp);
        }
        return gbString.toString();
    }

    /**
     * GB2312转汉字
     *
     * @param string gb3212码
     * @return
     * @throws Exception
     */
    public static String GbToString(String string) throws Exception{
        byte[] bytes = new byte[string.length() / 2];
        for(int i = 0; i < bytes.length; i ++){
            byte high = Byte.parseByte(string.substring(i * 2, i * 2 + 1), 16);
            byte low = Byte.parseByte(string.substring(i * 2 + 1, i * 2 + 2), 16);
            bytes[i] = (byte) (high << 4 | low);
        }
        String result = new String(bytes, "GB2312");
        return result;
    }

}

4.3 编码

public class MyEncoder extends MessageToByteEncoder<String> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, String s, ByteBuf byteBuf) throws Exception {
        //将16进制字符串转为数组
        byteBuf.writeBytes(hexString2Bytes(s));
    }
    /**
     * @Title:hexString2Bytes
     * @Description:16进制字符串转字节数组
     * @param src 16进制字符串
     * @return 字节数组
     */
    public static byte[] hexString2Bytes(String src) {
//        System.out.println("编码:" + src);
        int l = src.length() / 2;
        byte[] ret = new byte[l];
        for (int i = 0; i < l; i++) {
            ret[i] = (byte) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();
        }
        return ret;
    }

    /**
     * 计算出返回码的数据
     * @param str
     */
    public static String getCountNum(String str){
        StringBuilder sb = new StringBuilder();
        byte[] bytes =  hexString2Bytes(str);
        for (int i=bytes.length-3;i>13;i--){
            bytes[i] = (byte) (bytes[i] - 51);
            sb.append(Integer.toHexString(0xFF & bytes[i]));
        }
        return sb.toString();
    }
}

5. 客户端消息处理

@Slf4j
@Component
/*不加这个注解那么在增加到childHandler时就必须new出来*/
@ChannelHandler.Sharable
public class NettyServerHandler extends SimpleChannelInboundHandler<Object> {
    private static NettyServerHandler nettyServerHandler;

    private static SimpleDateFormat sdf = null;
    static {
        sdf = new SimpleDateFormat("yyMMddHHmmss");
    }

    @Autowired
    private  MsgSender msgSender;

    private byte[] req;

    private int counter;
    
    @PostConstruct
    public void init()
    {
        nettyServerHandler = this;
    }

    @Autowired
    private IDeviceTerminalService deviceTerminalService;

    @Autowired
    private RedisUtil redisUtil;

    //保留所有与服务器建立连接的channel对象
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    // 读取客户端消息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//        Channel channel = ctx.channel();
        String message=msg.toString();
        log.info("----------设备交互数据[channelRead0]---------" + message);
        if(message.length()<80){
            return;
        }
        DataPacket dataPacket = new DataPacket(message);
        String bizKey=dataPacket.getDeviceId();

        // 判断当前连接是否为新连接
        Tuple2<Boolean, NettySession> newSession = isNewSession(bizKey,ctx);
        Boolean isNewSession = newSession.getT1();
        NettySession nettySession = newSession.getT2();

        if(isNewSession){
            NettySessionManager.addSession(nettySession);
            deviceOnline(dataPacket);
        }
        handleMessage(dataPacket);
    }

    /**
     * 判断当前连接是否是新链接
     * @param bizKey 设备id
     * @param ctx  ChannelHandlerContext
     * @return
     */
    private Tuple2<Boolean, NettySession> isNewSession(String bizKey,ChannelHandlerContext ctx) {

        boolean isNewSession = true;
        Optional<NettySession> optionalSession = NettySessionManager.getSession(ctx);
        NettySession currentSession = null;

        if (optionalSession.isPresent()) {
            // 存在session,代表非第一次连接
            isNewSession = false;
            currentSession = optionalSession.get();
            currentSession.setLastTime(LocalDateTime.now());
            log.info("----------旧TCP连接[有Session]----------, bizKey=[{}], channelId=[{}]", bizKey, currentSession.getChannelId());
        } else {
            currentSession = NettySession.build(bizKey, ctx);
            log.info("----------新TCP连接[无Session]----------, bizKey=[{}], channelId=[{}]", bizKey, ctx.channel().id().asLongText());
        }
        return Tuples.of(isNewSession, currentSession);
    }

    private void handleMessage(DataPacket dataPacket) throws Exception {
        if (dataPacket == null) {
            return;
        }
        String commandCode = dataPacket.getCommand(); // 命令
        String strMessage = JSON.toJSONString(dataPacket);
        MsgSender msgSender = SpringUtil.getBean(MsgSender.class);
        switch (commandCode) {
            case ComConstant.CommandUp.SENTRY_HEARTBEAT:
                // 发送设备心跳消息
//                System.out.println("数据上报,接收心跳数据:" + strMessage);
                msgSender.asyncSendDeviceUpHeartBeat(strMessage);
                break;
            case ComConstant.CommandUp.SENTRY_RADAR_STATUS:
                // 雷达数据上报参数
//                System.out.println("数据上报,接收雷达数据:" + strMessage);
                msgSender.syncSendDeviceUpRadar(strMessage);
                break;
            default:
                //除了心跳,雷达以外的,所有数据上报
                msgSender.asyncSendDeviceUpCommand(strMessage);
                break;
        }
    }

    /*
     * 数据读取完毕
     *
     * 覆盖 channelActive 方法 在channel被启用的时候触发 (在建立连接的时候)
     *
     * */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("连接的客户端地址 : " + ctx.channel().remoteAddress() + " active !");
        // 获取业务键(假设从Channel获取业务键的方法)
        String bizKey = ctx.channel().id().asLongText();
        if (!NettySessionManager.isHaveSessionId(bizKey)) {
            List<NettySession> existingSessions = NettySessionManager.getSessionByBizKey(bizKey);
            if (!existingSessions.isEmpty()) {
                log.warn("Existing sessions found for bizKey: {}, removing old sessions", bizKey);
                for (NettySession session : existingSessions) {
                    NettySessionManager.removeSession(session.getChannel().id().asLongText());
                }
            }
        }

        super.channelActive(ctx);
    }

    // 设备上线
    private void deviceOnline(DataPacket dataPacket){
        log.info("----------设备:{},上线!---------", dataPacket.getDeviceId());
        if(dataPacket==null){
            return;
        }
        DeviceTerminal deviceTerminal = new DeviceTerminal();
        String bizKey=dataPacket.getDeviceId();
        deviceTerminal.setDeviceId(bizKey);
        deviceTerminal.setCode(bizKey);
        deviceTerminal.setProductionNumber(bizKey);
        deviceTerminal.setDeviceType(String.valueOf(dataPacket.getType()));
        deviceTerminal.setDeviceTime(dataPacket.getDatetime());
        deviceTerminal.setOnlineTime(DateUtil.date());
        deviceTerminal.setStatus(String.valueOf(DeviceStateEnum.on.getValue()));
        IDeviceTerminalService deviceTerminalService=SpringUtil.getBean(DeviceTerminalServiceImpl.class);
        deviceTerminalService.updateDeviceState(deviceTerminal);

    }

    //设备下线
    private void deviceDownline(Optional<NettySession> nettySessionOptional){
        nettySessionOptional.ifPresent(session -> {
            DeviceTerminal deviceTerminal = new DeviceTerminal();
            String bizKey=session.getBizKey();
            deviceTerminal.setDeviceId(bizKey);
            deviceTerminal.setOnlineTime(DateUtil.date());
            deviceTerminal.setStatus(String.valueOf(DeviceStateEnum.down.getValue()));
            IDeviceTerminalService deviceTerminalService=SpringUtil.getBean(DeviceTerminalServiceImpl.class);
            log.info("---------更新设备状态为:{},【deviceDownline】" , DeviceStateEnum.down.getValue());
            deviceTerminalService.updateDeviceState(deviceTerminal);
        });
    }



    //表示服务端与客户端连接建立
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();  //其实相当于一个connection

        /**
         * 调用channelGroup的writeAndFlush其实就相当于channelGroup中的每个channel都writeAndFlush
         *
         * 先去广播,再将自己加入到channelGroup中
         */
        channelGroup.writeAndFlush(" 【服务器】 -" +channel.remoteAddress() +" 加入\n");
        channelGroup.add(channel);
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIpPort = remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort();
        log.info("Client IP:Port: {}", clientIpPort);
        Optional<NettySession> sessionOptional=NettySessionManager.getSession(ctx.channel());
        deviceDownline(sessionOptional);
        log.info("----------设备下线[channelInactive]----------");
        NettySessionManager.removeSession(ctx);
        super.channelInactive(ctx);
    }

    /** 发生异常后的处理*/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIpPort = remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort();
        log.info("Client IP:Port: {}", clientIpPort);
        Optional<NettySession> sessionOptional = NettySessionManager.getSession(ctx.channel());
        if (ctx.channel().isActive()) {
            if (cause instanceof SocketTimeoutException) {
                deviceDownline(sessionOptional);
                log.error("----------设备连接超时异常下线[exceptionCaught]----------");
            } else if (cause instanceof IOException) {
                deviceDownline(sessionOptional);
                log.error("----------设备网络异常下线[exceptionCaught]----------");
                log.error("Network exception occurred: {}", cause.getMessage(), cause);
            } else {
                deviceDownline(sessionOptional);
                log.error("----------设备未知异常下线[exceptionCaught]----------");
                log.error("Unexpected exception occurred: {}", cause.getMessage(), cause);
            }

            NettySessionManager.removeSession(ctx);
            if (NettySessionManager.getSession(ctx.channel()).isPresent()) {
                log.error("Session not fully removed, additional cleanup needed.");
                NettySessionManager.removeSession(ctx.channel());
            }
            log.error("----------设备异常下线,移除会话[exceptionCaught],{}", ctx.channel());
            ctx.close();
            log.info("----------设备异常下线,关闭通道[exceptionCaught]----------");
        } else {
            log.error("----------Channel已经不活跃,无需处理下线逻辑[exceptionCaught]----------");
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIpPort = remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort();
        log.info("Client IP:Port: {}", clientIpPort);
        if (ctx.channel().isActive()) {
            Optional<NettySession> sessionOptional = NettySessionManager.getSession(ctx.channel());
            if (evt instanceof IdleStateEvent) {
                IdleStateEvent event = (IdleStateEvent) evt;
                switch (event.state()) {
                    case READER_IDLE:
                        log.info("----------客户端读取闲置超时异常下线[READER_IDLE]----------");
                        break;
                    case WRITER_IDLE:
                        log.info("----------客户端写入闲置超时异常下线[WRITER_IDLE]----------");
                        break;
                    case ALL_IDLE:
                        log.info("----------客户端全部闲置超时异常下线[ALL_IDLE]----------");
                        break;
                    default:
                        log.info("---------客户端闲置超时异常下线,IdleState.{} ----------", event.state());
                        break;
                }
                deviceDownline(sessionOptional);
                NettySessionManager.removeSession(ctx);
                ctx.channel().close();
            } else {
                super.userEventTriggered(ctx, evt);
                log.info("----------客户端闲置超时异常下线,evt不是IdleStateEvent类型的事件,未清除通道信息[ALL_IDLE]----------{}", evt.getClass().getName());
                deviceDownline(sessionOptional);
            }
        } else {
            log.info("----------Channel已经不活跃,无需处理下线逻辑[userEventTriggered]----------");
        }
}

有问题可以留言/私信我,帮你解答,有帮助请一键三连,点个再看,传下去!如果需要可以给你源文档+通讯手册!

欢迎关注微信公众号:小红的成长日记,一起学Java!

在这里插入图片描述

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个基本的 netty 程序,用于建立 TCP 通信连接。 ``` public class MyServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new MyServerInitializer()); ChannelFuture f = b.bind(8899).sync(); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 这里是客户端代码: ``` public class MyClient { public static void main(String[] args) throws Exception { EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class) .handler(new MyClientInitializer()); Channel channel = bootstrap.connect("localhost", 8899).sync().channel(); channel.closeFuture().sync(); } finally { eventLoopGroup.shutdownGracefully(); } } } ``` 这里的 `MyServerInitializer` 和 `MyClientInitializer` 是自定义的类,用于处理网络事件,例如,当客户端连接到服务器时,会触发 `channelActive` 事件。 ``` public class MyServerInitializer extends ChannelInitializer<SocketChannel> { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyServerHandler()); } } ``` ``` public class MyClientInitializer extends ChannelInitializer<SocketChannel> { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyClientHandler()); } } ``` 最后,你需要实现自己的事件处理器,例如: ``` public class MyServerHandler extends SimpleChannelInboundHandler<String> { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println(ctx.channel().remoteAddress() + ": " + msg); ctx.channel().writeAndFlush("from server: " + UUID.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值