Netty(内含Netty的简单实现)(下)

58 阅读25分钟

Netty入站与出站机制

Netty中的ChannelHandler充当了处理入站和出站数据逻辑的容器,只要实现了其下的对应接口,就可以自定义一个处理器用于处理出站或入站的数据

image-20230201033533450

对于客户端而言,如果数据的运动方向是从客户端到服务端的,那么这些事件我们就称之为出站,反之则是出站。当然这跟我们的目标系的选择有关,如果我们选择的是服务器,那么又反过来了

image-20230201033703022

当Netty发送或接受一个消息的时候,会进行对应的编码解码处理。Netty提供了一系列对应的接口,他们都重写了channelRead方法,对于每个入站的信息会调用该方法并调用decode()方法进行解码并将解码的字节转发给下一个处理器

image-20230201034146079

下面是将字节转为对象的解码器的继承图,TCP中囿于可能会分段发送信息,因此会出现粘包拆包的问题。为了解决该问题,该类会对入站数据进行缓冲直到其准备好被处理

image-20230201034451358

举个例子就是我们可以重写对应的解码方法,这里我们假设我们知道客户端传入的对象是以int类型为主,一个int占据四个字节,那么我们每次判断传入的字节数是否大于四,大于我们就将其添加到集合中,由于字节传入时就会执行该方法,因此我们这里不需要写入while,调用ByteBuf对应的方法可以令其读指针后移,这样就能成功将所有的字节都读入到我们指定的集合中

image-20230201034618484

最后我们将对应的集合传入给我们的下一个处理器,这个处理器就会执行对应业务处理

image-20230201033513653

案例演示

接着我们来做一个案例来加深我们的理解,首先我们来看看案例要求

image-20230201140318794

然后我们来看看业务内部的结构图,我们看到客户端的管道中存在ClientHandler处理数据,发送数据时会先发送给编码器,然后通过Socket发送服务器的解码器,解码器处理完后发送给服务器处理器,服务器发送数据时的流程也大差不差

image-20230201134003763

那么接下来我们来正式写代码,首先我们先写服务器,我们这里要往其中加如自定义的处理器,childHandler中的类我们不采用匿名内部类的形式

public class MyServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boosGroup,workerGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new MyServerInitializer());
​
            ChannelFuture channelFuture = bootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

往服务端中的管道中添加对应的处理器,加入对应的额编码解码器之后再添加自己的业务逻辑处理器

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //入站的handler进行解码 MyByteToLongDecoder
        pipeline.addLast(new MyByteToLongDecoder());
        //出站的handler,用于进行编码
        pipeline.addLast(new MyLongToByteEncoder());
        //自定义的handler处理业务逻辑
        pipeline.addLast(new MyServerHandler());
    }
}

解码器中我们继承ByteToMessageDecoder重写其decode方法,我们这里的解码规则很简单,就是判断传输的字节大小是否大于8,是的话我们就调用ByteBuf的对应的方法将字节写到集合中然后传给下一个handler进行处理

