第一步,netty服务搭建,本人整合了微服务,单机netty可以百度查到
<dependencies>
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.hope</groupId>
<artifactId>hope-common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.hope</groupId>
<artifactId>hope-common-rocketmq</artifactId>
<version>${hope.version}</version>
</dependency>
</dependencies>
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* socket服务
* @author zhangYuHui
* date 2022-09-30
*/
@Slf4j
@Component
public class SocketServer {
@Resource
private SocketInitializer socketInitializer;
@Getter
private ServerBootstrap serverBootstrap;
/**
* netty服务监听端口
*/
@Value("${netty.port:8088}")
private int port;
/**
* 主线程组数量
*/
@Value("${netty.bossThread:2}")
private int bossThread;
/**
* 启动netty服务器
*/
public void start() {
this.init();
try {
this.serverBootstrap.bind(this.port).sync();
} catch (InterruptedException e) {
e.printStackTrace();
log.error("Netty error on port: {} (TCP) with boss thread {}", this.port, this.bossThread);
}
log.info("Netty started on port: {} (TCP) with boss thread {}", this.port, this.bossThread);
}
/**
* 初始化netty配置
*/
private void init() {
// 创建两个线程组,bossGroup为接收请求的线程组,一般1-2个就行
NioEventLoopGroup bossGroup = new NioEventLoopGroup(this.bossThread);
// 实际工作的线程组
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
this.serverBootstrap = new ServerBootstrap();
// 两个线程组加入进来
this.serverBootstrap.group(bossGroup, workerGroup)
// 配置为nio类型
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
// 保持连接
.option(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 加入自己的初始化器
.childHandler(this.socketInitializer);
}
}
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 监听Spring容器启动完成,完成后启动Netty服务器
* @author zhangYuHui
* date 2022-09-30
*/
@Component
public class NettyStartListener implements ApplicationRunner {
@Resource
private SocketServer socketServer;
@Override
public void run(ApplicationArguments args) throws Exception {
this.socketServer.start();
}
}
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import org.springframework.stereotype.Component;
/**
* Socket 初始化器,每一个Channel进来都会调用这里的 InitChannel 方法
*
* @author zhangYuHui
* date 2022-09-30
*/
@Component
public class SocketInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel){
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(1024,false,true, Unpooled.copiedBuffer(new byte[]{0x23,0x23})));
pipeline.addLast(new XFSocketHandler());
}
}
import com.hope.netty.entity.QnResult;
import com.hope.netty.entity.SmartIotProtocol;
import com.hope.netty.service.FireSwitchboardService;
import com.hope.common.core.utils.SpringUtils;
import com.hope.common.netty.ByteUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 消防处理器,一般都是4040开头
* @author zhangYuHui
* date 2022-10-28
*/
@Slf4j
public class XFSocketHandler extends ChannelInboundHandlerAdapter {
public static final Map<String, ChannelHandlerContext> ctxMap = new ConcurrentHashMap<>(16);
public static FireSwitchboardService fireSwitchboardService;
static {
fireSwitchboardService = SpringUtils.getBean(FireSwitchboardService.class);
}
/**
* 读取到客户端发来的消息
* @param ctx ChannelHandlerContext
* @param msg msg
* @throws Exception e
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 由于我们配置的是 字节数组 编解码器,所以这里取到的用户发来的数据是 byte数组
ByteBuf buf = (ByteBuf) msg;
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
String strDate = ByteUtil.bytes2HexStr(data);
log.info("消防报文数据: " + strDate);
if (strDate.startsWith("4040") && strDate.endsWith("2323")){
try {
// 消防报文,目前只对接青鸟
SmartIotProtocol smartIotProtocol = SmartIotProtocol.geEntity(data);
fireSwitchboardService.beiDaQingNiao(smartIotProtocol,ctx.channel().id().asShortText());
} finally {
byte[] result = QnResult.success(data);
ctx.writeAndFlush(Unpooled.copiedBuffer(result));
}
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
String s = ctx.channel().id().asShortText();
log.info("消防新的客户端链接: " + s);
ctxMap.put( s,ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
String s = ctx.channel().id().asShortText();
log.info("消防有的客户端与服务器断开连接: " + s);
ctxMap.remove(s);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.channel().close();
log.info("消防出现异常,断开连接: " +ctx.channel().id().asShortText());
ctxMap.remove(ctx.channel().id().asShortText());
}
}
大概格式
4个实体
import com.hope.common.core.utils.DateUtils;
import com.hope.common.netty.ByteUtil;
import com.hope.common.netty.XfDict;
import lombok.Data;
import java.util.Arrays;
/**
* 青鸟建筑消防设施部件运行状态
* 青鸟用传装置 类型标志 02
* @author zhangYuHui
* date 2022-10-12
*/
@Data
public class QnComponentEntity {
/**设备id*/
private String id;
/**ctxId*/
private String ctxId;
/**系统类型 10=消防联动控制器 */
private String systemType;
/**系统地址 3=3号机 */
private int systemAddress;
/**部件类型*/
private String partsType;
/**部件地址*/
private String partsAddress;
/**状态 0测试状态 1 正常运行状态 */
private int status;
/**状态 0无火警 1 火警 */
private int isFire;
/**状态 0无故障 1 故障 */
private int isFault;
/**31字节的中文注释 字符集GB18030(GBK)C类型字符串数组(\0值尾部) 1-16分区消报消钮 */
private String notes;
/**状态发生时间*/
private String date;
/**系统当前时间*/
private String sysDateTime;
public static QnComponentEntity init(byte[] data){
if (data.length != 48){
return null;
}
int i = data[0] & 0xff;
if (i != 2){
return null;
}
QnComponentEntity entity = new QnComponentEntity();
entity.setSystemType(XfDict.systemType(data[2] & 0xff));
entity.setSystemAddress(data[3] & 0xff);
entity.setPartsType(XfDict.partsType(data[4] & 0xff));
entity.setPartsAddress(XfDict.partsAddress(Arrays.copyOfRange(data,5,9)));
entity.setStatus(data[9] & 0x1);
entity.setIsFire(data[9] >>> 1 & 0x1);
entity.setIsFault(data[9] >>> 2 & 0x1);
entity.setNotes(ByteUtil.bytes2HexStr(Arrays.copyOfRange(data,11,42)));
entity.setDate(XfDict.getDate(Arrays.copyOfRange(data,42,48)));
entity.setSysDateTime(DateUtils.getDateTime());
return entity;
}
}
import com.hope.common.core.utils.DateUtils;
import com.hope.common.netty.XfDict;
import lombok.Data;
import java.util.Arrays;
/**
* 上传用户信息传输装置操作信息记录
* 青鸟用传装置 类型标志 24
* @author zhangYuHui
* date 2022-10-12
*/
@Data
public class QnOperationEntity {
/**设备id*/
private String id;
/**ctxId*/
private String ctxId;
/**状态 0 无操作 1 复位 */
private int isReset;
/**状态 0 无操作 1 消音 */
private int isSilence;
/**状态 0 无操作 1 报警 */
private int isWarning;
/**状态 0 无操作 1 自检 */
private int isTesting;
/**状态 操作员编号 */
private int code;
/**状态发生时间*/
private String date;
/**系统时间**/
private String sysDateTime;
public static QnOperationEntity init(byte[] data){
if (data.length != 10){
return null;
}
int i = data[0] & 0xff;
if (i != 24){
return null;
}
QnOperationEntity entity = new QnOperationEntity();
entity.setIsReset(data[2] & 0x1);
entity.setIsSilence(data[2] >>> 1 & 0x1);
entity.setIsWarning(data[2] >>> 2 & 0x1);
entity.setIsTesting(data[2] >>> 4 & 0x1);
entity.setCode(data[3] & 0xff);
entity.setDate(XfDict.getDate(Arrays.copyOfRange(data,4,10)));
entity.setSysDateTime(DateUtils.getDateTime());
return entity;
}
}
import com.hope.common.netty.ByteUtil;
import lombok.Data;
import java.util.Arrays;
/**
* 自定义协议
*
* @author zhangYuHui
* date 2022-09-30
*/
@Data
public class SmartIotProtocol {
/**
* 数据包启动符号 @@
*/
public String start;
/**
* 业务流水号
*/
private String flowid;
/**
* 主版本
*/
private byte versionMajor;
/**
* 次版本
*/
private byte versionMinor;
/**
* 秒
*/
private byte second;
/**
* 分钟
*/
private byte minute;
/**
* 小时
*/
private byte hour;
/**
* 日
*/
private byte day;
/**
* 月
*/
private byte month;
/**
* 年
*/
private byte year;
/**
* 数据包的源地址
*/
private byte[] src;
/**
* 数据包的目的地址
*/
private byte[] dest;
/**
* 应用数据单元长度 长度不应大于1024;低字节传输在前
*/
private int dataLen;
/**
* 命令字节 为控制单元的命令字节
*/
private byte cmd;
/**
* 应用数据单元 对于确认/否认等命令包,此单元可为空
*/
private byte[] data;
/**
* 校验和 控制单元中各字节数据(第3~第27字节)及应用数据单元的算术校验和,舍去8位以上的进位位后所形成的1字节二进制数
*/
private byte checksum;
/**
* 协议结束符号 ##
*/
public String end ;
/**
* 打印调试信息
*/
public void printDebugInfo(){
System.out.println("---------完整数据包开始------------");
System.out.println("|开始标志: " + start);
System.out.println("|业务流水: " + flowid);
System.out.println("|协议版本: " + printHexByte(versionMajor) + printHexByte(versionMinor));
System.out.println("|时间标签: " + "20" + year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second);
System.out.println("|源地址 : " + ByteUtil.bytesToIp(src));
System.out.println("|目的地址: " + ByteUtil.bytesToIp(dest));
System.out.println("|数据长度: " + dataLen);
System.out.println("|命令字节: " + printHexByte(cmd));
System.out.println("|应用数据: " + printHexBytes(data));
System.out.println("|校验字节: " + printHexByte(checksum));
System.out.println("|结束标志: " + end);
System.out.println("---------------------------------");
}
private String printHexByte(byte b){
return String.format("%02X", b);
}
private String printHexBytes(byte[] bytes){
String str = "";
for(int i=0; i<bytes.length; i++){
str += String.format("%02X", bytes[i]);
}
return str;
}
private String printHexShort(int s){
byte[] bytes = hexShort(s);
return printHexBytes(bytes);
}
private byte[] hexShort(int s){
byte[] bytes = new byte[2];
bytes[0] = (byte)((s << 24) >> 24);
bytes[1] = (byte)((s << 16) >> 24);
return bytes;
}
private byte[] hexInt(int n){
byte[] bytes = new byte[4];
bytes[3] = (byte) ((n ) >> 24);
bytes[2] = (byte) ((n << 8) >> 24);
bytes[1] = (byte) ((n << 16) >> 24);
bytes[0] = (byte) ((n << 24) >> 24);
return bytes;
}
/**
* 初始化数据
* @param data 字节
* @return SmartIotProtocol
*/
public static SmartIotProtocol geEntity( byte[] data ){
SmartIotProtocol entity = new SmartIotProtocol();
entity.setStart(ByteUtil.bytes2HexStr(data, 0, 2));
entity.setFlowid(ByteUtil.bytes2HexStr(data, 2, 2));
entity.setVersionMajor(data[4]);
entity.setVersionMinor(data[5]);
entity.setSecond(data[6]);
entity.setMinute(data[7]);
entity.setHour(data[8]);
entity.setDay(data[9]);
entity.setMonth(data[10]);
entity.setYear(data[11]);
entity.setSrc(Arrays.copyOfRange(data,12,17));
entity.setDest(Arrays.copyOfRange(data,18,23));
entity.setDataLen(ByteUtil.byteArrayToInt(Arrays.copyOfRange(data,24,26)));
entity.setCmd(data[26]);
entity.setData(Arrays.copyOfRange(data,27,data.length-3));
entity.setChecksum(data[data.length-3]);
entity.setEnd(ByteUtil.bytes2HexStr(data, data.length-2, 2));
return entity;
}
}
import com.hope.common.netty.ByteUtil;
import java.util.Calendar;
/**
* 青鸟消防返回报文
*
* @author zhangYuHui
* date 2022-10-18
*/
public class QnResult {
/**秒*/
private static byte SECOND;
/**分*/
private static byte MINUTE;
/**时*/
private static byte HOUR;
/**日*/
private static byte DAY;
/**月*/
private static byte MONTH;
/**年*/
private static byte YEAR;
/**
* 成功回应,应答确认
* @param data 原数据 3
* @return byte[]
*/
public static byte[] success(byte[] data){
return getBytes(data,(byte)0x03);
}
/**
* 成功回应,否认确认
* @param data 原数据
* @return byte[]
*/
public static byte[] error(byte[] data){
return getBytes(data,(byte)0x06);
}
/**
* 返回对应的应答
* @param data 原数据
* @param b 3 对控制命令和发送信息的确认回答
* 6 对控制命令和发送信息的否认回答
* @return byte
*/
public static byte[] getBytes(byte[] data,byte b){
byte cac = getCac(data);
return new byte[]{(byte)0x40, (byte)0x40,
data[2], data[3], (byte)0x01, (byte)0x01,
SECOND, MINUTE, HOUR,DAY,MONTH,YEAR,
data[18], data[19], data[20],data[21],data[22],data[23],
data[12], data[13], data[14],data[15],data[16],data[17],(byte)0x00, (byte)0x00, b,
cac,(byte)0x23, (byte)0x23};
}
/**
* 控制单元中各字节数据(第3~27字节)及应用数据单元的算术校验和,舍去8位
* (1字节) 以上的进位位后所形成的 1 字节二进制数
* @param data 数据
* @return byte
*/
public static byte getCac(byte[] data){
Calendar c = Calendar.getInstance();
SECOND = (byte) (c.get(Calendar.SECOND) & 0xff);
MINUTE = (byte)(c.get(Calendar.MINUTE) & 0xff);
HOUR = (byte)(c.get(Calendar.HOUR_OF_DAY) & 0xff);
DAY = (byte)(c.get(Calendar.DATE) & 0xff);
MONTH = (byte)((c.get(Calendar.MONTH) + 1) & 0xff);
YEAR = (byte)(Integer.parseInt(String.valueOf(c.get(Calendar.YEAR)).substring(2)) & 0xff);
byte[] success = new byte[] { data[2], data[3], (byte)0x01, (byte)0x01,
SECOND, MINUTE, HOUR,DAY,MONTH,YEAR,
data[18], data[19], data[20],data[21],data[22],data[23],
data[12], data[13], data[14],data[15],data[16],data[17],(byte)0x00, (byte)0x00,(byte)0x03};
return ByteUtil.calCRC(success);
}
}
service 层
import com.hope.netty.entity.QnComponentEntity;
import com.hope.netty.entity.QnOperationEntity;
/**
* rocketmq 消息
* @author zhangyuhui
* @date 2022-07-20 16:03:07
*/
public interface RocketmqService {
/**
* 发送上传用户信息传输装置操作信息记录
* @param init 原数据
*/
void send(QnOperationEntity init);
/**
* 部件的运行状态
* @param init 原数据
*/
void send(QnComponentEntity init);
}
import com.hope.netty.entity.SmartIotProtocol;
/**
* 消防总机service
* @author hcbf
* @version 1.0
* @date 2022-07-20 16:03:07
*/
public interface FireSwitchboardService {
/**
* 北大青鸟消防主键
* @param smartIotProtocol 消防设备报文解析
* @param ctxId 客户端id
*/
void beiDaQingNiao(SmartIotProtocol smartIotProtocol,String ctxId);
}
import com.hope.netty.entity.QnComponentEntity;
import com.hope.netty.entity.QnOperationEntity;
import com.hope.netty.service.RocketmqService;
import com.hope.rocketmq.constant.MqConstants;
import com.hope.rocketmq.domain.NqOperationExchange;
import com.hope.rocketmq.domain.QnComponentExchange;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
/**
* rocketmq消息
* @author zhangYuHui
* date 2022-10-24
*/
@Service
@Slf4j
public class RocketmqServiceImpl implements RocketmqService {
public final RocketMQTemplate rocketMQTemplate;
public RocketmqServiceImpl(RocketMQTemplate rocketMQTemplate) {
this.rocketMQTemplate = rocketMQTemplate;
}
@Override
public void send(QnOperationEntity init) {
if (init ==null){return;}
int status = 0;
if (1 == init.getIsReset()){
status = 1;
}else if (1 == init.getIsSilence()){
status = 2;
} else if (1 == init.getIsWarning()){
status = 3 ;
} else if (1 == init.getIsTesting()){
status = 4;
}
NqOperationExchange exchange = new NqOperationExchange();
exchange.setId(init.getId())
.setCtxId(init.getCtxId())
.setCode(init.getCode())
.setDate(init.getDate())
.setSysDateTime(init.getSysDateTime()).setStatus(status);
Message<NqOperationExchange> springMessage = MessageBuilder.withPayload(exchange).build();
rocketMQTemplate.send(MqConstants.NQ_OPERATION_EXCHANGE,springMessage);
log.info("发送上传用户信息传输装置操作信息记录");
}
@Override
public void send(QnComponentEntity init) {
if (init == null){return;}
int status = init.getStatus();
if (1 == init.getIsFire()){
status = 3;
} else if (1 == init.getIsFault()){
status = 4;
}
QnComponentExchange exchange = new QnComponentExchange();
exchange.setId(init.getId())
.setCtxId(init.getCtxId())
.setDate(init.getDate())
.setSystemType(init.getSystemType())
.setSystemAddress(init.getSystemAddress())
.setPartsType(init.getPartsType())
.setPartsAddress(init.getPartsAddress())
.setSysDateTime(init.getSysDateTime()).setStatus(status);
Message<QnComponentExchange> springMessage = MessageBuilder.withPayload(exchange).build();
rocketMQTemplate.send(MqConstants.NQ_COMPONENT_EXCHANGE,springMessage);
log.info("发送部件运行状态信息");
}
}
import com.hope.netty.entity.QnComponentEntity;
import com.hope.netty.entity.QnOperationEntity;
import com.hope.netty.entity.SmartIotProtocol;
import com.hope.netty.service.FireSwitchboardService;
import com.hope.netty.service.RocketmqService;
import com.hope.common.core.constant.CacheConstants;
import com.hope.common.core.constant.Constants;
import com.hope.common.core.utils.SpringUtils;
import com.hope.common.netty.XfDict;
import com.hope.common.redis.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 消防总机
* @author zhangYuHui
* date 2022-10-03
*/
@Service
@Slf4j
public class FireSwitchboardServiceImpl implements FireSwitchboardService {
private static Map<String,String> maps = new ConcurrentHashMap<>(10);
@Autowired
private RocketmqService rocketmqService;
@Override
public void beiDaQingNiao(SmartIotProtocol smartIotProtocol, String ctxId) {
byte[] data = smartIotProtocol.getData();
if (data.length == 0){
return;
}
int i = data[0] & 0xff;
// 消防模块
if (i == 2){
QnComponentEntity init = QnComponentEntity.init(smartIotProtocol.getData());
if (init ==null){return;}
init.setId(maps.get(ctxId));
init.setCtxId(ctxId);
rocketmqService.send(init);
}else if (i == 24){
// 装置操作
QnOperationEntity init = QnOperationEntity.init(smartIotProtocol.getData());
if (init ==null){return;}
init.setId(maps.get(ctxId));
init.setCtxId(ctxId);
rocketmqService.send(init);
}else if (i==26){
//心跳检测
String id = XfDict.getId(smartIotProtocol.getData());
SpringUtils.getBean(RedisService.class).setCacheObject(CacheConstants.EQ_NQ_KEY+id, ctxId, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
maps.put(ctxId,id);
log.info("心跳检测: " + id);
}
}
}
测试结果