Netty线程模型

136 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1.1 简介

netty.io/ 官网

netty是对java nio api的封装,简化了nio程序的开发,jdk要求最低1.6

流行的网络编程通信框架,Dubbo Elasticsearch 等框架底层的网络通信框架都是 Netty

架构模型

版本

netty 共有 3.x 4.x 5.x三个大版本

3.x较老,5.x有重大bug,被官网废弃  现在主要使用4.x

1.2 线程模型

有以下几种线程模型

4.2.1 传统I/O阻塞模型

每一个链接都需要一个对应的线程进行处理,并且当链接建立后,如果当前链接没有数据传输时,此线程会被阻塞在read()方法

1.2.2 Reactor模式

原理图示如上

主要是针对了传统I/O模型一个连接会阻塞一个线程的问题进行了改进,当连接建立后都通过ServiceHandler调用线程池中的线程进行处理,这样就只用阻塞一个ServiceHandler线程,达到多路复用的目的

Reactor模式有三种实现方式

1.2.2.1单Reactor单线程

使用一个线程通过多路复用完成所有操作,包括读写连接

redis使用的就是这种模型 单线程

1.2.2.2单Reactor多线程

相对于单Reactor单线程,主线程不在进行业务处理,当有请求过来之后,具体的业务处理交与线程池中的线程处理,线程处理完成后再通过handler返回给Client

1.2.2.3 主从Reactor多线程

相比于单Reacotr,主从Reactor将Reactor分为MainReactor和SubReactor

MainReactor中负责分发和连接

SubReactor中负责读写

一个MainReactor可以对应多个SubReactor

1.2.3 Netty模型

简述Netty模型

  1. 角色
    1. BossGroup BossGroup的类型是NioEventLoopGroup,其中包含了很多NioEventLoop
    2. NioEventLoop nio事件循环,每个NioEventLoop中都有一个Selctor和一个任务队列
    1. WorkerGroup 类型是NioEventLoopGroup,与BossGroup类似,只不过功能不同,BossGroup只负责与客户端建立连接, WorkerGroup需要读写,处理业务
    2. PipeLine 管道,对Channel进行的封装,具体的业务处理是通过Pipline对Channel进行处理
  1. 具体流程
    1. 当客户端发送请求时,首先进入BossGroup,有NioEventLoop对请求进行事件轮询,如果是连接事件就进行处理
      1. 处理的步骤分为三步
      2. 轮询
      1. 注册 这里的注册指的是将生成的SocketChannel注册到workerGroup中的某个NioEventLoop中的Selector上
      2. 执行任务列表
    1. 当请求的事件是读写时,就有workerGroup对请求进行具体的业务处理
      1. 处理的步骤BossGroup类似
  1. 总结 由此可以看出,Netty的模型与主从Reactor模型类似,都是由一个主Reactor负责连接事件,由一个从Reactor负责读写事件

1.2.4 案例demo

1.2.4.1 服务端

1.2.4.1.1 NettyServer
package netty;

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;

/**
 * @author: zhangyao
 * @create:2020-09-03 08:55
 **/
public class NettyServer {

    public static void main(String[] args) {

        //创建BossGroup 和 WorkerGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        ChannelFuture channelFuture = null;
        try {
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel socketChannel) {
                        socketChannel.pipeline().addLast(new NettyServerHandler());
                    }
                });

        System.out.println("服务器就绪.....");


        //绑定端口
            channelFuture = serverBootstrap.bind(6668).sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                channelFuture.channel().closeFuture().sync();

                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
1.2.4.1.2 NettyServerHandler
package netty;

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

/**
 * @author: zhangyao
 * @create:2020-09-03 09:12
 **/
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));
        System.out.println("客户端地址:"+ ctx.channel().remoteAddress());

    }

    //数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello 客户端",CharsetUtil.UTF_8));
    }

    //处理异常 关闭ctx
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

1.2.4.2 客户端

1.2.4.2.1 NettyClient
package netty;

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

/**
 * @author: zhangyao
 * @create:2020-09-03 09:52
 **/
