JAVA【Netty】第三讲(持续更新)

Google Protobuf

编码和解码的基本介绍

  1. 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码[示意图]
  2. codec(编解码器)的组成部分有两个:decoder(解码器)和 encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成业务数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iitfEX3X-1658148670965)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0001.png?msec=1658148303932)]

Netty 本身的编码解码的机制和问题分析

  1. Netty 自身提供了一些 codec(编解码器)
  2. Netty 提供的编码器
  • StringEncoder:对字符串数据进行编码。
  • ObjectEncoder:对Java对象进行编码。
  1. Netty 提供的解码器
  • StringDecoder,对字符串数据进行解码
  • ObjectDecoder,对 Java 对象进行解码
  1. Netty 本身自带的 ObjectDecoderObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是Java序列化技术,而Java序列化技术本身效率就不高,存在如下问题
  • 无法跨语言
  • 序列化后的体积太大,是二进制编码的5倍多。
  • 序列化性能太低
  1. 引出新的解决方案[GoogleProtobuf]

Protobuf

  1. Protobuf 基本介绍和使用示意图
  2. ProtobufGoogle 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC [远程过程调用 remote procedure call ]数据交换格式。目前很多公司 从http + json 转向tcp + protobuf,效率会更高。
  3. 参考文档:https://developers.google.com/protocol-buffers/docs/proto 语言指南
  4. Protobuf 是以 message 的方式来管理数据的.
  5. 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的](支持目前绝大多数语言,例如 C++C#Javapython 等)
  6. 高性能,高可靠性
  7. 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用 .proto 文件进行描述。说明,在 idea 中编写 .proto 文件时,会自动提示是否下载 .ptoto 编写插件.可以让语法高亮。
  8. 然后通过 protoc.exe 编译器根据 .proto 自动生成 .java 文件
  9. protobuf 使用示意图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6NE6dSKB-1658148670966)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0002.png?msec=1658148303936)]

Protobuf 快速入门实例

编写程序,使用 Protobuf 完成如下功能

  1. 客户端可以发送一个 StudentPoJo 对象到服务器(通过 Protobuf 编码)
  2. 服务端能接收 StudentPoJo 对象,并显示信息(通过 Protobuf 解码)
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.6.1</version>
        </dependency>

Student.proto

syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message 管理数据
message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, 他是真正发送的POJO对象
    int32 id = 1; // Student 类中有 一个属性 名字为 id 类型为int32(protobuf类型) 1表示属性序号,不是值
    string name = 2;
}

编译
protoc.exe --java_out=.Student.proto
将生成的 StudentPOJO 放入到项目使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4JK5eOtQ-1658148670967)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0003.png?msec=1658148303995)]

生成的StudentPOJO代码太长就不贴在这里了

NettyServer

package com.qf.netty.protobuf;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        //创建boss group 和worker group
        //DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
        //bossGroup指定的是一条线程
        //workerGroup默认情况下是核心数*2
        EventLoopGroup  bossGroup=new NioEventLoopGroup(1);
        EventLoopGroup  workerGroup=new NioEventLoopGroup();

        //创建服务端的启动对象,设置配置参数
        ServerBootstrap bootstrap = new ServerBootstrap();

        //使用连式编程来进行设置
        try {
            bootstrap.group(bossGroup, workerGroup)     //设置两个线程组
                    .channel(NioServerSocketChannel.class)      //使用NioServerSocketChannel  作为数据通道
                    .option(ChannelOption.SO_BACKLOG,128)        //设置队列的连接数量
                    .childOption(ChannelOption.SO_KEEPALIVE, true)  //设置保持活动的连接数
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            System.out.println("服务器的值:"+sc.hashCode());
                            sc.pipeline().addLast("decoder",new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
                            sc.pipeline().addLast(new NettyServerHandler());
                        }
                    });

            System.out.println("服务器已经准备好....");

            //绑定端口
            ChannelFuture sync = bootstrap.bind(6668).sync();
            //对我们的future对象进行监听,异步关注响应是否成功
            sync.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (sync.isSuccess()){
                        System.out.println("端口 6668 成功");
                    }else if (sync.isCancelled()){
                        System.out.println("端口 6668 失败");
                    }
                }
            });

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

NettyServerHandler

package com.qf.netty.protobuf;

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

import java.util.concurrent.TimeUnit;

public class NettyServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {

    //读取数据实际(读取来自客户端的数据)

   /* @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      *//* StudentPOJO.Student student= (StudentPOJO.Student) msg;
        System.out.println("客户端发送的数据:"+student.getId()+"名字="+student.getName());*//*

    }
*/
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, StudentPOJO.Student student) throws Exception {
        System.out.println(student);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //服务器写出数据
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 ~~~喵喵喵2",CharsetUtil.UTF_8));
    }


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

NettyClient

package com.qf.netty.protobuf;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要第一个事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();

        try{
            //创建客户端启动
            Bootstrap bootstrap = new Bootstrap();

            //设置相关的参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast("encoder",new ProtobufEncoder());
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            //连接端口
            ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

NettyClientHandler

package com.qf.netty.protobuf;

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;

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 通道就绪就会触发次方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client:"+ctx);
        StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("xiaohdfhsf").build();
        ctx.writeAndFlush(student);
    }

    @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 {
        cause.printStackTrace();
        ctx.channel().close();
    }
}

Protobuf 快速入门实例 2

  1. 编写程序,使用 Protobuf 完成如下功能
  2. 客户端可以随机发送 StudentPoJo / WorkerPoJo 对象到服务器(通过 Protobuf 编码)
  3. 服务端能接收 StudentPoJo / WorkerPoJo 对象(需要判断是哪种类型),并显示信息(通过 Protobuf 解码)

