netty实现与智能手表通信对接

最近在做java服务端通过netty与手表通信,基于GPRS通信协议,可实现接收手表的信息,也可由服务端主动向手表发送信息。

服务端

@Component
public class NettyServer {
    /**
     * 日志
     */
    private Logger log = LoggerFactory.getLogger(getClass());

    /**
     * IP地址
     */
    @Value("${netty.ip}")
    private String ip;

    /**
     * 端口号
     */
    @Value("${netty.port}")
    private int port;

    //EventLoopGroup是用来处理IO操作的多线程事件循环器
    //负责接收客户端连接线程
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    //负责处理客户端i/o事件、task任务、监听任务组
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    private Channel channel = null;
    /**
     * 启动服务器方法
     */
//    @PostConstruct 交给启动项统一管理,不使用注解启动
    public void init() {
        InetSocketAddress address = new InetSocketAddress(ip,port);
        log.info("netty服务器启动地址:"+ip);
        try {
            //启动 NIO 服务的辅助启动类
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);//绑定线程池
            //配置 Channel
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.localAddress(address);
            serverBootstrap.childHandler(new NettyServerInitializer());编码解码
            //backlog用于构造服务端套接字ServerSocket对象,
            // 标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度
            serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);//服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            //socketchannel的设置,关闭延迟发送
            serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
            //保持长连接,2小时无数据激活心跳机制
            serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
            serverBootstrap.childHandler(new NettyServerInitializer());
            // 绑定端口,开始接收进来的连接
            //绑定服务端口监听
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channel = channelFuture.channel();
            System.out.println("netty服务启动: [port:" + port + "]");
            log.info("netty服务启动: [port:" + port + "]");

            service=newBlockingExecutorsUseCallerRun(Runtime.getRuntime().availableProcessors());

            // 等待服务器socket关闭
            //服务器关闭监听
            /*channel.closeFuture().sync()实际是如何工作:
            channel.closeFuture()不做任何操作,只是简单的返回channel对象中的closeFuture对象,对于每个Channel对象,都会有唯一的一个CloseFuture,用来表示关闭的Future,
            所有执行channel.closeFuture().sync()就是执行的CloseFuturn的sync方法,从上面的解释可以知道,这步是会将当前线程阻塞在CloseFuture上*/
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("netty服务启动异常-" + e.getMessage());
        } finally {
            //关闭事件流组
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }



    public static ExecutorService service;

    /**
     * 阻塞的ExecutorService
     *
     * @param size
     * @return
     */
    public ExecutorService newBlockingExecutorsUseCallerRun(int size) {
        return new ThreadPoolExecutor(size, size * 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        try {
                            executor.getQueue().put(r);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
    }

    /**
     * 停止服务
     */
    public void destroy() {
        log.info("Shutdown Netty Server...");
        if(channel != null) { channel.close();}
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
        log.info("Shutdown Netty Server Success!");
    }
}

初始化通道

public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
    /**
     * 初始化channel
     */
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //指定消息分割符处理数据
//        ByteBuf delimiter= Unpooled.copiedBuffer("\n".getBytes());
        如果取消了分割符解码,就会出现TCP粘包之类的问题了
//        pipeline.addLast(new DelimiterBasedFrameDecoder(1024*1024, delimiter));
        pipeline.addLast(new StringDecoder());
        pipeline.addLast(new StringEncoder());
        //读写超时设置
        pipeline.addLast(new IdleStateHandler(40,50,70, TimeUnit.SECONDS));
        //心跳机制处理器
        pipeline.addLast(new HearBeatHandler());
        pipeline.addLast(new NettyServerHandler());
    }
}

NettyServerHandler

