Bootstrap

Springboot集成Netty

本章具体讲解SpringBoot中如何集成Netty

1.搭建一个Springboot项目

一,服务端

1.项目结构目录

2.导入jar包

  <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>5.0.0.Alpha2</version>
  </dependency>

3.yml 配置

netty:
  port: 18080
  workThread: 2
  bossThread: 1
  keepAlive: true

4.创建Netty  ServerBootstrap

package com.student.demo.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NettyConfig {

    // 用户线程数
    @Value("${netty.bossThread}")
    private int bossThread;

    // 工作线程数
    @Value("${netty.workThread}")
    private int workThread;

    // 心跳
    @Value("${netty.keepAlive}")
    private boolean keepAlive;

    @Autowired
    private NettyInitalizer nettyInitalizer;

    @Bean(name = "bossGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(bossThread);
    }

    @Bean(name = "workerGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workThread);
    }

    // 定义
    @Bean(name = "serverBootstrap")
    public ServerBootstrap bootstrap(){
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup(), workerGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(nettyInitalizer);
        b.option(ChannelOption.SO_KEEPALIVE, keepAlive); // 是否使用TCP的心跳机制
        b.option(ChannelOption.SO_BACKLOG, 1000); // backLog的队列大小
        return b;
    }
}

5.Pipeline的初始化,定义自定义handler

package com.student.demo.netty;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

/**
 *
 */
@Component
public class NettyInitalizer extends ChannelInitializer<SocketChannel> {

    @Autowired
    private NettyHandle nettyHandle;

    @Autowired
    private MyDecoder myDecoder;

    @Autowired
    private MyEncoder myEncoder;

    @Override
    protected void initChannel(SocketChannel socketChannel) {
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 解码器
        pipeline.addLast("decoder", myDecoder);
        // 编码器
        pipeline.addLast("encoder", myEncoder);
        //  空闲检测处理器 触发空闲状态事件            读空闲:5秒      写空闲:7秒  读写空闲:10秒
        pipeline.addLast(new IdleStateHandler(5,7,3, TimeUnit.SECONDS));
        // 处理器
        pipeline.addLast("handler", nettyHandle);
        // 如果后期处理拆包粘包可以在这里处理
        /** LineBasedFrameDecoder:以行为单位进行数据包的解码,使用换行符\n或者\r\n作为依据遇
         到\n或者\r\n都认为是一条完整的消息。
         DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码。
         FixedLengthFrameDecoder:以固定长度进行数据包的解码。
         LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用)。
         */
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
    }

}

6.自定义解码器

package com.student.demo.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.springframework.stereotype.Component;

import java.nio.charset.Charset;
import java.util.List;

@Component
public class MyDecoder extends ByteToMessageDecoder {

    /**
     * 自定义解码器
     * @param channelHandlerContext
     * @param byteBuf
     * @param list
     * @throws Exception
     */
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {

        // 把int值加入到输出的List中
        list.add(byteBuf.toString(Charset.forName("gbk")));
        // 读取完之后,让readindex向前移动
        byteBuf.skipBytes(byteBuf.readableBytes());

        // 16进制数据
//        byte[] bs = new byte[byteBuf.readableBytes()];
//        byteBuf.readBytes(bs);
//        String order = bytesToHexString(bs);
//        list.add(order);
//        byteBuf.skipBytes(byteBuf.readableBytes());
    }

    public String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }
}

7.自定义编码器

package com.student.demo.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import org.springframework.stereotype.Component;
import java.nio.charset.Charset;
import java.util.List;

@Component
public class MyEncoder extends MessageToMessageEncoder<String> {

    /**
     * 编码器
     * @param channelHandlerContext
     * @param msg
     * @param list
     * @throws Exception
     */
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, String msg, List<Object> list) throws Exception {
        // 将String转换为ByteBuf
        ByteBuf buffer = channelHandlerContext.alloc().buffer();
        buffer.writeBytes(msg.getBytes(Charset.forName("gbk")));
        list.add(buffer);
    }
}

8.自定义Handler

package com.student.demo.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.springframework.stereotype.Component;

/**
 * Netty的事件处理
 */
@Component
public class NettyHandle extends SimpleChannelInboundHandler<String> {

    /**
     * 心跳检测和处理
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                System.out.println("读空闲");
            } else if (event.state() == IdleState.WRITER_IDLE) {
                System.out.println("写空闲");
            } else if (event.state() == IdleState.ALL_IDLE) {
                System.out.println("读写空闲");
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    /**
     * 读取消息
     * @param ctx
     * @param msg
     */
    @Override
    protected void messageReceived(ChannelHandlerContext ctx, String msg) {
        System.out.println(msg);
        // 发消息
        sendInfo(ctx, "netty over 收到");
    }