proto

syntax = "proto3";
option optimize_for = SPEED; // 加快解析
option java_package="com.atguigu.netty.codec2";   //指定生成到哪个包下
option java_outer_classname="MyDataInfo"; // 外部类名, 文件名


/*
1.protobuf 可以使用message 管理其他的message。最终决定使用哪一个message作为传输对象
2.假设你某个项目需要传输20个对象,你不可能新建20个proto文件吧。此时你就可以
在一个文件里定义20个message,最后再用一个总的message(比方说这里的MyMessage)
来决定在实际传输时真正需要传输哪一个对象
3.因为你实际传输的时候大部分情况传输的都是一个对象,所以下面用oneof进行了限制
4.是否可以传多个对象呢?我个人认为是可以的,比如可以通过map(目前我也不太了解proto的语法)
 */
message MyMessage {

    //定义一个枚举类型,DataType如果是0则表示一个Student对象实例,DataType这个名称自定义
    enum DataType {
        StudentType = 0; //在proto3 要求enum的编号从0开始
        WorkerType = 1;
    }

    //用data_type 来标识传的是哪一个枚举类型,这里才真正开始定义MyMessage的数据类型
    DataType data_type = 1;  //所有后面的数字都只是编号而已

    /*
    1.oneof关键字 表示每次枚举类型进行传输时,限制最多只能传输一个对象。
    dataBody名称也是自定义的
    2.为什么这里的序号是2呢?因为上面DataType data_type = 1  占了第一个序号了
    3.MyMessage里真正出现的类型只有两个
      ①DataType类型
      ②Student类型或者Worker类型(这两个在真正传输的时候只会有一个出现)
    */
    oneof dataBody {
        Student student = 2;  //注意这后面的数字也都只是编号而已
        Worker worker = 3;
    }


}


message Student {
    int32 id = 1;//Student类的属性
    string name = 2; //
}
message Worker {
    string name=1;
    int32 age=2;
}

NettyServer

package com.qf.netty.code2;

import com.qf.netty.protobuf.StudentPOJO;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        //创建boss group 和worker group
        //DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
        //bossGroup指定的是一条线程
        //workerGroup默认情况下是核心数*2
        EventLoopGroup  bossGroup=new NioEventLoopGroup(1);
        EventLoopGroup  workerGroup=new NioEventLoopGroup();

        //创建服务端的启动对象,设置配置参数
        ServerBootstrap bootstrap = new ServerBootstrap();

        //使用连式编程来进行设置
        try {
            bootstrap.group(bossGroup, workerGroup)     //设置两个线程组
                    .channel(NioServerSocketChannel.class)      //使用NioServerSocketChannel  作为数据通道
                    .option(ChannelOption.SO_BACKLOG,128)        //设置队列的连接数量
                    .childOption(ChannelOption.SO_KEEPALIVE, true)  //设置保持活动的连接数
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            System.out.println("服务器的值:"+sc.hashCode());
                            sc.pipeline().addLast("decoder",new ProtobufDecoder( MyDataInfo.MyMessage.getDefaultInstance()));
                            sc.pipeline().addLast(new NettyServerHandler());
                        }
                    });

            System.out.println("服务器已经准备好....");

            //绑定端口
            ChannelFuture sync = bootstrap.bind(6668).sync();
            //对我们的future对象进行监听,异步关注响应是否成功
            sync.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (sync.isSuccess()){
                        System.out.println("端口 6668 成功");
                    }else if (sync.isCancelled()){
                        System.out.println("端口 6668 失败");
                    }
                }
            });

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

NettyServerHandler

package com.qf.netty.code2;

import com.qf.netty.protobuf.StudentPOJO;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

public class NettyServerHandler extends SimpleChannelInboundHandler< MyDataInfo.MyMessage> {

    //读取数据实际(读取来自客户端的数据)

   /* @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      *//* StudentPOJO.Student student= (StudentPOJO.Student) msg;
        System.out.println("客户端发送的数据:"+student.getId()+"名字="+student.getName());*//*

    }
*/
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, MyDataInfo.MyMessage msg) throws Exception {
        MyDataInfo.MyMessage.DataType dataType = msg.getDataType();
        if (dataType==MyDataInfo.MyMessage.DataType.StudentType){
            MyDataInfo.Student student = msg.getStudent();
            System.out.println("学生的名字:"+student.getName()+"学生的学号:"+student.getId());
        }else if (dataType==MyDataInfo.MyMessage.DataType.WorkerType){
            MyDataInfo.Worker worker = msg.getWorker();
            System.out.println("工人的年龄:"+worker.getAge()+"工人的名字:"+worker.getName());
        }else {
            System.out.println("输入有误");
        }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //服务器写出数据
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 ~~~喵喵喵2",CharsetUtil.UTF_8));
    }


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

NettyClient

package com.qf.netty.code2;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要第一个事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();

        try{
            //创建客户端启动
            Bootstrap bootstrap = new Bootstrap();

            //设置相关的参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast("encoder",new ProtobufEncoder());
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });

            //连接端口
            ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

NettyClientHandler

package com.qf.netty.code2;

import com.qf.netty.protobuf.StudentPOJO;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.util.Random;

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 通道就绪就会触发次方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        int random=new Random().nextInt();
        MyDataInfo.MyMessage message=null;
        if (random==0){
            message=MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.StudentType).setStudent(MyDataInfo.Student.newBuilder().setId(5).setName("小荔枝的").build()).build();
        }else{
            message=MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.WorkerType).setWorker(MyDataInfo.Worker.newBuilder().setAge(22).setName("工人").build()).build();
        }
        ctx.writeAndFlush(message);
    }

    @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 {
        cause.printStackTrace();
        ctx.channel().close();
    }
}

