[Java网络编程] Netty之框架定位和其模型

在使用Java进行网络编程时,我们肯定经常会使用到java.netjava.iojava.nio中的类。但是这里面的类并不是十分好用,很难快速的实现高效,易用的程序。所以,Netty网络编程框架替我们封装了这一层的复杂性。提供了稳定,高性能,易编码的特性。那么,Netty到底是怎样一个框架呢?该怎样使用呢?这将是本文所需要讨论的内容。

注:我是新手,理解能力有限,如果本文有错误或者漏洞希望能得到指点

一、Netty - 一款异步和事件驱动的框架

Netty提供了同步和异步两种传输模型,并且由事件驱动。如同java.nio中的选择器(Selector)能监听:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

这些事件一样,Netty也有一组监听事件。比如入站数据可能触发的事件包括:

    连接已被激活或连接失活
    数据读取
    用户事件
    错误事件

Netty的主要由ChannelChannelPipelineEventLoop/EventLoopGropFutureByteBuf事件和ChannelHandlerChannelHandlerContext以及ServerBootstart/Bootstart构成。以下是他们的简要说明:

Channel和JDK中的Channel职能一样,是数据的载体,所以它可能的状态有打开或者关闭,连接或者断开连接。在Netty中,每个Channel都会被分配一个ChannelPipeline用于传输需要处理的事件和数据,和分配一个ChannelConfig用于配置该Channel所有的设置。Channel的具体类型有:

  • OioServerSocketChannel 用于阻塞传输
  • NioServerSocketChannel 用于非阻塞传输
  • EmbeddedChannel 用于单元测试,该通道两边都是本主机

ChannelPipeline持有所有将应用于入站或者出站的数据以及事件的ChannelHandler实例。是以拦截过滤器的设计模式实现的。它可以调用一系列方法来添加、删除或者替换ChannelHandler。

EventLoop/EventLoopGrop代替了JDK中的Selector,完全隐藏了事件循环的过程。它可以以非阻塞和阻塞的方式监听各种事件然后回调ChannelHandler中的对应方法。当需要将一个由Netty写的网络通信系统从非阻塞转换为阻塞只需要改变EventLoopGrop的类型和Channel类型即可。
EventLoopGrop的生命周期是从注册到ChannelPipeline直到程序结束。负责为每个新创建的Channel分配EventLoop。该EventLoop将会处理Channel上的所有IO事件。EventLoop由一个线程驱动,在异步环境下一个EventLoop通常承载了多个Channel,而在同步情况下,每个Channel都会分配一个新的EventLoop。

Future提供了另一种在操作完成时通知应用程序的方式。可以看做是异步操作结果的占位符。

ByteBuf是Netty用到的高可用,屏蔽了复杂性的字节容器。

事件和ChannelHandler是联系密切的,我们的业务逻辑代码写在ChannelHandler中的重载方法中,由事件驱动方法调用。它的典型用途包括:

  • 将数据从一种格式转换为另一种格式
  • 提供异常的通知
  • 提供Channel变为活动或者非活动的通知
  • 提供当Channel注册到EventLoop或者从EventLoop注销时的通知
  • 提供有关用户自定义事件的通知
    若需要将一个处理器应用到多个ChannelPipeline,则需要保证他是线程安全的。

ChannelHandlerContext代表着ChannelHandler和ChannelPipeline之间的关联,每个ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。

ServerBootstart/Bootstart 是对一个应用程序进行配置并使它运行起来的东西。一般流程是:
绑定事件循环组对象,指定通道类型,添加通道处理器,设置额外配置(比如通道配置,额外属性),再进行地址的连接或者绑定,这时程序就进入通信过程中了。最后还需要一个future来关闭该引导,并且释放其资源。

由一个最简单的通信模型可以对其整个架构窥之一二,所以这里给出Netty实战中的示例代码及其解释:
首先写服务器端:

/*
服务器端 通道处理器,用于处理某一通道上的事件
*/
package org.hournet.mynet;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.util.CharsetUtil;

