记一次用Netty实现与硬件设备通信的经历

2,402 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

起因

自报家门:Java程序员

最近接手一个项目,这个项目比较特殊需要和多种硬件设备进行数据交互,而我之前接触的项目都是“纯纯”的接口调用,要么是我方提供接口文档给客户那边使用,要么就是对接别人的接口文档来开发。对接硬件还是我入行以来第一次接触,欣喜的同时也伴随着忧虑。我该怎么和硬件交互呢?通过跟硬件厂商的工程师密切沟通后,他们说我只要利用socket技术和他们进行通信,然后将彼此双方交互的数据按照协议规定封装成字节数组发给他们就行了。双方的通信过程大致如下图一般:

image.png

然后对方提供的协议文档由于保密性原则无法展现出来,我随便列举一下大致长这样:

长度版本帧类型内容结束符
0x0A0x010x080x01020304050xFFFF

可能有和我一样之前没有接触过socket编程的兄弟们嘀咕道,这是啥?我给大伙稍稍解释一下你们应该就懂了,假设硬件设备通过socket给我方程序上送了如下一段数据:

0A 01 08 01 02 03 04 05 FF FF
注:硬件设备上送给我们的都是16进制的数据,我管这个叫帧数据,我们在接手到这一帧数据后需要转化
成自己语言对应的数据结构

我方程序接收后按照协议来解析:

  • 0x0A:代表长度表示这一帧有多长,0x0A转化为十进制解析结果为10代表总共有10个字节长度
  • 0x01:代表这个协议是1.0版本
  • 0x08:代表这是一帧什么类型的数据(例如:这是一个登录帧)
  • 0x0102030405:代表这一帧要上送的内容是什么(例如:假设这是一个登录帧,它告诉我们登录者的id是12345)
  • 0xFFFF:代表结束符,意思是这一帧数据到这就结束了,后面再有字节数据就是下一帧的数据了。主要用来切分字节流数据的。不然所有的数据都连在一起无法区分开。

解决方案

既然知道了用Socket技术可以和硬件设备通信,接下来就开始了解Socket技术方面的知识,Socket网络编程中有两个角色一个是客户端,另一个是服务端。服务端暴露端口号,客户端创建Socket对象然后填入服务端IP+端口建立TCP连接,连接成功后即可进行数据传输。过程听起来是挺简单的,但经过一番查阅资料后我果断放弃了使用Socket编程,动辄好几十行的代码,各种try catch异常处理块对于刚接触网络编程的我来说用起来感觉有点费力。整个功能的实现要写一大堆的代码不说,还要自己处理很多异常。看起来很不优雅!

image.png

那有没有可以快速上手的框架能完美解决Socket通信问题呢?答案肯定是有的,推荐你选择Netty肯定没错!Netty是业界最流行的NIO框架之一,其健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,其优点如下:

  • API使用简单,降低学习门槛
  • 功能强大,支持多种编解码功能
  • 定制功能强。可以灵活的对通信框架进行扩展
  • 跟业界主流的NIO框架相比,Netty综合性能最强
  • 已经迭代过很多版本,是一个成熟稳定的框架,且修复了已发现的JDK NIO BUG
  • 社区活跃这一点很重要,有啥问题直接反馈
  • 最后一点,Netty经历过大规模的商业应用的考验,质量有保证!

简单上手

下面我们通过简单的代码来体验一下Netty的优美之处,代码要实现的功能如下:

  • 编写Netty服务端和Netty客户端的代码,并添消息打印
  • 客户端尝试与服务端建立连接,连接成功后发送一个时间查询的消息给服务端
  • 服务端在接收到客户端的请求后进行应答
  • 客户端在接收到服务端的应答后打印相关内容

1、POM文件引入

首先新建一个SpringBoot项目,在Pom文件里面加入如下引用

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
</dependency>

2、编写Server端代码

首先编写TimeServer类

TimeServer主要用来启动一个Netty的服务端,并初始化服务端的一些参数配置

public class TimeServer {

    public void bind(int port) throws InterruptedException {
        /**
         * 配置服务端的NIO线程组,这里创建两个的原因是一个用于服务端接受客户端的连接,另一个
         * 用于进行SocketChannel的网络读写
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //ServerBootstrap是用于启动NIO服务端的辅助启动类,目的是降低服务端的开发难度
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // 临时存放已完成三次握手的请求的队列的最大长度。
                    // 如果未设置或所设置的值小于1,Java将使用默认值50。
                    // 如果大于队列的最大长度,请求会被拒绝
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChildChannelHandler());
            //绑定端口,同步等待成功。完成之后Netty会返回一个ChannelFuture,主要用于异步操作的通知回调。
            ChannelFuture future = bootstrap.bind(port).sync();
            //等待服务端监听端口关闭,阻塞方法
            future.channel().closeFuture().sync();
        }finally {
            //优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    private class ChildChannelHandler extends ChannelInitializer<SocketChannel>{

        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            socketChannel.pipeline().addLast(new TimeServerHandler());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new TimeServer().bind(9999);
    }
}

然后编写TimeServerHandler类

TimeServerHandler主要用来处理客户端的请求数据以及响应

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req, "UTF-8");
        System.out.println("the time server receive order :" + body);
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
        ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.write(resp);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //将发送到队列中的消息写入到SocketChannel中发给对方
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

3、编写Client端代码

首先编写TimeClient类

TimeClient主要实现初始化连接的一些参数配置,然后建立与服务端的连接

public class TimeClient {

    public void connect(int port, String host) throws InterruptedException {
        //配置客户端NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new TimeClientHandler());
                        }
                    });
            //发起异步连接操作
           ChannelFuture future = bootstrap.connect(host, port).sync();
           future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new TimeClient().connect(9999,"127.0.0.1");
    }
}

然后编写TimeClientHandler

TimeClientHandler主要用来发送请求数据和打印服务端返回的数据

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    private final ByteBuf firstMessage;

    public TimeClientHandler() {
        byte[] req = "QUERY TIME ORDER".getBytes();
        firstMessage = Unpooled.buffer(req.length);
        firstMessage.writeBytes(req);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(firstMessage);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        String body = new String(req,"UTF-8");
        System.out.println("now is :"+body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

4、测试

先运行TimeServer,然后运行TimeClient,可以看到实验结果如下:

image.png

image.png

小结

通过整个项目的历练让我明白了什么是Socket、Socket通信的过程、客户端和服务端的代码实现、数据的处理与接受,也不由的惊叹Netty框架设计的优雅之处,以上的样例代码还只是使用了Netty框架中的冰山一角的技术,希望后面有机会再深入的学习一下其中的关键类的具体用法。