public class MyByteToLongDecoder extends ByteToMessageDecoder {
​
    /**
     * decode会根据接收的数据被调用多次,直到确定没有新的元素被添加到list或ByteBuf中没有更多的可读字节
     * 如果list不为空,就会将list的内容传递给下一个ChannelInboundHandler处理,该处理器的方法也会被调用多次
     * @param ctx 上下文对象
     * @param in 入站的ByteBuf
     * @param out List集合,将解码的数据传给下一个handler
     * @throws Exception
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyByteToLongDecoder被调用");
        //因为Long是8个字节,需要判断有8个字节才能读取一个long
        if(in.readableBytes()>=8){
            out.add(in.readLong());
        }
    }
}

编码器的代码需要继承MessageToByteEncode并指定需要处理的对象,逻辑是打印对应的数据之后再将数据写出

public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {
​
    /**
     * 编码方法
     * @param ctx
     * @param msg
     * @param out
     * @throws Exception
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
        System.out.println("MyLongToByteEncoder中的encode方法被调用");
        System.out.println("msg= " + msg);
        out.writeLong(msg);
    }
}

这里需要注意的是,解码器会将所有传输的数据都按照指定的规则进行解码,但是编码器则只能编码指定对象,进行入该类MessageToByteEncoder中,我们可以看到其对应的方法先判断传出的对象是否是指定的对象,否则就直接调用写方法写出去,下面是其源码

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        if (acceptOutboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                encode(ctx, cast, buf);
            } finally {
                ReferenceCountUtil.release(cast);
            }
​
            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
}

最后是我们的自定义的业务逻辑处理器,我们这里做的事情无非就是打印接受的数据然后向客户端发送一个long类型的数据

public class MyServerHandler extends SimpleChannelInboundHandler<Long> {
​
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println("从客户端"+ctx.channel().remoteAddress()+"读取到long "+msg);
        //给客户端发送一个long
        ctx.writeAndFlush(98765L);
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

接着我们来写客户端,无非是创建一个对应的NioEventLoopGroup对象然后设置对应的参数而已,同样也是自定义一个初始化类

public class MyClient {
    public static void main(String[] args) throws Exception{
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                     .channel(NioServerSocketChannel.class)
                     .handler(new MyClientInitializer()); //自定义一个初始化类
​
            ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

自定义类中加入对应的编码解码器和自定义业务处理器

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //加入一个出站的handler对数据进行编码
        pipeline.addLast(new MyLongToByteEncoder());
        //加入一个入站的解码器(入站handler)
        pipeline.addLast(new MyByteToLongDecoder());
        //加入一个自定义handler用于处理业务
        pipeline.addLast(new MyClientHandler());
    }
}

自定义处理器中,我们打印对应的数据并重写channelActive方法,让通道准备就绪时就向服务器发送一个数据

public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
​
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println("服务器的ip="+ctx.channel().remoteAddress());
        System.out.println("收到服务器消息="+msg);
    }
​
    //重写channelActive方法发送数据
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("MyClientHandler 发送数据");
        ctx.writeAndFlush(123456L); //发送一个long
    }
}

最后打开程序我们可以得到下面的结果,我们可以看到调用的顺序时先调用其自定义的处理器发送数据,然后是编码器然后发送数据(msg那一列应该是我跟着课程写代码时搞错了导致在代码上找不到对应的打印代码),然后是解码器调用,最后是业务处理器处理服务器发送过来的消息

image-20230201135941895

服务端的代码首先嗲用解码器然后是自定义处理器类,接着是编码器然后发送数据

image-20230201140001072

这里我们还需要提一点,如果我们发送的属于不是long类型的,那么我们的编码器并不会对其正确编码,而是会直接写出,但我们的解码器会按照规则正确解码,这就导致如果我们发送的数据并不是我们预期的数据,那么就会导致我们最终得到的数据与预期的不一样

同时解码器一旦接收到足够大小的数据就会立刻交由自定义处理器进行处理,也就是说一个大文件发过来可能会有多次访问接收读取的过程

image-20230201132415106

其他编码解码器

实际上Netty还提供了ReplayingDecoder解码器,该编码器可以对起那么的案例进行简化

image-20230201150513946

最直接的简化就是不用判断数据是否足够,内部会自动进行解析,我们直接添加即可,下面是优化后的解码器代码

image-20230201145848189

不过其也有其局限性,比如其并不是所有的ByteBuf操作都支持,并且某些情况下效率可能会变低

Netty还提供了其他很多的编码和解码器,这些我们了解即可

image-20230201150934816

Netty整合Log4j

首先我们要引入对应的依赖

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>

然后我们在resource中创建log4j.properties的文件,写入其内容如下

log4j.rootLooger=DEBUG,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p] %C{1} - %m%n

接着我们再启动项目就可以看到我们的日志了

TCP粘包拆包

TCP是面向流和连接的协议,而面向流的通信是没有消息保护边界的,因此使用TCP发送数据时会出现粘包拆包问题

image-20230201154115871

下面是对应的图示和解释

image-20230201154227269

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

  1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
  2. 服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
  3. 服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
  4. 服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。

粘包拆包案例演示

接下来我们来做一个案例来演示TCP的粘包拆包

image-20230201162212107

首先编写服务端,跟之前的一样

public class MyServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boosGroup,workerGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new MyServerInitializer());
​
            ChannelFuture channelFuture = bootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

初始化类中我们添加入自定义的处理器

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyServerHandler());
    }
}

自定义的处理器中我们处理ByteBuf对象,接收到我们就将其转换为byte数组,将其转换为字符串并打印,我们同时也记录服务器收到的消息量,后面我们设置服务端每次接受数据就会回送给客户端一个随机ID

public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
    private int count;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);
        //将buffer转为一个字符串
        String message = new String(buffer, Charset.forName("utf-8"));
        System.out.println("服务器接受到数据 "+message);
        System.out.println("服务器接受到消息量="+(++this.count));
​
        //服务器会送数据给客户端,回送一个随机id
        ByteBuf response = Unpooled.copiedBuffer(UUID.randomUUID().toString()+" ", Charset.forName("utf-8"));
        ctx.writeAndFlush(response);
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

然后我们编写客户端,同样传入一个初始化类

public class MyClient {
    public static void main(String[] args) throws Exception{
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                     .channel(NioServerSocketChannel.class)
                     .handler(new MyClientInitializer()); //自定义一个初始化类
​
            ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

添加自定义处理器

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new MyClientHandler());
    }
}

客户端中我们对接受的消息同样进行处理并记录次数,当通道准备完毕时我们往服务端中发送十条数据

public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
​
    private int count;
​
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);
        String message = new String(buffer, Charset.forName("utf-8"));
        System.out.println("客户端接受到消息=" + message);
        System.out.println("客户端接受消息数量"+ (++this.count));
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
​
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送十条数据
        for (int i = 0; i < 10; i++) {
            ByteBuf byteBuf = Unpooled.copiedBuffer("hello,server" + i, StandardCharsets.UTF_8);
            ctx.writeAndFlush(byteBuf);
        }
    }
}

最后我们只要启动服务端和多个客户端就可以得到服务器中接受数据的情况,我们会看到有时候接受到一部分,有时候接收到全部,这就证明了此时发生了TCP粘包拆包问题

问题解决方案

要解决TCP粘包拆包的问题,关键是要解决服务器端每次读取数据长度的问题。下面我们就来实现解决该问题的案例,下图是案例要求

image-20230201170238949

首先我们要写入对应的传输数据的实体类

/**
 * 协议包
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MessageProtocol {
    private int len;
    private byte[] content;
}

服务端的代码没什么变化

public class MyServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boosGroup,workerGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new MyServerInitializer());
​
            ChannelFuture channelFuture = bootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            boosGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

初始化类里我们需要加入实体类的解码器和编码器

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //解码器
        pipeline.addLast(new MyMessageDecoder());
        //编码器
        pipeline.addLast(new MyMessageEncoder());
        pipeline.addLast(new MyServerHandler());
    }
}

实体类的解码里就是读取对应的属性字段数据然后封装到一个对象中并加入到集合

public class MyMessageDecoder extends ReplayingDecoder<Void> {
​
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MyMessageDecoder的decode方法被调用");
        //需要将得到的二进制字节码转化为MessageProtocol数据包(对象)
        int length = in.readInt();
        byte[] content = new byte[length];
        in.readBytes(content);
​
        //封装成MessageProtocol对象,放入out,传给下一个handler进行业务处理
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(length);
        messageProtocol.setContent(content);
        out.add(messageProtocol);
    }
}

在服务器的业务处理器中,我们会打印接受的消息并回复一个消息给客户端,这个消息是有UUID随机组成的字符串构建的指定的实体类

/**
 * 处理业务的handler
 */
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    private int count;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
        //接收到数据并处理
        int len = msg.getLen();
        byte[] content = msg.getContent();
        System.out.println("服务端接受到信息如下:");
        System.out.println("长度="+len);
        System.out.println("内容="+new String(content,Charset.forName("utf-8")));
        System.out.println("服务器接收到消息包数量: "+ (++this.count));
​
        //回复消息
​
        String responseContent = UUID.randomUUID().toString();
        int responseLen = responseContent.getBytes("utf-8").length;
        byte[] bytes = responseContent.getBytes("utf-8");
        //构建一个协议包
        MessageProtocol messageProtocol = new MessageProtocol();
        messageProtocol.setLen(responseLen);
        messageProtocol.setContent(bytes);
​
        ctx.writeAndFlush(messageProtocol);
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端也没啥变化

public class MyClient {
    public static void main(String[] args) throws Exception{
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                     .channel(NioServerSocketChannel.class)
                     .handler(new MyClientInitializer()); //自定义一个初始化类
​
            ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

初始类里要加入实体的编码器和解码器

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
​
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //加入对应实体类的编码器
        pipeline.addLast(new MyMessageEncoder());
        //解码器
        pipeline.addLast(new MyMessageDecoder());
        pipeline.addLast(new MyClientHandler());
    }
}

实体的编码器代码如下,我们这里只是获得对象的属性加入到buffer中

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MyMessageEncoder的encode方法被调用");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}

客户端的handler里我们就创建对应的对象并发送到服务端中,接受到服务端发送的消息之后就将消息进行打印

public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
​
    private int count;
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
​
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送5条数据
        for (int i = 0; i < 5; i++) {
            String msg = "永失吾爱,举目破败";
            byte[] content = msg.getBytes(StandardCharsets.UTF_8);
            int length = msg.getBytes(StandardCharsets.UTF_8).length;
​
            //创建协议包
            MessageProtocol messageProtocol = new MessageProtocol();
            messageProtocol.setLen(length);
            messageProtocol.setContent(content);
            ctx.writeAndFlush(messageProtocol);
        }
    }
