Netty介绍

介绍

Netty is an asynchronous event-driven network application framework 
for rapid development of maintainable high performance protocol servers & clients.

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.


Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

Netty的优点

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

  • 功能强大,预置了多种编解码功能,支持多种主流协议;

  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;

  • 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;

  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;

  • 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

选择Netty而非java nio的理由

  • NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、 ServerSocketChannel、SocketChannel、ByteBuffer等。

  • 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为 NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。

  • 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网 络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都 非常大。

  • JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询, 最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但 是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而 已,它并没有被根本解决。 

主要内容

线程模型

Netty的线程模型可以根据用户设定的参数支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。

我们可以从Netty服务端创建过程来了解Netty的线程模型:

在创建ServerBootstrap类实例前,先创建两个EventLoopGroup,它们实际上是两个独立的Reactor线程池,bossGroup负责接收客户端的连接,workerGroup负责处理IO相关的读写操作,或者执行系统task、定时task等。

用于接收客户端请求的线程池职责如下:

  1. 接收客户端TCP连接,初始化Channel参数;
  2. 将链路状态变更事件通知给ChannelPipeline;

处理IO操作的线程池职责如下:

  1. 异步读取远端数据,发送读事件到ChannelPipeline;
  2. 异步发送数据到远端,调用ChannelPipeline的发送消息接口;
  3. 执行系统调用Task;
  4. 执行定时任务Task,如空闲链路检测等;

通过调整两个EventLoopGroup的线程数、是否共享线程池等方式,Netty的Reactor线程模型可以在单线程、多线程和主从多线程间切换,用户可以根据实际情况灵活配置。

EventLoop/EventLoopGroup

 

为了提高性能,Netty在很多地方采用了无锁化设计。例如在IO线程的内部进行串行操作,避免多线程竞争导致的性能下降。尽管串行化设计看上去CPU利用率不高,并发程度不够,但是通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的设计相比一个队列——多个工作线程的模型性能更优。

 

Netty的Reactor模型设计如下: 

 

Netty的NioEventLoop读取到消息之后,调用ChannelPipeline的fireChannelRead方法,只要用户不主动切换线程,就一直由NioEventLoop调用用户的Handler,期间不进行线程切换。这种串行化的处理方式避免了多线程操作导致的锁竞争,从性能角度看是最优的。

Netty多线程编程的最佳实践如下: 

  1. 服务端创建两个EventLoopGroup,用于逻辑隔离NIO acceptor和NIO IO线程;
  2. 尽量避免在用户Handler里面启动用户线程(解码后将POJO消息发送到后端业务线程除外);
  3. 解码要在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码;
  4. 如果业务逻辑比较简单,没有复杂的业务逻辑计算,没有可能阻塞线程的操作如磁盘操作、数据库操作、网络操作等,可以直接在NIO线程中进行业务逻辑操作,不用切换到用户线程;
  5. 如果业务逻辑比较复杂,不要在NIO线程上操作,应将解码后的POJO封装成Task提交到业务线程池中执行,以保证NIO线程被尽快释放,处理其他IO操作;

ChannelHandler

ChannelHandler类似于Servlet中的filter,它负责对IO事件或者IO操作进行拦截和处理。ChannelHandler可以选择性地对感兴趣的事件进行拦截和处理,也可以透传和终止事件的传递。基于ChannelHandler,用户可以方便地定制自己的业务逻辑,如日志打印、编解码、性能统计等。Netty本身也提供了很多有用的ChannelHandler的实现类供用户使用。

 

功能说明

大多数的ChannelHandler只关心特定的一个或几个事件,对其进行拦截和处理,而对其不关心的事件则直接交给下一个ChannelHandler。这就导致一个问题:用户在实现ChannelHandler接口时必须实现ChannelHandler的所有方法,包括其不关心的事件处理方法,这就导致了代码的冗余。为了解决这个问题,Netty提供了ChannelHandler的实现基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter类。这两个类分别拦截和处理inbound事件和outbound事件,它们对所有的事件都直接透传,用户可以覆盖父类中感兴趣的事件的处理方法,而对其他事件则直接继承父类的实现,这样就可以保持代码的简洁和清晰。

 

部分自带handler:

  1. LoggingHandler:Netty提供的使用日志框架记录和打印全部的事件
  2. SslHandler:Netty实现的为Channel提供SSL支持的handler。
  3. ChunkedWriteHandler:Netty提供的为Channel提供大文件读写支持的handler。
  4. IdleStateHandler/ReadTimeoutHandler/WriteTimeoutHandler:Netty提供的用于空闲检测的ChannelHandler。
  5. ChannelTrafficShapingHandler/GlobalTrafficShapingHandler:Netty提供的流量整形相关的Handler,包括以Channel为单位的流量整形和全局的流量整形。

  6. codec包下的编解码器

ChannelPipeline

ChannelPipeline实际上是一个ChannelHandler的容器,它负责ChannelHandler的管理和事件的拦截与调度。

 

Netty中的ChannelPipeline和ChannelHandler类似于J2EE中的Filter过滤器机制,这类拦截器机制实际上是责任链模式的一种变形,主要目的是方便事件的拦截和用户业务逻辑的定制。Netty将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipeline中间流动和传递。ChannelPipeline持有一个包含一系列事件拦截器ChannelHandler的链表,由ChannelHandler负责对事件进行拦截和处理。用户可以方便的增加和删除ChannelHandler来达到定制业务逻辑的目的,而不需要对现有的ChannelHandler进行修改,实现对开放封闭原则的支持。

 

 