@Sharable  //该注解表示一个ChannelHandler可以被多个Channel安全地共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter
{
	// 通道读事件
    @Override
    public void channelRead(ChannelHandlerContext ctx,Object msg){
        ByteBuf in = (ByteBuf)msg;    //获取缓冲区
        System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8)); //打印到控制台
        ctx.write(in);  //写给发送者,而不冲刷出站消息
    }
	// 读完成事件
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx){
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
           .addListener(ChannelFutureListener.CLOSE);    //读完成后将所有消息冲刷到远程节点并且关闭该Channel
    }
	//异常事件
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
        cause.printStackTrace();    //打印异常
        ctx.close();				  //关闭Channel
    }
}

以上代码是处理服务器端某一通道的业务逻辑代码,引导其进行工作的是引导服务器类,引导类的写法大多类似,因为引导服务器类都需要为其分配网络资源以及确定传输类型等,示例代码为:

package org.hournet.mynet;

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

public class EchoServer{
    private final int port;
    
    public EchoServer(int port){
        this.port = port;
    }
    
    public static void main(String[] args)throws Exception{
        new EchoServer(60000).start();  //从给定端口号直接开启引导
    }

    public void start()throws Exception{
        final EchoServerHandler serverHandler = new EchoServerHandler();  //业务逻辑类/通道事件处理器
        EventLoopGroup group = new NioEventLoopGroup();					  //事件循环组的类型
        try{
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)  //将 异步事件循环组注册到引导中
             .channel(NioServerSocketChannel.class)  //指定所使用的通道类型
             .localAddress(new InetSocketAddress(port))  //绑定本机地址
             .childHandler(new ChannelInitializer<SocketChannel>(){    //在处理链上进行通道初始化
                @Override
                public void initChannel(SocketChannel ch)throws Exception{
                   ch.pipeline().addLast(serverHandler);			//在管道上增加业务逻辑对象/事件处理对象
                }
            }
                );
             ChannelFuture f = b.bind().sync();			//异步地绑定服务器,调用sync()方法阻塞直到绑定完成
             f.channel().closeFuture().sync();			//异步地关闭服务器,调用sync()方法阻塞直到Channel关闭
        }finally{
            group.shutdownGracefully().sync();  //关闭事件循环组释放所有资源
        }
    }
}

客户机代码是类似的:

package org.hournet.mynet;

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 EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf>{

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

    @Override
    public void channelRead0(ChannelHandlerContext ctx,ByteBuf in){
        System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
    }

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

引导服务器

package org.hournet.mynet;

import java.net.InetSocketAddress;

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;

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

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

    public static void main(String[] args)throws Exception{
        new EchoClient("127.0.0.1", 60000).start();
    }

    public void start()throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();
        try{
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .remoteAddress(new InetSocketAddress(host, port))
             .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch)throws Exception{
                        ch.pipeline().addLast(new EchoClientHandler());
                    } 
            });
             ChannelFuture f = b.connect().sync();
             f.channel().closeFuture().sync();
        }finally{
            group.shutdownGracefully().sync();
        }
    }
}

从以上代码中,以及开始的介绍我认为Netty的架构模型为:
[Java网络编程] Netty之框架定位和其模型_第1张图片
如上图所示,一个事件循环组包含多个事件循环,其职责是为创建的通道(Channel)分配事件循环,事件循环将在Channel的整个生命周期处理IO事件。需要注意的是,一个Channel上有一个ChannelHandler链,用于链式处理一个事件。

  • 一个EventLoopGroup包含一个或者多个EventLoop
  • 一个EventLoop在它的生命周期内只和一个Thread绑定
  • 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理
  • 一个Channel在它的生命周期内只注册一个EventLoop
  • 一个EventLoop可能会被分配给一个或多个Channel

Netty的容器

Netty使用ByteBuf作为字节容器,其直接优点有:
1.容量可以按需增长
2.在读和写这两种模式之间切换不需要调用flip()方法(因为维护了读写两个索引)
3.支持方法的链式调用
4.支持引用计数(可以使用直接内存,但需要手动释放)
5.支持池化

ByteBuf的使用模式:

