項目背景
因爲需求是和硬件對接,需要定時對硬件設備進行檢查,因此決定使用netty作爲通信中間件。使用netty的ChannelDuplexHandler 來接收、下發硬件數據。
硬件通過TCP長連接向服務端發送指令,服務端使用netty監聽固定端口,接收並處理指令。
硬件發送的是16進制字節流,使用netty的ByteArrayDecoder、ByteArrayEncoder 對數據進行編碼處理。
因爲要處理TCP粘包的問題,所以同硬件約定在傳送數據的末尾加上兩對\r\n用於區分。因此,netty中我用了DelimiterBasedFrameDecoder 對數據做截取。
Netty版本:netty-all-4.0.46.Final
<!-- Maven POM-->
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.46.Final</version>
</dependency>
服務端代碼
/**
* 讀操作的超時時間,0表示不監控
*/
private Integer readerTimeout = 720;
/**
* 寫操作的超時時間,0表示不監控
*/
private Integer writeTimeout = 0;
/**
* 寫操作的超時時間,0表示不監控
*/
private Integer allTimeout = 0;
/**
* listen鏈接
*/
private Channel channel;
private final static String DISCONNECT = "disconnect";
private final static String STOP = "server_stop";
private ServerBootstrap strap;
@Resource
private DeviceHandler handler;
@Value("${netty.server.port}")
private int port;
/**
* 啓動netty程序
*
* @throws Exception
*/
public void start() throws Exception {
logger.info("netty server start !");
EventLoopGroup bossGroup = new NioEventLoopGroup(0, new DefaultThreadFactory("bossGroup"));
EventLoopGroup workGroup = new NioEventLoopGroup(0, new DefaultThreadFactory("workGroup"));
logger.info("connection is already!");
this.strap = new ServerBootstrap();
strap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024 * 2, Unpooled.copiedBuffer("\r\n\r\n".getBytes())));
ch.pipeline().addLast("byteArrayDecoder", new ByteArrayDecoder());
ch.pipeline().addLast("byteArrayEncoder", new ByteArrayEncoder());
ch.pipeline().addLast("idleState", new IdleStateHandler(readerTimeout, writeTimeout, allTimeout));
ch.pipeline().addLast(handler);
}
}).option(ChannelOption.SO_BACKLOG, 128).option(ChannelOption.SO_LINGER, 0)
.childOption(ChannelOption.SO_KEEPALIVE, true);
channel = strap.bind(port).sync().channel();
}
/**
* 關閉netty程序
*/
public void stop() {
if (channel != null) {
channel.close();
}
logger.info("disconnected: server stop");
for (DeviceChannel dc : DeviceHandler.getChannels()) {
if (dc != null) {
String deviceId = dc.getDevice() == null ? DeviceHandler.E0E0E0 : dc.getDevice().getDeviceId();
AddressDto addressDto = StringParamUtil.getAddressInfo(dc.getChannel().remoteAddress().toString());
DataCollector.collectConnectInfo(deviceId, addressDto.getIp(), addressDto.getPort(),
dc.getChannel().toString(), NetUtil.hostName(), new Date(), STOP, DISCONNECT);
if (dc.getState() == AbstractChannel.DeviceState.CONNECTED){
dc.disconnect();
}
dc.closeChannel();
}
}
if (strap != null) {
strap.group().shutdownGracefully();
strap.childGroup().shutdownGracefully();
}
DeviceHandler.getChannels().clear();
MDC.remove(ConstantUtil.WTRACEID);
}
<!-- 服務端配置了兩個方法,分別用於netty的啓動和停止。這兩個方法使用spring來調用,如下所示: -->
<bean class="***.***.***" init-method="start" destroy-method="stop"/>
業務代碼
我們在服務中用到的DeviceHandler,其實是繼承自Netty的
ChannelDuplexHandler,其中核心的兩個方法是channelRead、write。前者用於接收服務器發來的數據,後者用於向服務器發送指令。代碼實例如下:
@Component
@ChannelHandler.Sharable
public class DeviceHandler extends ChannelDuplexHandler {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//在這裏可以處理硬件發送過來的數據
logger.debug("數據對象長度:" + ((byte[]) msg).length);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise);
}
}
下面是DeviceHandler 的升級版。當硬件與服務器建立一條鏈接(channel),我們將活動鏈接存儲到Map中,失效的鏈接則從map中移除。
下面的代碼用到了DeviceChannel ,它其實是繼承自AbstractChannel ,我們對於每個硬件的管理便是依賴於它。
在我們的項目中會將硬件發來的數據傳遞給DeviceChannel,之後使用DeviceChannel將數據轉發給其他系統。
@Component
@ChannelHandler.Sharable
public class DeviceHandler extends ChannelDuplexHandler {
private static final Map<SocketAddress, DeviceChannel> channels = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DeviceChannel dc = channels.get(ctx.channel().remoteAddress());
if (dc == null) {
dc = new DeviceChannel();
dc.init();
channels.put(ctx.channel().remoteAddress(), dc);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
DeviceChannel dc = channels.remove(ctx.channel().remoteAddress());
if (dc != null && dc.getState() == DeviceState.CONNECTED) {
dc.disconnect();
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//在這裏可以處理硬件發送過來的數據
logger.debug("數據對象長度:" + ((byte[]) msg).length);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise);
}
}
最後
因爲這是成熟的商業項目,所以更詳細的代碼就不方便透露了。
總的來說,設計思路就是使用DeviceHandler重寫ChannelDuplexHandler 的channelRead、write方法,使用它們來處理硬件數據的接收、下發等操作。