    // 给客户端发消息
    private void sendInfo(ChannelHandlerContext ctx , String info) {
        ctx.writeAndFlush(info);
        ctx.flush();
    }


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("链接成功");
        super.channelActive(ctx);
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.out.println("异常关闭");
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("离线");
        super.channelInactive(ctx);
    }

}

9.Netty启动

package com.student.demo.netty;


import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class NettyStart {

    @Autowired
    private ServerBootstrap serverBootstrap;

    @Value("${netty.port}")
    private int nettyPort;

    private ChannelFuture serverChannelFuture;

    @PostConstruct
    public void start() throws Exception {
        System.out.println("Starting server at " + nettyPort);
        serverChannelFuture = serverBootstrap.bind(nettyPort).sync();
    }

}

二、客户端

1.NClient

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

/**
 * @Author:hmz
 * @date:2019/07/08 14:01
 * @Explanation:
 */
public class NClient {

    private  String host;
    private  int port;

    public NClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup nioEventLoopGroup = null;
        try {
            //创建Bootstrap对象用来引导启动客户端
            Bootstrap bootstrap = new Bootstrap();
            //创建EventLoopGroup对象并设置到Bootstrap中,EventLoopGroup可以理解为是一个线程池,这个线程池用来处理连接、接受数据、发送数据
            nioEventLoopGroup = new NioEventLoopGroup();
            //创建InetSocketAddress并设置到Bootstrap中,InetSocketAddress是指定连接的服务器地址
            bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        //添加一个ChannelHandler,客户端成功连接服务器后就会被执行
                        @Override
                        protected void initChannel(SocketChannel ch)
                                throws Exception {
                            ch.pipeline().addLast(new NClientHandler());
                        }
                    });
            // • 调用Bootstrap.connect()来连接服务器
            ChannelFuture f = bootstrap.connect().sync();
            // • 最后关闭EventLoopGroup来释放资源
            f.channel().closeFuture().sync();
        } finally {
            nioEventLoopGroup.shutdownGracefully().sync();
        }
    }

}

2.NClientHandler

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author:hmz
 * @date:2019/07/08 14:07
 * @Explanation:
 */
public class NClientHandler extends ChannelInboundHandlerAdapter {

    public static List<ChannelHandlerContext> cts = new ArrayList<ChannelHandlerContext>();


    /**
     * 向服务端发送数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        System.out.println("客户端与服务端通道-开启:" + ctx.channel().localAddress() + "channelActive");
        cts.add(ctx);
        String sendInfo = "你好服务端";
        System.out.println("客户端准备发送的数据包:" + sendInfo);
        write(ctx,sendInfo);
    }

    public void write(ChannelHandlerContext ctx , String mess) throws Exception {
        String sendInfo = mess;
        ctx.writeAndFlush(Unpooled.copiedBuffer(sendInfo, CharsetUtil.UTF_8)); // 必须有flush
        ctx.flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        //读取数据

        //读取数据
        ByteBuf buf1 = (ByteBuf) msg;
        byte[] req = readClientData((ByteBuf) msg);
        String body = new String(req, "UTF-8"); //获取到的值
        System.out.println("客户端的数据------>"+body);
        //写数据
        write(ctx,"wits写的数据");

    }

    //将netty的数据装换为字节数组
    private byte[] readClientData(ByteBuf msg) {
        ByteBuf buf = msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);
        buf.release();
        return req;
    }

    /**
     * channelInactive
     *
     * channel 通道 Inactive 不活跃的
     *
     * 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
     *
     */
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端与服务端通道-关闭:" + ctx.channel().localAddress() + "channelInactive");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cts.remove(ctx);
        ctx.close();
        System.out.println("异常退出:" + cause.getMessage());
    }

}

3.mainTest

import com.herbert.client.NClient;
import com.herbert.finalPool.ConstantPool;

/**
 * @Author:hmz
 * @date:2019/07/08 14:01
 * @Explanation:
 */
public class TestMain {

    public static void main(String[] args) throws Exception {
        new NClient(ConstantPool.HOST, ConstantPool.PORT).start(); // 连接127.0.0.1/65535,并启动


    }

}

附件:Netty参数配置详情

