Netty入门篇

一、netty概述

1、NIO存在的问题:

  • NIO的API比较复杂,需要熟练掌握3个核心组件,channel、buffer和selector;
  • 需要熟悉多线程、网络编程等技术;
  • 开发工作量大,难度也比较大,需要解决断连、重连、网络闪断、半包读写、失败缓存、网络拥堵等各种情况;
  • NIO存在bug,一个叫Epoll的bug,会导致选择器空轮询,形成死循环,最后CPU飙到100%

正是因为NIO存在这些问题,netty就应运而生了。


欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将文章搬到公众号,以后和公众号文章将同步更新,且上的付费文章在公众号上将免费。


2、Netty简介:
netty是一个异步的,基于事件驱动的网络应用框架。可以快速地开发高性能的服务器端和客户端,像dubbo和elasticsearch底层都用了netty。它具有以下优点:

  • 设计优雅,灵活可扩展;
  • 使用方便,用户指南清晰明确;
  • 安全,完整的SSL/TLS和StartTLS支持;
  • 社区活跃,不断地更新完善

官方下载地址:https://bintray.com/netty/downloads
我本次下载的版本是:4.1.51.Final

二、netty的架构设计

1、线程模型:
目前存在的线程模式:

  • 传统阻塞IO的服务模型
  • Reactor模式

根据Reactor的数量和1处理资源的线程数不同,又分3种:

  • 单Reactor单线程;
  • 单Reactor多线程;
  • 主从Reactor多线程

Netty的线程模型是基于主从Reactor多线程做了改进。

2、传统阻塞IO的线程模型:
采用阻塞IO获取输入的数据,每个连接都需要独立的线程来处理逻辑。存在的问题就是,当并发数很大时,就需要创建很多的线程,占用大量的资源。连接创建后,如果当前线程没有数据可读,该线程将会阻塞在读数据的方法上,造成线程资源浪费。

3、Reactor模式(分发者模式/反应器模式/通知者模式):
针对传统阻塞IO的模型,做了以下两点改进:

  • 基于IO复用模型:多个客户端共用一个阻塞对象,而不是每个客户端都对应一个阻塞对象
  • 基于线程池复用线程资源:使用了线程池,而不是每来一个客户端就创建一个线程

Reactor模式的核心组成:

  • Reactor:Reactor就是多个客户端共用的那一个阻塞对象,它单独起一个线程运行,负责监听和分发事件,将请求分发给适当的处理程序来进行处理
  • Handler:处理程序要完成的实际事件,也就是真正执行业务逻辑的程序,它是非阻塞的

4、单Reactor单线程:

模型图

这个图其实就跟之前的NIO群聊系统对应。多个客户端请求连接,然后Reactor通过selector轮询判断哪些通道是有事件发生的,如果是连接事件,就到了Acceptor中建立连接;如果是其他读写事件,就有dispatch分发到对应的handler中进行处理。这种模式的缺点就是Reactor和Handler是在一个线程中的,如果Handler阻塞了,那么程序就阻塞了。

5、单Reactor多线程:

单reactor多线程

处理流程如下:

  • Reactor对象通过Selector监听客户端请求事件,通过dispatch进行分发;
  • 如果是连接事件,则由Acceptor通过accept方法处理连接请求,然后创建一个Handler对象响应事件;
  • 如果不是连接请求,则由Reactor对象调用对应handler对象进行处理;handler只响应事件,不做具体的业务处理,它通过read方法读取数据后,会分发给线程池的某个线程进行业务处理,并将处理结果返回给handler;
  • handler收到响应后,通过send方法将结果返回给client。

相比单Reactor单线程,这里将业务处理的事情交给了不同的线程去做,发挥了多核CPU的性能。但是Reactor只有一个,所有事件的监听和响应,都由一个Reactor去完成,并发性还是不好。

6、主从Reactor多线程:

主从reactor多线程

这个模型相比单reactor多线程的区别就是:专门搞了一个MainReactor来处理连接事件,如果不是连接事件,就分发给SubReactor进行处理。图中这个SubReactor只有一个,其实是可以有多个的,所以性能就上去了。

  • 优点:父线程与子线程的交互简单、职责明确,父线程负责接收连接,子线程负责完成后续的业务处理;
  • 缺点:编程复杂度高

7、netty的模型:
netty模型是基于主从Reactor多线程模型设计的,其工作流程如下:

  • Netty有两组线程池,一个Boss Group,它专门负责客户端连接,另一个Work Group,专门负责网络读写;
  • Boss Group和Work Group的类型都是NIOEventLoopGroup;
  • NIOEventLoopGroup相当于一个事件循环组,这个组包含了多个事件循环,每一个循环都是NIOEventLoop;
  • NIOEventLoop表示一个不断循环执行处理任务的线程,每个NIOEventLoop都有一个Selector,用于监听绑定在其上的socket;
  • Boss Group下的每个NIOEventLoop的执行步骤有3步:(1). 轮询accept连接事件;(2). 处理accept事件,与client建立连接,生成一个NioSocketChannel,并将其注册到某个work group下的NioEventLoop的selector上;(3). 处理任务队列的任务,即runAllTasks;
  • 每个Work Group下的NioEventLoop循环执行以下步骤:(1). 轮询read、write事件;(2). 处理read、write事件,在对应的NioSocketChannel处理;(3). 处理任务队列的任务,即runAllTasks;
  • 每个Work Group下的NioEventLoop在处理业务时,会使用pipeline(管道),pipeline中包含了channel,即通过pipeline可以获取到对应的channel,pipeline中维护了很多的处理器。