@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 日志
     */
    private Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    WatchLocationDataMapper watchLocationDataMapper;
    @Autowired
    private RedisUtil redisUtil;
    private static NettyServerHandler nettyServerHandler;

    /**
     * 初始化
     * 由于在自定义类中注入service或者mapper会报空指针异常
     * 所以采用这种方式
     */
    @PostConstruct
    public void init(){
        nettyServerHandler = this;
    }

    /**
     * 管理一个全局map,保存连接进服务端的通道数量
     */
    private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();
    /**
     * 存储手表设备ID连入时的ChannelId
     */
    public static final ConcurrentHashMap<String,ChannelId> WATCHID_MAP = new ConcurrentHashMap<>();

    /**
     * 对每一个传入的消息都要调用;
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            String tcpString = msg.toString();
            System.out.println("tcpString::::"+tcpString);
            //[厂商*设备ID*内容长度*内容] 内容长度为16进制
            String[] tcpArray = tcpString.substring(1).split("\\*");
            //内容数组
            String[] contentArray = tcpArray[3].split(",");

            //存储手表连接的唯一id
            ChannelId channelId = ctx.channel().id();
            if(WATCHID_MAP.get(tcpArray[1])==null){
                WATCHID_MAP.put(tcpArray[1],channelId);
            }else{
                if(!WATCHID_MAP.get(tcpArray[1]).equals(channelId.toString())){
                    WATCHID_MAP.put(tcpArray[1],channelId);
                }
            }

            //内容长度
            String length = "";
            //回复终端内容
            String tcpResult ="";
            switch (contentArray[0]){
                //链路保持
                case "LK":
                    //[CS*YYYYYYYYYY*LEN*LK,步数,翻滚次数,电量百分数]
                    //将长度转换为四个字节的ascll
                    length = toAscll(contentArray[0],1);
                    tcpResult= "["+tcpArray[0]+"*"+ tcpArray[1]+"*"+length+"*"+contentArray[0]+"]";
                    System.out.println("tcpResult"+tcpResult);
                    ctx.writeAndFlush(tcpResult);
                    break;
                //报警数据
                case "AL":
                    //创建手表上传数据类对象,封装数据
                    WatchLocationData watchLocationData = new WatchLocationData();
                    watchLocationData.setWatchId(tcpArray[1]);
                    watchLocationData.setUploadTime(TimeUtil.getDateToString(new Date()));
                    watchLocationData.setIsLocation(contentArray[3]);
                    watchLocationData.setLatitude(contentArray[4]);
                    watchLocationData.setLatitudeLogo(contentArray[5]);
                    watchLocationData.setLongitude(contentArray[6]);
                    watchLocationData.setLongitudeLogo(contentArray[7]);
                    watchLocationData.setSpeed(Double.valueOf(contentArray[8]));
                    watchLocationData.setOrientation(Double.valueOf(contentArray[9]));
                    watchLocationData.setElevation(Double.valueOf(contentArray[10]));
                    watchLocationData.setElectricQuantity(Integer.valueOf(contentArray[13]));
                    watchLocationData.setStepNumber(Integer.valueOf(contentArray[14]));
                    watchLocationData.setRollNumber(Integer.valueOf(contentArray[15]));
                    watchLocationData.setTerminalStatusCode(contentArray[16]);
                    watchLocationData.setDisposeState(0);
                    watchLocationData.setLocationDataType(contentArray[0]);
                    //将终端状态码(16进制)转换成二进制
                    String terminalState = hexadecimalToBinarySystem(contentArray[16]);
                    //将字符串倒叙并转换成字符数据,遍历得到值为1的索引,该索引表示手表的状态或报警类型
                    StringBuffer buffer = new StringBuffer(terminalState);
                    char[] chars = buffer.reverse().toString().toCharArray();
                    String watchDataType = "";
                    for (int i = 0; i < chars.length; i++) {
                        if(chars[i]=='1'){
                            watchDataType += i+",";
                        }
                    }
                    watchLocationData.setWatchStateType(watchDataType);
                    //保存报警数据
                    boolean save = nettyServerHandler.watchLocationDataMapper.save(watchLocationData);
                    if(save){
                        //回复终端
                        length = toAscll(contentArray[0],1);
                        tcpResult= "["+tcpArray[0]+"*"+ tcpArray[1]+"*"+length+"*"+contentArray[0]+"]";
                        System.out.println("tcpResult"+tcpResult);
                        ctx.writeAndFlush(tcpResult);
                    }
                    break;
                //设置中心号码终端回复信息
                case "CENTER":
                    //将终端回复的消息存入Redis
//                    System.out.println("tcpString"+tcpString);
                    break;
                default:
                    break;
            }
        }catch (Exception e){
            e.printStackTrace();
            log.error("channelRead出现系统错误"+e.toString());
        }
    }

    /**
     * 16进制转换成二进制
     * @param str
     * @return
     */
    public static String hexadecimalToBinarySystem(String str){
        String s = Integer.toBinaryString(Integer.valueOf(str, 16));
        int length = s.length();
        int residue = 32-length;
        for (int i = 0; i < residue; i++) {
            s = "0"+s;
        }
        return s;
    }

    /**
     * 转换为四个字节的ascll(1长度,2电话号码)
     * @param str
     * @return
     */
    public static String toAscll(String str,Integer type){
        if(type==1){
            String s = (Integer.toHexString(str.length())).toUpperCase();
            if(s.length()==1){
                s = "000"+s;
            }else if(s.length()==2){
                s= "00"+s;
            }else if(s.length()==3){
                s= "0" + s;
            }
            return s;
        }else if(type==2){
            String s = (Long.toHexString(Long.parseLong(str))).toUpperCase();
            return s;
        }
        return null;
    }

    /**
     * 将姓名转成unicode
     * @param str 待转字符串
     * @return unicode字符串
     */
    public static String convertUnicode(String str) {
        str = (str == null ? "" : str);
        String tmp;
        StringBuffer sb = new StringBuffer(1000);
        char c;
        int i, j;
        sb.setLength(0);
        for (i = 0; i < str.length(); i++) {
            c = str.charAt(i);
//            sb.append("\\u");
            j = (c >>>8); //取出高8位
            tmp = Integer.toHexString(j);
            if (tmp.length() == 1)
                sb.append("0");
            sb.append(tmp);
            j = (c & 0xFF); //取出低8位
            tmp = Integer.toHexString(j);
            if (tmp.length() == 1)
                sb.append("0");
            sb.append(tmp);

        }
        return (new String(sb));
    }

    /**
     * @param msg        需要发送的消息内容
     * @param channelId 连接通道唯一id
     * @author xiongchuan on 2019/4/28 16:10
     * @DESCRIPTION: 服务端给客户端发送消息
     * @return:
     */
    public String channelWrite(ChannelId channelId, Object msg) throws Exception {
        ChannelHandlerContext ctx = null;
        try {
            ctx = CHANNEL_MAP.get(channelId);
        } catch (Exception e) {
            return "设备不在线!";
        }
        if (ctx == null) {
            log.info("通道【" + channelId + "】不存在");
            return "设备不在线!";
        }
        if (msg==null || StringUtils.isEmpty(msg.toString())) {
            log.info("服务端响应/发送空的消息");
            return "服务端响应/发送空的消息";
        }
        //将客户端的信息直接返回写入ctx
        ctx.writeAndFlush(msg);
        return "成功";
    }

    /**
     * 有客户端连接服务器会触发此函数
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();
        int clientPort = insocket.getPort();

        //获取连接通道唯一标识
        ChannelId channelId = ctx.channel().id();
        System.out.println();
        //如果map中不包含此连接,就保存连接
        if (CHANNEL_MAP.containsKey(channelId)) {
            log.info("客户端【" + channelId + "】是连接状态,连接通道数量: " + CHANNEL_MAP.size());
        } else {
            //保存连接
            CHANNEL_MAP.put(channelId, ctx);
            log.info("客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }

    }

    /**
     * 有客户端终止连接服务器会触发此函数
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();

        String clientIp = insocket.getAddress().getHostAddress();

        ChannelId channelId = ctx.channel().id();

        //包含此客户端才去删除
        if (CHANNEL_MAP.containsKey(channelId)) {
            //删除连接
            CHANNEL_MAP.remove(channelId);

            System.out.println();
            log.info("客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
            log.info("连接通道数量: " + CHANNEL_MAP.size());
        }
    }

    /**
     * 异常捕获
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.out.println();
        ctx.close();
        log.info(ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + CHANNEL_MAP.size());
        cause.printStackTrace();
    }
}

HearBeatHandler

public class HearBeatHandler extends ChannelInboundHandlerAdapter {

    /**
     * 日志
     */
    private Logger log = LoggerFactory.getLogger(getClass());

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        String socketString = ctx.channel().remoteAddress().toString();
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
//                log.info("Client: " + socketString + " READER_IDLE 读超时");
//                ctx.disconnect();
            } else if (event.state() == IdleState.WRITER_IDLE) {
//                log.info("Client: " + socketString + " WRITER_IDLE 写超时");
//                ctx.disconnect();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("Client: " + socketString + " ALL_IDLE 总超时");
                ctx.channel().close();
            }
        }
    }

}

springboot启动类启动netty

@EnableAsync//用线程池启动Netty,不然会阻塞springboot
public class CommunityApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(CommunityApplication.class, args);
    }

    @Autowired
    private IinitService iinitService;

    @Autowired
    private HikCloudUtil hikCloudUtil;
	@Async//异步启动
    @Override
    public void run(String... args) throws Exception {

        //文件夹维护
        iinitService.initFolder();

        //获取token
        hikCloudUtil.Login();

        //资源初始化
        iinitService.initConstant();

        // 维护数据库
        //iinitService.updateMysqlDB();

        //启动netty
        iinitService.nettyInit();

    }

}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值