Spring Boot框架整合Netty并自定义返回报文协议

本文介绍了如何在SpringBoot项目中整合Netty,定义双方交互的二进制报文协议,实现客户端和服务器之间的高效通信。讲解了从需求分析、依赖导入、数据结构设计、编码解码器编写、SocketHandler、SocketInitializer到服务器启动的全过程,并给出了客户端的编写方法。
摘要由CSDN通过智能技术生成

关于Spring Boot和Netty的前置知识

在阅读本文章他之前,首先需要了解Spring Boot和Netty的相关知识,在这里就简单说一下。
Spring Boot是一个基于Java的开源框架,用于创建微服务。它由Pivotal Team开发,用于构建独立的生产就绪Spring应用。平常我们使用Spring Boot搭建后端服务器主要使用的是其内部集成的Tomcat服务器。
Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
也就是说,Netty是和Tomcat一个层级的,但是Netty可以采用的网络传输协议十分广泛,Tomcat基本只能使用Http协议进行网络传输。基于Netty实现的网络传输,数据会以二进制流的方式进行传输,因此从效率上会比Json格式的数据传输要高出不少。

在Spring Boot这个框架当中,IOC是其最大的亮点之一,很多对象不需要显式的去声明,可以通过注入的方式进行自动装配,这在下面的代码当中将会得到充分的体现。

第一步,分析需求

需求分析永远是开发的前提,开发之前要明白自己要做一个什么东西出来

要基于Netty开发客户端和服务器,这个好理解,服务器就要负责接受从客户端发来的请求,并进行相关的处理之后返回结果。客户端就要根据自己的需求来发送请求给服务器并获得自己想要的结果。

在这个过程当中,我们要定义好服务器和客户端之间交互的数据格式,基于这个格式来进行交流,在这里格式暂定为:
前四个字节表示数据长度,后跟Json格式的字符串
数据格式

第二步,开始搭建项目

接下来我将会逐步的开始进行项目的搭建,其中会配合代码来进行解释

依赖导入

首先进行依赖的导入,需要netty相关io操作的依赖

由于要求当中需要Json格式的数据,因此我选用了阿里巴巴的fastJson库,这是一个能够非常简便的对Json和Java Bean操作的API

另外我使用了SL4J库进行日志的打印

需要导入以下依赖:

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.36.Final</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.4</version>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>


        <dependency>
            <groupId>net.sf.ezmorph</groupId>
            <artifactId>ezmorph</artifactId>
            <version>1.0.6</version>
        </dependency>

        <!--        JSONObject.fromObject()的依赖, JSONArray.fromObject()-->
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>

依赖导入之后注意其中的版本,必要的话需要修改部分依赖的版本来适应本地已经存在的依赖,同时上述依赖是不包括spring boot项目基本依赖的,自行创建spring boot项目导入即可,在这里不过多赘述了。

基础数据结构搭建

创建一个Person类,作为我们服务器和客户端传递的主体数据:

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class Person {
    private String name;
    private String gender;
}

然后创建返回报文的数据结构:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
public class MessageRecord {
    // 长度,int类型自身就占四个字节
    private int length;
    // 要发送的内容,JsonObject格式
    private JSONObject message;
}

encoder和decoder的编写

因为我们规定传输的数据结构,因此再服务器或客户端接收到数据之后和发送数据之前,我们需要对数据进行解码和编码
Netty强大就强大在给我们事先准备好了模板类,我们只需要继承这些模板类并实现对应的方法即可完成解码工具类和编码工具类的编写
首先看解码

@Slf4j
public class NettyDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list)
        throws Exception {
        log.info(">>>>>>>>>>>消息解码 start>>>>>>>>>>>");
        // 通过byteBuf获取数据
        int length = byteBuf.readInt();// 获取4个字节
        if (length > 0){// 消息长度大于0
            MessageRecord messageRecord = new MessageRecord();
            messageRecord.setLength(length);
            // 获取消息体
            byte[] bytes = new byte[length];
            byteBuf.readBytes(bytes);// 读取消息体内容到 bytes
            ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);// java自带反序列化工具
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            messageRecord.setMessage((JSONObject) JSONObject.toJSON(objectInputStream.readObject()));
            System.out.println("收到消息内容为:" + messageRecord);

            // 注意:需要将消息传输对象添加到 `List<Object> list` 中,如果不添加,服务端接收处理不到消息内容
            list.add(messageRecord);
        } else {
            log.info("消息内容为空,不解析");
        }
        log.info(">>>>>>>>>>>消息解码 end>>>>>>>>>>>");
    }
}

