SpringBoot+Netty 结合物联网初步开发搭建 共享硬件+手机APP+电脑通信(1)

目录

  • Netty初始项目结构
    • Pom引入
    • 项目搭建
    • 1.首先先创建服务端 Server代码
    • 2.有了服务器端 加入编码器
    • 编码器加入完成 第三加入处理器
    • 最后再SpringBoot的启动类加上Netty启动

Netty初始项目结构

SpringBoot+Netty 结合物联网初步开发搭建 共享硬件+手机APP+电脑通信(1)_第1张图片

Pom引入

这边采用的是4.*版本的netty 稳定

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.25.Final</version>
</dependency>

项目搭建

1.首先先创建服务端 Server代码

1.我们这边搭建的是Netty的服务端

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.AdaptiveRecvByteBufAllocator;
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.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @author KeShuai
 * @Auther: KeShuai
 * @Date: 2020/08/11/10:41
 * @Description:
 */
public class BootNettyServer {
    public void bind(int port) throws Exception {

        /**
         * 配置服务端的NIO线程组
         * NioEventLoopGroup 是用来处理I/O操作的Reactor线程组
         * bossGroup:用来接收进来的连接,workerGroup:用来处理已经被接收的连接,进行socketChannel的网络读写,
         * bossGroup接收到连接后就会把连接信息注册到workerGroup
         * workerGroup的EventLoopGroup默认的线程数是CPU核数的二倍
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            /**
             * ServerBootstrap 是一个启动NIO服务的辅助启动类
             */
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            /**
             * 设置group,将bossGroup, workerGroup线程组传递到ServerBootstrap
             */
            serverBootstrap = serverBootstrap.group(bossGroup, workerGroup);
            /**
             * ServerSocketChannel是以NIO的selector为基础进行实现的,用来接收新的连接,这里告诉Channel通过NioServerSocketChannel获取新的连接
             */
            serverBootstrap = serverBootstrap.channel(NioServerSocketChannel.class);
            /**
             * option是设置 bossGroup,childOption是设置workerGroup
             * netty 默认数据包传输大小为1024字节, 设置它可以自动调整下一次缓冲区建立时分配的空间大小,避免内存的浪费    最小  初始化  最大 (根据生产环境实际情况来定)
             * 使用对象池,重用缓冲区
             */
            serverBootstrap = serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 6291456));
            serverBootstrap = serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 6291456));
            /**
             * 设置 I/O处理类,主要用于网络I/O事件,记录日志,编码、解码消息
             */
            serverBootstrap = serverBootstrap.childHandler(new BootNettyChannelInitializer<SocketChannel>());

            System.out.println("netty server start success!");
            /**
             * 绑定端口,同步等待成功
             */
            ChannelFuture f = serverBootstrap.bind(port).sync();
            ChannelFuture f1 = serverBootstrap.bind(8778).sync();
            if (f.isSuccess()) {
                System.out.println("启动 8777 成功");
            }
            if (f1.isSuccess()) {
                System.out.println("启动 8778 成功");
            }
            /**
             * 等待服务器监听端口关闭
             */
            f.channel().closeFuture().sync();

        } catch (InterruptedException e) {

        } finally {
            /**
             * 退出,释放线程池资源
             */
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

2.有了服务器端 加入编码器

这边加入netty的编码器进行数据加解码操作,我们这边定义的是用分隔符进行对报文解析
//加入分隔符解码器(结束符)
ByteBuf delimiter = Unpooled.copiedBuffer("_$".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(102410246,delimiter));
因为共享硬件发送的报文全量数据太长 这边客户端定义了个分隔符 服务端对检测到的分隔符 裁剪取出全量数据。不然会产生TCP黏包拆包的问题 可以百度一下TCP黏包拆包。

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;


/**
 * @Auther: KeShuai
 * @Date: 2020/08/11/10:42
 * @Description:
 */
public class BootNettyChannelInitializer<SocketChannel> extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        //加入分隔符解码器(结束符)
        ByteBuf delimiter = Unpooled.copiedBuffer("_$".getBytes());
        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*1024*6,delimiter));
        //加载 解码数据
        ch.pipeline().addLast("encoder", new StringEncoder());
        // 属于ChannelInboundHandler,依照顺序执行
        ch.pipeline().addLast("decoder", new StringDecoder());
        /**
         * 自定义ChannelInboundHandlerAdapter
         */
        ch.pipeline().addLast(new BootNettyChannelInboundHandlerAdapter());

}

编码器加入完成 第三加入处理器

/**
* 自定义ChannelInboundHandlerAdapter
*/
ch.pipeline().addLast(new BootNettyChannelInboundHandlerAdapter());
上图解码器这块就是加入处理器

    刚开始搭建主要是用端口号区分是电脑发送 还是设备发送  
    采用String接收解析成JSON 在对设备发送的报文命令解析自动回复发送代码,或者转发给电脑端 。
    电脑端发送给设备端命令开关区分
    以下是处理器代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gson.JsonArray;
import com.qykfa.dto.AttrListDto;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;
/**
 * @author KeShuai
 * @Auther: KeShuai
 * @Date: 2020/08/11/10:44
 * @Description:
 */
