前言
公司需要与硬件对接,需要接收硬件传过来的消息数据。根据这个前提,技术选型为Netty。软件作为服务端,硬件作为客户端,双方进行通讯。
代码
导包
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.68.Final</version>
</dependency>
在pom文件中加上Netty的依赖
搭建Netty服务端
-
创建服务端初始化类
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
/**
* @program: lxy
* @author: lxy
* @create: 2024-03-22 15:04
* @description: 服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
**/
public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
//自定义的进出站编码器
channel.pipeline().addLast("decoder",new TimeDecoder());
channel.pipeline().addLast("encoder",new TimeDecoder());
//自带的进出站String编码器
channel.pipeline().addLast("decoder",new StringDecoder());
channel.pipeline().addLast("encoder",new StringEncoder());
channel.pipeline().addLast(new NettyServerHandler());
}
}
2. 自定义进出站编码器(因为此项目接受的16进制数据,所以需要自定义进出站编码器;如果没有特殊要求,可以直接使用自带的String编码器)下面代码中的RFIDTO类是根据业务自定义的
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
// 定义一个缓冲区用于存储数据
ByteBuf buffer = ctx.alloc().buffer();
try {
// 定义每次读取的最大字节数
int maxChunkSize = 1024; // 假设每次最多读取1024字节
while (in.isReadable()) {
// 计算本次读取的字节数
int readableBytes = Math.min(in.readableBytes(), maxChunkSize);
// 从输入缓冲区中读取数据到临时缓冲区中
ByteBuf tempBuffer = in.readSlice(readableBytes).retain();
try {
// 将临时缓冲区中的数据写入到总缓冲区中
buffer.writeBytes(tempBuffer);
} finally {
// 释放临时缓冲区
tempBuffer.release();
}
}
// 进行数据处理
RFIDTO rfidto = processReadData(buffer, out);
out.add(rfidto);
} finally {
// 释放总缓冲区
buffer.release();
}
// // 将ByteBuf转换为字节数组
// System.out.println(in.readableBytes());
}
// 将十六进制字节数组转换为字符串
public static String hexBytesToString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
public static String hexBytesToString(String hexString) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hexString.length(); i += 2) {
String hex = hexString.substring(i, i + 2);
sb.append((char)Integer.parseInt(hex, 16));
}
return sb.toString();
}
public static RFIDTO processReadData (ByteBuf in,List<Object> out){
int i = in.readableBytes();
System.out.println("接收的字节数:"+i);
byte[] byteArray = new byte[in.readableBytes()];
in.readBytes(byteArray);
int len=byteArray[0] & 0XFF;
System.out.println("len:"+len);
String adr=String.format("%02X",byteArray[1]);
System.out.println("adr:"+adr);
String reCmd=String.format("%02X",byteArray[2]);
System.out.println("reCmd:"+reCmd);
String status=String.format("%02X",byteArray[3]);
System.out.println("status:"+status);
String ant=String.format("%02X",byteArray[4]);
System.out.println("Data-ant:"+ant);
int dateLen=byteArray[5] & 0XFF;
System.out.println("Data-len:"+dateLen);
int startIndex = 6; // 起始下标
int length = len-8; // 要拷贝的元素数量
byte[] newArray = new byte[length]; // 新数组
// 使用 System.arraycopy() 方法进行拷贝
System.arraycopy(byteArray, startIndex, newArray, 0, length);
String dataString = hexBytesToString(newArray);
String string = hexBytesToString(dataString);
System.out.println(string);
System.out.println("Data-data:"+dataString);
String rssi=String.format("%02X",byteArray[len-2]);
System.out.println("Data-rssi:"+rssi);
String lsb=String.format("%02X",byteArray[len-1]);
System.out.println("LSB:"+lsb);
String msb=String.format("%02X",byteArray[len]);
System.out.println("MSB:"+msb);
RFIDTO rfidto = new RFIDTO();
rfidto.setLen(len);
rfidto.setAdr(adr);
rfidto.setReCmd(reCmd);
rfidto.setStatus(status);
rfidto.setAnt(ant);
rfidto.setDateLen(dateLen);
rfidto.setDataString(dataString);
rfidto.setRssi(rssi);
rfidto.setLsb(lsb);
rfidto.setMsb(msb);
return rfidto;
}
3. 创建Netty服务端启动类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
/**
* @program: ksf
* @author: lxy
* @create: 2024-03-22 15:04
* @description: netty服务启动类
**/
@Slf4j
@Component
public class NettyServer {
public void start(InetSocketAddress address) {
//配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup) // 绑定线程池
.channel(NioServerSocketChannel.class)
.localAddress(address)
.childHandler(new NettyServerChannelInitializer())//编码解码
.option(ChannelOption.SO_BACKLOG, 128) //服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
.childOption(ChannelOption.SO_KEEPALIVE, true); //保持长连接,2小时无数据激活心跳机制
// 绑定端口,开始接收进来的连接
ChannelFuture future = bootstrap.bind(address).sync();
log.info("netty服务器开始监听端口:" + address.getPort());
//关闭channel和块,直到它被关闭
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
4.创建服务端处理类
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
/**
* @program: ksf
* @author: lxy
* @create: 2024-03-22 15:04
* @description: netty服务端处理类
**/
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 管理一个全局map,保存连接进服务端的通道数量
*/
private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();
/**
* @param ctx
* @author lxy
* @DESCRIPTION: 有客户端连接服务器会触发此函数
* @return: void
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
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
* @author lxy
* @DESCRIPTION: 有客户端终止连接服务器会触发此函数
* @return: void
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
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
* @author lxy
* @DESCRIPTION: 有客户端发消息会触发此函数
* @return: void
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("加载客户端报文......");
if (msg==null){
log.info("无效信息");
return;
}
RFIDTO rfidto=(RFIDTO) msg;
log.info("【" + ctx.channel().id() + "】" + " :" + rfidto.toString());
/**
* 下面可以解析数据,保存数据,生成返回报文,将需要返回报文写入write函数
*
*/
//响应客户端
this.channelWrite(ctx.channel().id(), "Hello Netty Client!");
}
/**
* @param msg 需要发送的消息内容
* @param channelId 连接通道唯一id
* @author lxy
* @DESCRIPTION: 服务端给客户端发送消息
* @return: void
*/
public void channelWrite(ChannelId channelId, Object msg) throws Exception {
ChannelHandlerContext ctx = CHANNEL_MAP.get(channelId);
if (ctx == null) {
log.info("通道【" + channelId + "】不存在");
return;
}
if (msg == null || msg == "") {
log.info("服务端响应空的消息");
return;
}
//将客户端的信息直接返回写入ctx
ctx.write(msg);
//刷新缓存区
ctx.flush();
}
@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.disconnect();
}
}
}
/**
* @param ctx
* @author lxy
* @DESCRIPTION: 发生异常会触发此函数
* @return: void
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println();
ctx.close();
log.info(ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + CHANNEL_MAP.size());
cause.printStackTrace();
}
}
以上就是服务端的整个代码,但是我们通常需要服务端与整个项目一起启动,所以需要写监听类
5.创建Spring监听类,实现Netty服务与项目同时启动(代码中的port是指Netty服务端的端口号,我这里是配置在nacos的,也可以直接写死)
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
/**
* @Author Li XuAn Yi
* @Date 2024/3/19 15:30
* @Version 1.3.0
* @Describe
*/
@Slf4j
@Component
public class NettyServerApplicationListener implements ApplicationListener<ApplicationEvent> {
@Resource
private NettyServer nettyServer;
@Value("${netty.server.port}")
private int port;
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent){
String localIpAddress = this.getLocalIpAddress();
InetSocketAddress inetSocketAddress = new InetSocketAddress(localIpAddress,port);
nettyServer.start(inetSocketAddress);
}
}
private String getLocalIpAddress(){
log.info("********开始获取IP地址********");
String ipAddress="127.0.0.1";
try {
InetAddress localhost = InetAddress.getLocalHost();
ipAddress = localhost.getHostAddress();
log.info("******成功获取本地ID地址:{}*******",ipAddress);
return ipAddress;
} catch (UnknownHostException e) {
log.info("******获取本地IP地址失败*******");
e.printStackTrace();
}
return ipAddress;
}
}
如果加上监听类依然不能启动Netty服务端,检查一下项目的启动类是否启动了监听类;如下图的项目启动类
import com.frank.netty.NettyServerApplicationListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author 80540
*/
@SpringBootApplication
public class WebSocketTestApplication {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(WebSocketTestApplication.class);
springApplication.addListeners(new NettyServerApplicationListener());
springApplication.run(args);
}
}
6.Netty服务端注意事项
处理类NettyServerHandler中,如果需要依赖注入其它Service类,需要静态初始化
第一步,将Handler交给spring 管理 添加@Component
@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
第二步,静态初始化
@Slf4j
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
public static NettyServerHandler nettyServerHandler;
@Resource
private IConeRecordService coneRecordService;
@PostConstruct
public void init() {
nettyServerHandler = this;
nettyServerHandler.coneRecordService = this.coneRecordService;
// 初使化时将已静态化的testService实例化
}
第三步,引用,注意引用需要加前缀,注意注意!!!
/**
* 客户端发消息会触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf msgBuf = (ByteBuf) msg;
byte[] bytes = new byte[msgBuf.readableBytes()];
msgBuf.readBytes(bytes);
// 16进制转码后的内容
String hexStr = HexUtil.encodeHexStr(bytes);
log.info("收到服务器 {} 初始消息: {}",getClientIp(ctx),hexStr);
// 16进制转 字符串
String decodeHexStr = HexUtil.decodeHexStr(hexStr);
log.info("服务器收到 {} 解码消息: {}",getClientIp(ctx), decodeHexStr);
// 这里需要加前缀,避免为空
nettyServerHandler.coneRecordService.addRecord(decodeHexStr);
ctx.flush();
}
为了测试我们可以再简单搭建一个客户端
1.客户端初始化类,与服务端一样需要设置出入站编码器,如果不设置那就是字节数据,我这里没有设置,因为需要硬件的原始字节数据。一般也是设置为String编码器
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
/**
* @program: ksf
* @author: lxy
* @create: 2024-03-22 15:04
* @description: 客户端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器,客户端服务端编解码要一致
**/
public class NettyClientChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new NettyServerHandler());
}
}
2.创建客户端启动类,绑定的端口号要与服务端对应
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
/**
* @program: ksf
* @author: lxy
* @create: 2024-03-22 15:04
* @description: 客户端
**/
@Slf4j
@Data
public class NettyClient implements Runnable {
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8080"));
static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));
private String content;
public NettyClient(String content) {
this.content = content;
}
@Override
public void run() {
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
int num = 0;
boolean boo =true;
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new NettyClientChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new NettyClientHandler());
}
});
ChannelFuture future = b.connect(HOST, PORT).sync();
while (boo) {
num++;
future.channel().writeAndFlush(content + "--" + new Date());
try { //休眠一段时间
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//每一条线程向服务端发送的次数
if (num == 1) {
boo = false;
}
}
log.info(content + "-----------------------------" + num);
//future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
}
3.客户端处理类 channelActive方法是发送消息的具体方法,代码中是本人项目中硬件的模拟消息,如果配上我的服务端,可以直接发送成功
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
/**
* @program: ksf
* @author: lxy
* @create: 2024-03-22 15:04
* @description: 客户端处理类
**/
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 计算有多少客户端接入,第一个string为客户端ip
*/
private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CLIENT_MAP = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) {
CLIENT_MAP.put(ctx.channel().id(), ctx);
// 创建一个10字节的ByteBuf
ByteBuf byteBuf = Unpooled.buffer(60);
// 要放入的内容
byte[] content = {(byte)0x14,(byte) 0x01,(byte) 0x02,(byte) 0x00,(byte) 0x08,(byte) 0x03,
(byte)0x41,(byte) 0x30,(byte) 0x38,(byte) 0x30,(byte) 0x31,(byte) 0x30,(byte) 0x30,(byte) 0x30, (byte)0x30,(byte) 0x30,(byte) 0x30,(byte) 0x31,
(byte) 0xBE,(byte) 0x09,(byte) 0x09};
// 将内容写入ByteBuf
byteBuf.writeBytes(content);
int i = byteBuf.readableBytes();
System.out.println("发送的字节数量:"+i);
ctx.writeAndFlush(byteBuf);
}
/**
* @param ctx
* @author lxy on 2024-03-22 15:04
* @DESCRIPTION: 有服务端端终止连接服务器会触发此函数
* @return: void
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
ctx.close();
log.info("服务端终止了服务");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
log.info("回写数据:" + msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
//cause.printStackTrace();
log.info("服务端发生异常【" + cause.getMessage() + "】");
ctx.close();
}
/**
* @param msg 需要发送的消息内容
* @param channelId 连接通道唯一id
* @author lxy
* @DESCRIPTION: 客户端给服务端发送消息
* @return: void
*/
public void channelWrite(ChannelId channelId, String msg) {
ChannelHandlerContext ctx = CLIENT_MAP.get(channelId);
if (ctx == null) {
log.info("通道【" + channelId + "】不存在");
return;
}
//将客户端的信息直接返回写入ctx
ctx.write(msg + " 时间:" + new Date());
//刷新缓存区
ctx.flush();
}
}
4.测试类
**
* @program: ksf
* @author: lxy
* @create: 2024-03-22 16:06
* @description: 模拟多客户端发送报文
**/
public class TestNettyClient {
public static void main(String[] args) {
//开启10条线程,每条线程就相当于一个客户端
for (int i = 1; i <= 1; i++) {
new Thread(new NettyClient("Hello Netty Server!" + i)).start();
}
}
}
ok,启动项目然后运行测试类没可以看到Netty的运行日志了