1.堆缓冲区
最常用的ByteBuf模式是存储在JVM堆空间中的,这种模式也叫做支撑数组。示例代码:

ByteBuf heapBuf = ...;
if(heapBuf.hasArray()){
	byte array = heapBuf.array()
	.....
}

2.直接缓冲区
直接缓冲区使用了直接内存,这样避免了每次调用本地I/O操作前后将缓冲区的内容复制到中间缓冲区。示例代码为:

ByteBuf directBuf = ...;
if(!directBuf.hasArray()){
	int length = directBuf.readableBytes();
	byte[] array = new byte[length];
	directBuf.getBytes(directBuf.readerIndex(),array);
	....
}

3.复合缓冲区
复合缓冲区是为多个ByteBuf提供的一个聚合视图,在这里你可以根据需要添加或删除ByteBuf实例。Netty通过ByteBuf的子类 - CompositeByteBuf 实现了这个模式。示例代码:

CompositeByteBuf compBuf = Unpooled.compositeBuffer();
byte[] array = new byte[compBuf.readableBytes()];
compBuf.getBytes(compBuf.readerIndex(),array);
......

字节级操作

1.随机访问索引
2.顺序访问索引
3.可丢弃字节
4.可读字节
5.可写字节

Channel的生命周期

创建->注册 -> 活动 -> 没有连接到远程节点

ChannelHandler的生命周期

添加到管道 -> 从管道移除 | 处理过程中产生异常

Bootstrap和ServerBootstrap的区别

Bootstrap是用于引导客户端的,ServerBootstrap是引导服务器的
Bootstrap可以通过remoteAddress和connect来连接到远程节点,而ServerBootstrap不能
ServerBootstrap能通过childHandler和childAttr以及childOption调整具体设置,而Bootstrap不能
ServerBootstrap和Bootstrap都能通过bind以及localAddress来设置本地地址。

Channel与EventLoopGroup的兼容性

channel
|-----nio
|    NioEventLoopGroup
|-----oio
|    OioEventLoopGroup
|-----socket
        |—nio
        |   NioDatagramChannel
        |   NioServerSocketChannel
        |   NioSocketChannel
        |—oio
            OioDatagramChannel
            OioServerSocketChannel
            OioSocketChannel

若使用不同前缀的事件循环和通道则会出现IllegalStateException ,因为这样的组合不能互相兼容。

Netty预置工具

在网络编程中,如果稍稍接近底层就离开不了编码器和解码器, Netty为我们预置了编码器与解码器的抽象类以及部分协议的编解码器的具体类以供开发者方便开发。
首先看看抽象类部分:

比特-消息解码器 描述
ByteToMessageDecoder 比特解码器(需要自己判断比特是否足够)
ReplayingDecoder 自动判断比特数目的比特解码器
LineBasedFrameDecoder 使用行尾控制符(\n或\r\n)解析消息的比特解码器
DelimiterBasedFrameDecoder 通用的基于分隔符的比特解码器
HttpObjectDecoder HTTP数据解码器
HttpRequestDecoder 将字节解码为HttpRequest,HttpContent和LastHttpContent消息
HttpResponseDecoder 将字节解码为HttpResponse,HttpContent和LastHttpContent消息
消息-消息编码器 \ 消息-消息解码器 描述
MessageToMessageEncoder 将 I 类型编码为另一种对象的消息编码器,用于出站
MessageToMessageDecoder 将 I 类型解码为另一种对象的消息解码器,用于入站
消息-比特编码器 描述
MessageToByteEncoder 将 I 类型的消息编码为比特的编码器
HttpRequestEncoder 将HttpRequest,HttpContent和LastHttpContent消息编码为字节
HttpResponseEncoder 将HttpResponse,HttpContent和LastHttpContent消息编码为字节
编解码器 描述
MessageToByteEncoder 双向使用的比特- I 对象编解码器
MessageToMessageCodec 双向使用的消息编解码器
CombinedByteCharCodec 双向使用比特 - 对象编解码器,使用编码器和解码器作为构造参数

Netty学习案例: 穆书伟的Github

你可能感兴趣的:(Java)