Netty编解码器和 Handler 调用机制

基本说明

  1. Netty 的组件设计:Netty 的主要组件有 ChannelEventLoopChannelFutureChannelHandlerChannelPipe
  2. ChannelHandler 充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现 ChannelInboundHandler 接口(或 ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从 ChannelInboundHandler 冲刷数据。业务逻辑通常写在一个或者多个 ChannelInboundHandler 中。ChannelOutboundHandler 原理一样,只不过它是用来处理出站数据的
  3. ChannelPipeline 提供了 ChannelHandler 链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过 pipeline 中的一系列 ChannelOutboundHandler,并被这些 Handler 处理,反之则称为入站的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5ZnjeSv-1658155028090)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0004.png?msec=1658148303875)]

出站,入站如果搞不清楚,看下面的Netty的handler链的调用机制,通过一个例子和图讲清楚

编码解码器

  1. Netty 发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如 java 对象);如果是出站消息,它会被编码成字节。
  2. Netty 提供一系列实用的编解码器,他们都实现了 ChannelInboundHadnler 或者 ChannelOutboundHandler 接口。在这些类中,channelRead 方法已经被重写了。以入站为例,对于每个从入站 Channel 读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的 decode() 方法进行解码,并将已经解码的字节转发给 ChannelPipeline 中的下一个 ChannelInboundHandler

解码器 - ByteToMessageDecoder

  1. 关系继承图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pEwh1vwG-1658155028090)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0005.png?msec=1658148303930)]

  1. 由于不可能知道远程节点是否会一次性发送一个完整的信息,tcp 有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理.【后面有说TCP的粘包和拆包问题】
  2. 一个关于 ByteToMessageDecoder 实例分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TgIELxGn-1658155028092)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0006.png?msec=1658148304117)]

Netty的handler链的调用机制

实例要求:

  1. 使用自定义的编码器和解码器来说明 Nettyhandler 调用机制
    客户端发送 long -> 服务器
    服务端发送 long -> 客户端

读者可以看下这个图,带着这个图去看下面的例子。
在这里插入图片描述

MyServer

package com.qf.netty.Inboudandoutbound;

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;

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

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

        try {
            //创建服务端的启动对象,设置配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new MyServerInitializer());

            //绑定端口
            ChannelFuture sync = bootstrap.bind(6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }


    }
}

MyServerInitializer

package com.qf.netty.Inboudandoutbound;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;


public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        //先编码
        pipeline.addLast(new MyByteToLongEncoder());
        pipeline.addLast(new MyByteToLongDecoder2());
        pipeline.addLast(new MyServerHandler());
    }
}

MyServerHandler

package com.qf.netty.Inboudandoutbound;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class MyServerHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Long aLong) throws Exception {
        System.out.println("服务器接收到消息:"+channelHandlerContext.channel().remoteAddress()+"数据:"+aLong);
        channelHandlerContext.writeAndFlush(6888L);
    }

}

MyClient

package com.qf.netty.Inboudandoutbound;

import com.qf.netty.code2.NettyClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;

public class MyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要第一个事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();

        try{
            //创建客户端启动
            Bootstrap bootstrap = new Bootstrap();

            //设置相关的参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer());

            //连接端口
            ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

MyClientInitializer

package com.qf.netty.Inboudandoutbound;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

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

MyClientHandler

package com.qf.netty.Inboudandoutbound;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Long aLong) throws Exception {
        System.out.println("服务器的ip=" + channelHandlerContext.channel().remoteAddress());
        System.out.println("收到服务器消息=" + aLong);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("MyClientHandler 被调用");
        ctx.writeAndFlush(88888L);
        super.channelActive(ctx);
    }
}

MyByteToLongEncoder

package com.qf.netty.Inboudandoutbound;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;

public class MyByteToLongEncoder extends MessageToByteEncoder<Long> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Long o, ByteBuf byteBuf) throws Exception {
        System.out.println("MyByteToLongEncoder 被调用");
        System.out.println("msg:"+o);
        byteBuf.writeLong(o);
    }
}

MyByteToLongDecoder

package com.qf.netty.Inboudandoutbound;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;

import java.util.List;

public class MyByteToLongDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
        System.out.println("MyByteToLongDecoder 被调用");

        if (in.readableBytes()>=8){
            list.add(in.readLong());
        }
    }
}

效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xtHhFktG-1658155558692)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0008.png?msec=1658148303998)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUxcInKc-1658155558693)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0009.png?msec=1658148303999)]

出站入站

关于出站入站,很多人可能有点迷糊
1)客户端有出站入站,服务端也有出站入站
2)以客户端为例,如果有服务端传送的数据到达客户端,那么对于客户端来说就是入站;
如果客户端传送数据到服务端,那么对于客户端来说就是出站;
同理,对于服务端来说,也是一样的,有数据来就是入站,有数据输出就是出站
3)为什么服务端和客户端的Serverhandler都是继承SimpleChannelInboundHandler,而没有ChannelOutboundHandler出站类?
实际上当我们在handler中调用ctx.writeAndFlush()方法后,就会将数据交给ChannelOutboundHandler进行出站处理,只是我们没有去定义出站类而已,若有需求可以自己去实现ChannelOutboundHandler出站类
4)总结就是客户端和服务端都有出站和入站的操作
**服务端发数据给客户端:**服务端—>出站—>Socket通道—>入站—>客户端

​ **客户端发数据给服务端:**客户端—>出站—>Socket通道—>入站—>服务端
在这里插入图片描述
下面是Netty官方源码给的图,我个人觉的不是太好理解,上面的图好理解一些

