基于Netty + springBoot 联机 C 的停车终控主程序 socket通信

业务场景:

基于商业停车场的主程序控制系统(C语言开发)socket TCP 通信协议,建设B/S模式的门岗收费系统。

系统简介:

1、门岗系统(gatekeeper),选型 springBoot-2.1.16.RELEASE ,netty-4.1.12.Final,websocket等主要核心技术,

    部署在门口老大爷的window台式机上。

2、停车终控系统(parking),基于C语言设计研发,

    部署在树莓派上。

gatekeeper为什么部署在门口老大爷的window台式机上?系统的高可用不考虑了?,parking为什么部署在树莓派上?

一句话:控制成本!!!没有那么多6个9或5个9的高可用要求,这就是二三线城市,低成本投入,活着很重要。

 

核心技术实现:

1、 SocketClientImpl.java

@Slf4j
@Order(value = 1)//这里表示启动顺序
@Component("com.pasq.gatekeeper.service.impl.SocketClientImpl")
public class SocketClientImpl implements ISocketClient , CommandLineRunner {
    public static ChannelFuture channelFuture;
    public static Bootstrap bootstrap;
    // 注册线程池
    public static EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

    public static String ip;
    public static int port;

    @Value("${gate.socket.ip:192.168.200.218}")
    public  void setIp(String ip) {
        SocketClientImpl.ip = ip;
    }
    @Value("${gate.socket.port:8313}")
    public  void setPort(int port) {
        SocketClientImpl.port = port;
    }
    public static String getIp() {
        return ip;
    }

    public static int getPort() {
        return port;
    }

    public void init() {

    }

