业务场景:
基于商业停车场的主程序控制系统(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 通信核心基本实现。支持断线重连,心跳消息发送。