ByteToMessageDecoder的小细节

package com.qf.netty.Inboudandoutbound;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Long aLong) throws Exception {
        System.out.println("服务器的ip=" + channelHandlerContext.channel().remoteAddress());
        System.out.println("收到服务器消息=" + aLong);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("MyClientHandler 被调用");
        ctx.writeAndFlush(88888L);
        super.channelActive(ctx);
    }
}

package com.qf.netty.Inboudandoutbound;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;

import java.util.List;

public class MyByteToLongDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
        System.out.println("MyByteToLongDecoder 被调用");

        if (in.readableBytes()>=8){
            list.add(in.readLong());
        }
    }
}

  1. 由于发送的字符串是16字节,根据上面注释说的内容,decode会被调用两次

如下图验证结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hKxQ8ULW-1658194825668)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0010.png?msec=1658148304056)]

  1. 同时又引出了一个小问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wr2zSdU6-1658194825669)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0011.png?msec=1658148303927)]

当我们MyClientHandler传一个Long时,会调用我们的MyLongToByteEncoder的编码器。那么控制台就会打印这样一句话:MyLongToByteEncoder encode 被调用。但是这里并没有调用编码器,这是为什么呢?

  1. MyClientHandler这个处理器的后一个处理器是MyLongToByteEncoder
  2. MyLongToByteEncoder的父类是MessageToByteEncoder,在MessageToByteEncoder中有下面的一个方法
@Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        ByteBuf buf = null;
        try {
            //这里会判断当前msg 是不是应该处理的类型,如果是就处理,不是就跳过encode
            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();
            }
        }
    }

3.当我们以这样的形式发送数据

ctx.writeAndFlush(Unpooled.copiedBuffer("abcdabcdabcdabcd",CharsetUtil.UTF_8));

在这里插入图片描述
这两个类型并不匹配,也就不会走编码器。因此我们编写 Encoder 是要注意传入的数据类型和处理的数据类型一致

结论:

  • 不论解码器 handler 还是编码器 handler 即接收的消息类型必须与待处理的消息类型一致,否则该 handler 不会被执行
  • 在解码器进行数据解码时,需要判断缓存区(ByteBuf)的数据是否足够,否则接收到的结果会期望结果可能不一致。

解码器 - ReplayingDecoder

  1. public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
  2. ReplayingDecoder 扩展了 ByteToMessageDecoder 类,使用这个类,我们不必调用 readableBytes() 方法,也就不用判断还有没有足够的数据来读取。参数 S 指定了用户状态管理的类型,其中 Void 代表不需要状态管理
  3. 应用实例:使用 ReplayingDecoder 编写解码器,对前面的案例进行简化[案例演示]
package com.qf.netty.Inboudandoutbound;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        list.add(byteBuf.readLong());
    }
}

  1. ReplayingDecoder 使用方便,但它也有一些局限性:
  • 并不是所有的 ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个 UnsupportedOperationException
  • ReplayingDecoder 在某些情况下可能稍慢于 ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢

其它编解码器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5jAQP9cM-1658195160402)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0013.png?msec=1658148303876)]

  1. LineBasedFrameDecoder:这个类在 Netty 内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。
  2. DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。
  3. HttpObjectDecoder:一个 HTTP 数据的解码器
  4. LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。

Log4j 整合到 Netty

  1. Maven 中添加对 Log4j 的依赖在 pom.xml
<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>
  1. 配置 Log4j,在 resources/log4j.properties
log4j.rootLogger=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
  1. 演示整合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GsveWxtG-1658195251000)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0014.jpg?msec=1658148304117)]

TCP 粘包和拆包及解决方案

TCP 粘包和拆包基本介绍

  1. TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
  2. 由于 TCP 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图
  3. TCP 粘包、拆包图解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-To1LNTgf-1658195251001)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0015.png?msec=1658148303876)]

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

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

TCP 粘包和拆包现象实例

在编写 Netty 程序时,如果没有做处理,就会发生粘包和拆包的问题

看一个具体的实例:

MyServer

package com.qf.netty.Inboudandoutbound;

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;

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

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

        try {
            //创建服务端的启动对象,设置配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new MyServerInitializer());

            //绑定端口
            ChannelFuture sync = bootstrap.bind(6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }


    }
}

MyServerInitializer

package com.qf.netty.tcp;

import com.qf.netty.Inboudandoutbound.MyByteToLongDecoder2;
import com.qf.netty.Inboudandoutbound.MyByteToLongEncoder;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;


public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        //先编码
        pipeline.addLast(new MyServerHandler());
    }
}

MyServerHandler

package com.qf.netty.tcp;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.nio.charset.Charset;
import java.util.UUID;

public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
    public int count;
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf msg) throws Exception {
        int i = msg.readableBytes();
        byte[] bytes=new byte[i];
        msg.readBytes(bytes);

        String message=new String(bytes, Charset.forName("utf-8"));
        System.out.println("来自客户端的数据:"+message);
        System.out.println("来自客户端的消息次数:"+(++count));
        System.out.println();
        String uuid= UUID.randomUUID().toString();
        ByteBuf byteBuf = Unpooled.copiedBuffer((uuid).getBytes("utf-8"));
        channelHandlerContext.writeAndFlush(byteBuf);
    }

}

MyClient

package com.qf.netty.tcp;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class MyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要第一个事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();

        try{
            //创建客户端启动
            Bootstrap bootstrap = new Bootstrap();

            //设置相关的参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer());

            //连接端口
            ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

MyClientInitializer

package com.qf.netty.tcp;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

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

MyClientHandler

package com.qf.netty.tcp;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.nio.charset.Charset;

public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    public int count;
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf msg) throws Exception {
        int i = msg.readableBytes();
        byte[] bytes=new byte[i];
        msg.readBytes(bytes);

        String message=new String(bytes, Charset.forName("utf-8"));
        System.out.println("接收到来自服务器的数据"+message+"   "+(++count));
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 10; i++) {
            ByteBuf byteBuf = Unpooled.copiedBuffer(("hello server" + i).getBytes("utf-8"));
            ctx.writeAndFlush(byteBuf);
        }
    }
}