    public void start(){
        log.info("========MyClient==start===========ip:{},port:{}",ip,port);
        try{

            if(eventLoopGroup == null){
                eventLoopGroup = new NioEventLoopGroup();
            }

            // bootstrap 可重用
            bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer());

            // 服务器异步创建绑定
            channelFuture = bootstrap.connect(ip,port).sync();

            channelFuture.addListener(new ConnectionListener());

            //TODO 断线重连 自动静默登录
            if(Contants.AUTO_LOGIN){
                Contants.AUTO_LOGIN = false;
                auotLogin();
            }

            // 关闭服务器通道
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            try{
                //使用过程中断线重连
                eventLoopGroup.schedule(new Runnable() {
                    private SocketClientImpl nettyClient = new SocketClientImpl();
                    @Override
                    public void run() {
                        nettyClient.init();
                        nettyClient.start();
                    }
                }, 15, TimeUnit.SECONDS);
            }catch (Exception e1){
                e1.printStackTrace();
            }
            //eventLoopGroup.shutdownGracefully();
            e.printStackTrace();
        }

    }

    @PreDestroy
    public void stop() {
        log.info("========eventLoopGroup.stop==end===========");
        if (channelFuture != null) {
            log.info("Netty Server close");
            // 释放线程池资源
            eventLoopGroup.shutdownGracefully();
        }
    }

    @Async//注意这里,组件启动时会执行run,这个注解是让线程异步执行,这样不影响主线程
    @Override
    public void run(String... args) throws Exception {
        this.init();
        this.start();
    }

    @Override
    public void sendMsg(String msg) {
        channelFuture.channel().writeAndFlush(msg);
    }
    /**
     * 静默自动登录
     */
    public void auotLogin(){
        log.info("=========================auotLogin==========================");
        Iterator<String> keys = WebSocketBiz.webSocketMap.keySet().iterator();
        while (keys.hasNext()){
            String toUserId = "";
            try{
                toUserId = keys.next();
                String reqStr = Contants.AUTO_LOGIN_PARAM.get(toUserId);
                LoginUserDTO reqDTO = JSON.toJavaObject(JSON.parseObject(reqStr),LoginUserDTO.class);

                LoginVO vo = new LoginVO();
                //登录
                CBaseDTO dto = new CBaseDTO(Contants.CODE_10,Contants.CODE_10);
                dto.setData(reqDTO);
                String sendMsg = JSON.toJSONString(dto, SerializerFeature.WriteMapNullValue);
                sendMsg(sendMsg);

                //登录失败信息
                String msg = getCurrentSocketMsg(Contants.CODE_19);
                if(StringUtils.isEmpty(msg)){
                    //成功登录
                    msg = getCurrentSocketMsg(Contants.CODE_20);
                }
                if(!StringUtils.isEmpty(msg)){
                    String dataStr = JSON.parseObject(msg).getString("data");
                    vo = JSON.parseObject(dataStr, LoginVO.class);
                }
                log.info("====autoLogin=toUserId:{},reqDTO:{},vo:{}",toUserId, JSON.toJSONString(reqDTO),JSON.toJSONString(vo));
            }catch (Exception e){
                log.error("====autoLogin==error==toUserId:{}",toUserId,e.getMessage(),e);
            }
        }

    }

    /**
     * 通过 code 获取 对象的socket 返回信息
     * @param code
     * @return
     */
    public  String getCurrentSocketMsg(int code){
        String msg = null;
        int i =0 ;
        //尝试取 3次
        while (i < 3 && StringUtils.isEmpty(msg)){
            i ++;
            msg = ISocketClient.CALL_MSG.get(code);
            if(StringUtils.isEmpty(msg)){
                try {
                    Random random = new Random();
                    int k = random.nextInt(10);
                    Thread.sleep(100 * k);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        //仅支持一个在线 访问
        //ISocketClient.CALL_MSG.clear();
        Iterator<Integer> iter = ISocketClient.CALL_MSG.keySet().iterator();
        while(iter.hasNext()){
            Integer key = iter.next();
            if(key==code){
                iter.remove();
                //ISocketClient.CALL_MSG.remove(key);
            }
        }

        return msg;
    }

}

 

2、ConnectionListener.java

/**
 * 监控 netty连接是否断线重连
 */
@Slf4j
public class ConnectionListener implements ChannelFutureListener {
    private SocketClientImpl nettyClient = new SocketClientImpl();

    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        log.info("=============ConnectionListener==operationComplete====================");
        if (!channelFuture.isSuccess()) {
            log.info("ConnectionListener: 服务端链接不上,开始重连操作=======");
            final EventLoop loop = channelFuture.channel().eventLoop();
            loop.schedule(new Runnable() {
                @Override
                public void run() {
                    nettyClient.init();
                    nettyClient.start();
                }
            }, 15, TimeUnit.SECONDS);
        } else {
            log.info("=====================ConnectionListener服务端链接成功========");
        }


    }


}

 

3、MyClientInitializer.java

/**
 * 定义 netty client 初始化配置
 */
@Slf4j
@Component("com.pasq.gatekeeper.socketc.MyClientInitializer")
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Autowired
    private MyClientHandler clientHandler;

    private static final int MAX_FRAME_LENGTH = 1024 * 1024;  //最大长度

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("ping", new IdleStateHandler(25, 15, 10, TimeUnit.SECONDS));
//控制先追加待发送消息的长度 且是4位 低位在后。
        pipeline.addFirst(new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN,MAX_FRAME_LENGTH,0,4,0,4,false));

        //以$为分隔符
        ByteBuf buf = Unpooled.copiedBuffer("$".getBytes());
        //pipeline.addLast(new DelimiterBasedFrameDecoder(8192, buf));
        //增加消息头size
        pipeline.addLast(new LengthFieldPrepender(ByteOrder.LITTLE_ENDIAN,4,0,false));

        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        if(clientHandler == null){
            pipeline.addLast(new MyClientHandler());
        }else{
            pipeline.addLast(clientHandler);
        }

    }

}

 

4、MyClientHandler.java

 

/**
 * netty client handler 消息消费的具体的客户端 实现
 */