netty模型图如下,对应了上面那段流程:


netty模型图

三、netty入门实例

使用netty创建一个服务端与客户端,监听6666端口。

1、服务端:

  • NettyServer:
public class NettyServer {

    public static void main(String[] args) throws Exception {
        // 1. 创建boss group (boss group和work group含有的子线程数默认是cpu数 * 2)
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 2. 创建work group
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            // 3. 创建服务端启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 4. 配置启动参数
            bootstrap.group(bossGroup, workGroup) // 设置两个线程组
                     .channel(NioServerSocketChannel.class) // 使用NioSocketChannel 作为服务器的通道
                     .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列等待连接个数
                     .childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
                     .childHandler(new ChannelInitializer() { // 创建通道初始化对象
                        // 给pipeline设置处理器
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            // 传入自定义的handler
                            sc.pipeline().addLast(new NettyServerHandler());
                        } 
                    });
            // 5. 启动服务器并绑定端口
            ChannelFuture cf = bootstrap.bind(6666).sync();
            // 6. 对关闭通道进行监听
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
        
    }
}
  • NettyServerHandler:
public class NettyServerHandler extends ChannelInboundHandlerAdapter{
    
    // 读取数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("接收到客户端消息:" + buf.toString(CharsetUtil.UTF_8));
    }
    
    // 数据读取完毕后
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我是服务端", CharsetUtil.UTF_8));
    }
    
    // 处理异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

2、客户端:

  • NettyClient:
public class NettyClient {

    public static void main(String[] args) throws Exception {
        // 1. 创建事件循环组
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            // 2. 创建启动对象
            Bootstrap bootstrap = new Bootstrap();
            // 3. 设置相关参数
            bootstrap.group(eventLoopGroup) // 设置线程组
                     .channel(NioSocketChannel.class) // 设置通道
                     .handler(new ChannelInitializer() {
                        // 给pipeline设置处理器
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            sc.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            // 4. 连接服务端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            // 5. 监听通道关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
        
    }
}
  • NettyClientHandler:
public class NettyClientHandler extends ChannelInboundHandlerAdapter{

    // 通道就绪就被触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client:" + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我是客户端", CharsetUtil.UTF_8));
    }
    
    // 读取数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("接收到服务端消息:" + buf.toString(CharsetUtil.UTF_8));
    }
    
    // 处理异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

先启动服务端,然后启动客户端,就能看到服务端和客户端在控制台打印出来的消息了。

3、TaskQueue自定义任务:
上面服务端的NettyServerHandler的channelRead方法中,假如有一个非常耗时的业务,那么就会阻塞在那里,直到业务执行完。比如将NettyServerHandler的channelRead方法改成下面这样:

// 读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 线程休眠10秒,模拟耗时业务
    TimeUnit.SECONDS.sleep(10);
    ByteBuf buf = (ByteBuf) msg;
    System.out.println("接收到客户端消息:" + buf.toString(CharsetUtil.UTF_8));
}

启动后会发现,10秒钟后,服务端才会收到客户端发送的消息,客户端也要10秒后,才会接收到服务端的消息,即服务端的channelReadComplete方法是要在channelRead方法执行完后才会执行的。

一直阻塞在那里也不是办法,希望可以异步执行,那我们就可以把该任务提交到该channel对应的NioEventLoop的TaskQueue中。有以下解决方案:

  • 用户程序自定义的普通任务:将channelRead方法改成下面这样:
// 读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.channel().eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) { e.printStackTrace();}
            ByteBuf buf = (ByteBuf) msg;
            System.out.println("接收到客户端消息:" + buf.toString(CharsetUtil.UTF_8));
        }
    });
}

这里仍旧休眠10秒。启动服务端,再启动客户端,发现服务端要10s后才会打印出客户端发送的消息,但是客户端立刻就可以收到服务端在channelReadComplete方法里发送的消息,说明这次是异步的。

  • 用户自定义定时任务:与上面的区别不大,代码如下:
// 读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.channel().eventLoop().schedule(new Runnable() {
        @Override
        public void run() {
            try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) { e.printStackTrace();}
            ByteBuf buf = (ByteBuf) msg;
            System.out.println("接收到客户端消息:" + buf.toString(CharsetUtil.UTF_8));
        }
    }, 5, TimeUnit.SECONDS);
}

