Netty系列教程(一)Netty是什么

921 阅读10分钟

系列教程:

Netty简介

Netty是一个基于NIO的网络编程框架,它提供了异步、事件驱动的网络编程工具,用于快速开发高性能、高可靠性的网络服务器和客户端应用程序。

Netty在各个行业都被广泛使用,常用于开发各种高性能的网络通信框架,如:

  • 互联网行业:互联网行业的特点的并发量高,对性能要求高,Netty刚好满足行业的需求,在分布式系统中,各个节点之间都要进行通信,高性能的RPC框架必不可少,而Netty就是构建这些通信基础组件的基础。常见的应用有:阿里的RPC框架Dubbo
  • 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
  • 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

传统I/O编程介绍

在网络编程领域初期,由于JAVA对I/O的支持并不完善,因此,高性能服务端开发领域一直被C++和C长期占据,JAVA的同步阻塞I/O被大家所诟病。

UNIX中网络编程提供了5种I/O模型:

  • 阻塞I/O模型

  • 非阻塞I/O模型

  • I/O复用模型

  • 信号驱动I/O模型

  • 异步模型

在JAVA1.4 NIO推出以前,JAVA开发的所有Socket通信都采用了同步阻塞模式,该模式下的通信在性能和可靠性方面存在巨大的瓶颈。

因此,在JAVA1.4 NIO类库推出之后,JAVA开发的Socket通信才正式迈入了一个新台阶。

并且,在NIO2.0中推出了异步非阻塞I/O。

BIO

BIO的名称是同步阻塞I/O模式。

网络编程通信模型为客户端/服务端模式,数据在客户端和服务端之间的通道内进行传输。该模型特点是:有一个独立的Acceptor线程负责监听客户端的连接,一旦有新的连接请求,就会创建一个新的线程进行处理,然后处理完成之后返回客户端,销毁线程。对应的通信模型图如下:

由于每个连接都要创建一个线程来处理,因此该I/O模型的弊端也比较明显:

  • 线程多时占用内存大,并且线程频繁切换开销也大

  • 线程多时会导致性能急剧下降

因此 BIO只适用与连接数少的场景。

为了解决BIO频繁创建线程来处理连接的问题,有人引入了线程池来进行优化,主要是在服务端维护一个线程池,有客户端进行连接时,就从线程池中获取线程进行连接处理。通过线程池可以灵活调配线程资源,设置线程核心数和最大数,防止由于海量并发连接数导致线程资源耗尽。

引入线程池的I/O模型图如下:

通信流程如下:

  • Acceptor接收到新的连接请求后,将客户端的Socket请求封装为一个Task

  • 将该Task放入线程池队列中

  • 线程池根据当前线程利用情况,取出线程来执行Task

该I/O模型的优点:

  • 资源占用可控,大量的并发不会导致资源耗尽

但是同时也有一定的缺点:

  • 线程池队列积满之后,新的Task会被排队阻塞,除非采用无界队列,但是无界队列会让问题回到起点

  • 由于前端只有一个Acceptor线程接收客户端接入,在高并发场景下很容易成为瓶颈,并且可能会导致客户端连接阻塞

NIO

有人说NIO是NEW IO,但是其实更准确的说法应该是NON-Blocking IO(非阻塞IO),它的出现主要是解决传统IO的阻塞问题。

与Socket和ServerSocket相对应,NIO也提供了SocketCHannel和ServerSocketChannel两种不同的套接字通道实现。并且这两种通道都支持阻塞和非阻塞模式。分别适用于低并发、连接数少的场景和高并发、连接数量大的网络应用程序。

NIO中的核心组件有:

1、缓冲区Buffer

Buffer是NIO类库中一个新的概念,在NIO中,所有数据都是用缓冲区处理的。在写入数据时,是先写入缓冲区,在读取数据时,也是从缓冲区中读取。

缓冲区其实是个数组,但是不仅仅是一个数组,它提供了对数据的结构化访问以及维护读写位置的信息。

常见的缓冲区有:

  • ByteBuffer。最常见,用于操作字节数组。

  • CharBuffer

  • SHortBuffer

  • IntBuffer

  • LongBuffer

  • FlaotBuffer

  • DoubleBuffer

除了boolean类型,JAVA中每种基本数据类型都有对应的缓冲区类型。

2、通道Channel

通道是指在客户端和服务端连接建立后的传输数据的管道,可以通过它读取和写入数。通道与流不同之处在于流只是在一个方向上写入或者读取,二通道可以用于读也可以用于写,还可以同时用于读写。

Channel可以分为两大类:

  • 用于网络读写的Selectablechannel

  • 用于文件操作的FileChannel

前面提到的SocketCHannel和ServerSocketChannel其实是Selectablechannel的两个子类。

3、多路复用器Selector

Selector是NIO中一个比较重要的基础概念,Selector会不断轮询注册在其上面的channel,如果某个channel有新的TCP连接接入、读写事件,该channel就变成就绪状态,并进行后续的I/O操作。

