Netty入门

NIO的三个重要组件

Channel

传统IO操作对read()或write()方法的调用,可能会因为没有数据可读/可写而阻塞,直到有数据响应。也就是说读写数据的IO调用,可能会无限期的阻塞等待,效率依赖网络传输的速度。最重要的是在调用一个方法前,无法知道是否会被阻塞。
NIO的Channel抽象了一个重要特征就是可以通过配置它的阻塞行为,来实现非阻塞式的通道。
Channel是一个双向通道,与传统IO操作只允许单向的读写不同的是,NIO的Channel允许在一个通道上进行读和写的操作。
几个常用的Channel:

  • FileChannel:从文件中读取数据。(不能使用非阻塞模式,不能使用在Selector)
  • DatagramChannel:能通过UDP读写网络中的数据
  • SocketChannel:能通过TCP读写网络中的数据
  • ServerSocketChannel:可以监听新进来的TCP连接,像web服务器一样,对每一个新的连接都会创建一个SocketChannel

Buffer

Buffer顾名思义,它是一个缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。
Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该模块内存。为了理解Buffer的工作原理,需要熟悉它的三个属性:capacity、position和limit。
在这里插入图片描述
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。见下图:
在这里插入图片描述
capacity: 作为一个内存块,Buffer有固定的大小值,也叫作“capacity”,只能往其中写入capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空才能继续写数据。
position: 当你写数据到Buffer中时,position表示当前的位置。初始的position值为0,当写入一个字节数据到Buffer中后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。当读取数据时,也是从某个特定位置读,将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取一个字节数据后,position向前移动到下一个可读的位置。
limit: 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。

Buffer的读写模式切换,其实就是position与limit数值的变化,Buffer本身并没有读写模式的表示。

Selector

Selector与Channel是相互配合使用的,将Channel注册在Selector上之后,才可以正确的使用Selector,但此时Channel必须为非阻塞模式。Selector可以监听Channel的四种状态(Connect、Accept、Read、Write),当监听到某一Channel的某个状态时,才允许对Channel进行相应的操作。

  • Connect:某个Channel成功连接到另一个服务器称为“连接就绪”
  • Accept:一个ServerSocketChannel准备好接收新进入的连接,称为“接收就绪”
  • Read:一个有数据可读的通道可以说是“读就绪”
  • Write:等待写数据的通道可以说是“写就绪”

NIO开发的问题

  1. 跨平台和兼容性问题:NIO依赖于操作系统,在Linux和Windows平台上表现的结果有所不同
  2. 扩展ByteBuffer:ByteBuffer允许包装一个byte[]来获得一个实例,可以尽量减少内存拷贝。但是它不能被扩展,ByteBuffer的构造函数是私有的
  3. epoll BUG:可能会导致无效的状态和100%CPU利用率

Netty HelloWorld

Netty简介

Netty是一个Java的开源框架。提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
Netty是一个NIO客户端,服务端框架。允许快速简单的开发网络应用程序。例如:服务端和客户端之间的协议,它简化了网络编程规范。
Netty的优点:

  • API使用简单,开发门槛低
  • 功能强大,预置了多种编解码功能,支持多种主流协议
  • 定制功能强,可以通过ChannelHandler对通信框架进行灵活的扩展
  • 性能高,通过与其他业界主流的NIO框架对比,Netty综合性能最优
  • 成熟、稳定,Netty修复了已经发现的NIO所有BUG
  • 社区活跃
  • 经历了很多商用项目的考验

Hello World

pom.xml

      <dependency>
          <groupId>io.netty</groupId>
          <artifactId>netty-all</artifactId>
          <version>4.1.5.Final</version>
      </dependency>
      <dependency>
          <groupId>org.msgpack</groupId>
          <artifactId>msgpack</artifactId>
          <version>0.6.12</version>
      </dependency>
      <dependency>
          <groupId>com.google.protobuf</groupId>
          <artifactId>protobuf-java</artifactId>
          <version>3.12.2</version>
      </dependency>

TimeServer

public class TimeServer {
    public static void main(String[] args) throws InterruptedException {
        int port = 8080;
        new TimeServer().bind(port);
    }
    public void bind(int port) throws InterruptedException {
        //用于服务端接收客户端的链接
        EventLoopGroup paentGroup = new NioEventLoopGroup();
        //用于SocketChannel的网络读写
        EventLoopGroup childGroup = new NioEventLoopGroup();
        try {
            //Netty用于启动NIO服务器的辅助启动类
            ServerBootstrap bootstrap = new ServerBootstrap();
            //将两个NIO线程组传入辅助启动类中
            bootstrap.group(paentGroup, childGroup).
                    //设置创建的Channel为NioServerSocketChannel类型
                    channel(NioServerSocketChannel.class).
                    //配置NioServerSocketChannel的TCP参数
                    option(ChannelOption.SO_BACKLOG, 1024).
                    //设置绑定IO事件的处理类
                    childHandler(new ChannelInitializer<SocketChannel>() {
                        //创建NIOSocketChannel成功后,在进行初始化时,将它的ChannelHandler设置到ChannelPipeline中,用于处理网络IO事件
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new TimeServerHandler());
                        }
                    });
            //绑定端口,同步等待成功(sync():同步阻塞方法,等待bind操作完成才继续)
            //ChannelFuture主要用于异步操作的通知回调
            ChannelFuture future = bootstrap.bind(port).sync();
            System.out.println("服务启动在" + port + "端口");
            //等待服务端监听端口关闭
            future.channel().closeFuture().sync();
        } finally {
            paentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}

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(Thread:"+Thread.currentThread()+") receive order : "+body);
        String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
        ByteBuf res = Unpooled.copiedBuffer(currentTime.getBytes());
        ctx.writeAndFlush(res);
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("starting。。。。。。。。。。");
    }
}

TimeClient

public class TimeClient {
    public static void main(String[] args) throws InterruptedException {
        int port = 8080;
        new TimeClient().connect(port, "127.0.0.1");
    }
    public void connect (int port, String host) throws InterruptedException {
        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();
        }
    }
}

TimeClientHandler

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuf firstMessage = Unpooled.buffer(req.length);
        firstMessage.writeBytes(req);
        ctx.writeAndFlush(firstMessage);

//        ByteBuf firstMessage2 = Unpooled.buffer(req.length);
//        firstMessage2.writeBytes(req);
//        ctx.writeAndFlush(firstMessage2);
    }
    @Override
    //接收服务器的响应
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        //buf.readableBytes():获取缓冲区中可读的字节数;
        //根据可读字节数创建数组
        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();
    }

Netty 粘包拆包

粘包拆包的问题

TCP是一个“流”协议,所谓流,就是没有界限的一串数据。可以想象为河流中的水,并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
TCP粘包拆包问题示例:
在这里插入图片描述
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案可归纳如下:

  • 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格
  • 在包尾增加回车换行符进行分割,例如FTP协议
  • 将消息分为消息头和消息体,消息头中包含消息总长度(或消息体总长度)的字段
  • 更复杂的应用层协议

Netty内置的解决方案

常见的几个编解码器:

  • LineBasedFrameDecoder:以换行符为结束标志的编解码,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度
  • DelimiterBasedFrameDecoder:实现自定义分隔符作为消息的结束标志,完成解码
  • FixedLengthFrameDecoder:是固定长度解码器
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值