ChannelPipeline的主要特性

ChannelPipeline支持在运行时动态地添加或删除ChannelHandler,比如可以根据系统时间判断是否处于业务高峰期,然后动态地添加或删除拥塞控制的逻辑。还有一点就是ChannelPipeline对Handler的添加和删除操作都是线程安全的,这意味着多个业务线程可以并发的修改ChannelPipeline而不会出现并发问题。但是ChannelHandler不是线程安全的,用户需要自己保证ChannelHandler的线程安全。

 

ByteBuf

ByteBuf类似于JDK里的ByteBuffer,但JDK里的ByteBuffer有几个局限性,如固定长度、需要手动flip等,所以Netty提供了类似ByteBuffer的实现ByteBuf。

与ByteBuffer 类似,ByteBuf提供以下几类基本功能:

  • Java基础类型、数组、ByteBuf的读写操作
  • 缓冲区自身的copy和slice
  • 设置网络字节序 order(ByteOrder)
  • 构造缓冲区实例
  • 操作读写索引

ByteBuf与ByteBuffer的不同之处主要有以下两点:

  • ByteBuf用两个位置指针来协助读写操作。读操作用readerIndex,写操作用writerIndex。读操作不会修改writerIndex,写操作不会修改readerIndex,简化了操作,避免了忘记flip而导致的异常。
  • ByteBuf可以动态扩容。在对JDK里的ByteBuffer进行put操作时,通常需要检查剩余空间,如果剩余空间不够时还需要创建新的ByteBuffer,再进行复制操作,然后释放旧的ByteBuffer。而ByteBuf在写操作里包含了检查剩余空间和动态扩容的逻辑,当ByteBuf剩余空间不足时会自动扩充,而使用者无需关心实现细节。

由于NIO操作中的参数都是ByteBuffer,所以ByteBuf内部包含一个ByteBuffer的引用,用来表示对应的ByteBuffer。

Bootstrap

Bootstrap是用于启动Netty的辅助类,提供一系列方法设置启动参数。因为Bootstrap需要设置的各项信息很多,包括线程池、TCP选项、ChannelHandler等,所以这里采用builder模式实现。

Decoder/Encoder

预置了多种编解码功能,支持多种主流协议

解码:
ByteToMessageDecoder MessageToMessageDecoder
编码:
MessageToByteEncoder MessageToMessageEncoder
解码过程示意:

使用示例:基于Netty的HTTP Server/Client

NettyHttpServer
package http;
 
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
 
public class NettyHttpServer {
    public NettyHttpServer(int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
 
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new HttpRequestDecoder());//HTTP请求解码
                        ch.pipeline().addLast(new HttpObjectAggregator(Integer.MAX_VALUE));//chunk聚合器
                        ch.pipeline().addLast(new HttpResponseEncoder());//HTTP响应编码
                        ch.pipeline().addLast(new HttpServerInboundHandler());//业务逻辑
                    }
                });
 
        final ChannelFuture f = bootstrap.bind(port);
        try {
            f.addListener(new GenericFutureListener<Future<? super Void>>() {
                @Override
                public void operationComplete(Future<? super Void> future) throws Exception {
                    System.out.println("server started");
                }
            });
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //优雅关闭
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
 
    public static void main(String[] args) {
        new NettyHttpServer(8888);
    }
}
HttpServerHandler
package http;
 
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
 
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
 
public class HttpServerHandler extends ChannelInboundHandlerAdapter{
 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = ((FullHttpRequest) msg);
            System.out.println(request.content().toString(io.netty.util.CharsetUtil.UTF_8));
            String res = "I am OK";
            FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
                    OK,
                    Unpooled.wrappedBuffer(res.getBytes("UTF-8")));
            response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaders.Names.CONTENT_LENGTH,
                    response.content().readableBytes());
            if (HttpHeaders.isKeepAlive(request)) {
                response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            }
            ctx.write(response);
            ctx.flush();
        } else {
            System.out.println(msg.getClass().getName());
        }
 
        ctx.channel().parent().close().sync();
        System.out.println("closed");
    }
}

NettyHttpClient
package http;
 
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponseDecoder;
 
import java.net.InetSocketAddress;
 
public class NettyHttpClient {
    public NettyHttpClient() {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup);
        bootstrap.channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
 
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new HttpResponseDecoder());
                        ch.pipeline().addLast(new HttpRequestEncoder());
                        ch.pipeline().addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
                        ch.pipeline().addLast(new HttpClientHandler());
                    }
                });
 
        ChannelFuture f = bootstrap.connect(new InetSocketAddress("localhost", 8888));
        try {
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
 
    public static void main(String[] args) {
        new NettyHttpClient();
    }
}

HttpClientHandler
package http;
 
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
 
import java.net.URI;
 
public class HttpClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        URI uri = new URI("http://localhost:8888");
        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
                HttpMethod.GET,
                uri.toASCIIString(),
                Unpooled.wrappedBuffer("Are you OK".getBytes("UTF-8")));
 
        // 构建http请求
        request.headers().set(HttpHeaders.Names.HOST, "localhost");
        request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, request.content().readableBytes());
        ctx.writeAndFlush(request);
    }
 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpResponse) {
            FullHttpResponse response = ((FullHttpResponse) msg);
            System.out.println(response.content().toString(io.netty.util.CharsetUtil.UTF_8));
        } else {
            System.out.println(msg.getClass().getName());
        }
 
        ctx.channel().close().sync();
    }
}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值