​
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
        int len = msg.getLen();
        byte[] content = msg.getContent();
        System.out.println("客户端接收到的消息如下:");
        System.out.println("长度="+len);
        System.out.println("内容="+new String(content,Charset.forName("utf-8")));
        System.out.println("客户端接受消息数量"+(++this.count));
    }
}

最后我们只要案例就能发现我们的案例已经正确运行了,并且服务器也能正确接受到数据并且组成对象,不会出现粘包拆包的问题,说明解决粘包拆包的问题只要指定好了接受数据的大小就可以解决,并且解决粘包拆包问题之后,我们的客户端的服务端都能按顺序准确接受并组装到我们传入的数据

Netty源码剖析

现在我们进入Netty学习的最后一部分,看源码

image-20230202134133554

我们通过源码分析的方式来看一下Netty的启动过程

image-20230202134202479

我们主要分析Netty调用的doBind方法,需要Debug到NioEventLoop中的run代码,其实无限循环的在服务端运行的一块代码

image-20230202134248720

服务端初始化

我们直接从源码中复制出我们的Debug的源码来运行,这样便于我们分析,客户端代码如下

/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package com.haotongxue.learn;
​
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
​
/**
 * Sends one message when a connection is open and echoes back any received
 * data to the server.  Simply put, the echo client initiates the ping-pong
 * traffic between the echo client and server by sending the first message to
 * the server.
 */
public final class EchoClient {
​
    static final boolean SSL = System.getProperty("ssl") != null;
    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
    static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));
​
    public static void main(String[] args) throws Exception {
        // Configure SSL.git
        final SslContext sslCtx;
        if (SSL) {
            sslCtx = SslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        } else {
            sslCtx = null;
        }
​
        // Configure the client.
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoClientHandler());
                 }
             });
​
            // Start the client.
            ChannelFuture f = b.connect(HOST, PORT).sync();
​
            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }
    }
}

客户端自定义处理器代码如下

/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package com.haotongxue.learn;
​
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
​
/**
 * Handler implementation for the echo client.  It initiates the ping-pong
 * traffic between the echo client and server by sending the first message to
 * the server.
 */
public class EchoClientHandler extends ChannelInboundHandlerAdapter {
​
    private final ByteBuf firstMessage;
​
    /**
     * Creates a client-side handler.
     */
    public EchoClientHandler() {
        firstMessage = Unpooled.buffer(EchoClient.SIZE);
        for (int i = 0; i < firstMessage.capacity(); i ++) {
            firstMessage.writeByte((byte) i);
        }
    }
​
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(firstMessage);
    }
​
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }
​
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
       ctx.flush();
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

服务端代码如下

public final class EchoServer {
​
    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
​
    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }
​
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     p.addLast(new LoggingHandler(LogLevel.INFO));
                     //p.addLast(new EchoServerHandler());
                 }
             });
​
            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();
​
            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

服务器端自定义处理器类代码如下