效果

第一次运行:

Client

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0MvAh2jq-1658195594682)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0016.png?msec=1658148304220)]

Server

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4lUWEMDK-1658195594682)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0017.png?msec=1658148304297)]

第二次运行:

Client

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hLgi9GM2-1658195594683)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0018.png?msec=1658148304297)]

Server

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aiFI3vE8-1658195594684)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0019.png?msec=1658148304164)]

可以看到第一次运行时,服务器一次性将10个数据都接收了,第二次运行时分六次接收的,这就很形象的看出了TCP的粘包现象。

TCP 粘包和拆包解决方案

  1. 常用方案:使用自定义协议+编解码器来解决
  2. 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的 TCP 粘包、拆包。

看一个具体的实例

  1. 要求客户端发送 5Message 对象,客户端每次发送一个 Message 对象
  2. 服务器端每次接收一个 Message,分 5 次进行解码,每读取到一个 Message,会回复一个 Message 对象给客户端。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zcLxcFlj-1658195594684)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0020.png?msec=1658148303876)]

MessageProtocol

package com.qf.netty.tcpprotoco;

public class MessageProtocol {
    private int len;
    private byte[] content;

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }
}

MyServer

package com.qf.netty.tcpprotoco;

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;

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

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

        try {
            //创建服务端的启动对象,设置配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new MyServerInitializer());

            //绑定端口
            ChannelFuture sync = bootstrap.bind(6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }


    }
}

MyServerInitializer

package com.qf.netty.tcpprotoco;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;


public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();
        //先编码
        pipeline.addLast(new MyByteToLongDecoder2());
        pipeline.addLast(new MyMessageEncoder());
        pipeline.addLast(new MyServerHandler());
    }
}

MyServerHandler

package com.qf.netty.tcpprotoco;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.nio.charset.Charset;
import java.util.UUID;

public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    public int count;
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol msg) throws Exception {
        int i = msg.getLen();
        byte[] content = msg.getContent();

        String message=new String(content, Charset.forName("utf-8"));
        System.out.println("来自客户端的数据:"+message);
        System.out.println("来自客户端的消息次数:"+(++count));
        System.out.println();

        String uuid= UUID.randomUUID().toString();
        int len=uuid.getBytes(Charset.forName("utf-8")).length;
        byte[] bytes = uuid.getBytes(Charset.forName("utf-8"));
        MessageProtocol messageProtocol=new MessageProtocol();
        messageProtocol.setLen(len);
        messageProtocol.setContent(bytes);
        channelHandlerContext.writeAndFlush(messageProtocol);
    }

}

MyClient

package com.qf.netty.tcpprotoco;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

public class MyClient {
    public static void main(String[] args) throws InterruptedException {
        //客户端需要第一个事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();

        try{
            //创建客户端启动
            Bootstrap bootstrap = new Bootstrap();

            //设置相关的参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer());

            //连接端口
            ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();

            //对关闭通道进行监听
            sync.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

MyClientInitializer

package com.qf.netty.tcpprotoco;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;

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

MyClientHandler

package com.qf.netty.tcpprotoco;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.nio.charset.Charset;

public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    public int count;
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol msg) throws Exception {
        int len = msg.getLen();
        byte[] content = msg.getContent();

        String message=new String(content, Charset.forName("utf-8"));
        System.out.println("接收到来自服务器的数据"+message+"   "+(++count));
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 5; i++) {
          String msg="今天天气热,适合吃火锅";
            byte[] content = msg.getBytes(Charset.forName("utf-8"));
            int length=content.length;
            MessageProtocol messageProtocol=new MessageProtocol();
            messageProtocol.setLen(length);
            messageProtocol.setContent(content);
            ctx.writeAndFlush(messageProtocol);
        }
    }
}

MyMessageEncoder

package com.qf.netty.tcpprotoco;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol msg, ByteBuf byteBuf) throws Exception {
        int len=msg.getLen();
        byte[] content = msg.getContent();
        byteBuf.writeInt(len);
        byteBuf.writeBytes(content);
    }
}

MyByteToLongDecoder2

package com.qf.netty.tcpprotoco;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        int i = byteBuf.readInt();
        byte[] content=new byte[i];
        byteBuf.readBytes(content);

        MessageProtocol messageProtocol=new MessageProtocol();
        messageProtocol.setLen(i);
        messageProtocol.setContent(content);
        list.add(messageProtocol);
    }
}

效果

Client输出

MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用


//下面是客户端开始一个一个的收到服务端的回复
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=1b5286dd-0fc2-4f62-9bf7-d5fad84179b5
客户端接收消息数量=1


MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=653d18cb-ab72-4163-8b95-09c94ecac873
客户端接收消息数量=2


MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=3be6e403-91bb-4437-ada8-6cdb9eb7ef00
客户端接收消息数量=3


MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=94c8f306-fd9c-455a-956c-16698ce4150b
客户端接收消息数量=4


MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=7890de9c-0fa2-4317-8de1-1d464315fa1b
客户端接收消息数量=5

Server输出

MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=1
服务端开始回复消息------
MyMessageEncoder encode 方法被调用


MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=2
服务端开始回复消息------
MyMessageEncoder encode 方法被调用


MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=3
服务端开始回复消息------
MyMessageEncoder encode 方法被调用


MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=4
服务端开始回复消息------
MyMessageEncoder encode 方法被调用


MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=5
服务端开始回复消息------
MyMessageEncoder encode 方法被调用

无论运行几次,Server都是分5次接收的,这样就解决了TCP粘包问题。

Netty 心跳(heartbeat)服务源码剖析

源码剖析目的
Netty 作为一个网络框架,提供了诸多功能,比如编码解码等,Netty 还提供了非常重 要的一个服务-----心跳机制heartbeat。通过心跳检查对方是否有效,这是 RPC 框架 中是必不可少的功能。下面我们分析一下Netty内部 心跳服务源码实现。
在这里插入图片描述
源码剖析

说明

  1. Netty 提供了 IdleStateHandler ,ReadTimeoutHandler,WriteTimeoutHandler 三 个Handler 检测连接的有效性,重点分析 IdleStateHandler .
  2. 如图
    在这里插入图片描述
    3)ReadTimeout事件和WriteTimeout事件都会自动关闭连接,而且,属于异常处理,所以,这里只是介绍以下,我们重点看IdleStateHandler。

IdleStateHandler分析

    private final boolean observeOutput;		//是否考虑出站慢的问题,默认是false
    private final long readerIdleTimeNanos;		//读空闲的时间,单位纳秒,默认为0,禁用
    private final long writerIdleTimeNanos;		//写空闲的时间,单位纳秒,默认为0,禁用
    private final long allIdleTimeNanos;		//读或者写空闲的时间,单位纳秒,默认为0,禁用

handlerAdded方法
当该handler被添加到pipeline中时,则调用initialize方法

 private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        initOutputChanged(ctx);

        lastReadTime = lastWriteTime = ticksInNanos();
        if (readerIdleTimeNanos > 0) {
        //这里的schedule方法会调用eventLoop的schedule方法,将定时任务添加进队列中
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (writerIdleTimeNanos > 0) {
            writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

只要给定的参数大于0,就创建一个定时任务,每个事件都创建。同时,将state状态设置为1,防止重复初始化调用initOutputChanged方法,初始化“监控出站数据属性”。
三个任务类
在这里插入图片描述

这3个定时任务分别对应读,写,读或者写事件。共有一个父类(AbstractIdleTask)。这个父类提供了一个模板方法

 private final class ReaderIdleTimeoutTask extends AbstractIdleTask {

        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
            super(ctx);
        }

        @Override
        protected void run(ChannelHandlerContext ctx) {
            long nextDelay = readerIdleTimeNanos;
            if (!reading) {
                nextDelay -= ticksInNanos() - lastReadTime;
            }

            if (nextDelay <= 0) {
                // Reader is idle - set a new timeout and notify the callback.
                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstReaderIdleEvent;
                firstReaderIdleEvent = false;

                try {
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Read occurred before the timeout - set a new timeout with shorter delay.
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }

说明:
1)得到用户设置的超时时间。
2)如果读取操作结束了(执行了channelReadComplete方法设置),就用当前时间减去给定时间和最后一次读(执操作的时间行了channelReadComplete方法设置),如果小于O,就触发事件。反之,继续放入队
列。间隔时间是新的计算时间。
3)触发的逻辑是:首先将任务再次放到队列,时间是刚开始设置的时间,返回一个promise对象,用于做
取消操作。然后,设置first属性为false,表示,下一次读取不再是第一次了,这个属性在channelRead方
法会被改成rue。
4)创建一个IdleStateEvent类型的写事件对象,将此对象传递给用户的UserEventTriggered方法。完成触
发事件的操作。
5)总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发UserEventTriggered方法。∥前面介绍IdleStateHandler说过,可以看一下

写事件的run方法(即VriterIdleTimeoutTask的run方法)分析

        @Override
        protected void run(ChannelHandlerContext ctx) {

            long lastWriteTime = IdleStateHandler.this.lastWriteTime;
            long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime);
            if (nextDelay <= 0) {
                // Writer is idle - set a new timeout and notify the callback.
                writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstWriterIdleEvent;
                firstWriterIdleEvent = false;

                try {
                    if (hasOutputChanged(ctx, first)) {
                        return;
                    }

                    IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Write occurred before the timeout - set a new timeout with shorter delay.
                writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }

说明:
写任务的代码逻辑基本和读任务的逻辑一样,唯一不同的就是有一个针对出站较慢数据的判断
hasOutputChanged

所有事件的run方法(即AllldleTimeoutTask的rum方法)分析

 @Override
        protected void run(ChannelHandlerContext ctx) {

            long nextDelay = allIdleTimeNanos;
            if (!reading) {
                nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
            }
            if (nextDelay <= 0) {
                // Both reader and writer are idle - set a new timeout and
                // notify the callback.
                allIdleTimeout = schedule(ctx, this, allIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstAllIdleEvent;
                firstAllIdleEvent = false;

                try {
                    if (hasOutputChanged(ctx, first)) {
                        return;
                    }

                    IdleStateEvent event = newIdleStateEvent(IdleState.ALL_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Either read or write occurred before the timeout - set a new
                // timeout with shorter delay.
                allIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }

说明:
1)表示这个监控着所有的事件。当读写事件发生时,都会记录。代码逻辑和写事件的的基本一致:
2)需要大家注意的地方是
long nextDelay allldleTimeNanos;
if (!reading){
∥当前时间减去最后一次写或读的时间,若大于0,说明超时了
nextDelay -ticksInNanos()-Math.max(lastReadTime,last Write Time);
3)这里的时间计算是取读写事件中的最大值来的。然后像写事件一样,判断是否发生了写的慢的情况。

10.小结Nety的心跳机制
l)IdleStateHandler可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会
触发用户handler的userEventTriggered方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关
闭连接。
2)IdleStateHandler的实现基于EventLoop的定时任务,每次读写都会记录一个值,在定时任务运行的时候,
通过计算当前时间和设置时间和上次事件发生时间的结果,来判断是否空闲。
3)内部有3个定时任务,分别对应读事件,写事件,读写事件。通常用户监听读写事件就足够了。
4)同时,IdleStateHandler内部也考虑了一些极端情况:客户端接收缓慢,一次接收数据的速度超过了设置的
空闲时间。Netty通过构造方法中的observeOutput属性来决定是否对出站缓冲区的情况进行判断。
5)如果出站缓慢,Ny不认为这是空闲,也就不触发空闲事件。但第一次无论如何也是要触发的。因为第一
次无法判断是出站缓慢还是空闲。当然,出站缓慢的话,可能造成OOM,OOM比空闲的问题更大。
6)所以,当你的应用出现了内存溢出,OOM之类,并且写空闲极少发生(使用了observeOutput为true),
那么就需要注意是不是数据出站速度过慢。
7)还有一个注意的地方:就是ReadTimeoutHandler,它继承自IdleStateHandler,当触发读空闲事件的时候,
就触发ctx.fireExceptionCaught方法,并传入一个ReadTimeoutException,然后关闭Socket。
8)而WriteTimeoutHandler的实现不是基于IdleStateHandler的,他的原理是,当调用write方法的时候,会
创建一个定时任务,任务内容是根据传入的promise的完成情况来判断是否超出了写的时间。当定时任务根据指
定时间开始运行,发现promise的isDone方法返回false,表明还没有写完,说明超时了,则抛出异常。当write
方法完成后,会打断定时任务。