好我们开始分析代码,Netty提供了一个名为ByteToMessageDecoder 的工具类模板,看名字很好理解,就是将比特流转换成消息的解码工具
继承这个类之后需要实现一个decode方法,在需要客户端和服务器需要解码的时候便会调用这个方法
第一个参数,channelHandlerContext,上下文变量,不用管,因为我们只需要做数据的解码,跟上下文没关系
第二个参数,byteBuf,很重要。前面说过,数据传输的过程当中都是以比特流的形式传输的,这个byteBuf是一个比特流缓冲区,也就是其中存放着我们的数据的比特流
第三个参数,list,是服务器或客户端接受消息处理的对象,我们将比特流解码生成我们想要的数据结构之后,要将其放入这个list当中,才会被客户端或服务器看到拿出来并进行处理
我们事先定义好了前四个字节是长度,因此直接通过readInt()方法即可获取数据长度,当长度大于零,也就是其中数据不为空的时候,将比特流读取,并反序列化成Java 对象,然后按照我们所要求的组装数据结构装入list中
这就是解码工具要做的事情, 将传输的比特流转换成我们需要的数据结构

编码工具和解码工具的逻辑刚好反过来

@Slf4j
public class NettyEncoder extends MessageToByteEncoder<MessageRecord> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MessageRecord messageRecord, ByteBuf byteBuf)
        throws Exception {
        log.info(">>>>>>>>>>>消息编码 start>>>>>>>>>>>");
        Object message = messageRecord.getMessage();
        if (message != null){// 消息内容不为空
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(outputStream);
            out.writeObject(message);
            byte[] bytes = outputStream.toByteArray();
            // 消息长度
            byteBuf.writeInt(bytes.length);
            // 消息内容
            byteBuf.writeBytes(bytes);
        }else {// 消息内容为空
            byteBuf.writeInt(0);
        }
        // 写入并且刷新
        channelHandlerContext.writeAndFlush(messageRecord);
        log.info(">>>>>>>>>>>消息编码 end>>>>>>>>>>>");
    }
}

相比较decoder,第一个参数含义一样,代表上下文变量
第二个参数是我们封装好的数据对象,第三个参数是我们传输时使用的二进制缓冲区
我们获取到封装好的数据之后,将其转换成比特流,然后根据长度重新将比特流封装后写入缓冲区准备发送,最后我们将数据对象写入上下文进行保存

Netty SockerHandler编写

SocketHandler可以理解成当服务器接收到客户端的请求时触发的方法:

@Slf4j
public class SocketHandler extends ChannelInboundHandlerAdapter {
    public static final ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 读取到客户端发来的消息
     *
     * @param ctx ChannelHandlerContext
     * @param msg msg
     * @throws Exception e
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        MessageRecord messageRecord =(MessageRecord)msg;
        log.info("收到消息: " + messageRecord);

        // 把消息写回客户端
        MessageRecord newMessage = new MessageRecord();
        Person person = new Person("真步", "女");
        newMessage.setMessage((JSONObject)JSONObject.toJSON(person));
        ctx.channel().writeAndFlush(newMessage);
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("新的客户端链接:" + ctx.channel().id().asShortText());
        clients.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        clients.remove(ctx.channel());
    }

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

好我们现在逐句分析代码,第一个方法是最主要的方法
第一行

public static final ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

这是一个代表客户端集合的常量,每当一个新的客户端连接到服务器,其中的client数量就会加一

第一个方法,含有两个参数,ctx和msg
ctx顾名思义,也就是代表上下文的变量,通过这个变量进行客户端和服务端的通信
msg是客户端发来的数据,由于我们已经事先定义好了传输的数据结构,所以需要将其强制类型转换成MessageRecord类型的数据,然后打印出来,再创建一个新的变量,通过上下文变量ctx装入channel中,发送给客户端

下面三个方法很简单,分别代表当客户端接入,移除,抛出错误时候要做的事情

Netty Socket Initializer的编写

然后进行服务端初始化器的编写,同样netty准备好了ChannelInitializer工具类,先上代码:

@Component
public class SocketInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 添加对byte数组的编解码,netty提供了很多编解码器,你们可以根据需要选择
        pipeline.addLast(new NettyDecoder());
        pipeline.addLast(new NettyEncoder());
        // 添加上自己的处理器
        pipeline.addLast(new SocketHandler());
    }
}