@Slf4j
@Component("com.pasq.gatekeeper.socketc.MyClientHandler")
public class MyClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        boolean flag = true;
        //服务端的远程地址
        log.info("==accept msg:{}==channelRead0==ip:{},client accept==",msg,ctx.channel().remoteAddress());
        if(msg == null || "".equals(msg.trim())){
            log.error("==========MyClientHandler result error===");
        }else{
           try{
               JSONObject obj = JSON.parseObject(msg);
               int code = obj.getInteger("code");
               //0 代表心跳
               if(code != 0){
                   flag = false;
                   //接受消息
                   ISocketClient.CALL_MSG.put(code,msg);
                   //TODO 推送WS socket
                   WebSocketBiz.sendMsgToWS(code,msg);
               }else{
                   //推送心跳,防止ws 关闭
                   WebSocketBiz.sendMsgToWS(code,msg);
               }
           }catch (Exception e){
               log.error("===error===============================error=msg:{}=====:{}",msg,e.getMessage(),e);
           }
        }


        if(flag){
            JumpDTO jump = new JumpDTO();
            jump.setIp("192.168.1.218");
            CBaseDTO dto = new CBaseDTO();
            dto.setData(jump);
            String sendMsg = JSON.toJSONString(dto);
            //发送心跳
            ctx.writeAndFlush(sendMsg);
        }

    }

    /**
     * 当服务器端与客户端进行建立连接的时候会触发,如果没有触发读写操作,则客户端和客户端之间不会进行数据通信,
     * 也就是channelRead0不会执行,
     * 当通道连接的时候,触发channelActive方法向服务端发送数据触发服务器端的handler的channelRead0回调,然后
     * 服务端向客户端发送数据触发客户端的channelRead0,依次触发。
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String hostAddress = "";
        try{
            InetAddress address = InetAddress.getLocalHost();//获取的是本地的IP地址 //PC-20140317PXKX/192.168.0.121
            hostAddress = address.getHostAddress(); //192.168.0.121
        }catch (Exception e){
            hostAddress = "192.168.1.200";
        }
        JumpDTO jump = new JumpDTO();
        jump.setIp(hostAddress);

        CBaseDTO dto = new CBaseDTO();
        dto.setData(jump);
        String sendMsg = JSON.toJSONString(dto);

        log.info("===channelActive===client start connect server:" + ctx.channel().localAddress() + "channelActive");
        ctx.writeAndFlush(sendMsg);

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("===exceptionCaught===error:{}",cause.getMessage(),cause);
        cause.printStackTrace();
        ctx.close();
    }

    //服务端突然挂了
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("============channelInactive===掉线了==============");
        //掉线重连时 自动静默登录
        Contants.AUTO_LOGIN = true;
        //使用过程中断线重连
        final EventLoop eventLoop = ctx.channel().eventLoop();
        eventLoop.schedule(new Runnable() {
            private SocketClientImpl nettyClient = new SocketClientImpl();
            @Override
            public void run() {
                nettyClient.init();
                nettyClient.start();
            }
        }, 15, TimeUnit.SECONDS);
        super.channelInactive(ctx);
    }

        /**
         * 一段时间未进行读写操作 回调
         */
    /*@Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        log.info("========userEventTriggered===================");
        super.userEventTriggered(ctx, evt);

        if (evt instanceof IdleStateEvent) {

            IdleStateEvent event = (IdleStateEvent) evt;

            if (event.state().equals(IdleState.READER_IDLE)) {
                //未进行读操作
                log.error("READER_IDLE");
                // 超时关闭channel
                ctx.close();

            } else if (event.state().equals(IdleState.WRITER_IDLE)) {

            } else if (event.state().equals(IdleState.ALL_IDLE)) {
                //未进行读写
                log.error("=============ALL_IDLE==============userEventTriggered==");
                // 发送心跳消息
                JumpDTO jump = new JumpDTO();
                jump.setIp("192.168.1.218");
                CBaseDTO dto = new CBaseDTO();
                dto.setData(jump);
                String sendMsg = JSON.toJSONString(dto);
                //发送心跳
                ctx.writeAndFlush(sendMsg);
            }
        }
    }*/

}

 

至此:netty client 与 C service 的 socket 通信核心基本实现。支持断线重连,心跳消息发送。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值