Netty 基础使用

一、前言

我会重写这一篇的。。。。

1. BIO、NIO、AIO

1.1 介绍

  • BIO - 同步阻塞IO : 传统的IO操作,在读/写文件时线程会一直阻塞,直到文件读/写结束。

  • NIO - 同步非阻塞IO:NIO在读/写文件时并不阻塞当前线程,也就是说在读/写文件时是线程是可以继续执行其他任务的。NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程。IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高

    NIO多路复用主要步骤和元素:
    1. 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。
    2. 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。
    3. Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。
    4. 在 具体的 方法中,通过 SocketChannel 和 Buffer 进行数据操作

  • AIO - 异步非阻塞IO:AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。

关于这一部分的详细内容可以参考 : https://www.cnblogs.com/sxkgeek/p/9488703.html

1.2 NIO 核心思路

详参: https://mp.weixin.qq.com/s/wVoHfhh28Vh5sgKQbPXk8w
结合示例代码,总结NIO的核心思路:

  1. NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在上面例子中serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读

  2. 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用BIO模型中1w 个while循环在阻塞,参见(1)

  3. clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过clientSelector.select(1)方法可以轮询出来,进而批量处理,参见(2)

  4. 数据的读写面向 Buffer,参见(3)


public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        new Thread(() -> {
            try {
                // 对应IO编程中服务端启动
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isAcceptable()) {
                                try {
                                    // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }

        }).start();


        new Thread(() -> {
            try {
                while (true) {
                    // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // (3) 面向 Buffer
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();


    }
}

2. Netty简介

2.1 什么是Netty

Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。

2.2 Netty应用场景

1.分布式开源框架中dubboZookeeperRocketMQ底层rpc通讯使用就是netty。
2.游戏开发中,底层使用netty通讯。

2.3 为什么选择netty

我们总结下为什么不建议开发者直接使用JDK的NIO类库进行开发的原因:

  1. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
  2. 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
  3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;
  4. JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。

二、 Netty 基本使用

1. 基本使用

Netty 客户端消息处理器

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * @Data: 2019/10/18
 * @Des: 客户端处理器
 */
class ClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 接收数据
        System.out.println("客户端收到消息 : " + msg);
    }
}

Netty 服务端消息处理器

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * @Data: 2019/10/18
 * @Des: 服务端处理器
 *
 * ` channelActive() `         >        在到服务器的连接已经建立之后将被调用(成为活跃状态)
 * ` channelRead0()`           >        当从服务器接受到一条消息时被调用
 * ` exceptionCaught()`        >        在处理过程中引发异常时调用
 * ` channelReigster() `       >        注册到EventLoop上
 * ` handlerAdd() `            >        Channel被添加方法
 * ` handlerRemoved()`         >        Channel被删除方法
 * ` channelInActive() `       >        Channel离开活跃状态,不再连接到某一远端时被调用
 * ` channelUnRegistered()`    >        Channel从EventLoop上解除注册
 * ` channelReadComplete()`    >        当Channel上的某个读操作完成时被调用
 */
public class ServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        super.handlerAdded(ctx);
        System.out.println("handlerAdded....");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        System.out.println("exceptionCaught...");
    }

    /**
     * 读取客户端请求,向客户端响应的方法,所以这里要构造响应返回给客户端。
     * 注意:这里面跟Servlet没有任何关系,也符合Servlet规范,所以不会涉及到HttpServerltRequest和HttpServeletResponse对象。
     *
     * @param ctx
     * @param msg
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String value = (String) msg;
        System.out.println("服务器端收到客户端msg:\n" + value);
        // 回复客户端
        ctx.writeAndFlush(Unpooled.copiedBuffer("我收到了你的信息\r\n".getBytes()));
    }
}

Netty 客户端

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @Data: 2019/10/18
 * @Des: Netty 客户端
 */
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
        bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 固定消息定长,长度不够空格补全,不实用
                // ch.pipeline().addLast(new FixedLengthFrameDecoder(10));

                // 包尾添加特殊分隔符, 以特殊分隔符作为结尾(接收消息时会把特殊分隔符去掉)
                ByteBuf buf = Unpooled.copiedBuffer("\r\n".getBytes());
                ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
                ch.pipeline().addLast(new StringDecoder());
                ch.pipeline().addLast(new StringEncoder());
                ch.pipeline().addLast(new ClientHandler());
            }
        });
        ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
        Channel channel = channelFuture.channel();
        channel.writeAndFlush(Unpooled.wrappedBuffer("天下第一666\r\n".getBytes()));
        channel.writeAndFlush(Unpooled.wrappedBuffer("天下第二666\r\n".getBytes()));
        channel.writeAndFlush(Unpooled.wrappedBuffer("天下第三666\r\n".getBytes()));

        System.out.println("消息已发送");

        // 等待客户端端口号关闭
        channelFuture.channel().closeFuture().sync();
        nioEventLoopGroup.shutdownGracefully();
    }
}