一个Selector可以同时轮询多个channel,因此只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端连接,这不得不说是一个巨大的进步。

NIO服务端处理客户端连接的流程如下:

NIO编程难度比BIO大很多,但是也有很大的优势:

  • 通过Selector多路复用轮询机制,客户端的连接不会像之前那样被同步阻塞

  • SocketChannel的读写操作都是异步的, 如果没有可读写的操作,不会同步等待,而是直接返回,这样I/O同学线程就可以去处理其他链路,不需要同步等待

  • 由于对线程模型的优化,使得一个Selector可以处理成千上万个客户端连接,并且性能不会线性下降,因此非常适合做高性能、高并发的网络服务器。就

AIO

NIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。

虽然AIO的编程比NIO更为简单,但是目前并没有得到很广泛的应用,主要原因有以下几个:

  • Linux对AIO的实现不够成熟
  • Linux下AIO相比NIO的性能提升不够明显把

基于以上几个因素,AIO的应用很少,Netty旧版本也支持AIO,但是后面的版本中仅仅支持NIO了。

为什么要用Netty

为什么不用原生NIO

虽然NIO功能很强大,但是实际中使用起来并不容易:

  • 类库和API繁杂,使用麻烦

  • 对开发者编程水平要求高,需要熟练掌握额外的技能,如多线程

  • 开发工作量和工作难度很大,如:心跳检测、断线重连、粘包拆包、网络阻塞等

  • JDK NIO的BUG,如epoll可能会在某些情况下导致Selector空转,导致CPU 100%

基于上述原因,在大多数场景下,并不适合直接使用NIO来进行开发,除非非常精通NIO或者有特殊的需求。因此在大多数场景下我们可以用一个基于NIO封装的Netty框架来代替原生的NIO进行开发。

为什么要用Netty

Netty是基于NIO的框架之一,它的健壮性、功能、性能、可扩展性都是首屈一指的。它在很多框架中都有着出色的表现,比如Hadoop的avro使用Netty来作为底层通信框架,Dubbo RPC通信也是基于Netty来实现的。

Netty有着如下优点:

  • API使用简单,开发门槛低

  • 设计优雅

  • 高性能、高吞吐量、可扩展性强

  • 比较成熟稳定,社区活跃,更新比较快

第一个Netty案例

我们将通过一个入门案例来演示第一个Netty开发的应用程序,在该程序中,服务端监听8080端口,客户端去连接本地的8080端口,连接成功后,会发送"Hello Netty!"给服务端,服务端接收到消息之后,也回复"Hello Netty!"给客户端。

引入maven坐标

在该示例中,引入的是netty-all4.1版本

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

服务端开发

服务端主程序:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;


public class HelloNettyServer {

    private static final Integer port = 8080;

    public static void main(String[] args) throws InterruptedException {
        //设置线程组
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //启动引导
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(workerGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //加入自定义的handler处理器
                            pipeline.addLast(new HelloNettyServerHandler());
                        }
                    });

            //绑定端口号
            ChannelFuture future = bootstrap.bind(port).sync();

            //等待服务端监听端口关闭
            future.channel().closeFuture().sync();
        } finally {
            //关闭资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

主程序的主要逻辑是创建两个线程组,监听8080端口号,接收客户端连接请求,并将请求交给HelloNettyServerHandler进行处理。因此HelloNettyServerHandler是我们业务的核心。

对应的HelloNettyServerHandler:

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

public class HelloNettyServerHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("服务端接收到一条消息:" + in.toString(CharsetUtil.UTF_8));
        ctx.writeAndFlush(in);
    }

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

该handler中可以接收到客户端发送的消息内容,并且可以通过channel将消息回复给客户端。

客户端开发

客户端主程序:

import io.netty.bootstrap.Bootstrap;
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.net.InetSocketAddress;


public class HelloNettyClient {

    private static final String host = "127.0.0.1";
    private static final Integer port = 8080;

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline().addLast(new HelloNettyClientHandler());
                        }
                    });

            ChannelFuture sync = bootstrap.connect().sync();

            sync.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

客户端的逻辑比较简单,主要是创建一个线程组,并与服务端建立连接,连接建立成功后,发送消息给服务端,并且接收服务端返回的消息。

对应的handler处理器HelloNettyClientHandler:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

public class HelloNettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty!", CharsetUtil.UTF_8));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        System.out.println("客户端接收到消息: " + byteBuf.toString(CharsetUtil.UTF_8));
    }

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

该handler中的channelActive方法在连接建立成功后被调用,channelRead0方法在接收到服务端消息后被调用,exceptionCaught在当有异常发生时被调用。

效果演示

启动服务端,然后再启动客户端,可以看到服务端控制台输出:

客户端控制台随后输出:

由此可见,客户端和服务端的通信已经可以正常进行。

由于此处只是demo入门示例,主要是看netty实现的效果,对其中的代码解析会放到后续章节进行。

总结

通过简单的编程代码,就可以完成客户端和服务端的通信,大大简化了NIO实现网络编程的开发。