public class BootNettyChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
    /**
     * 存链接客户端的map
     */
    public static Map<String, ChannelHandlerContext> map = new HashMap<String, ChannelHandlerContext>();

    public static String dnFlag = "8778";
    public static String sbFlag = "8777";
    /**
     * 从客户端收到新的数据时,这个方法会在收到消息时被调用
     *
     * @param ctx
     * @param msg
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception, IOException
    {
        //获取连接的端口号
        String servicePortSub=ctx.channel().localAddress().toString();
        String servicePort=servicePortSub.substring(servicePortSub.length()-4, servicePortSub.length());
        //设备->电脑
        if (sbFlag.equals(servicePort)){
        //对数据解析代码 例子 
//            JSONObject json = new JSONObject();
//            json.put("oject",msg);
//            System.out.println(json);
//            if (json.getJSONObject("oject").get("msgType")!=null){
//                System.out.println(json.getJSONObject("oject").get("msgType"));
//                JSONArray array = json.getJSONObject("oject").getJSONArray("attrList");
//                System.out.println(json.getJSONObject("oject").getJSONArray("attrList").size());
//                List list = JSONArray.toList(array, new AttrListDto(), new JsonConfig());
//                System.out.println("list :" + list.size());
//
//                int num = 1;
//                for (AttrListDto dto:list) {
//
//                    System.out.println(num + " ssss "+dto.getId());
//
//                    num++;
//                }
//            }

            map.get("用户标识对应").channel().writeAndFlush(msg);

        }
        //电脑->设备
        if (dnFlag.equals(servicePort)){
            //设备接收需要JSON格式
            JSONObject json= JSONObject.fromObject(msg);
            map.get("用户标识对应").channel().writeAndFlush(json.toString());
        }
    }

    /**
     * 从客户端收到新的数据、读取完成时调用
     *
     * @param ctx
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws IOException
    {
        System.out.println("channelReadComplete");
        ctx.flush();
    }

    /**
     * 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
     *
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException
    {
        System.out.println("exceptionCaught");
        cause.printStackTrace();
        ctx.close();//抛出异常,断开与客户端的连接
    }

    /**
     * 客户端与服务端第一次建立连接时 执行
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception, IOException
    {
        super.channelActive(ctx);
        ctx.channel().read();
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = insocket.getAddress().getHostAddress();
        String servicePortSub=ctx.channel().localAddress().toString();
        String servicePort=servicePortSub.substring(servicePortSub.length()-4, servicePortSub.length());
        //获取客户端的请求地址  取到的值为客户端的 ip+端口号
        //设备请求地址(个人将设备的请求地址当作 map 的key)
        String url=ctx.channel().remoteAddress().toString();
        System.out.println(url);
        //如果不为空就不存
        if(map.get(clientIp)!=null){

        }else{//否则就将当前的设备ip+端口存进map
            map.put(clientIp+servicePort, ctx);
        }
        //此处不能使用ctx.close(),否则客户端始终无法与服务端建立连接
        System.out.println("channelActive:"+clientIp+servicePort);
    }

    /**
     * 客户端与服务端 断连时 执行
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception, IOException
    {
        super.channelInactive(ctx);
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = insocket.getAddress().getHostAddress();
        ctx.close(); //断开连接时,必须关闭,否则造成资源浪费,并发量很大情况下可能造成宕机
        System.out.println("channelInactive:"+clientIp);
    }

    /**
     * 服务端当read超时, 会调用这个方法
     *
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception, IOException
    {
        super.userEventTriggered(ctx, evt);
        InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = insocket.getAddress().getHostAddress();
        ctx.close();//超时时断开连接
        System.out.println("userEventTriggered:"+clientIp);
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception{
        System.out.println("channelRegistered");
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception{
        System.out.println("channelUnregistered");
    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception{
        System.out.println("channelWritabilityChanged");
    }

    public String convertByteBufToString(ByteBuf buf) {
        String str;
        if(buf.hasArray()) { // 处理堆缓冲区
            str = new String(buf.array(), buf.arrayOffset() + buf.readerIndex(), buf.readableBytes());
        } else { // 处理直接缓冲区以及复合缓冲区
            byte[] bytes = new byte[buf.readableBytes()];
            buf.getBytes(buf.readerIndex(), bytes);
            str = new String(bytes, 0, buf.readableBytes());
        }
        return str;
    }
}

最后再SpringBoot的启动类加上Netty启动

@SpringBootApplication
@EnableAsync
public class SocketServerApplication extends SpringBootServletInitializer implements CommandLineRunner {

   public static void main(String[] args) {
      SpringApplication.run(SocketServerApplication.class, args);
   }

   @Override
   protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(SocketServerApplication.class);
   }
   @Async
   @Override
   public void run(String... args) throws Exception {
      /**
       * 使用异步注解方式启动netty服务端服务
       */
      new BootNettyServer().bind(8777);
   }
}

以上是Netty服务器 初始化搭建 第一次接触硬件4G远程通信这块 代码比较初略 可能考虑的还不够周全后续还在更改,还望大家可以指出错误或者有更好的搭建方法一起探讨。后续还有手机 Websocket链接进来这块加上 工厂 策略设计模式,netty+kafka实时等

下文 :SpringBoot+Netty 结合物联网通信 加入WebSocket协议(2)

你可能感兴趣的:(java,netty,网络,物联网,网络协议)