Netty学习之初探

前言

Netty是一款用于创建高性能网络应用程序的高级框架,它的优势在于:

  • 不必精通网络编程,已经为你预置大量细节
  • 比直接使用Java本地API要简单的多
  • 有良好的设计实践,将应用程序逻辑和网络层解耦

本文代码可以通过这里查看地址

Netty特性

分类Netty特性
设计统一的 API,支持多种传输类型,阻塞的和非阻塞的
简单而强大的线程模型
真正的无连接数据报套接字支持
链接逻辑组件以支持复用
易于使用详实的Javadoc和大量的示例集
不需要超过JDK 1.6+3的依赖。(一些可选的特性可能需要Java 1.7+和/或额外的依赖)
性能拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟
得益于池化和复用,拥有更低的资源消耗
最少的内存复制
健壮性不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError
消除在高速网络中 NIO 应用程序常见的不公平读/写比率
安全性完整的 SSL/TLS 以及 StartTLS 支持
可用于受限环境下,如 Applet 和 OSGI
社区驱动发布快速且频繁

第一个简单的Netty程序

我们将要创建一个简单netty的程序,包含客户端和服务器端,其功能很简单就是客户端连接到服务器端,服务器端收到并返回给客户端一个消息

Server端

public class EchoServer {
    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws InterruptedException {
        if( args.length != 1){
            //提示设置端口值
            System.err.println("Usage: "+ EchoServer.class.getSimpleName()+" <port>");
        }
        int port = Integer.parseInt(args[0]);
        new EchoServer(port).start();
    }

    public void start() throws InterruptedException {
        final EchoServerHandler echoServerHandler = new EchoServerHandler();
        //创建EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建ServerBootstrap
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //因为你正在使用的是 NIO 传输,所以你指定了NioEventLoopGroup来接受和处理新的连接
            serverBootstrap.group(group)
                    //指定所使用的NIO传输channel,这里就是父Channel
                    .channel(NioServerSocketChannel.class)
                    //使用指定的端口设置套接字地址
                    .localAddress(new InetSocketAddress(port))
                    //添加一个echoServerHandler到子Channel的ChannelPipeline,
                    // 当一个新的连接被接受时,一个新的子Channel将会被创建,用于处理入站消息通知
                    // 而ChannelInitializer将会把一个你的EchoServerHandler的实例添加到该Channel的ChannelPipeline中
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //echoServerHandler 中@Sharable表示我们可以使用同样的实例
                            ch.pipeline().addLast(echoServerHandler);
                        }
                    });

            //异步绑定服务器,调用sync方法阻塞等待直到绑定完成
            ChannelFuture channelFuture = serverBootstrap.bind().sync();
            //获取Channel的closeFuture,并且阻塞当前线程直到它完成
            channelFuture.channel().closeFuture().sync();
        }finally {
            //关闭EventLoopGroup释放所有资源
            group.shutdownGracefully().sync();
        }
    }
}
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server receive :"+ in.toString(CharsetUtil.UTF_8));
        ctx.write(in);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

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

Client端

public class EchoClient {
    private final String host;
    private final int port;

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

    public static void main(String[] args) throws InterruptedException {
        if (args.length != 2) {
            System.err.println(
                    "Usage: " + EchoClient.class.getSimpleName() + " <host> <port>");
            return;
        }
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
    public void start() throws InterruptedException {
        EventLoopGroup 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 ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });

            ChannelFuture f = bootstrap.connect().sync();
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully().sync();
        }
    }
}
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //当被通知Channel是活跃的时候,发送一条消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",
                CharsetUtil.UTF_8));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        //记录已接收消息的转储
        System.out.println(
                "Client received: " + in.toString(CharsetUtil.UTF_8));
    }

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

我们可以看出,使用Netty构建的程序远比Java API的要简单多了,而且最终要的是它将网络处理和业务处理进行了分离,使我们更加专注于业务构建,这一点我觉得在大型程序中非常有优势。

Netty的组件和设计

网络组件

Channel、EventLoop和ChannelFuture这三个接口,被认为是netty网络抽象的代表,其中每个负责的职责如下:

  • Channel:Socket
  • EventLoop:控制流、多线程处理、并发
  • ChannelFuture:异步通知

Channel接口

基本的IO操作(bind(),connect(),read()和write())依赖于底层网络传输所提供的原语。对于Java网络编程,可以类比为Socket类。

EventLoop接口

EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件,下图在高层次上说明了Channel、EventLoop、Thread以及EventLoopGroup之间的关系。