/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package com.haotongxue.learn;
​
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
​
/**
 * Handler implementation for the echo server.
 */
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
​
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }
​
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        super.handlerAdded(ctx);
    }
​
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        super.handlerRemoved(ctx);
    }
​
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

服务端启动类中首先创建了SSL的配置类,也就是bossGroup和workerGroup这两个对象,他们是Netty的核心对象,整个Netty的运作都依赖于他们

image-20230202141024156

我们Debug位置定在workerGroup那里,我们能看到如果不指定创建的NioEventLoop数量,则默认为cpu核数*2

image-20230202141702656

内部创建对应的NioEventLoop数组时会往其children属性中创建对应的数组,然后通过for遍历赋值

image-20230202142121048

如果这个赋值过程中出现了异常,那么就会对对应的时间循环组进行关闭,同时生成对应的时间循环组依靠executor对象,因此也会对该对象进行对应的处理

image-20230202142439609

客户端引导类

Bootstrap对象是一个引导类,与ServerChannel关联,其内部中的group方法将bossGroup和workerGroup设置到其属性中,这里重命名为parentGroup和childGroup,因此这两个时间循环组其是也有后面的两个别名

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    super.group(parentGroup);
    ObjectUtil.checkNotNull(childGroup, "childGroup");
    if (this.childGroup != null) {
        throw new IllegalStateException("childGroup set already");
    } else {
        this.childGroup = childGroup;
        return this;
    }
}

Bootstrap内部通过Class对象反射创建ChannelFactory,接着通过下图的步骤运行

image-20230202142949500

image-20230202143207197

Bootstrap启动类中handler方法是将对应的处理器交由bossGroup,而childrenHandler方法则是交由给workGroup

image-20230202134612226

在创建具体的时间循环组数组给其赋值的方法中,使用的是下面的方法,其各个参数有其代表的意义

image-20230202144718714

如果executor为null则使用Netty默认的线程工厂,同时其会将每一个创建的时间循环组都放入到HashSet中

image-20230202145116623

服务端引导类

ServerBootstrap也就是服务器端的引导类,其实一个空构造,但是有默认的成员变量,其属性中的config对象后面会有很大作用

image-20230202145140225

其调用的方法的配置说明大概如下

image-20230202145308694

服务端绑定端口

服务器的端口绑定是在bind方法中完成的,bind方法中的核心方法是doBind

image-20230202151531569

doBind的核心方法是initAndRegister和doBind

image-20230202151603633

initAndRegister方法通过ServerBootstrap创建一个事件循环组,同时有下面的结论

image-20230202151702530

那么最终init初始化NioServerSocketChannel具体追踪源码可以得到如下结论

image-20230202151812995

image-20230202152018188

init(channel)方法是在反射的channelFactory对象创建channel对象之后对该对象进行初始化的方法,其过程如下

image-20230202152304342

image-20230202152350435

addLast处于是Pipeline的核心,其会检查handler是否符合标准并将对应的handler加入到pipeline中

image-20230202152736519

doBind方法需要不断进入断点

image-20230202152908154

image-20230202152934326

image-20230202152957213

当开到下图中的最终方法时就说明到了最终执行绑定端口的方法了

image-20230202153016262

doBind方法会追踪到NioServerSocketChannel中的doBind,说明其底层使用的是Nio

image-20230202153255067

此时继续debug就会进入到时间循环组中监听发生的时间

image-20230202153324194

接受请求源码

接着我们来分析Netty接受请求的源码

image-20230202155407955

首先我们知道,服务器是等待客户端的链接的,也就是说NioServerSocketChannel将自己注册到了boss单例线程池上,也就是EventLoop,因此我们要从该类中分析,进入其processSelectedKey中

image-20230202160338992

我们可以看到其先判断传入的请求是否是接受请求,实际请求的标志数值为16,正好为接受请求,因此执行unsafe.read()方法

image-20230202160725342

read方法中首先检查对应线程是否是当前线程,经过一些步骤之后执行doReamdMessage方法

image-20230202161009220

image-20230202161441638

doReadMessage方法中通过工具类获取内部封装的accept方法,获得对应的SocketChannel进行封装最后添加到容器中

image-20230202161638238

回到read方法中,read方法会循环执行pipeline.fireChannelRead方法image-20230202161742400

pipeline.fireChannelRead方法会执行管道中handler的ChannelRead方法

image-20230202170708920

该方法会将客户端连接注册到worker线程池并增加监听器

image-20230202171315172

下图是对上面的操作的说明

image-20230202171539532

接着我们继续最终channelRead()方法中的register()方法,其会调用register0()方法

image-20230202172507597

image-20230202172536718

最终其会调用doBeginRead方法,也是AbstractNioChannel,执行完时就说明针对客户端的连接已经完成,接下来执行监听读事件

image-20230202172909305

image-20230202173130002

最后是总体流程的梳理

image-20230202174107754

Pipeline

接着我们来讲管道

image-20230202211604304

ChannelSocket中会联系一个ChannelPipeline,每个管道中有多个ChannelHandlerContext对象,该对象会包装处理器类

当一个请求进来时,会进入Socket对应的pipeline,并经过pipeline所有的handler,为过滤器模式

image-20230202195022248

ChannelPipeline的接口被下面的接口类所继承

image-20230202200330461

下面是ChannelPipeline实现类的部分源码

image-20230202200555939

下面是ChannelPipeline的类的流程图,可以将请求分为入站和出站请求。

image-20230202201334897

不过这里值得一提的是,实际上Netty处理入站和出站时都只使用一个管道中的类来进行处理,上图表示的只是两个不同的过程,而不是说Netty中有两个专门处理出站入站的两个链表