public class NettyClient {
    public static void main(String[] args) {


        EventLoopGroup executors = new NioEventLoopGroup();


        Bootstrap bootstrap = new Bootstrap();

        try {
            bootstrap.group(executors)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            System.out.println("客户端就绪........");

            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //关闭通道
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                executors.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
1.2.4.2.2 NettyClientHandler
package netty;

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

/**
 * @author: zhangyao
 * @create:2020-09-03 10:00
 **/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {


    //就绪时触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("ctx: "+ctx);

        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,服务端", CharsetUtil.UTF_8));

    }

    //读取信息
    //这里读取的是服务器返回的信息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf byteBuf = (ByteBuf) msg;

        System.out.println("服务端发送消息: "+ byteBuf.toString(CharsetUtil.UTF_8));
        System.out.println("服务端地址: "+ ctx.channel().remoteAddress());

    }

    //异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

一个简单的TCP服务通信,客户端发送消息,服务端接收消息并返回消息给客户端

1.2.4.3 案例demo源码分析

1.2.4.3.1 NioEventGroup
public NioEventLoopGroup() {
    this(0);
}

public NioEventLoopGroup(int nThreads) {
    this(nThreads, (Executor)null);
}

可以看到,如果使用无参的NioEventGroup,默认传递的是0,也可以指定线程数

一层一层找下去:

private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); //NettyRuntime.availableProcessors()获取当前计算机的核数(逻辑处理器)

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

发现最后找到NioEventGroup父类的方法

如果指定了NioEventGroup的线程数,且不为0的时候,就使用指定的线程数

否则,**就使用当前计算机的核数2作为线程数

debug 看结果

电脑是12核,默认是24个线程

指定一个线程

就只有一个线程数

1.2.5 异步模型

上文中的案例Demo中的 ChannelFuture

ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();

异步模型Future是相对与同步来说的

异步指的是当一个异步调用发出后,不会立刻得到结果,而是通过回调,状态来通知调用者调用的结果

Netty 中的 connect 和 bind() sync方法就是返回了一个异步的结果,之后再通过监听获取到结果

也就是 Future-Listener机制

当Future对象刚创建的时候,处于未完成的状态,可以通过返回的ChannelFuture查看操作执行的状态,也可以注册监听函数来执行完成后的操作

isSucess()是否成功

isDone()是否完成

isCancelable() 是否取消

cause() 失败原因

addListener 增加监听器

//绑定端口
            channelFuture = serverBootstrap.bind(6668).sync();
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if(channelFuture.isSuccess()){
                        System.out.println("监听端口成功");
                    }else {
                        System.out.println("监听端口失败");
                    }
                }
            });

4.2.6 Netty Http服务

做一个简单的demo  浏览器(客户端)访问服务器端7001端口,返回一个字符串信息

浏览器访问是http请求,服务器也需要相应一个httpResponse

1.2.6.1 服务端

NettyHttpServer 启动类
package netty.http;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @author: zhangyao
 * @create:2020-09-04 11:16
 **/
public class NettyHttpServer {

    public static void main(String[] args) throws InterruptedException {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap serverBootstrap = new ServerBootstrap();

        serverBootstrap.group(bossGroup,workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new NettyHttpInitialize());

        ChannelFuture channelFuture = serverBootstrap.bind(7001).sync();

        channelFuture.channel().closeFuture().sync();

        bossGroup.shutdownGracefully();

        workerGroup.shutdownGracefully();

    }
}
NettyHttpInitialize 处理器类

对之前的ChannelInitialize(SocketChannel)进行封装

package netty.http;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;

/**
 * @author: zhangyao
 * @create:2020-09-04 11:21
 **/
public class NettyHttpInitialize extends ChannelInitializer<SocketChannel> {


    @Override
    protected void initChannel(SocketChannel sc) throws Exception {
        //得到管道
        ChannelPipeline pipeline = sc.pipeline();

        //管道中加入处理器 主要是处理Http请求的,解析请求头之类的
        pipeline.addLast("myDecoder",new HttpServerCodec());

        //加入处理器
        pipeline.addLast("myServerHandler",new NettyServerHandler());
    }
}
NettyServerHandler 具体的处理(返回http响应)
package netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.springframework.http.HttpStatus;

/**
 * @author: zhangyao
 * @create:2020-09-04 14:01
 **/
public class NettyServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    //读信息
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
        System.out.println(httpObject);
        System.out.println("客户端地址+"+ channelHandlerContext.channel().remoteAddress());


        ByteBuf byteBuf = Unpooled.copiedBuffer("hello , im 服务端", CharsetUtil.UTF_8);

        //返回客户端信息
        FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK,byteBuf);

        fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");

        fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());


        channelHandlerContext.writeAndFlush(fullHttpResponse);



    }

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

出现的问题记录:

  1. 第一次绑定6668端口,浏览器访问失败,换成7001就可以了
    原因: 谷歌浏览器禁用了6665-6669以及其他一些不安全的端口
  1. 访问后第一次请求 出现异常,可以正常返回数据

    原因: NettyServerHandler中没有对异常处理方法进行重写
    加上这部分就可以了,报错信息也报的很明显
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    ctx.close();
}

1.2.6.2 Http服务的过滤

可以对一些不希望处理的请求进行过滤,其实就是在对http请求的处理过程中判断一下请求的uri

在上文中 NettyServerHandler类中 加入以下代码,即可拦截/favicon.ico请求

HttpRequest re  = (HttpRequest) httpObject;

String uri = re.uri();
if(uri.equals("/favicon.ico")){
    System.out.println("不想处理,返回");
    return;
}