Netty 服务端

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @Data: 2019/10/17
 * @Des: Netty 服务端
 * <p>
 * **基于netty构建服务基本流程总结:**
 * 1. 创建EventLoopGroup实例
 * 2. 通过ServerBootstrap启动服务,bind到一个端口. 如果是客户端,则使用Bootstrap,连接主机和端口.
 * 3. 创建ChannelInitializer实例,通过ChannelPipieline初始化处理器链.
 * 4. 创建ChannelServerHandler实例,继承SimpleChannelInboundHandler,重写channelRead0方法(netty4.x).
 * 5. 将ChannelServerHandler实例addLast到ChannelPipeline上.
 * 6. 将ChannelInitializer实例childHandler到bootstrap上.
 */
public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程池,一个负责接收消息,一个负责发送消息
        // bossGroup目的是获取客户端连接,连接接收到之后再将连接转发给workerGroup去处理。
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup wordGroup = new NioEventLoopGroup();
        // 创建一个辅助操作类
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, wordGroup).channel(NioServerSocketChannel.class)
                // 缓存队列缓存的请求的大小,默认50
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 设置发送与接收的缓冲区的大小
                .option(ChannelOption.SO_SNDBUF, 32 * 1024).option(ChannelOption.SO_RCVBUF, 32 * 1024)
                // 通过ChannelPipeline初始化处理器,类似于拦截器Chain,当每个客户端首次连接后即调用initChannel方法完成初始化动作。
                // 初始化"拦截器"链
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        System.out.println(" 有客户端连接...");
                        // 固定消息定长,长度不够空格补全,不实用
                        // ch.pipeline().addLast(new FixedLengthFrameDecoder(10));

                        // 包尾添加特殊分隔符, 以特殊分隔符作为结尾(接收消息时会把特殊分隔符去掉)
                        ByteBuf buf = Unpooled.copiedBuffer("\r\n".getBytes());
                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new StringEncoder());
                        ch.pipeline().addLast(new ServerHandler());
                    }
                });
        // 启动
        ChannelFuture channelFuture = serverBootstrap.bind("localhost", 8080).sync();
        // 关闭
        channelFuture.channel().closeFuture().sync();
        bossGroup.shutdownGracefully();
        wordGroup.shutdownGracefully();
    }
}

2. Option 参数详解

参数名释义
ChannelOption.SO_BACKLOGChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
ChannelOption.SO_REUSEADDRChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口, 比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用, 比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。
ChannelOption.SO_KEEPALIVEChanneloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
ChannelOption.SO_SNDBUFChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
ChannelOption.SO_RCVBUFChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
ChannelOption.SO_LINGERChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送
ChannelOption.TCP_NODELAYChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
IP_TOSIP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。
ALLOW_HALF_CLOSURENetty参数,一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。

3. Netty粘包、拆包问题

什么是粘包/拆包
一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。
下面可以看一张图,是客户端向服务端发送包:
在这里插入图片描述

  1. 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。
  2. 第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。
  3. 第三种情况,Data2被分离成Data2_1和Data2_2,并且Data2_1在Data1之前到达了服务端,这种情况就产生了拆包。
    由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包/

解决方案:

  1. 消息定长 : 报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。不过这样对于超过定长的报文一定会拆开,所以一般不使用这种方式。
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
  1. 特殊分隔符分隔: 给每条报文结束后都添加特殊字符作为结束符进行分隔。
  // 包尾添加特殊分隔符, 以特殊分隔符作为结尾(接收消息时会把特殊分隔符去掉)
                ByteBuf buf = Unpooled.copiedBuffer("\r\n".getBytes());
                ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
  1. 自定义协议: 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段。下面详述。

三、Netty 自定义协议

一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?
主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。自定义协议通过定义了协议格式,来规范了报文头和报文长度来使得Netty可以知道每次的报文数据有多大,从而可以解析出来每次的报文。

  1. 自定义协议封装类
/**
 * @Data: 2019/10/19
 * @Des: 自定义协议封装类,发送报文时按照这个类型进行发送
 */
public class NettyDataProtocol {
    private static final int hearder = 0X76;     // 自定义协议,报文开始标志,十六进制
    private long contentLength;     // 数据长度
    private byte[] content;         // 报文内容


    public NettyDataProtocol(long contentLength, byte[] content) {
        this.contentLength = contentLength;
        this.content = content;
    }

    public int getHearder() {
        return hearder;
    }

    public long getContentLength() {
        return contentLength;
    }

    public void setContentLength(long contentLength) {
        this.contentLength = contentLength;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }
}
  1. 自定义报文编码器
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**

 * @Data: 2019/10/19
 * @Des: 自定义协议的编码器
 */
public class NettyDataProtocolEncoder extends MessageToByteEncoder<NettyDataProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, NettyDataProtocol msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getHearder());
        out.writeLong(msg.getContentLength());
        out.writeBytes(msg.getContent());
    }
}
  1. 自定义协议解码器
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
 * @Data: 2019/10/19
 * @Des: 自定义协议的解码器 : 当接收报文数据时,通过此解码器进行解码
 */