image-20230202201543252

handler在pipeline处理业务请求时会调用其下的fireChannelRead的方法转发给其最近的处理程序进行处理

image-20230202201805916

其下的逻辑时不断进行遍历,直到遍历到下一个对应的处理器进行处理然后继续转发

image-20230202201819099

入站和出站事件的处理逻辑决定了为什么我们要加入编码解码器且它们都必须遵守一定的顺序

最后要注意的是,处理业务的处理器不能阻塞IO线程,如果我们的业务处理的时间真的很长,那就应该要将其设置成异步的

image-20230202202102802

ChannelHandler

ChannelHandler其下有对应的方法可以调用,我们之前已经使用过了

image-20230202203601221

ChannelHandler的作用是要用于处理IO事件,事件分为入站和出站因此其有链各个子接口

image-20230202203829393

入站接口

image-20230202203905192

出站接口

image-20230202203948544

还有可以同时处理入站和出站实现的类,不过我们不用推荐使用

image-20230202204026484

ChannelHandlerContext

接着我们来讲解ChannelHandlerContext对象,首先来看看其UML图,可以看到其实现了入站和出站的接口

image-20230202204918449

其实现的接口就是专门针对入站和出站方法

image-20230202205045023

其不但继承了入站和出站接口的方法,同时还定义了自己的方法

image-20230202205128254

创建过程

再来看看其创建过程,首先创建ChannelSocket时就会同时创建一个pipeline,当调用其下的方法添加handler时,就会对该对象进行包装组成Context对象,该对象在pipeline中组成双向链表

下面我们来看看源码

image-20230202205235619

可以看到我们首先创建pipeline

image-20230202205343094

然后通道同时创建future和promise用于异步回调,然后我们创建两个对应对象形成双向链表

image-20230202205830220

在添加处理器时创建Context

image-20230202205837727

下面是对应的代码

image-20230202205813770

image-20230202205911122

image-20230202205930222

image-20230202210009340

添加handler时做了线程安全的处理,每添加一个handler都会关联一个Context

image-20230202210058736

最后我们来看看总结

image-20230202211630671

调度handler

现在我们来解析Pipeline是如何调度handler处理器的,首先我们要明确,如果是入站事件,则调用的处理方法为fire开头的方法

image-20230202211716454

ChannelPipieline了的真实对象是DefaultChannelPipeline

image-20230202212314188

image-20230202212332076

image-20230202212344605

该类内部都是inbound的方法,传入的也是inbound类型的head里的handler

这些静态方法会调用head里的方法,然后再调用其处理器的真正方法

image-20230202212432492

image-20230202212443943

之所以我们的处理器会处理消息或者是预备时会先执行我们设置的方法,是因为我们的源码里有做个对应的设定

image-20230202213004415

再来看看outbound的fire方法的实现

image-20230202213106284

里面存在的都是出站的实现,当然,也很正常,因为我们这里本来就是处理出站的类

image-20230202213123150

再来看看其执行流程图和说明

image-20230202213318380

image-20230202213928128

最后我们来看看本节梳理

image-20230202215546801

心跳源码

接着来讲Netty的心跳功能的源码

image-20230202215843087

我们的的讲述重点在于IdleStateHandler这个类

image-20230202215859201

该类提供了四个对应的属性用于判断,这里的时间是以纳秒为单位的

image-20230202220008828

image-20230202220249195

当handler被添加到pipeline中时,会调用initialize方法

image-20230202220313192

该方法中通过swtich对对应不同状态的情况进行了处理,比如说读超时,写超时,读写超时等