用 Netty 自己实现简单的RPC

RPC 基本介绍

  1. RPC(Remote Procedure Call)—远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
  2. 两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sp8un8pY-1658216188925)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0021.png?msec=1658148303933)]

过程:

  1. 调用者(Caller),调用远程API(Remote API)

  2. 调用远程API会通过一个RPC代理(RpcProxy)

  3. RPC代理再去调用RpcInvoker(这个是PRC的调用者)

  4. RpcInvoker通过RPC连接器(RpcConnector)

  5. RPC连接器用两台机器规定好的PRC协议(RpcProtocol)把数据进行编码

  6. 接着RPC连接器通过RpcChannel通道发送到对方的PRC接收器(RpcAcceptor)

  7. PRC接收器通过PRC协议进行解码拿到数据

  8. 然后将数据传给RpcProcessor

  9. RpcProcessor再传给RpcInvoker

  10. RpcInvoker调用Remote API

  11. 最后推给被调用者(Callee)

  12. 常见的 RPC 框架有:比较知名的如阿里的 DubboGooglegRPCGo 语言的 rpcxApachethriftSpring 旗下的 SpringCloud

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NEZGy5oX-1658216188926)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0022.png?msec=1658148304059)]

我们的RPC 调用流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LfOUw8QW-1658216188927)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0023.png?msec=1658148303933)]

RPC 调用流程说明

  1. 服务消费方(client)以本地调用方式调用服务
  2. client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
  3. client stub 将消息进行编码并发送到服务端
  4. server stub 收到消息后进行解码
  5. server stub 根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给 server stub
  7. server stub 将返回导入结果进行编码并发送至消费方
  8. client stub 接收到消息并进行解码
  9. 服务消费方(client)得到结果

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

己实现 Dubbo RPC(基于 Netty)

需求说明

  1. Dubbo 底层使用了 Netty 作为网络通讯框架,要求用 Netty 实现一个简单的 RPC 框架
  2. 模仿 Dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.20

设计说明

  1. 创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
  2. 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
  3. 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据
  4. 开发的分析图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwXAMwf2-1658216188927)(file://C:\Users\Administrator\Downloads\netty\image\introduction\chapter_003\0024.png?msec=1658148304061)]

代码

封装的RPC

可以把这块代码理解成封装的dubbo

NettyServer

package com.qf.netty.dubbo.netty;

import com.qf.netty.group.NettyGroupChatInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyServer {
    //端口
    private int port;
    //ip
    private String hostname;

    public static void startServer(String hostname,int port) throws InterruptedException {
        startServer0(hostname,port);
    }

    public static void startServer0(String hostname, int port) throws InterruptedException {
        //bossGroup指定的是一条线程
        //workerGroup默认情况下是核心数*2
        EventLoopGroup bossGroup=new NioEventLoopGroup(1);
        EventLoopGroup  workerGroup=new NioEventLoopGroup();

        //创建服务端的启动对象,设置配置参数
        ServerBootstrap bootstrap = new ServerBootstrap();

        try {
            bootstrap.group(bossGroup, workerGroup)     //设置两个线程组
                    .channel(NioServerSocketChannel.class)      //使用NioServerSocketChannel  作为数据通道
                    .option(ChannelOption.SO_BACKLOG,128)        //设置队列的连接数量
                    .childOption(ChannelOption.SO_KEEPALIVE, true)  //设置保持活动的连接数
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                            ChannelPipeline pipeline = sc.pipeline();
                            pipeline.addLast("decoder",new StringDecoder());
                            pipeline.addLast("encoder",new StringEncoder());
                            pipeline.addLast(new NettyServerHandler());
                        }
                    });
            //绑定端口
            ChannelFuture sync = bootstrap.bind(hostname,port);
            //对关闭通道进行监听
            sync.channel().closeFuture().sync();

        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

NettyServerHandler

package com.qf.netty.dubbo.netty;

import com.qf.netty.dubbo.provider.HelloServiceImpl;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
    	System.out.println("---服务端开始收到来自客户单的消息---");
        System.out.println("接收到来自客户端的信息"+msg);
        String hello = new HelloServiceImpl().hello(msg);
        ctx.writeAndFlush(hello);
    }


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