在类前声明@Component,spring初始化的时候就会将这个类加载到IOC容器当中,可以理解成Springhi自动的初始化一个类的对象供我们使用。
继承ChannelInitializer类之后需要实现initChannel方法,其中参数是固定的socketChannel,只需要获取pipeline对象,向其中添加解码器,编码器,Hanlder对象即可。
添加之后,接受和发送请求时,数据进入pipeline后都会被执行相应的解码,编码,处理方法。

ScoketServer 启动类的编写

@Slf4j
@Component
public class SocketServer {
    @Resource
    private SocketInitializer socketInitializer;

    @Getter
    private ServerBootstrap serverBootstrap;

    /**
     * netty服务监听端口
     */
    private int port = 8088;
    /**
     * 主线程组数量
     */
    private int bossThread =1;

    /**
     * 启动netty服务器
     */
    public void start() {
        this.init();
        this.serverBootstrap.bind(this.port);
        log.info("Netty started on port: {} (TCP) with boss thread {}", this.port, this.bossThread);
    }

    /**
     * 初始化netty配置
     */
    private void init() {
        // 创建两个线程组,bossGroup为接收请求的线程组,一般1-2个就行
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(this.bossThread);
        // 实际工作的线程组
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        this.serverBootstrap = new ServerBootstrap();
        this.serverBootstrap.group(bossGroup, workerGroup) // 两个线程组加入进来
            .channel(NioServerSocketChannel.class)  // 配置为nio类型
            .childHandler(this.socketInitializer); // 加入自己的初始化器
    }
}

这个类主要实现server的启动方法,定义启动时的端口号,主线程数量等,基于netty的I/O模型,我们建立两个NioEventLoopGroup,分别是BossGroup和workGroup,简单来说前者用于接受请求,后者用于处理请求,因为实际处理请求的时后者,所以需要给后者加入自己的初始化类,前者只需要定义为NIO类型即可。

让netty随Spring启动而启动

如果我们想要Spring在启动的时候,netty也随之启动,就需要一个监听类,实现ApplicationRunner接口,这样当spring运行的时候被监听到,随后启动Netty

@Component
public class NettyStartListener implements ApplicationRunner {
    @Resource
    private SocketServer socketServer;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        this.socketServer.start();
    }
}

同样这也是为什么上面SocketServer要加@Component的原因,加了之后在这里即可获取到一个socketServer实例。

至此,服务端代码编写完成,点击启动Spring Boot,即可看到控制台打印:
在这里插入图片描述

客户端编写

客户端的编写其实和服务端流程基本一致,因为客户端和服务端的本质都是收发请求并进行一系列处理。
但是从编程规范的角度来讲,客户端还是单独拎出来写一个main方法实现。
首先编写客户端的Handler

public class ClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        MessageRecord messageRecord = (MessageRecord) msg;
        System.out.println("Client 收到消息内容为:" + messageRecord);
    }
}

这里客户端只需要接受服务端发来的消息,因此我只定义了这一个方法

然后定义客户端的初始化器,添加进我们编写好的解码和编码方法,以及客户端的hanlder

public class ClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
            .addLast(new NettyEncoder())
            .addLast(new NettyDecoder())
            .addLast(new ClientHandler());
    }
}

然后编写客户端的启动类

public class Client {
    public static void main(String[] args) {

        EventLoopGroup work = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 2);
        Bootstrap bootstrap = new Bootstrap();

        bootstrap.group(work).channel(NioSocketChannel.class)
            .handler(new ClientInitializer());

        try {
            ChannelFuture future = bootstrap.connect(new InetSocketAddress("localhost", 8088)).sync();
            Channel channel = future.channel();
            MessageRecord msg = new MessageRecord();
            Person person = new Person("泥岩","女");
            msg.setMessage((JSONObject) JSONObject.toJSON(person));
            channel.writeAndFlush(msg);
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            work.shutdownGracefully();
        }
    }
}

前半部分和server的基本一致,但是只有一个线程池,来工作即可。
后续代码的含义为,通过bootstrap来建立指定ip端口的TCP连接,然后生成一条消息,将此消息包装成自己想要的格式,发送给服务器。需要注意一点的是,在客户端或服务端包装数据的时候只需要关心数据的核心内容部分,数据长度这个属性是归解码和编码器来设置或获取的。

补充:发送消息这块代码其实应该单独拎出来写的hhh,偷懒了

客户端编写完成后直接运行main方法,可以看到控制台打印:
在这里插入图片描述
服务端控制台打印:
在这里插入图片描述

流程为,客户端发送数据,数据被编码发送,服务端收到数据,解码数据并获取,服务端向客户端发送数据,数据被编码发送,客户端收到数据,解码数据并获取

以上就是netty的简单实现和操作,希望对你有所帮助!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值