private void initialize(ChannelHandlerContext ctx) {
    switch(this.state) {
    case 1:
    case 2:
        return;
    default:
        this.state = 1;
        this.initOutputChanged(ctx);
        this.lastReadTime = this.lastWriteTime = this.ticksInNanos();
        if (this.readerIdleTimeNanos > 0L) {
            this.readerIdleTimeout = this.schedule(ctx, new IdleStateHandler.ReaderIdleTimeoutTask(ctx), this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
​
        if (this.writerIdleTimeNanos > 0L) {
            this.writerIdleTimeout = this.schedule(ctx, new IdleStateHandler.WriterIdleTimeoutTask(ctx), this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
​
        if (this.allIdleTimeNanos > 0L) {
            this.allIdleTimeout = this.schedule(ctx, new IdleStateHandler.AllIdleTimeoutTask(ctx), this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
​
    }
}

只要给定的对应参数那么其就会创建对应的定时人物同时将state状态设置为1,防止重复初始化

image-20230202220747626

该类中有三个定时内部类,它们都共有一个AbstractIdleTask父类

image-20230202220816444

这个父类提供了一个处理读写事件的模板方法

private abstract static class AbstractIdleTask implements Runnable {
    private final ChannelHandlerContext ctx;
​
    AbstractIdleTask(ChannelHandlerContext ctx) {
        this.ctx = ctx;
    }
​
    public void run() {
        if (this.ctx.channel().isOpen()) {
            this.run(this.ctx);
        }
    }
​
    protected abstract void run(ChannelHandlerContext var1);
}

首先是读事件处理的内部类

private final class ReaderIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
    ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
        super(ctx);
    }
​
    protected void run(ChannelHandlerContext ctx) {
        long nextDelay = IdleStateHandler.this.readerIdleTimeNanos;
        if (!IdleStateHandler.this.reading) {
            nextDelay -= IdleStateHandler.this.ticksInNanos() - IdleStateHandler.this.lastReadTime;
        }
​
        if (nextDelay <= 0L) {
            IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.readerIdleTimeNanos, TimeUnit.NANOSECONDS);
            boolean first = IdleStateHandler.this.firstReaderIdleEvent;
            IdleStateHandler.this.firstReaderIdleEvent = false;
​
            try {
                IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.READER_IDLE, first);
                IdleStateHandler.this.channelIdle(ctx, event);
            } catch (Throwable var6) {
                ctx.fireExceptionCaught(var6);
            }
        } else {
            IdleStateHandler.this.readerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
        }
​
    }
}

该类首先获取到时间,然后用当前的时间减去最后一次读的时间,结果小于0说明超时,此时触发事件,反之则继续放入队列中继续计算时间

触发事件后会创建一个对应的写事件对象并传递给用户的自定义处理器处理

image-20230202222011211

然后是写事件的代码,和读事件扎不多,但是我们这里会判断这次写时间是不是因为业务执行的时间过长导致的(有时发生读空闲并不是因为业务空间,而是因为网络波动的原因导致业务处理时间变长)

private final class WriterIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
    WriterIdleTimeoutTask(ChannelHandlerContext ctx) {
        super(ctx);
    }
​
    protected void run(ChannelHandlerContext ctx) {
        long lastWriteTime = IdleStateHandler.this.lastWriteTime;
        long nextDelay = IdleStateHandler.this.writerIdleTimeNanos - (IdleStateHandler.this.ticksInNanos() - lastWriteTime);
        if (nextDelay <= 0L) {
            IdleStateHandler.this.writerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.writerIdleTimeNanos, TimeUnit.NANOSECONDS);
            boolean first = IdleStateHandler.this.firstWriterIdleEvent;
            IdleStateHandler.this.firstWriterIdleEvent = false;
​
            try {
                if (IdleStateHandler.this.hasOutputChanged(ctx, first)) {
                    return;
                }
​
                IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.WRITER_IDLE, first);
                IdleStateHandler.this.channelIdle(ctx, event);
            } catch (Throwable var8) {
                ctx.fireExceptionCaught(var8);
            }
        } else {
            IdleStateHandler.this.writerIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
        }
​
    }
}

image-20230202223005496

最后是处理读写事件的代码

private final class AllIdleTimeoutTask extends IdleStateHandler.AbstractIdleTask {
    AllIdleTimeoutTask(ChannelHandlerContext ctx) {
        super(ctx);
    }
​
    protected void run(ChannelHandlerContext ctx) {
        long nextDelay = IdleStateHandler.this.allIdleTimeNanos;
        if (!IdleStateHandler.this.reading) {
            nextDelay -= IdleStateHandler.this.ticksInNanos() - Math.max(IdleStateHandler.this.lastReadTime, IdleStateHandler.this.lastWriteTime);
        }
​
        if (nextDelay <= 0L) {
            IdleStateHandler.this.allIdleTimeout = IdleStateHandler.this.schedule(ctx, this, IdleStateHandler.this.allIdleTimeNanos, TimeUnit.NANOSECONDS);
            boolean first = IdleStateHandler.this.firstAllIdleEvent;
            IdleStateHandler.this.firstAllIdleEvent = false;
​
            try {
                if (IdleStateHandler.this.hasOutputChanged(ctx, first)) {
                    return;
                }
​
                IdleStateEvent event = IdleStateHandler.this.newIdleStateEvent(IdleState.ALL_IDLE, first);
                IdleStateHandler.this.channelIdle(ctx, event);
            } catch (Throwable var6) {
                ctx.fireExceptionCaught(var6);
            }
        } else {
            IdleStateHandler.this.allIdleTimeout = IdleStateHandler.this.schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
        }
​
    }
}

这里每次计算时会取两次时间的最大值来计算

image-20230202223623576

最后我们来看看本章小结

image-20230202223819763

image-20230202224130697

最后要注意的是ReadTimeoutHandler继承IdleStateHandler,当触发读空闲时会触发对应方法并关闭Socket。而WriteTimeoutHandler是通过传入的promise的完成情况来判断是否超时的,两者内部的运行逻辑有所不同

EventLoop

接着我们来讲解EventLoop的源码

image-20230203020420098

先来看看其UML图

image-20230203020559647

image-20230203020615596

其间接继承或者实现了许多接口,这些接口使得EventLoop可以实现许多功能,其是一个单例的线程池,内部的死循环不断做着监听端口、处理端口事件、处理队列事件这三件事情。同时每隔EventLoop可以绑定多个Channel,但是每个Channel始终只能有一个EventLoop来处理

image-20230203020852525

来看看Executor其下拥有的类,我们主要分析SingleThreadEventExecutor类

image-20230203021231183

其下的excute方法会启动线程并将任务添加到对应的队列中去,这个队列就是我们之前说过的每个EventLoop都会维护的Task队列,注意任务其实本身是一个新线程来做的,这也是为什么我们这里传入的对象是Runnable对象

public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    } else {
        boolean inEventLoop = this.inEventLoop();
        this.addTask(task);
        if (!inEventLoop) {
            this.startThread();
            if (this.isShutdown()) {
                boolean reject = false;
​
                try {
                    if (this.removeTask(task)) {
                        reject = true;
                    }
                } catch (UnsupportedOperationException var5) {
                }
​
                if (reject) {
                    reject();
                }
            }
        }
​
        if (!this.addTaskWakesUp && this.wakesUpForTask(task)) {
            this.wakeup(inEventLoop);
        }
​
    }
}

下面是其启动线程的步骤

image-20230203021441114

image-20230203021513529

再来看看addTask方法

image-20230203022134047

下面使其添加任务的步骤

