netty:使用ChannelDuplexHandler 來接收、下發數據

項目背景

因爲需求是和硬件對接,需要定時對硬件設備進行檢查,因此決定使用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,其中核心的兩個方法是channelReadwrite。前者用於接收服務器發來的數據,後者用於向服務器發送指令。代碼實例如下:

@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重寫ChannelDuplexHandlerchannelRead、write方法,使用它們來處理硬件數據的接收、下發等操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章