关系图

  1. 一个EventLoopGroup包含一个或者多个EventLoop
  2. 一个EventLoop在它的生命周期内只和一个Thread绑定
  3. 所有由EventLoop处理的IO事件都将在它专有的Thread上被处理
  4. 一个Channel在它的生命周期内只注册一个EventLoop
  5. 一个EventLoop可能会被分配给一个或者多个Channel

ChannelFuture接口

Netty中所有操作都是异步的,因为一个操作不回立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。可以将ChannelFuture看作一个将来要执行的操作的结果的占位符,无法确定什么时候执行,可以确定的是它一定会执行。并且,所有属于同一个Channel的操作都被保证其将以它们被调用的顺序被执行

业务组件

ChannelHandler和ChannelPipeline用于管理数据流和处理应用程序业务逻辑。

ChannelHandler接口

对于程序开发人员来说,接触的最多的就是ChannelHandler了,它充当了所有处理入站和出站数据的业务逻辑的容器。举个例子,ChannelInbounudHandler是以后会经常实现的子接口,接收入站数据和事件,这些数据稍后会被你自己的业务代码处理,当需要发送给客户端响应的时候,也可以直接冲ChannelInbounudHandler冲刷数据。

ChannelPipeline接口

ChannelPipeline是一个容器,用于存储ChannelHandler接口,我们先看一段代码:

...
.childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("连接2");
                            //echoServerHandler 中@Sharable表示我们可以使用同样的实例
                            ch.pipeline().addLast(echoServerHandler);
                        }
                    })

这是上面EchoServer截取的一段代码,当Channel被创建的时候会分配到它专属的ChannelPipeline中,ChannelHandler安装到ChannelPipeline中的过程如下:

  • 一个ChannelInitializer的实现被注册到ServerBootstrap中
  • 当ChannelInitializer.initChannel()方法被调用的时候,ChannelInitializer将在ChannelPipeline安装一组自定义的ChannelHandler
  • ChannelInitializer将自己从ChannelPipeline中移除

ChannelPipeline实际上是一个双向链表,内部是由序的,具体可以看下图:
ChannelPipeline顺序

深入ChannelHandler

ChannelHandler有不同类型,它的功能取决于它的超类。Netty以适配器的形式提供了大量的ChannelHandler默认实现,目的是为了简化开发,下面这些是编写自定义 ChannelHandler 时经常会用到的适配器类:

  • ChannelHandlerAdapter

  • ChannelInboundHandlerAdapter

  • ChannelOutboundHandlerAdapter

  • ChannelDuplexHandler

编码器和解码器

数据在网络之中传输的是字节的形式,入站的时候消息会被解码,即消息从字节转换成另一种格式,通常是Java对象;如果是出站消息,则会发生相反方向的转换,它将从当前的格式被编码为字节。

Netty为了编码器和解码器提供了不同类型的抽象,通常来说这些基类的名称类似于ByteToMessageDecoder或者ByteToByteEncoder。对于特殊的类型,会发现类似于 ProtobufEncoder 和 ProtobufDecoder 这样的名称——预置的用来支持 Google 的 Protocol Buffers。

所有Netty内置的编码和解码器适配器类都实现了ChannelOutboundHandler 或者 ChannelInboundHandler 接口,所以编码器解码器也是一种特殊的ChannelHandler。

抽象类SimpleChannelInboundHandler

假如我们需要利用一个ChannelHandler来接收解码消息,并对该数据进行业务处理,只需要继承SimpleChannelInboundHandler<T>,其中T代表需要处理消息的Java类型。在这个 ChannelHandler 中, 你将需要重写基类的一个或者多个方法,并且获取一个到 ChannelHandlerContext 的引用, 这个引用将作为输入参数传递给 ChannelHandler 的所有方法

Bootstrap

就是启动类,Netty为应用程序配置了两个容器,Bootstrap(用于客户端)ServerBootstrap(用于服务端),下面简单比较了两个类型:

类型BootstrapServerBootstrap
网络编程中的作用连接到远程主机和端口绑定到一个本地端口
EventLoopGroup 的数目12

我们可以看出引导客户端只需要质一个EventLoopGroup,而服务端需要两个EventLoopGroup。

为什么呢?

因为服务器需要两组不同的Channel,第一组只包含一个ServerChannel,代表服务器自身已经绑定到某个本地端口的正在监听套接字;第二组包含所有已创建的用来处理传入客户端连接的Channel。下图可以作为一个简单的说明:
具有两个EventLoopGroup的服务器

总结

目前为止,我们使用Netty写了一个简单的小程序。然后介绍了,netty的一些组件和设计。大家看下来,其实应该有一点点感觉了。别急,我们先自己回忆并练习一下,然后在开始后面的学习。

加油!