前言:
项目为SpringMVC架构,在此基础上新增加了Netty服务用来接收客户端发来的消息,在Netty的handler业务处理类内需要将消息存入数据库.
后续补充: 限制了客户端接入的数量不超过两个
流程:
先看一下包的结构图
按照此包结构图奉上精心修改过的伪代码,基本上可以拿走就直接使用的
图片蓝色背景的都为netty(ats)相关服务新增加的文件
1.netty服务端,实现了Runnable接口,为的是可以在重写的run()方法内启动服务端(没整合之前是在下边写了一个main函数启动的,当然也没有实现Runnable接口);因为在初始化时候使用了new关键字创建出来的ChannelInitializer《SocketChannel》对象以及包含的对象,这些对象是在netty创建的,并没有将此对象交由spring容器管理(SpringMVC启动时,只有被扫描到的bean才会被初始化,只有被初始化的bean才会加入spring容器,只有加入spring容器的bean才可以用@Autowired或@Resource来获得),所以这是在handler业务处类或是数据存库类里注入的dao层依赖为一直为null的原因
/**
* netty服务端
*/
public class ServerP implements Runnable{
public void bind(int port) throws InterruptedException {
//创建用于接受客户端的线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
//创建用于处理网络操作的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务端启动助手用于配置参数
ServerBootstrap b = new ServerBootstrap();
//设置2个线程组
b.group(bossGroup,workerGroup)
//设置通道的底层实现
.channel(NioServerSocketChannel.class)
//option是NioServerSocketChannel提供的用来接收进来的连接
.option(ChannelOption.SO_BACKLOG,2048)
//childOption是NioServerSocketChannel接收到的连接
.childOption(ChannelOption.SO_KEEPALIVE,true)
//childHandler为建立连接之后的执行器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//0.ByteOrder.LITTLE_ENDIAN设置接收的字节序为小端传输(ByteOrder.BIG_ENDIAN默认为大端传输)
//1.单个包最大长度
//2.长度字段开始位置偏移量
//3.长度字段所占字节数
//4.长度字段默认表示后续报文的长度(公式:数据包总长度-长度字段偏移量-长度字段字节数-长度字段表示的长度值)
//5.解码过程中没有丢弃任何数据
//6.默认为true,表示读取到长度域的值超过Integer.MAX_VALUE时抛异常,false表示真正读取完才抛异常,建议不要修改,否则可能会造成内存溢出
sc.pipeline().addLast("frameDecoder",new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN,Integer.MAX_VALUE,2,2,3,0,true));
//对接收到的客户端消息进行解码
sc.pipeline().addLast("decoder",new MsgDeCode());
//sc.pipeline().addLast("encode",new MsgEnCode());
sc.pipeline().addLast("handler",new SHandler());
}
});
//绑定端口,同步等待
ChannelFuture cf = b.bind(port).sync();
//监听服务器端口关闭
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//优雅推出,释放线程池资源
System.out.println("服务器关闭 >>>");
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
/**
* run方法开启netty
*/
@Override
public void run() {
int port = 9527;
System.out.println("netty服务端已经启动 >>>");
try {
new ServerP().bind(port);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.消息解码类,将接收到的消息解析为java对象;继承MessageToMessageDecoder《ByteBuf》,重写decode方法;对于处理ByteBuf缓冲区数据,个人目前使用的有两种方法,第一可以直接读取ByteBuf缓冲区内的数据进行操作,第二也可以将接收到的数据存放arr数组内,后续对arr数组内的数据进行操作;两种方法可以根据实际情况使用
/**
* Netty解码类
* 将消息解析并存放到java对象
*/
public class MsgDeCode extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> list) throws Exception {
//可读字节数
int i = msg.readableBytes();
System.out.println("可读字节 = " + i);
//先判断已知字节数
if(i < 28){
System.out.println("单个包长度小于28字节,退出!");
return;
}
//将读到的数据放入arr数组,后续也可以直接操作数组内存放的数据
byte[] arr = new byte[i];
//从指定的绝对索引开始,将缓冲区的数据传输到指定的目的地
msg.getBytes(msg.readerIndex(),arr,0,i);
//准备存储对象:
// 1 接收的数据
MsgInfo msgInfo = new MsgInfo();
// 2 存库数据
ArrayList<DataInfo> listDataInfo = new ArrayList<>();
//标记ByteBuf缓存内指针的起始位
//msg.markReaderIndex();
//获取帧头
byte head1 = msg.readByte();
byte head2 = msg.readByte();
if((head1 & 0xff) != 0xb0 || (head2 & 0xff) != 0xb0){
//指针复位
//msg.resetReaderIndex();
System.out.println("帧头不正确,退出!");
return;
}
int head = (head1 & 0xff * 256) + (head2 & 0xff);
msgInfo.setHead(head);
System.out.println("head = " + head);
//获取len
byte len1 = msg.readByte();
byte len2 = msg.readByte();
int len = (len1 & 0xff * 256) + (len2 & 0xff);
msgInfo.setDataLen(len);
System.out.println("len = " + len);
//获取帧类型
byte type = msg.readByte();
if(type != 0x51){
//指针复位
//msg.resetReaderIndex();
System.out.println("帧类型不正确,退出!");
return;
}
msgInfo.setHeadType(type);
System.out.println("type = " + type);
//存入时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String inTime = simpleDateFormat.format(new Date());
System.out.println("inTime = " + inTime);
//获取carNum
byte num = msg.readByte();
int carNum = num & 0xff;
String carNums = String.valueOf(carNum);
System.out.println("carNum = " + carNum);
//准备后续需要校验的数据
byte[] crc = new byte[(carNum * 20) + 1];
System.arraycopy(arr,5,crc,0,crc.length);
//定义一个变量用来控制do-while循环
int count = 0;
do {
DataInfo dataInfo = new DataInfo();
//存入接收时间
dataInfo.setInTime(inTime);
//存入carNum
dataInfo.setCarNum(carNums);
//获取id
byte id1 = msg.readByte();
byte id2 = msg.readByte();
String s1 = String.valueOf(id1 & 0xff);
String s2 = String.valueOf(id2 & 0xff);
StringBuilder sb = new StringBuilder();
StringBuilder append = sb.append(s1).append(s2);
dataInfo.setCarId(append.toString());
System.out.println("id = " + append.toString());
//获取经度
long jd1 = msg.readLong();
String jds = String.valueOf(jd1);
String jd = sTos(3, 7, jds);
dataInfo.setLongitude(jd);
System.out.println("经度 = " + jd);
//获取纬度
long wd1 = msg.readLong();
String wds = String.valueOf(wd1);
String wd = sTos(2, 7, wds);
dataInfo.setLatitude(wd);
System.out.println("纬度 = " + wd);
//获取速度
byte spe1 = msg.readByte();
byte spe2 = msg.readByte();
int speed = (spe1 & 0xff * 256) + (spe2 & 0xff);
String speeds = String.valueOf(speed);
dataInfo.setCarSpeed(speeds);
System.out.println("speed = " + speed);
listDataInfo.add(dataInfo);
count++;
}while (count < carNum);
//获取校验码
byte check1 = msg.readByte();
byte check2 = msg.readByte();
int checkCode = (check1 & 0xff * 256) + (check2 & 0xff);
msgInfo.setCheckCode(checkCode);
System.out.println("checkCode = " + checkCode);
msgInfo.setDataInfo(listDataInfo);
int crc16yh = CRC16.getCRC16yh(crc);
if(checkCode == crc16yh){
list.add(msgInfo);
}else {
System.out.println("CRC校验码异常!");
return;
}
}
}
3.服务端处理器类,继承的SimpleChannelInboundHandler《MsgInfo》,泛型为接收完整消息的实体类,个人感觉还是很方便后续操作的
/**
* Netty服务端处理器类
*/
public class SHandler extends SimpleChannelInboundHandler<MsgInfo> {
//用于高并发计数的类,保证在自增或者自减的情况下的线程安全
//private AtomicInteger atomicInteger;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("调用 channelActive方法 >>>");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = simpleDateFormat.format(new Date());
System.out.println("来自客户端:" + ctx.channel().remoteAddress() + " >>> 时间:" + time);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MsgInfo msg) throws Exception {
System.out.println("进入业务处理 >>>");
//每有一个连接就自增1
//atomicInteger.incrementAndGet();
//判断是否大于2个连接
if(atomicInteger.get() > 2){
ctx.channel().close();
}
//将msg消息json格式化
String s = JSON.toJSONString(msg);
System.out.println("s = " + s);
//声明生产者发送数据给kafka的类
//ProducerGotoKafka producer = new ProducerGotoKafka();
//输入kafka的topic,传入json
//将数据发送至kafka
//producer.send("",s);
//将数据存入数据库
MsgInfo msgInfo= JSON.parseObject(s, MsgInfo.class);
System.out.println("msgInfo= " + msgInfo.toString());
//创建存库类
AddInfo addInfo = new AddInfo(msgInfo);
addInfo.start();
String resp = "OK";
ByteBuf buf = Unpooled.copiedBuffer(resp.getBytes());
ctx.write(buf);
}
//@Override
//public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//连接结束自减1
//atomicInteger.decrementAndGet();
//}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
4.数据存库类,该类继承了Thread重写了run()方法,在这个类里边对需要的数据存入数据库,这里用到了dao层的依赖注入,首先在本类加上@Component注解(将这个对象交由spring容器管理),然后声明静态变量,依赖正常注入,声明传入构造方法的参数,声明构造方法,然后@PostConstruct这个是重点
/**
* 开启新线程执行数据存库
*/
//此注解等同于在xml文件内注册
//xml文件写法
//<bean id="AddInfo" class="com.xx.xx.AddInfo"></bean>
@Component
public class AddInfo extends Thread{
private static AddInfo addInfo;
//注入依赖
@Resource
private DaoSupport dao;
//声明传入构造方法的参数
private MsgInfo msgInfo;
public AddInfo(MsgInfo msgInfo) {
this.msgInfo = msgInfo;
}
public AddInfo() {
}
//容器初始化的时候执行这里,这里是重点
//因为没有交给spring容器管理,所以一定要先初始化,否则依赖注入失效
@PostConstruct
public void init(){
addInfo = this;
addInfo.dao = this.dao;
}
@Override
public void run() {
List<DataInfo> dataInfo = msgInfo.getDataInfo();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = simpleDateFormat.format(new Date());
for (DataInfo info : dataInfo) {
try {
int row1 = (int) addInfo.dao.save("AtsMapper.add1", info);
int row2 = (int) addInfo.dao.save("AtsMapper.add2", info);
if(row1 > 0){
System.out.println(">>> 信息存库成功 >>>" + time);
}else {
System.out.println("<<< 信息存库失败 <<<" + time);
}
if(row2 > 0){
System.out.println(">>> 信息存库成功 >>>" + time);
}else {
System.out.println("<<< 信息存库失败 <<<" + time);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
5.监听项目启动同时启动netty服务,实现的是ServletContextListener接口:对应监控application内置对象的创建和销毁,注解相比修改配置文件内容来说既方便又快捷,不用再去修改别人之前写好的配置文件内容,这样也更有利于维护代码
//此注解方法等同于在web.xml文件内注册
//web.xml写法
//<listener>
//<listener-class>com.xx.xx.StartNettyContextListener</listener-class>
//</listener>
@WebListener
public class StartNettyContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
ServerP serverP = new ServerP();
System.out.println("项目启动netty启动 >>>");
//需要在新建的线程内启动netty,如果在这里直接调用serverP.run()方法则会造成项目主线程的阻塞
Thread t = new Thread(serverP);
t.start();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
最后奉上Netty API Reference (5.0.0.Alpha2),希望大家在使用过程中更顺手
https://netty.io/5.0/api/?spm=a2c4e.10696291.0.0.7fde19a4ZR9zc3
总结:参考了网上很多的方法,里边还有很多可以优化的地方,写这样的业务经验也不足,如有更好的解决办法,还请各位不吝赐教,共同研讨,共同进步!