启动服务端,然后启动客户端,发现客户端还是会立即收到服务端发出的消息,而服务端,首先要等待5秒才去执行run方法,run方法里面线程又休眠了10秒,所以总共要15秒后才会打印出客户端发送的消息。

  • 非当前Reactor线程调用channel的各种方法:这个的意思就是,如果我别的业务代码,比如消息推送系统,也想给客户端发送消息,该咋整?其实很简单,在NettyServerHandler的channelRead方法里,我们是通过ChannelHandlerContext 拿到Channel然后进行各种操作的,所以拿到了Channel就可以进行操作。那么可以在NettyServer中将Channel保存到集合中去,然后遍历集合,拿到Channel就可以进行操作了。
// 给pipeline设置处理器
@Override
protected void initChannel(SocketChannel sc) throws Exception {
    // 传入自定义的handler
    sc.pipeline().addLast(new NettyServerHandler());
    // 在这里,可以将SocketChannel sc保存到集合中,别的线程拿到集合就可以调用channel的方法了
} 

四、Netty的异步模型

1、基本介绍:

  • Netty中的I/O操作都是异步的,包括bind、write和connect。这些操作会返回一个ChannelFuture对象,而不会立即返回操作结果。
  • 调用者不能立即得到返回结果,而是通过Futrue-Listener机制,用户可以主动获取或者通过通知机制获得IO操作的结果。
  • Netty的异步是建立在future和callback之上的。callback是回调,future表示异步执行的结果,它的核心思想是:假设有个方法fun(),计算过程可能非常耗时,等待fun()返回要很久,那么可以在调用fun()的时候,立马返回一个future,后续通过future去监控fun()方法的处理过程,这就是future-listener机制。
  • 用户可以通过注册监听函数,来获取操作真正的结果,ChannelFuture常用的函数如下:
// 判断当前操作是否完成
isDone
// 判断当前操作是否成功
isSuccess
// 获取操作失败的原因
getCause
// 判断当前操作是否被取消
isCancelled
// 注册监听器
addListener

2、使用监听器:
在NettyServer中的“启动并绑定端口”下面加上如下代码:

// 5. 启动服务器并绑定端口
ChannelFuture cf = bootstrap.bind(6666).sync();
// 注册监听器
cf.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture cf) throws Exception {
        if (cf.isSuccess()) {
            System.out.println("绑定端口成功");
        } else {
            System.out.println("绑定端口失败");
        }
    }
});

这样就注册了监听器,会监听绑定端口的结果,如果成功了,就会打印出绑定成功这段话。

五、使用Netty开发Http服务

开发一个Netty服务端,监听80端口,浏览器访问localhost,可以返回信息给浏览器。代码如下:

  • NettyHttpServer:
public class NettyHttpServer {

    public static void main(String[] args) throws Exception {
        // 1. 创建boss group (boss group和work group含有的子线程数默认是cpu数 * 2)
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 2. 创建work group
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            // 3. 创建服务端启动对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 4. 配置启动参数
            bootstrap.group(bossGroup, workGroup) // 设置两个线程组
                    .channel(NioServerSocketChannel.class) // 使用NioSocketChannel 作为服务器的通道
                    .childHandler(new NettyHttpServerInitializer());
            // 5. 启动服务器并绑定端口
            ChannelFuture cf = bootstrap.bind(80).sync();
            // 6. 对关闭通道进行监听
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }

}
  • NettyHttpServerInitializer:
public class NettyHttpServerInitializer extends ChannelInitializer {

    // 向管道加入处理器
    @Override
    protected void initChannel(SocketChannel sc) throws Exception {
        // 1. 得到管道
        ChannelPipeline pipeline = sc.pipeline();
        // 2. 加入一个编码器解码器
        pipeline.addLast("codec", new HttpServerCodec());
        // 3. 增加一个自定义的handler处理器
        pipeline.addLast("handler", new NettyHttpServerHandler());
    }
}
  • NettyHttpServerHandler:
public class NettyHttpServerHandler extends SimpleChannelInboundHandler{

    // 读取数据
    @Override
    protected void channelRead0(ChannelHandlerContext chc, HttpObject msg) throws Exception {
        // 1. 判断msg是不是httpRequest请求
        if (msg instanceof HttpRequest) {
            System.out.println("msg类型:" + msg.getClass());
            System.out.println("客户端地址:" + chc.channel().remoteAddress());
            // 对特定资源不做响应
            HttpRequest httpRequest = (HttpRequest) msg;
            URI uri = new URI(httpRequest.uri());
            if ("/favicon.ico".equals(uri.getPath())) {
                System.out.println("请求了图标,不做响应");
                return;
            }
            // 2. 创建回复给浏览器的信息
            ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器,are you ok?", CharsetUtil.UTF_8);
            // 3. 构造http响应
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
            // 4. 将response返回
            chc.writeAndFlush(response);
        }
    }
}

启动server后,在浏览器就访问localhost就可以返回相关内容了。

你可能感兴趣的:(Netty入门篇)