NettyClientHandler

package com.qf.netty.dubbo.netty;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.util.concurrent.Callable;

public class NettyClientHandler extends SimpleChannelInboundHandler<String> implements Callable {

    private ChannelHandlerContext context;      //上下文
    private String result;              //最后返回的结果
    private String para;                //客户端传入的参数
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channelActive 被调用");
        context=ctx;
    }

    @Override
    protected synchronized void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        result=msg;
        notify();
    }

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

    //被代理对象调用,发送数据到服务器-》wait-》等待被唤醒-》返回结果
    @Override
    public synchronized Object call() throws Exception {
        System.out.println("call1被调用");
        context.writeAndFlush(para);

        wait();
        //等待被唤醒之后,返回结果
        System.out.println("call2被调用");
        return result;
    }

    void setPara(String para){
        System.out.println("set para");
        this.para=para;
    }
}

NettyClient

package com.qf.netty.dubbo.netty;

import com.qf.netty.group.NettyGroupChatClientHandler;
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.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.lang.reflect.Proxy;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NettyClient {

    //创建线程池
    private static ExecutorService executor= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    private static NettyClientHandler client;

    private int count;


    public Object getBean(final Class<?> serivceClass, final String providerName) {

        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{serivceClass}, (proxy, method, args) -> {

                    System.out.println("(proxy, method, args) 进入...." + (++count) + " 次");
                    //{}  部分的代码,客户端每调用一次 hello, 就会进入到该代码
                    if (client == null) {
                        initClient();
                    }

                    //设置要发给服务器端的信息
                    //providerName:协议头,args[0]:就是客户端要发送给服务端的数据
                    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 ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast(new StringDecoder());
                                pipeline.addLast(new StringEncoder());
                                pipeline.addLast(client);
                            }
                        }
                );

        try {
            bootstrap.connect("127.0.0.1", 7000).sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接口HelloService

package com.qf.netty.dubbo.publicinterface;

public interface HelloService {
    String hello(String name);
}

HelloServiceImpl

package com.qf.netty.dubbo.provider;

import com.qf.netty.dubbo.publicinterface.HelloService;

public class HelloServiceImpl implements HelloService {
    private static int count = 0;

    @Override
    public String hello(String name) {
        System.out.println("收到来自客户端的信息"+name);
        if (name!=null){
            return "你好,客户端我已接收到你的消息"+"客户端的消息:"+name+"第" + (++count) + " 次 \n";
        }else{
            return "你好,客户端我已接收到你的消息";
        }
    }
}

ServerBootStrap

package com.qf.netty.dubbo.provider;

import com.qf.netty.dubbo.netty.NettyServer;

public class ServerBootStrap {
    public static void main(String[] args) throws InterruptedException {
        NettyServer.startServer("127.0.0.1",7000);
    }
}

ClientBootStrap

package com.qf.netty.dubbo.consumer;

import com.qf.netty.dubbo.netty.NettyClient;
import com.qf.netty.dubbo.publicinterface.HelloService;

public class ClientBootStrap {
    //这里定义协议头
    public static final String providerName = "HelloService#hello#";
    public static void main(String[] args) throws InterruptedException {
        //创建一个消费者
        NettyClient customer = new NettyClient();

        //创建代理对象
        HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);

        for (;;){
            String hello = service.hello("你好,dubbo~");
            System.out.println("调用的结果"+hello);
            Thread.sleep(2000);
        }
    }
}

调用过程

  1. ClientBootstrap#main发起调用
  2. 走到下面这一行代码后
 HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
  1. 调用NettyClient#getBean,在此方法里与服务端建立链接。

  2. 于是就执行NettyClientHandler#channelActive

  3. 接着回到NettyClient#getBean调用NettyClientHandler#setPara,调用完之后再回到NettyClient#getBean,用线程池提交任务

  4. 因为用线程池提交了任务,就准备执行NettyClientHandler#call线程任务

  5. NettyClientHandler#call中发送数据给服务提供者

context.writeAndFlush(para);

由于还没收到服务提供者的数据结果,所以wait住

  1. 来到了服务提供者这边,从Socket通道中收到了数据,所以执行NettyServerHandler#channelRead,然后因为此方法中执行了
String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
  1. 就去HelloServiceImpl#hello中执行业务逻辑,返回数据给NettyServerHandler#channelReadNettyServerHandler#channelRead再把数据发给客户端

  2. NettyClientHandler#channelRead收到服务提供者发来的数据,唤醒之前wait的线程

  3. 所以之前wait的线程从NettyClientHandler#call苏醒,返回result给NettyClient#getBean

  4. NettyClient#getBeanget()到数据,ClientBootstrap#main中的此函数调用返回,得到服务端提供的数据。

 String res = service.hello("你好 dubbo~");

13.至此,一次RPC调用结束。

效果

ClientBootstrap打印

(proxy, method, args) 进入....1 次
 setPara  
 channelActive 被调用  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第1(proxy, method, args) 进入....2 次
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第2(proxy, method, args) 进入....3 次
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第3(proxy, method, args) 进入....4 次
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第4(proxy, method, args) 进入....5 次
 setPara  
 call1 被调用  
 channelRead 被调用  
 call2 被调用  
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第5

ServerBootstrap打印

服务提供方开始提供服务~~
---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~

---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~

---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~

---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~

---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
posted @ 2022-08-30 22:40  雾托邦  阅读(79)  评论(0编辑  收藏  举报