image-20230203022532520

在启动线程的方法中首先判断是否已经启动,若没有则更改状态然后调用doStartThread方法,若失败会回滚

image-20230203023014879

doStratThread方法中红框的代码就是真正启动线程的代码

image-20230203023146747

点进run方法会发现这是一个抽象方法,我们具体进入到NioEventLoop中就会发现其实现了对应的方法并进行了监听

这些过程的具体的执行步骤如下

image-20230203023049336

下面是run方法的代码

image-20230203023322156

image-20230203023344781

整个run方法只做了下面三件事情,第一件是select获取感兴趣的事件,第二是processSelectedKeys处理事件,第三件是runAllTasks执行队列中的任务,执行第三件事情的比例还会根据设定的ioRatio来变化

image-20230203023356349

select方法的代码和运行过程如下,其本质是一个死循环用于处理事件

image-20230203023547167

image-20230203023612413

如果有发生下面的事件,则会跳出循环

image-20230203023754877

最后我们来看看小结

image-20230203023846828

image-20230203023935799

加入异步线程池

我们之前讲过在Netty处理耗时操作时需要将该操作设置为异步,我们前面的解决方式是通过调用channel对象的execute方法并new一个Runnable对象来解决的

但实际上这种方式会使得我们的业务处理一直都是使用同一个线程,那这本质其实还是阻塞的,因此我们需要使用别的方式,我们这里有下图两种方式,我们接下来慢慢讲

image-20230203040801860

handler加入线程池

首先我们来讲handler加入线程池的方式,我们直接往对应的处理器类中创建一个线程池属性,这里的DefaultEventExecutorGroup是一个线程池对象,是基于JDK的线程池创建封装而来的对象,实际上我们直接创建JDK的线程池对象也是可以的

image-20230203041302629

然后我们调动对应线程池的提交方法,重写其Callable函数,内部写入我们的业务处理代码

image-20230203041815735

这样我们的代码就不会阻塞了,而且采用这种方式,即使我们同时处理多个业务,也是可以并行的,不需要等待

image-20230203042159693

实际得到的结果里,当我们开启服务端后,我们开启多个客户端,会发现处理请求的线程会变化,而我们具体将任务交由线程池中的线程也会变,都是轮询,之所以会这样是因为我们的workGroup中有多个线程,而这个线程中又有多个线程,因此会有两种变化

我们实际的业务流程图如下,我们Socket接受到请求之后将请求传递给对应的业务处理器类进行处理,接着我们会将其提交给另外的线程进行处理,当我们的业务处理器类处理完后,我们会重新调用workerGroup中原来的线程将结果传递给客户端

image-20230203042307838

下面是流程图的解释

image-20230203042419433

然后我们来看看源码,workerGroup中的业务处理鳄梨中的write方法进入时会判断当前线程是否是业务处理线程,若不是会将当前的任务封装为task加入到队列中,队列中的任务执行完毕后会获得结果

image-20230203042506417

image-20230203042547762

Context加入线程池

第二种方式是直接在服务器类中创建线程池

image-20230203043939763

嗲用addLast方法时传入对应的线程池

image-20230203044244351

这样我们的业务就会直接和线程池中的线程绑定,也能实现调用,显示源码逻辑的说明

image-20230203044350915

这两种方式第一种方式更为灵活,第二种方式更为方便,自己选择吧

image-20230203044643775

Netty实现DubboRPC

到此为止我们的Netty的内容就全部讲完了,最后我们来用Netty做一个DubboRPC的案例作为总结

RPC指的是远程过程调用,是一个计算机通信协议,其允许运行于一台计算机的程序调用另外计算机的子程序而无需额外的编程

image-20230203050322045

下面是其调用的流程图

image-20230203045818924

常见的PRC框架有阿里的Dubbo、Google的gRPC等

image-20230203045835923

RPC调用流程图简化版,这里值得一提的是在RPC中Clinet叫服务消费者,而Server叫服务提供者

image-20230203050454880

RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用

image-20230203050609359

案例需求

底层的网络通信我们使用Netty4.1.20,同时我们需要创建共有的接口以及对应的提供者和消费者以及它们的处理器

image-20230203051435640

下面是其结构图说明

image-20230203051329048

首先我们编写共同的接口

/**
 * 该类为接口,是服务提供方和服务都需要的
 */
public interface HelloService {
    String hello(String mes);
}

然后我们来编写其实现类,经典接受数据并进行处理

public class HelloServiceImpl implements HelloService {
​
    private int count = 0;
​
    /**
     * 当有消费者调用方法时,就返回一个结果
     * @param mes
     * @return
     */
    @Override
    public String hello(String mes) {
        System.out.println("收到客户端消息="+mes);
        //根据mes返回不同的结果
        if(mes!=null){
            return "Hello Client,我已经收到你的消息["+mes+"] 第"+(++count)+"";
        }
        return "Hello Client,我已经收到你的消息";
    }
}

然后来编写服务端,我们这里做的事情和前面的差不多,同样加入一个自定义处理器