public class NettyDataProtocolDecoder extends ByteToMessageDecoder {
    // 报文开始,header int型,占4个字节,contentLength long型,占8个字节,所以基础长是12字节
    public final int BASE_LENGTH = 12;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 可读长度必须大于基本长度
        if (in.readableBytes() >= BASE_LENGTH) {
            // 防止socket字节流攻击
            // 防止客户端传来的数据过大(太大的数据是不合理的)
            if (in.readableBytes() > 2048) {
                in.skipBytes(in.readableBytes());
            }
            // 记录包头开始的index,用于后面不匹配时还原
            int beginReader;
            // 循环目的是为找到报文的header下标。
            while (true) {
                // 获取包头开始的index
                beginReader = in.readerIndex();
                // 标记包头开始的index
                in.markReaderIndex();
                // 读到协议的开始标志,结束while循环,则下面一段数据应为一段报文,所以跳出循环
                if (in.readInt() == 0X76) {
                    break;
                }
                // 未读到包头,缓缓报头标记位
                in.resetReaderIndex();
                // 跳过一个字节
                in.readByte();
                // 判断数据报长度是否满足最低限制,若不满足,则结束此次解码,等待后边数据流的到达
                if (in.readableBytes() < BASE_LENGTH) {
                    return;
                }
            }
            // 获取到报文数据报长度
            long length = in.readLong();
            // 判断数据是否完整
            if (in.readableBytes() < length) { // 不完整,回退读指针
                // 还原读指针
                in.readerIndex(beginReader);
                return;
            }
            // 至此,读到一条完整报文
            byte[] data = new byte[(int)length];
            in.readBytes(data);
            NettyDataProtocol protocol = new NettyDataProtocol(data.length, data);
            out.add(protocol);
        }
    }
}
  1. 服务端:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;


public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程池,一个负责接收消息,一个负责发送消息
        // bossGroup目的是获取客户端连接,连接接收到之后再将连接转发给workerGroup去处理。
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup wordGroup = new NioEventLoopGroup();
        // 创建一个辅助操作类
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, wordGroup).channel(NioServerSocketChannel.class)
                // 缓存队列缓存的请求的大小,默认50
                .option(ChannelOption.SO_BACKLOG, 1024)
                // 设置发送与接收的缓冲区的大小
                .option(ChannelOption.SO_SNDBUF, 32 * 1024).option(ChannelOption.SO_RCVBUF, 32 * 1024)
                // 通过ChannelPipeline初始化处理器,类似于拦截器Chain,当每个客户端首次连接后即调用initChannel方法完成初始化动作。
                // 初始化"拦截器"链
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        System.out.println(" 有客户端连接...");
                        // 添加自定义的协议编码、解码器
                        ch.pipeline().addLast(new NettyDataProtocolDecoder());
                        ch.pipeline().addLast(new NettyDataProtocolEncoder());
                        ch.pipeline().addLast(new ServerHandler());
                    }
                });
        // 启动
        ChannelFuture channelFuture = serverBootstrap.bind("localhost", 8080).sync();
        // 关闭
        channelFuture.channel().closeFuture().sync();
        bossGroup.shutdownGracefully();
        wordGroup.shutdownGracefully();
    }
}

  1. 服务端处理器
public class ServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        System.out.println("exceptionCaught...");
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
    	// 接收自定义报文
        NettyDataProtocol value = (NettyDataProtocol) msg;
        try {
            System.out.println("服务器端收到客户端msg:    " + new String(value.getContent(), "utf-8"));
            // 回复客户端
            String reply = "收到了你的信息";
            ctx.writeAndFlush(new NettyDataProtocol(reply.getBytes().length, reply.getBytes("utf-8")));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}
  1. 客户端:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.io.UnsupportedEncodingException;

/**
 * @Data: 2019/10/18
 * @Des: Netty 客户端
 */
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
        bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new NettyDataProtocolDecoder());
                ch.pipeline().addLast(new NettyDataProtocolEncoder());
                ch.pipeline().addLast(new ClientHandler());
            }
        });
        ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
        Channel channel = channelFuture.channel();
        // 向服务端发送消息
        for (int i = 0; i < 10; i++) {
            try {
                String msg = "....................................天下第" + i + "..." + i;
                channel.writeAndFlush(new NettyDataProtocol(msg.getBytes().length, msg.getBytes("utf-8")));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        System.out.println("消息已发送");
        // 等待客户端端口号关闭
        channelFuture.channel().closeFuture().sync();
        nioEventLoopGroup.shutdownGracefully();
    }
}
  1. 客户端处理器
class ClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当通道被调用,执行该方法
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        NettyDataProtocol value = (NettyDataProtocol) msg;
        try {
            System.out.println("客户端收到服务端回复:   " + new String(value.getContent(), "utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}

结果如下:
客户端发送消息,并受到服务端的回复
在这里插入图片描述

服务端接收到消息并回复
在这里插入图片描述

四、 Spring 整合 Netty

https://www.cnblogs.com/tdg-yyx/p/8376842.html


以上:内容部分参考
《Sping实战》
https://www.cnblogs.com/sidesky/p/6913109.html
https://www.jianshu.com/p/975b30171352
https://www.cnblogs.com/imstudy/p/9908791.html
https://www.cnblogs.com/sxkgeek/p/9488703.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值