ChannelOption套接字配置
参数解释
SO_BROADCAST对应套接字层的套接字:SO_BROADCAST,将消息发送到广播地址。
如果目标中指定的接口支持广播数据包,则启用此选项可让应用程序发送广播消息。
SO_KEEPALIVE对应套接字层的套接字:SO_KEEPALIVE,保持连接。
在空闲套接字上发送探测,以验证套接字是否仍处于活动状态。
SO_SNDBUF对应套接字层的套接字:SO_SNDBUF,设置发送缓冲区的大小。
SO_RCVBUF对应套接字层的套接字:SO_RCVBUF,获取接收缓冲区的大小。
SO_REUSEADDR对应套接字层的套接字:SO_REUSEADDR,本地地址复用。
启用此选项允许绑定已使用的本地地址。
SO_LINGER对应套接字层的套接字:SO_LINGER,延迟关闭连接。
启用此选项,在调用close时如果存在未发送的数据时,在close期间将阻止调用应用程序,直到数据被传输或连接超时。
SO_BACKLOG对应TCP/IP协议中<font color=red>backlog</font>参数,<font color=red>backlog</font>即连接队列,设置TCP中的连接队列大小。如果队列满了,会发送一个ECONNREFUSED错误信息给C端,即“ Connection refused”。
SO_TIMEOUT等待客户连接的超时时间。
IP_TOS对应套接字层的套接字:IP_TOS,在IP标头中设置服务类型(TOS)和优先级。
IP_MULTICAST_ADDR对应IP层的套接字选项:IP_MULTICAST_IF,设置应发送多播数据报的传出接口。
IP_MULTICAST_IF对应IP层的套接字选项:IP_MULTICAST_IF2,设置应发送多播数据报的IPV6传出接口。
IP_MULTICAST_TTL对应IP层的套接字选项:IP_MULTICAST_TTL,在传出的 多播数据报的IP头中设置生存时间(TTL)。
IP_MULTICAST_LOOP_DISABLED取消 指定应将 传出的多播数据报的副本 回传到发送主机,只要它是多播组的成员即可。
TCP_NODELAY对应TCP层的套接字选项:TCP_NODELAY,指定TCP是否遵循<font color=#35b998>Nagle算法</font> 决定何时发送数据。Nagle算法代表通过减少必须发送包的个数来增加网络软件系统的效率。即尽可能发送大块数据避免网络中充斥着大量的小数据块。如果要追求高实时性,需要设置关闭Nagle算法;如果需要追求减少网络交互次数,则设置开启Nagle算法。
ChannelOption通用配置
参数解释
ALLOCATORByteBuf的分配器,默认值为ByteBufAllocator.DEFAULT。
RCVBUF_ALLOCATOR用于Channel分配接受Buffer的分配器,默认值为AdaptiveRecvByteBufAllocator.DEFAULT,是一个自适应的接受缓冲区分配器,能根据接受到的数据自动调节大小。可选值为FixedRecvByteBufAllocator,固定大小的接受缓冲区分配器。
MESSAGE_SIZE_ESTIMATOR消息大小估算器,默认为DefaultMessageSizeEstimator.DEFAULT。估算ByteBuf、ByteBufHolder和FileRegion的大小,其中ByteBuf和ByteBufHolder为实际大小,FileRegion估算值为0。该值估算的字节数在计算水位时使用,FileRegion为0可知FileRegion不影响高低水位。
CONNECT_TIMEOUT_MILLIS连接超时毫秒数,默认值30000毫秒即30秒。
WRITE_SPIN_COUNT一个Loop写操作执行的最大次数,默认值为16。也就是说,对于大数据量的写操作至多进行16次,如果16次仍没有全部写完数据,此时会提交一个新的写任务给EventLoop,任务将在下次调度继续执行。这样,其他的写请求才能被响应不会因为单个大数据量写请求而耽误。
WRITE_BUFFER_WATER_MARK
ALLOW_HALF_CLOSURE一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。
AUTO_READ自动读取,默认值为True。Netty只在必要的时候才设置关心相应的I/O事件。对于读操作,需要调用channel.read()设置关心的I/O事件为OP_READ,这样若有数据到达才能读取以供用户处理。该值为True时,每次读操作完毕后会自动调用channel.read(),从而有数据到达便能读取;否则,需要用户手动调用channel.read()。需要注意的是:当调用config.setAutoRead(boolean)方法时,如果状态由false变为true,将会调用channel.read()方法读取数据;由true变为false,将调用config.autoReadCleared()方法终止数据读取。
AUTO_CLOSE

;