public class NettyServer {
​
    public static void startServer(String hostName,int port){
        startServer0(hostName,port);
    }
​
    /**
     * 编写一个方法来完成NettyServer的初始化和启动
     * @param hostname
     * @param port
     */
    private static void startServer0(String hostname,int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
​
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap(1);
            serverBootstrap.group(bossGroup,workerGroup)
                           .channel(NioServerSocketChannel.class)
                           .childHandler(new ChannelInitializer<SocketChannel>() {
                               @Override
                               protected void initChannel(SocketChannel socketChannel) throws Exception {
                                   ChannelPipeline pipeline = socketChannel.pipeline();
                                   pipeline.addLast(new StringDecoder());
                                   pipeline.addLast(new StringEncoder());
                                   pipeline.addLast(new NettyServerHandler());
                               }
                           });
            ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

自定义的handler逻辑很简单,就是规定我们的协议,对对应的消息进行处理并回写到客户端上

/**
 * 服务器端的handler逻辑比较简单
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
​
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //获取客户端的消息,并调用服务
        System.out.println("msg="+msg);
        //客户端在调用服务器api时,需要定义规范,要求满足一定条件才允许调用
        if(msg.toString().startsWith("HelloService#hello#")) {
            String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
            ctx.writeAndFlush(result);
        }
    }

然后我们来写客户端,客户端中同样是有初始化客户端的方法,我们这里同时还编写一个代理方法,可以获取一个代理对象并通过该代理对象将对应的参数传入来获取我们所需要的结果

如果客户端为null,那么会进行对应的初始化,同样也加入了自定义的处理器,这里值得一提是每次请求出现都会新创建一个NettyClinet对象用于处理请求

public class NettyClinet {
    //创建线程池
    private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
​
    private static NettyClientHandler client;
​
    private int count = 0;
​
    //编写一个方法使用代理模式,获取一个代理对象
    public Object getBean(final Class<?> serviceClass,final String providerName) {
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{serviceClass},(proxy,method,args) -> {
                    //这部分的代码挥别反复执行,客户端每调用一次api就执行一次
                    System.out.println("(proxy,method,args)方法被调用..."+(++count)+"次");
                    if(client==null){
                        initClient();
                    }
                    //设置要发给服务器的信息
                    //providerName协议头args[0],就是客户端调用api时发送的参数
                    client.setPara(providerName+args[0]);
                    return executor.submit(client).get();
                });
    }
​
    //初始化客户端
    private static void initClient() {
        client = new NettyClientHandler();
        //创建EventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                 .channel(NioSocketChannel.class)
                 .option(ChannelOption.TCP_NODELAY,true)
                 .handler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel socketChannel) throws Exception {
                         ChannelPipeline pipeline = socketChannel.pipeline();
                         pipeline.addLast(new StringDecoder());
                         pipeline.addLast(new StringEncoder());
                         pipeline.addLast(client);
                     }
                 });
        try {
            bootstrap.connect("localhost",7000).sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

自定义的处理器中我们有三个属性,我们这里在处理数据的方法中需要唤醒线程,这是因为我们的回调函数在向服务器发送数据之后,由于需要过一段时间才能得到结果,因此先将该线程休眠,等到获得服务端的数据时再唤醒该线程进行处理,休眠的逻辑我们可以在call方法中看到

同时由于接收业务处理和休眠的两份方法存在线程安全问题,因此需要加入synchronized关键字

public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {
​
    /**
     * 上下文
     */
    private ChannelHandlerContext context;
    /**
     * 返回的结果
     */
    private String result;
    /**
     * 客户端调用方式时传入的参数
     */
    private String para;
​
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive方法被调用");
        //因为在其他方法中会使用到ctx
        context =  ctx;
    }
​
    /**
     * 收到服务器的数据后就会调用该方法
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        result = msg.toString();
        //该方法或唤醒等待的线程
        notify();
    }
​
    /**
     * 被代理对象调用,发送数据给服务器 -> 等待被唤醒 -> 返回结果
     * @return
     * @throws Exception
     */
    @Override
    public synchronized Object call() throws Exception {
        System.out.println("call方法被调用");
        context.writeAndFlush(para);
        //进行wait,等待channelRead方法获取到服务器的结果后唤醒
        wait();
        System.out.println("call方法再次被调用");
        //服务方返回的结果
        return result;
    }
​
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
​
    void setPara(String para){
        System.out.println("setPara方法被调用");
        this.para=para;
    }
}

然后我们需要服务端的启动类,我们这里直接调用创建类的启动服务的方法

public class ServerBootstrap {
    public static void main(String[] args) {
        //代码代填...
        NettyServer.startServer("localhost",7000);
        System.out.println("服务提供方开始提供服务~~~");
    }
}

首先定义协议头,创建消费者,然后创建代理对象,接着循环一直向服务器发送消息

public class ClientBootstrap {
    //这里定义协议头
    public static final String PROVIDER_NAME = "HelloService#hello#";
​
    public static void main(String[] args) throws InterruptedException {
        //创建一个消费者
        NettyClinet customer = new NettyClinet();
​
        //创建代理对象
        HelloService service = (HelloService) customer.getBean(HelloService.class, PROVIDER_NAME);
​
        while (true){
            Thread.sleep(10 * 1000);
            //通过代理对象调用服务提供者的方法(服务)
            String res = service.hello("你好 dubbo~");
            System.out.println("调用的结果res="+res);
        }
    }
}

我们这里的流程是通过服务器启动类创建启动类,然后我们初始化服务器,接着通过客户端启动类开启客户端,首先创建一个客户端类,然后通过该类获取代理对象,然后通过代理对象不断调用服务器的方法

发送方法时我们指定字符串即可,客户端中重写了动态代理的方法,我们传入对应的对象就可以得到我们需要代理对象,我们调用代理对应的发送方式时,实际上其就在执行我们重写的动态代理中的方法,该方法执行时先判断客户端处理器是否为空,若真为空则初始化该类,同时创建客户端并将该处理器加入到客户端中,然后设置对应的参数之后调用线程中的提交方法,传入该客户端处理器类就可以之后就可以得到我们想要的结果,业务处理器中我们对对应的结果进行了相应的处理