知识星球项目_API网关中间件设计和实践:问题描述/解决方案/Note

Note:Netty

Netty封装了Java NIO的底层API,同时也提供了一系列扩展和优化,使得开发者可以更加简单高效地进行网络编程开发

同步异步阻塞非阻塞

同步异步

描述的是 请求-响应的方式
同步是 请求方发送请求后,被请求方 处理完请求才向 请求方 发送结果
异步是 被请求方接收到请求方的请求后马上进行相关响应,这样请求方知道对面接受到了自己的请求,返回结果会通过事件回调等机制来获取

阻塞非阻塞

阻塞:请求方一直等待请求返回结果,干等着
非阻塞:边干别的边等

Channel、Thread、EventLoop 和 EventLoopGroup关系


Q:AsynchronousSocketChannel对象的read方法

A:

AsynchronousSocketChannel是Java NIO 2.0中提供的一种异步套接字通道,用于建立异步TCP连接并进行数据交换。其中,read方法是AsynchronousSocketChannel对象中的一个重要方法,用于向服务端发起一个异步读取操作,并在操作完成后进行回调处理。

具体来说,read方法有以下几个参数:

dst:一个ByteBuffer对象,表示用于接收读取到的数据的缓冲区。
timeout:一个long类型的时间值,表示读取操作的超时时间,单位为毫秒。如果在指定时间内未读取到任何数据,操作将会被视为超时,返回失败结果。
unit:一个TimeUnit枚举类型的值,表示超时时间的单位,可以是毫秒、秒、分钟等。
attachment:一个Object类型的值,表示传递给读取操作的附加对象,通常是一个带状态的回调函数(CompletionHandler)对象。读取操作完成后,系统会自动回调CompletionHandler中的completed和failed方法,以便进行相应的数据处理或异常处理。

在调用read方法后,操作不会立即执行,而是被提交到I/O线程池中排队等待执行。当I/O线程从线程池中获取到该操作时,会开始读取网络上的数据并将其写入到dst缓冲区中。如果读取成功,则调用CompletionHandler对象的completed方法,并返回已读取的字节数;如果读取超时或出错,则调用failed方法进行相应的异常处理。需要注意的是,read方法并不保证读取到的数据一定是完整的一段数据,可能会出现数据分包、合包等情况,需要在业务层进行适当的处理。此外,如果需要连续读取多个数据包,可以在CompletionHandler对象中再次提交异步读取操作,以便持续接收数据。


Note: 概念_接口回调

也涉及最简单的服务器架构
首先接口回调的意思是 客户端将消息发给服务器,服务器将原样回传给客户端
如何实现
首先看服务器
一个简单的例子,服务器由WdServer和WdServerHandler组成,WdServerHandler类负责功能的实现

public class WdServerHandler extends ChannelInboundHandlerAdapter{}

里面有三个方法:

  • 将接收到的消息写回客户端(read)
  • 刷新缓冲区(确保所有待发送的消息都被写入SocketChannel中)
  • 关闭连接

接着需要在服务器(WdServer)启动时绑定监听端口,并为每个客户端连接创建一个WdServerHandler对象,以便处理该连接上的消息
需要做的:设置要监听的端口地址、绑定端口,并同步等待操作完成、同步等待服务器信道关闭结束后释放资源等等
并在main方法启动

再看客户端,一样是有WdCilent和WdCilentHandler
WdCilent:
要有一个异步套接字通道 …SocketChannel
向指定地址和端口号的服务器发起连接请求,并返回连接操作的结果
等待连接操作完成,阻塞当前线程直到连接成功或失败
读取操作,保存得到的数据,以及WdClientHandler对象用于处理读取完成事件

NIO、BIO、AIO写服务端和客户端的区别

服务端:

BIONIOAIO
服务端需要为每一个客户端连接创建一个线程来处理每个连接上注册一个SelectionKey,然后轮询这些SelectionKey来处理读写事件AIO的读取和写入操作都是由操作系统完成
服务端通过ServerSocket.accept()方法监听客户端连接请求,当有连接请求到达时,accept()方法会返回一个新的Socket对象,该Socket即为与客户端建立的通信管道服务端需要通过ServerSocketChannel来接受客户端的连接请求服务端需要使用AsynchronousServerSocketChannel来接受客户端的连接请求,并在接受完成后调用accept方法等待下一次连接
服务端可以通过该Socket进行数据读写NIO中的读取和写入操作都是由服务端主动发起的,服务端需要准备好读取和写入的缓冲区,然后在对应的通道上调用read或write方法,将数据读取到缓冲区或从缓冲区中写出数据每个客户端连接都会被封装为AsynchronousSocketChannel对象
需要建立一个或多个ServerSocket来监听客户端的连接请求AIO的读写操作是异步的,通过回调的方式在读写完成时通知应用程序进行后续处理
每个接收到的客户端请求,都需要通过一个阻塞式IO的Socket.getInputStream()方法获取该Socket的输入流来进行数据读取

客户端:
根据不同I/O模型的特点,AIO、BIO和NIO的客户端在代码编写上也存在一些区别。

对于BIO(Blocking I/O),客户端的编写主要包括Socket的创建、连接、读取和写入操作。常见的做法是创建一个Socket对象,通过该对象进行连接远程服务器,并使用Socket.getInputStream()和Socket.getOutputStream()方法来获取数据输入流和输出流实现数据的读取和写入。由于BIO的阻塞式I/O模型,当客户端进行I/O操作时,会一直阻塞等待,直到数据准备就绪或者超时才会继续执行。因此,在代码编写上,需要考虑到可能会发生长时间的阻塞等待,需要使用多线程编写代码,针对每个Socket开启一个独立的线程来处理I/O操作。

对于NIO(Non-blocking I/O),客户端的代码编写主要涉及channel的创建、连接、读写操作以及Selector的使用。在NIO中,客户端需要使用SocketChannel或者DatagramChannel替代传统的Socket和DatagramSocket,使用Selector来同时监听多个Channel的就绪状态,从而可以做到异步非阻塞的I/O操作。在客户端连接时可以使用SocketChannel.connect()方法完成连接操作。当客户端进行读写操作时,调用SocketChannel.read()和SocketChannel.write()方法进行数据的读取和写入操作。相比于BIO,NIO是异步非阻塞的I/O模型,在代码编写上不需要对每个Socket开启单独的线程来处理I/O操作。

对于AIO(Asynchronous I/O),客户端代码编写主要包括AsynchronousSocketChannel的创建、连接以及读写操作。客户端使用AsynchronousSocketChannel替代传统的Socket对象,使用connect方法完成连接操作。当客户端进行读写操作时,调用AsynchronousSocketChannel.read()和AsynchronousSocketChannel.write()方法进行数据的读取和写入操作。相比于BIO和NIO,AIO是基于回调机制的异步I/O模型,在代码编写上更加简洁,能够更好地处理高并发的I/O操作。

Channel

代表一个到实体(如一个硬件设备、文件、网络套接字或一个能够执行一个或多个不同I/O操作的软件组件)的开放连接,如一个打开的文件或网络连接。每个Channel都有一个与之相关联的EventLoop,当Channel注册到EventLoop时,就可以开始进行I/O操作。

在Netty中,Channel是所有I/O操作的核心组件,通过它我们可以对底层数据进行读写操作,并与客户端或服务器进行交互。每个Channel都可以被分配一个或多个ChannelHandler对象,这些Handler会被添加到该Channel的Pipeline中,用于对入站和出站数据进行处理。
方法:pipeline():获取与Channel相关联的Pipeline对象。

ChannelInitializer

ChannelInitializer 是 Netty 中的一个抽象类,用于初始化每个新连接创建的 channel。它通常被用作服务端启动器(ServerBootstrap)的 handler() 或客户端启动器(Bootstrap)的 handler() 方法中的参数传递。

当一个新的连接建立时,Netty 会自动为其创建一个 channel,并将该 channel 加入到事件循环器中,然后再通过 ChannelInitializer 的实现类对新建的 channel 进行初始化。通常,ChannelInitializer 的实现类会将一些 handler 添加到 channel pipeline 中,比如编码/解码 handler、消息处理 handler 等等。这些 handler 将在后续的 channel 事件中被执行,用于处理收到的消息、发送消息和错误等情况。

同时,ChannelInitializer 还可以通过调用 channel.pipeline() 方法获取到 channel 对应的 pipeline,然后可以对该 pipeline 进行配置,比如添加自定义的 handler,修改 handler 的顺序等等。

需要注意的是,ChannelInitializer 仅在新的 channel 被创建时被调用一次,因此它通常只用于一些初始化操作,不负责后续的处理。如果需要进行长时间的业务逻辑处理,一般需要将任务放入 eventLoop 中异步执行,以免阻塞 IO 线程。

eg.ChannelInitializer< SocketChannel >

ChannelInitializer< SocketChannel >是用于初始化SocketChannel的ChannelInitializer子类。它重写了initChannel(SocketChannel ch)方法,该方法在SocketChannel被注册时会被调用,我们可以在该方法中进行Channel的初始化操作,比如添加编解码器、添加业务逻辑处理等

channel.pipeline().addLast(new MyServerHandler());

向 ChannelPipeline 中添加一个 StringDecoder
ChannelPipeline是 Netty 中处理事件的 Handler 链,它负责在事件传播过程中管理和调用相应的 ChannelHandler。当有事件发生时,ChannelPipeline 将事件沿着 Handler 链进行传递,直到某个 Handler 处理成功或者到达链尾。
StringDecoder是Netty提供的一个解码器,用于将字节流解码成字符串

在这个示例中,我们向 ChannelPipeline 中添加了一个 StringDecoder,它可以将消息的二进制字节流解码成字符串,方便后续业务逻辑的处理。当 ChannelPipeline 中的 Handler 链路接收到字节数组时,StringDecoder 会将其转换成 Java 字符串对象,以便后续的 ChannelHandler 处理。
总之,通过向 ChannelPipeline 中添加不同的 Handler,我们可以对事件进行编解码、加解密、压缩解压等操作,从而实现完整的网络通信过程。

Channel、ChannelHandler和ChannelPipeline/Netty简单工作原理

ChannelHandler
ChannelHandler是Netty中用于处理事件的基本单元,它可以拦截进来的事件或者修改事件传播的顺序。在Netty中,每个ChannelHandler都有自己的职责,例如数据编解码、消息处理、异常处理等等。
ChannelHandler主要有两种类型:入站和出站Handler。入站Handler处理从远程节点传输到本地节点的数据,而出站Handler则处理从本地节点传输到远程节点的数据。

ChannelPipeline
ChannelPipeline是Netty中用于管理ChannelHandler的链表结构,它负责对入站和出站事件进行流式处理。在Netty中,每个Channel都绑定有一个唯一的ChannelPipeline。当有事件触发时,该事件会从Channel的入站方向开始传播,经由一系列入站的Handler处理后,再通过出站方向依次传播,经由出站的Handler处理后再发回给客户端。
Netty的工作原理基于事件驱动模型,即Netty会不断轮询线程池中的事件,当有事件发生时,Netty会触发对应的事件处理器来处理该事件。在Netty的工作流程中,事件被分为两类:入站事件和出站事件。例如,一个连接请求就是一个入站事件,在连接建立成功后,服务器响应客户端的数据传输请求就是一个出站事件。

整个工作流程可以简单概括为:Channel 接收事件 -> 传递给 ChannelPipeline -> 经过一系列 ChannelHandler 处理 -> 传递给目标 Channel 或发送出去。

Channel如何接受事件和传递给管道

当Channel接收到一个事件,例如一个数据包时,它会触发一个对应的事件类型,例如ChannelRead事件,然后将该事件交由ChannelPipeline中的第一个ChannelHandler进行处理。这个Handler处理完之后,如果需要继续传递该事件,就会调用 ChannelHandlerContext.fireChannelRead(Object msg) 方法,将事件从当前Handler传递到下一个Handler进行处理。

这样,事件依次经过ChannelPipeline中的所有Handler,最终得到了处理或者被丢弃。值得注意的是,从ChannelHandlerContext传递事件总是从当前Handler处理器开始,所以出站事件需要从链表的末端向前传递,入站事件则相反,需要从链表的头部向尾部传递。

Q:什么是字节码

字节码是一种用于描述 Java 类型的二进制格式。每个 Java 类型都会被编译成字节码,这些字节码可以在任何支持 Java 虚拟机 (JVM) 的平台上运行。Java 字节码被设计为高度可移植的,因此程序只需要编译一次,就可以在任何支持 Java 的操作系统上运行。

Java 字节码由一组指令集组成,这些指令集被称为 JVM 汇编语言。JVM 的指令集是在设计时就考虑到了安全性、可移植性和高效性等因素。JVM 可以根据字节码执行指令,以实现 Java 应用程序的功能。

字节码包含操作码和操作数等信息,它们被组织成一个类似于汇编语言的指令序列。
通过使用命令 javap -c 来查看该类的字节码内容

Netty基础_NettyServer字符串解码器

在实际开发中,server端接收数据后我们希望他是一个字符串或者是一个对象类型,而不是字节码,那么;
在netty中是否可以自动的把接收的Bytebuf数据转String,不需要我手动处理? 答;有,可以在管道中添加一个StringDecoder
在网络传输过程中有半包粘包的问题,netty能解决吗? 答:能,netty提供了很丰富的解码器,在正确合理的使用下就能解决半包粘包问题。
常用的String字符串下有什么样的解码器呢? 对于String的有以下常用的三种:
LineBasedFrameDecoder 基于换行
DelimiterBasedFrameDecoder 基于指定字符串
FixedLengthFrameDecoder 基于字符串长度

channel.pipeline().addLast(new LineBasedFrameDecoder(1024));

LineBasedFrameDecoder解码器是基于换行符号的解码器,它可以将按行划分的ByteBuf数据自动解码为字符串。在此处设置了最大长度为1024,表示每行解码的最大长度不超过1024个字节

 channel.pipeline().addLast(new StringDecoder(Charset.forName("GBK")));

接下来,StringDecoder解码器将LineBasedFrameDecoder已经将ByteBuf数据解码为字符串后的ChannelBuffer转换成字符串,指定了解码格式为GBK编码格式,如果是UTF-8编码格式可以修改为Charset.forName(“UTF-8”)

写自己的Handler_ChannelInboundHandlerAdapter

ChannelInboundHandlerAdapter是Netty提供的一个ChannelInboundHandler的适配器类,实现了ChannelInboundHandler接口的所有方法,并对这些方法进行了空实现。因此,我们可以通过继承ChannelInboundHandlerAdapter类来定制自己的ChannelInboundHandler,只需要重写我们需要的方法即
里面有 void channelActive(ChannelHandlerContext ctx) 方法
channelActivie意思是:当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
举一反三,那就还有inactive、read等

netty收发数据

//ChannelHandlerContext ctx
ByteBuf buf = Unpooled.buffer(str.getBytes().length);
buf.writeBytes(str.getBytes("GBK"));
ctx.writeAndFlush(buf);

ByteBuf是Netty中的一个缓冲区类
Unpooled.buffer()方法创建一个新的ByteBuf对象,其中的参数str.getBytes().length表示ByteBuf缓冲区的大小
通过buf.writeBytes(str.getBytes(“GBK”))方法,将指定编码格式下的字符串str写入到ByteBuf
通过ctx.writeAndFlush(buf)方法将封装好的ByteBuf对象发送到客户端,并将数据刷新到远程节点。其中ctx是Netty的一个ChannelHandlerContext对象,表示通道处理器上下文,通过它可以获取到对应的Channel对象以及其他一些相关的信息

NIO非阻塞IO

Java NIO是一种非阻塞I/O模型,即其提供了异步非阻塞的I/O操作方式,与传统的阻塞I/O有所区别。

在传统的阻塞I/O模型中,线程需要等待I/O操作完成后才能继续执行下一条指令,称之为“同步阻塞”。例如,在使用InputStream读取文件时,如果当输入流中无数据可读时,则read()方法会被阻塞,直到有数据可读时再返回

而NIO则引入了“事件驱动”模型,通过Selector机制进行非阻塞I/O操作。一个Selector对象可以管理多个通道,从而实现多路复用。当某个通道上的数据准备好时,Selector会立即返回,通知程序去进行I/O操作,这样就可以避免线程长时间阻塞并等待I/O操作完成,提升了系统响应速度和处理效率。

为什么Netty是非阻塞的
Netty NIO的不阻塞I/O模型主要是基于以下两个方面实现的:

Channel非阻塞模式

Netty使用java.nio.Channel来进行通信,通过将Channel设置为非阻塞模式(channel.configureBlocking(false)),可以使得读取操作变成非阻塞的。在非阻塞模式下,当读取缓存区中没有数据时,read()方法会立即返回0 ,而不是等待数据到来。这样就避免了读取操作被阻塞的情况。

Selector轮询机制

在Netty中,使用Selector来进行异步事件轮询,当有事件触发时(包括连接、读取、写入等),Selector 会通知应用程序进行相应的处理,而不需要等待事件触发。Selector采用的是NIO的异步非阻塞I/O模型,可以同时处理多个Channel的事件,从而提高了应用程序的并发性能。

在实际应用中,Netty还使用了内存池技术,通过管理ByteBuf缓冲区的方式避免了阻塞的情况。ByteBuf是Netty专门开发的一种内存池,可以进行零拷贝、直接内存等优化,从而提供了更高效的读取和写入性能。当有数据可读时,Netty会自动将数据读入到ByteBuf缓冲区中,而不需要等待数据到来,避免了阻塞的情况

解码编码

StringDecoder 解码
StringEncoder 编码
进行字符串解码,这样我们在收取数据就不需要手动处理字节码。
进行字符串编码,用以实现服务端在发送数据的时候只需要传输字符串内容即可

消息群发_例子

首先要创建一个静态的ChannelGroup,相当于一个群聊,假设有多个群可以创建ConcurrentHashMap
设置为静态变量,意味着它可以被该类的所有实例共享,而不是每个实例都分别创建一个 channelGroup,这样就避免了重复创建的问题
通常会在服务端的 ChannelInitializer 中创建一个 channelGroup 对象,并在每个新的连接建立时将其加入到 channelGroup 中

 @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //当有客户端链接后,添加到channelGroup通信组
        ChannelHandler.channelGroup.add(ctx.channel());

客户端

在客户端场景中,通常只需要使用一个 workerGroup 即可。该 workerGroup 用于处理客户端与服务端之间传输的数据,其内部包含多个 NIO 线程,每个线程都维护着一个独立的 Selector 对象,用于监听客户端与服务端之间的通信事件,并处理相应的事件回调函数

通过调用 connect() 方法启动客户端,并将客户端的连接请求发送到服务端的 IP 地址和指定的端口

基础08_NettyClient半包粘包处理、编码解码处理、收发数据方式

Q:什么是半包粘包

在网络通信中,由于 TCP 协议将数据划分为一个个的数据包进行传输,在数据包到达接收端时,可能存在多个数据包合并成一个数据包的情况,被称为“粘包”;或者将一个数据包拆分成多个数据包传送,被称为“半包”。

Q:为什么会出现半包粘包

粘包和半包的出现是由于 TCP 的自适应发送机制,以及内核接收缓冲区和用户进程缓冲区之间的协作。TCP 的自适应发送机制可以根据网络状况和流量控制等因素动态调整数据的发送和接收窗口,试图在发送效率和可靠性之间达到一个平衡点。而内核接收缓冲区和用户进程缓冲区之间的协作则会引入一定的延迟和不确定性,进一步增加了粘包和半包的发生概率。

Q:如何解决半包粘包

为了解决粘包和半包的问题,在网络通信中常使用的方法是添加消息边界或长度限制,即在每条消息的头部添加消息长度等元信息,以便接收方正确分离每条消息,并按顺序组装完整的消息。
另外,还可以使用 Netty 提供的编解码器等工具,在应用层面上对消息进行处理和封装,以充分利用 TCP 协议的优势,并提高网络通信的可靠性和效率。

Q:内核接收缓冲区和用户进程缓冲区是如何协作的

当远端主机向本地主机发送数据时,内核通过一系列网络协议层的处理,最终将数据放置在内核接收缓冲区中。此时,内核会向用户进程发出通知,告知其有新数据到达,需要将数据读取到用户进程缓冲区。

由于内核接收缓冲区和用户进程缓冲区的位置和结构存在差异,因此需要通过一定的协作来进行数据的传输。具体来说,当用户进程调用 recv() 等系统调用时,内核会将内核接收缓冲区中的数据复制到用户进程缓冲区中,并返回复制的字节数,同时将内核接收缓冲区中的数据删除。

在复制数据时,一般会采用“零拷贝”技术,即将数据从内核空间直接复制到用户空间,避免了数据的重复拷贝,提高了数据复制的效率。此外,为了避免数据的丢失或重复传输等问题,内核还会使用一些技术如滑动窗口、确认响应等来保证传输的可靠性。

总的来说,内核接收缓冲区和用户进程缓冲区之间的协作是通过系统调用实现的,在协同工作过程中,内核负责数据的接收和存储,用户进程负责数据的读取和处理。

在写服务端的时候也添加了解决半包粘包的处理方法(但我不知道那是)
:向服务端添加解码器和添加业务逻辑处理器都属于解决半包粘包的操作。

  • **解码器:**在服务端添加解码器,可以通过解析消息头中的长度信息,判断每条消息的边界并将其拆分成完整的消息。常用的解码器有FixedLengthFrameDecoder、LineBasedFrameDecoder、DelimiterBasedFrameDecoder和LengthFieldBasedFrameDecoder等,在使用时需根据具体的业务需求选择合适的解码器。
  • **业务逻辑处理器:**添加业务逻辑处理器,可以对接收到的数据进行解码、处理和回写等操作,通常需要重写channelRead()方法,在该方法中进行数据读取、处理和回写等操作。

总结:客户端和服务端的配置(需要什么)

  • 设置工作组线程,调用group()方法设置客户端工作线程组,指定Netty应该使用哪个EventLoopGroup处理客户端的I/O事件和任务
  • 指定客户端的Channel类型,调用channel()方法设置这个客户端要使用的Channel实现类
  • option()方法配置指定的Channel选项,比如AUTO_READ选项表示自动读取数据
  • 添加ChannelHandler,调用handler()方法添加一个新的ChannelHandler,即编写自己的ChannelInitializer类

Channel类型有哪些,各有什么区别

在Netty中,常见的Channel类型有以下几种:

NioSocketChannel:是基于Java NIO库实现的客户端Socket通道,支持异步非阻塞模式。
NioServerSocketChannel:是基于Java NIO库实现的服务端Socket通道,用于监听进来的TCP连接,并把连接转发给业务处理线程。
NioDatagramChannel:是基于Java NIO库实现的UDP通道,用于支持UDP协议及相关应用的通信。
EmbeddedChannel:嵌入式通道,用于测试处理器实现的通道。

区别在于使用场景和主要特点两个方面,简而言之就是用在TCP和UDP

09_自定义编码解码器,处理半包、粘包数据

markReaderIndex()标记位置,resetReaderIndex()重新回读

使用markReaderIndex()方法将当前的读取位置进行标记,当需要重新读取之前标记位置的数据时,可以使用resetReaderIndex()方法将读取位置重置到之前标记位置的位置,重新读取数据
在使用 markReaderIndex() 和 resetReaderIndex() 方法时,应该使用 ByteBuf.isReadable() 方法进行判断,以确保能够安全地重读数据

⭐解码器

这个解码器最后会作为一个Handler加入管道
channel.pipeline().addLast(new MyDecoder());

需要一个数据包的基本长度(读取消息的长度)
开始我们的解码方法:
decode(… ByteBuf in, List< Object > out)
关于参数:对传入的 ByteBuf 进行解码操作,将解码后的消息添加到 out 列表中
对消息内容进行解析,并将解析出来的消息内容添加到 out 集合

  1. 先判断进来的数据够不够规定的读取消息的长度
// BASE_LENGTH =4
if (in.readableBytes() < BASE_LENGTH) {
            return;
        }
  1. 判断剩余的可读取字节数是否小于等于1个字节,小于的话就不够解析(无法组成完整数据包),要直接返回
int readableCount = in.readableBytes();
        if (readableCount <= 1) {
            in.readerIndex(beginIdx);
            return;
        }
  1. 根据长度域的定义读取消息(长度域也占用字节,先读取长度域的字节,再把后面的规定的字节作为整体解析出来)
//长度域占4字节,读取int
        ByteBuf byteBuf = in.readBytes(1);
        String msgLengthStr = byteBuf.toString(Charset.forName("GBK"));
        int msgLength = Integer.parseInt(msgLengthStr);
  1. 判断剩余的可读取字节数是否大于等于消息内容长度加上1个字节(这里的消息结尾标识占用的一个字节),如果小于就代表数据流中的消息内容不完整,需要更多的数据到来,需要恢复上次标记的读指针并返回
readableCount = in.readableBytes();
        if (readableCount < msgLength + 1) {
            in.readerIndex(beginIdx);
            return;
        }
  1. 根据消息长度读取消息的内容部分,根据结尾标识判断消息内容是否完整,没有识别到结尾标识就会还原读指针位置,等下次数据到来并重新解析。识别到了就添加再out集合
ByteBuf msgContent = in.readBytes(msgLength);

        //如果没有结尾标识,还原指针位置[其他标识结尾]
        byte end = in.readByte();
        if (end != 0x03) {
            in.readerIndex(beginIdx);
            return;
        }

        out.add(msgContent.toString(Charset.forName("GBK")));

Q:对5. 为什么没有读到结尾标识就要还原指针位置

由于网络延迟或其他原因,每次读取到的数据可能只是完整数据包的一部分。为了确保解析出来的数据是完整的,我们需要等待后续数据的到来再进行解析,继续等待后续数据的到来才能组成一个完整的消息

Q:读指针一般标记在哪个位置,举例说明

假设我们正在接收一个消息,它的消息体长度为 10,消息内容为 “HelloWorld”,并且消息的结尾标识为
\r\n。在网络传输过程中,这个消息被分成两个 TCP 数据包进行传输,每个数据包的大小为 6 和 7 字节。因此,第一个 TCP
数据包包含了消息体长度的前两个字节以及消息体的前四个字母,第二个 TCP 数据包包含了消息体的后六个字母和结尾标识 \r\n。

在接收端,我们使用 Netty 编写的解码器对接收到的数据进行解码和处理。假设在第一个 TCP 数据包到达时,解码器发现消息体的长度为
10,并且读取了前四个字母 “Hell”。此时,读指针的位置为 6(已经读取的字节数),并且 beginIdx 的值也为 6(即
in.readerIndex() + 1)。

接着,第二个 TCP 数据包到达,并且解码器将其与之前读取的数据拼接在一起。由于第二个 TCP 数据包包含了结尾标识
\r\n,解码器可以正确地解析出完整的消息,并且读指针的位置会移动到消息体的结尾标识位置。

然而,如果第二个 TCP 数据包没有包含结尾标识 \r\n,解码器就会判断消息没有接收完整,此时就会调用
in.readerIndex(beginIdx) 将读指针恢复到刚刚处理消息长度时的位置
6,并且等待后续数据包的到来,直到完整的消息被拼接出来。

Q:假设没有读取到结尾标识,前面读取的数据包放哪

一个数据包读取的数据会被保存在 ByteBuf 对象的读缓冲区(read buffer)中。读缓冲区是一个字节数组,它的初始容量会根据指定的缓冲区大小进行初始化。当数据被读取到缓冲区中时,读指针会随之移动,标记已经读取的字节数。
当第二个数据包到达时,解码器会将其与读缓冲区中的数据进行拼接,并且再次尝试解析消息的完整性。如果仍然无法构成完整的消息,就会继续等待后续数据包到达,并且将新到达的数据包中的数据存放到 ByteBuf 对象的读缓冲区中。直到能够构成完整的消息为止。

10_处理入站数据和出站数据

使用这两个抽象类 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter
区别在于:
1.一个处理入站(读取数据、连接建立),一个处理出站(数据发送、连接关闭、写操作完成)
2.执行顺序不同:在Pipeline中,入站数据会从头部开始经过入站处理器进行处理,而出站数据则从尾部开始流经出站处理器进行处理
3. 根据上面的概念,入站的常见放法有:channelActive(ChannelHandlerContext ctx): Channel已经处于活动状态,可以进行读写操作、channelRead//从Channel中读取到数据时调用。出站常见有:bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise): 绑定本地地址,write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise): 向Channel中写入数据等

关于read

ChannelHandlerContext ctx

触发当前Channel的读取操作。具体而言,ctx.read()方法会将一个读取任务添加到EventLoop的任务队列中,由EventLoop在下一个周期中调度执行。

在网络通信中,当接收缓冲区中有新的数据到达时,NIO EventLoop线程会调用read()方法来把数据从网络中读入到内存中,并且会触发相应的入站事件,最终被传递给Pipeline中的下一个入站处理器进行处理。

具体来说,Netty使用NIO Selector机制来实现非阻塞IO,并通过ChannelHandlerContext对象来获取当前Channel所绑定的NIO EventLoop线程。当ChannelHandlerContext对象的read()方法被调用时,它会将读取事件注册到NIO Selector中,当对应的通道可读时,会触发NIO EventLoop线程的select()方法返回,然后继续执行read()方法,最终将数据读取到内存中

12_基于Netty搭建的Http服务

概念

Socket服务

在Socket通信模型中,客户端和服务器端通过TCP或UDP协议进行数据交换。
以TCP为例,Socket服务工作流程为:
服务器端建立一个Socket服务,指定IP地址和端口号,开始监听客户端的连接请求;
客户端根据服务器端的IP地址和端口号向服务器发起连接请求;服务器接收到客户端的连接请求后,建立一条与客户端的连接,双方开始进行数据交换。

HTTP服务

HTTP(超文本传输协议)是一种应用层协议,主要用于在Web应用程序中传输数据。使用HTTP协议的应用程序在客户端和服务器端之间进行数据传输。

HTTP服务工作流程如下:客户端向服务器发送一个HTTP请求,包括请求方法、请求头和请求体等信息;
服务器接收到请求后,根据请求内容生成响应并返回给客户端,响应包括响应状态码和响应正文等信息;
客户端接收到服务器返回的响应,根据响应状态码判断是否成功,并处理响应正文。

HTTPS服务

HTTPS是基于HTTP协议的安全传输协议,它使用SSL或TLS协议对传输的数据进行加密,提高了数据传输的安全性。

HTTPS服务工作流程如下:客户端向服务器发起连接请求,服务器返回一个证书给客户端;
客户端利用证书中的公钥加密一个随机数并发送给服务器,服务器利用私钥解密得到这个随机数;
客户端和服务器使用这个随机数生成一个共享的密钥,用于加密和解密数据;之后客户端和服务器之间的所有数据传输均使用该密钥进行加密和解密。

区别:

Socket服务和HTTP/HTTPS服务不同,Socket是一种通用的数据传输方式,可以用于任何类型的数据传输
而HTTP和HTTPS是基于应用层的协议,主要用于Web应用程序中的数据传输。

HTTP和HTTPS协议的交互是“请求-响应”模式,客户端通过HTTP请求向服务器请求数据,服务器返回HTTP响应。
而Socket是双向的,客户端和服务器可以相互发送和接收数据。

HTTPS通过SSL或TLS协议对传输的数据进行加密,提高了数据传输的安全性。而HTTP是明文传输,安全性较低。

HTTP和HTTPS服务需要通过Web服务器(如Apache、Nginx等)来提供服务,而Socket服务只需要在代码中实现即可。

FullHttpResponse

用于封装HTTP响应信息。它实现了HttpResponse和HttpContent两个接口,因此可以同时包含响应头部和响应正文等信息

DefaultHttpRequest和LastHttpContent有什么区别

它们之间的区别在于:DefaultHttpRequest主要用于表示HTTP请求的头部和请求行等信息,并不包含HTTP正文的内容;而LastHttpContent则主要用于表示HTTP请求或响应的最后一部分内容,通常包含HTTP正文的最后一部分。

需要注意的是,对于包含完整的HTTP请求或响应消息,通常需要将DefaultHttpRequest和所有的HttpContent对象(包括LastHttpContent)按照顺序组合起来才能得到完整的消息体。

Netty框架下代码实现

Server和之前一样,还是try_catch_finally 配置服务端NIO线程组
MyChannelInitializer也和之前一样,初始化是往piple里面添加解码编码以及自己定义的handler

自定义handler不同:

  • ChannelRead(当接收到数据时,会触发该方法的执行)
    总结:
    对HTTP请求和HTTP正文进行了不同的处理
    对于HTTP请求,该方法打印了请求URI和HTTP请求内容
    对于HTTP正文,则将其转换为字符串并输出

具体实现:

/**
DefaultHttpRequest是Netty中的一个实现了HttpRequest接口的类,
表示HTTP请求的头部和请求行等信息。它包含了请求方法(GET、POST等)、URI、HTTP版本、请求头部等属性
*/
if (msg instanceof HttpRequest) {
            DefaultHttpRequest request = (DefaultHttpRequest) msg;}

/**
LastHttpContent是Netty中的一个实现了HttpContent接口的类,表示HTTP请求或响应的最后一部分内容,即HTTP消息的结束标记。
在HTTP响应中,它包含了响应正文的最后一部分;在HTTP请求中,它只包含请求头部和请求行等信息
*/

if (msg instanceof HttpContent) {
            LastHttpContent httpContent = (LastHttpContent) msg;

响应部分:

FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                Unpooled.wrappedBuffer(sendMsg.getBytes(Charset.forName("UTF-8"))));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        ctx.write(response);
        ctx.flush();
HttpVersion.HTTP_1_1:表示HTTP协议的版本号。
HttpResponseStatus.OK:表示HTTP响应状态码,这里是200,表示请求成功。
Unpooled.wrappedBuffer(sendMsg.getBytes(Charset.forName("UTF-8"))):将响应正文sendMsg按照UTF-8的编码方式转换成字节数组并封装成一个ByteBuf对象。ByteBuf是Netty中的一个缓冲区,用于处理各种类型的数据。

使用response.headers()获取响应头部,并设置Content-Type、Content-Length和Connection等属性,分别表示响应正文的类型、长度和保持连接方式。最后,使用ctx.write(response)将响应写回到客户端。注意,本次写入并未发送到对端,需要调用ctx.flush()在当前时间点写入并发送到对端节点。

  • channelReadComplete
  • 当读取数据完成时,会触发该方法的执行。该方法将缓存中的数据刷新到远程节点上

API网关接口

Start

概念

RPC

Nginx:
HTTP和反向代理服务器,采用了事件驱动的异步非阻塞处理方式

反向代理:反向代理(Reverse Proxy)是指客户端不直接访问后端服务器,而是通过中间的代理服务器来访问。代理服务器负责将请求转发给后端的真实服务器,并将响应结果返回给客户端。这种方式可以隐藏真实服务器的IP地址、提高安全性、优化访问速度等。

负载均衡:负载均衡(Load Balancing)是指将请求分摊到多个后端服务器上,以避免单个服务器负载过重而导致性能下降或故障。

反向代理的实现:
配置upstream模块:在Nginx配置文件中定义一个upstream块,指定后端的服务器列表,也可以设置权重和健康检查等参数。
配置location模块:在Nginx配置文件中定义一个location块,指定反向代理的目标地址,例如反向代理到后端的应用服务器上。
配置proxy_pass指令:在location模块中配置proxy_pass指令,指定反向代理的目标地址,例如http://127.0.0.1:8000。

Nginx 和 Netty 的区别

Nginx和Netty都是网络框架
Nginx是一个HTTP服务器和反向代理服务器,主要应用于Web服务器、负载均衡、反向代理等领域
Netty是一种基于Java的异步事件驱动网络应用框架,主要应用于网络客户端和服务端开发。Netty采用了基于Channel的事件模型,支持多种协议(TCP/UDP/HTTP/WebSocket)和编解码器

Channel的事件模型包含以下组件:
Channel:表示一个网络连接或者一个传输协议的一部分。
EventLoop:表示一个执行循环,用于监听和处理Channel发生的事件。
ChannelHandler:表示一个处理器,用于处理Channel上的数据读写事件和状态变更事件等。
ChannelPipeline:表示一个处理器链,用于将多个处理器组合起来按顺序处理Channel上的事件。

为什么要搞这个API网关接口
既可以满足RPC服务的注册,同时又能提供相关HTTP请求协议的转发

01

概念:泛型

泛型是 Java 中的一种特性,用于在定义类、接口和方法时指定类型参数。所谓泛型,就是不指定具体数据类型,在使用时再确定数据类型

boss、worker

Netty 框架下的 HTTP 服务端需要有:
EventLoopGroup:用于处理 I/O 操作的多线程事件循环组
一般创建一个 EventLoopGroup boss 和 EventLoopGroup worker
区别在:bossGroup 主要负责接收客户端连接请求并进行初步的分配,而 workerGroup 则负责具体的 I/O 操作

插入一条

Docker 使用(除了自带desktop 还可以用portainer)

概念:

镜像image

是一个软件包,包含了应用程序运行所需的所有文件和配置信息,每个镜像都是只读的
镜像是根据 Dockerfile 文件构建而成的,Dockerfile 是一个文本文件,其中包含了构建镜像的指令和参数
构建镜像后通过命令运行:
docker run -d -p 8000:8000
意思是:将容器内部的 8000 端口映射到主机的 8000 端口

将容器内部的端口映射到主机上的端口,是为了让主机可以通过这个端口访问到容器中运行的应用程序。
在 Docker 中,每个容器都有自己的网络命名空间和 IP 地址,它们之间相互隔离。如果只是在容器内部运行应用程序,其他的计算机或者服务是无法直接访问该应用程序的。
因此,需要将容器内部的端口与主机上的端口进行映射,并将流量从主机端口转发到容器内的端口。这样,主机就可以通过指定的端口连接到运行在容器内的应用程序,从而实现了容器内外通信的目的。
具体来说,在本例中,容器内部的 8000 端口被映射到主机的 8000 端口上,这意味着任何连接到主机 IP 地址的客户端,都可以通过访问主机的 8000 端口来访问到该容器内运行的应用程序。

image 和 container 有什么区别

容器是镜像的动态实例
具体来说,镜像是一个只读的模板,包含了运行应用程序所需的所有代码、库文件和依赖项等信息。它基本上相当于一个独立的操作系统环境,可以被用来创建容器,而容器则是由镜像创建的运行实例,是用户的一个可写的单独环境


多个容器之间可以通过网络方式进行关联和通信。假设自己写的应用程序容器中需要使用 Redis、MySQL 等服务时,可以通过容器链接或者自定义网络的方式,将这些服务容器与应用程序容器进行关联
使用容器链接 或者 创建一个自定义网络,并将 Redis、MySQL 等服务容器和应用程序容器加入到该网络中。这样,它们就可以直接通过容器名称或者 IP 地址相互通信

02_代理RPC的泛化调用

the point:
给网关接口绑定对应的RPC服务,建立代理关系封装RPC泛化调用
这样调用网关接口 就会调用到 对应的RPC服务接口上 并返回对应的数据

代理模式
分为静态代理和动态代理
静态代理:
举例:IService 和他的实现类 ServiceImpl
它的代理:ServiceProxy,在里面private一个Service接口变量,在自己的方法中调用那个接口变量的方法(一般取的方法名都一样)
动态代理:
还是IService 和他的实现类 ServiceImpl
ServiceHandler则作为代理类的处理器,在构造代理对象时,需要指定代理类所实现的接口、处理器对象等参数

ServiceHandler implements InvocationHandler{}

InvocationHandler:
InvocationHandler 是 Java 标准库中的一个接口,用于实现动态代理模式。它只有一个方法 invoke(),在代理对象调用目标对象方法时会被回调,用于处理目标对象方法的调用。

添加的新依赖部分介绍

org.apache.dubbo: Java RPC 框架
org.apache.zookeeper: 分布式协调服务,在 Dubbo 中,ZooKeeper 通常被用作服务注册与发现中间件,用于存储和更新服务的元数据
org.apache.curator:curator-framework: ZooKeeper 的一个客户端框架,Dubbo 中,Curator 通常用于实现对 ZooKeeper 的访问
org.apache.curator:curator-recipes 提供了一些常用的分布式应用场景实现,如分布式计数器、缓存、队列等。在 Dubbo 中,Curator Recipes 通常被用于实现分布式服务发现和负载均衡功能
cglib:cglib:3.3.0: 基于 ASM(Java 字节码操作框架)的字节码生成库,可以在运行时动态生成新的类。在 Dubbo 中,CGLIB 通常被用于实现动态代理功能,通过生成目标类的子类来实现对目标类的代理。

final

final 标记的成员变量表示该成员变量是不可变的,即该成员变量的赋值后不能再被修改
(声明还没赋值的话)必须得有有参构造

⭐概念:泛化调用

泛化调用是一种针对 Dubbo 服务的通用调用方式,可以通过传递参数的方式实现对任何服务接口的调用,不需要显式地指定调用的接口和方法。
泛化调用将请求参数封装成一个 GenericService 对象,该对象包含了调用的接口名、方法名和参数列表等信息。
通过传递 GenericService 对象,将请求发送到目标 Dubbo 服务中进行处理。
由于泛化调用的通用性很强,因此它常常被用于一些动态化的场景,例如 API 网关、自定义脚本引擎等。

只针对dubbo?
在 Dubbo 中,泛化调用是指通过调用 Dubbo 服务提供者暴露出来的一个通用接口,该接口可以接收任意类型和数量的参数,并返回一个 Object 类型的结果。借助于 Dubbo 的序列化和反序列化技术,Dubbo 能够将不同类型的数据在网络中进行传输,并在服务提供者端进行解析和处理。

Netty 和 Dubbo 的(Channel的)区别
Netty 是网络通信框架,在分布式系统中,Netty 可以用来实现各种消息传输和 RPC 框架
Dubbo 是一款分布式服务框架,它提供了服务注册和发现、负载均衡、容错、动态代理等功能
在 Netty 和 Dubbo 中,它们都有一个叫做 Channel 的概念,但是意义不同
Netty 的 Channel 是用于表示一个连接的抽象概念,可以读取和写入数据
Dubbo 的 Channel 则是一个底层通信层的实例,用于实现 Dubbo 协议进行数据传输。在 Dubbo 中,Channel 主要用于消息的编解码和转发

概念:CGLB

可以用于动态生成代理对象、方法拦截等操作
CGLIB 主要由两个核心组件构成:Enhancer 和 MethodInterceptor。
Enhancer 类:可以动态创建一个子类,并在该子类中实现所需的业务逻辑
MethodInterceptor 接口则是一个拦截器接口,它可以在调用代理类的方法时,拦截并处理相关逻辑

CGLIB的工作流程

目标类:

1 、定义一个方法拦截器类,该类实现了 MethodInterceptor 接口

package cn.bugstack.gateway.bind;
...
public class GenericReferenceProxy implements MethodInterceptor{
//泛化调用接口
 private final GenericService genericService;
 ...}

并且覆盖了 intercept() 方法。方法拦截器类是用来实现代理逻辑的类

 @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    //获取方法的参数类型列表
    //将参数类型的名称存储到 parameters 数组中
 
 	// 返回方法调用结果
    return 泛化调用接口
}

2、(使用示例)使用 Enhancer(增强器)类创建一个代理对象

通过 setSuperclass() 或 setInterfaces() 方法为其指定被代理对象的父类或实现的接口,通过 setCallback() 方法将方法拦截器类实例传给该代理对象

package cn.bugstack.gateway;
public class CglibTest implements MethodInterceptor {
		 //创建代理
        Enhancer enhancer = new Enhancer();
            //将代理类的超类设置为 Object
        enhancer.setSuperclass(Object.class);
            //将代理类实现的接口设置
        enhancer.setInterfaces(new Class[]{interfaceClass});
        enhancer.setCallback(this);
        
}

3 、调用代理对象的方法
Enhancer 类实际上是通过创建目标对象的子类来实现的动态代理。子类中的方法会调用代理对象的 MethodInterceptor 对象的 intercept() 方法,从而触发代理逻辑

//接着上面
Object obj = enhancer.create();

插入一条的使用简单例子

 		// 构造目标对象
        UserService userService = new UserService();
        ...
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        ...
        // 创建代理对象
        UserService userServiceProxy = (UserService) enhancer.create();
        

CGLIB的回调

回调是实现动态代理和方法拦截的核心机制之一。通过回调机制,可以动态创建子类或者实现类,并在其中添加对目标类方法的拦截逻辑,从而实现代理和拦截的功能。
核心: Callback 接口及其实现类
CGLIB 的回调机制包含以下几个步骤:
1、 定义目标类,类中一些方法需要被代理拦截
2、 创建Enhancer对象,设置相关属性(目标类等)//动态创建一个子类,并在该子类中实现所需的业务逻辑
3、创建CallbackFilter对象,决定将那个回调对象应用于特定的方法调用
4、创建一个 Callback 对象(通常是代理类自身),并实现 intercept() 方法,该方法用于完成方法拦截和代理逻辑。
5、将 Callback 对象传递给 Enhancer 对象中,并使用 create() 方法生成一个代理实例。
6、在代理实例上直接调用目标方法,CGLIB 会自动将方法调用转发到 Callback 对象的 intercept() 方法中,完成代理和拦截的功能


问题描述:服务端(provider rpc 接口)的dubbo连接不是zookeeper

在配置文件里要添加zookeeper的IP 地址和端口,但是我不知道IP地址是哪个,(且当时还不知道要发布端口)

解决方案

点醒我了
1.检查是不是在运行
2.检查端口有没有放行和暴露

然后那个ip地址其实不是在
在这里插入图片描述
显示的那个ip
是在配置里:
在这里插入图片描述
注意看host格式
没错


问题描述:没有调用成功rpc的接口

解决方案:

可能:

检查客户端是否正确代理了 RPC 接口。通过 CGLIB 进行代理时,需要确保生成的代理类能够正确地访问 RPC 接口,否则可能会导致调用失败。

检查客户端的请求参数是否正确。可以通过日志或者调试等方式查看客户端发送的请求数据,以确定请求参数是否正确。如果请求参数错误,可能会导致服务端返回错误响应或者抛出异常。

在这里插入图片描述
这个方法没有用,没有把它添加到会话服务处理器

package cn.bugstack.gateway.session.handlers;
public class SessionServerHandler extends BaseHandler<FullHttpRequest> {...}

以及后面的初始配置SessionChannelInitializer、 SessionServer都要修改
以及SessionServerHander里面的响应

复盘

设计:把HTTP地址中的接口方法 (localhost://xxxx//方法)与RPC接口对应的服务建立关联

开始的地方:

line.addLast(new SessionServerHandler(configuration));

在管道中添加修改过后的处理

从三个方面实现

一、泛化调用

首先是泛化调用:
01和02的区别就是,01为硬编码,而02只需要提供接口的方法名称、入参信息就可以调用到对应的rpc接口服务了
对比:

//之前设置的写回路径是这样的,写死了一种处理方法

public class SessionServerHandler extends BaseHandler<FullHttpRequest> 
{@Override
    protected void session(ChannelHandlerContext ctx,final Channel channel, FullHttpRequest request) {
response.content().writeBytes(JSON.toJSONBytes("网关管理该路径, URI:" + request.uri(), SerializerFeature.PrettyFormat));
}}

进行泛化调用后:

//还是在这个session方法:
//private final Configuration configuration;
/** 服务泛化调用 */
        IGenericReference reference = configuration.getGenericReference("sayHi");
        String result = reference.$invoke("test") + " " + System.currentTimeMillis();
		IGenericReference reference = configuration.getGenericReference("sayHi");

发现只用传入个“sayHi”就能获取接口了

关于session,session是自己写的,但是在重写方法SimpleChannelInboundHandler< T >的channelRead0,意思是一读到消息这个cread就会动,等于运行session

以上就是概念和目的,现在是如何去实现

从上面开始跳转:public IGenericReference getGenericReference(String methodName) {
return registry.getGenericReference(methodName);
}
return的是:IGenericReference
这也是为什么先从统一接口入手

首先 一个统一泛化调用的接口

public interface IGenericReference {
String $invoke (String args);
}

然后封装泛化调用
代码很长但是只要关注return就好了(后面添加别的服务再看分解部分)

public class GenericReferenceProxy implements MethodInterceptor{
... ...
/**
     *
     * @param o             被代理对象
     * @param method        代理的方法对象
     * @param args          代理方法的参数
     * @param methodProxy   代理方法的 MethodProxy 对象
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //获取方法的参数类型列表
        Class<?>[] parameterTypes = method.getParameterTypes();
        //将参数类型的名称存储到 parameters 数组中
        String[] parameters = new String[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            parameters[i] = parameterTypes[i].getName();
        }

        return genericService.$invoke(methodName,parameters,args);
    }
}
}

return genericService.$invoke(methodName,parameters,args);
GenericService是dubbo提供的一个service

插入一条dubbo的作用

服务端通过dubbo将自己的方法注册到注册中心去,然后客户端通过注册中心去查看获取这个服务
而GenericService是Dubbo框架提供的一种通用的服务接口,可以用于消费者调用任何暴露在Dubbo服务目录中的服务,它的核心方法是$invoke,该方法用于调用远程服务

//官方提供的,上锁的
public interface GenericService {
    Object $invoke(String var1, String[] var2, Object[] var3) throws GenericException;

    default CompletableFuture<Object> $invokeAsync(String method, String[] parameterTypes, Object[] args) throws GenericException {
        Object object = this.$invoke(method, parameterTypes, args);
        return object instanceof CompletableFuture ? (CompletableFuture)object : CompletableFuture.completedFuture(object);
    }
}

这里就算是封装好了,一种常见的代理操作,在自己写的实现封装类里面创建官方(或者自己写的想要被封装或者隐藏)提供的接口实例:

private final GenericService genericService;

然后在自己写的方法中调用这个接口实例的方法

代理包装和Cglib

这里合二唯一了,因为相辅相成!
来自ai的回答为啥要进行代理包装rpc接口:(从怎么做得出为什么)

在RPC框架的实现中添加代理类包装逻辑,可以将RPC服务接口之间的调用过程进行透明化处理,避免硬编码的风险。以下是一个可能的实现方式:

在客户端端口,定义一个代理类Proxy,该类实现与RPC服务接口相同的方法,并在其中封装RPC调用的细节;
在Proxy中,通过RPC框架提供的API获取到对应的RPC服务实例,并调用其对应的方法;
在Proxy中,对泛化参数和泛化响应进行转换,避免硬编码;
当客户端调用代理类的方法时,实际上是调用了代理类中的RPC服务,而不是直接调用RPC服务实现类;
在服务端,根据自己的业务需求来选择使用代理类进行封装,或者直接暴露RPC服务接口。

总结一下就是:封装RPC调用细节、对泛化参数和泛化响应进行转换,避免硬编码

为什么CGLIB和代理包装一起,因为CGLIB就是拿来代理的
关于CGLIB:
通过生成目标类的子类来实现动态代理(更牛的功能:在运行期间动态生成类的子类,并重写需要代理的方法,从而实现AOP)
等下要用到的主要类型:
Enhancer:用于创建目标类的子类,实现动态代理的主要功能
Callback:用于实现对目标类方法的拦截和增强(AOP)

以上是概念,现在看看如何实现
构造一个Proxy和一个ProxyFactory
Proxy负责对目标的拦截和处理

public class GenericReferenceProxy implements MethodInterceptor{}

通过实现MethodInterceptor接口,并在intercept方法中实现对目标方法的拦截和处理

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
//obj表示被代理的对象,method表示目标方法,args表示方法的参数列表,proxy表示调用方法的代理对象

02中的拦截处理:

return genericService.$invoke(methodName,parameters,args);

ProxyFactory
通过Map管理 String名称对应的接口

Map<String,IGenericReference> genericReferenceCache = new ConcurrentHashMap<>();

获取接口的方法:

public IGenericReference newInstance (String method)
return genericReferenceCache.computeIfAbsent(method,k->{

如果不存在就创建一个新的接口
如何创建新接口,得new一个代理对象enhancer,然后配置,创造这个接口并返回

		Enhancer enhancer = new Enhancer();
        //创建一个代理对象,并继承自 Object 类,以便可以在代理中访问 Object 的相关方法
        enhancer.setSuperclass(Object.class);
        //设置代理对象要实现的接口列表
        enhancer.setInterfaces(new Class[]{IGenericReference.class, interfaceClass});
        enhancer.setCallback(genericReferenceProxy);
        //使用 create() 方法生成代理对象,并将其转换为 IGenericReference 类型返回
        return(IGenericReference) enhancer.create();

一些AI问答(当前水平无法判断是不是都是正确的)

Q:newInstance中创建的新接口是不是由Enhancer对象来管理的,是的话它是如何管理这个接口的
A:是的,在newInstance方法中创建的新接口是由Enhancer对象来管理的。Enhancer对象使用setInterfaces方法来设置代理对象要实现的接口列表,其中就包括了新生成的接口类。当调用enhancer.create()方法时,Enhancer会根据这些接口以及其他设置(如Superclass和Callback等)生成代理类,并将其缓存到CGLIB缓存中。代理类中实现了新生成的接口方法以及IGenericReference接口方法,并在代理类的回调函数中调用GenericReferenceProxy的回调方法,从而实现了将HTTP请求转发至相应的Dubbo服务上

Q:返回的接口都是IGenericReference类型,但是封装不同的Dubbo方法?
A:虽然返回的接口类型都是IGenericReference,但实际上每个接口所代表的Dubbo泛化调用方法却是不一样的。在newInstance方法中,会根据传入的method参数来创建新的接口,即新接口中仅有一个方法,而这个方法就是对应的Dubbo泛化调用方法。通过这种方式,每个创建出来的IGenericReference接口实例都是专为某个具体的Dubbo服务方法而生成的,因此它们之间是不同的。

Q:cglib的enhancer和dubbo的关系
A:cglib的Enhancer和Dubbo并没有直接的依赖关系。cglib是一个独立的Java字节码生成库,它可以在运行时动态生成新的类以及修改现有类的字节码。而Dubbo只是在GenericReferenceProxyFactory中使用cglib来动态生成接口的实现类,并不直接使用cglib库。因此,在使用Dubbo时,并不需要显式地引用cglib的依赖

解决完以上就是使用时是从注册开始的
整合:
Configuration 类中用来初始化配置信息
GenericReferenceRegistry用来向Dubbo引用服务并启动Dubbo框架用于注册和发现服务
在Configuration类的构造函数中,使用关键字this创建了一个GenericReferenceRegistry对象,为的是使用GenericReferenceRegistry对象中的方法

public class Configuration {
//这个this代表的是当前的Configuration对象
  private final GenericReferenceRegistry registry = new GenericReferenceRegistry(this);
  }

this:通过this关键字在构造函数中将当前对象的引用传递给其他对象,可以方便地在其他对象中访问该对象的成员变量和方法。这种方式可以使相关联的对象高度解耦,提高代码的可维护性和复用性

 addGenericReference(String application, String interfaceName, String methodName)
 /**
     * 注册泛化调用服务接口方法
     * @param application           服务:api-gateway-test
     * @param interfaceName         接口:cn.bugstack.gateway.rpc.IActivityBooth
     * @param methodName            方法:sayHi 全局唯一
     */
    public void addGenericReference(String application, String interfaceName, String methodName){
        //获取基础服务

        //ApplicationConfig 对象表示服务提供方应用的信息
        ApplicationConfig applicationConfig = configuration.getApplicationConfig(application);
        //RegistryConfig 对象表示连接注册中心的配置信息
        RegistryConfig registryConfig = configuration.getRegistryConfig(application);
        //引用 Dubbo 服务的配置信息,用于指定要调用的 Dubbo 服务接口
        ReferenceConfig<GenericService> reference = configuration.getReferenceConfig(interfaceName);

        // 构建Dubbo服务
        //DubboBootstrap 类是 Dubbo 框架中用来构建 Dubbo 服务的工具类,主要作用是将 ApplicationConfig、RegistryConfig 和 ReferenceConfig 这三个配置对象组合起来创建一个 Dubbo 服务
        DubboBootstrap bootstrap = DubboBootstrap.getInstance();
        bootstrap.application(applicationConfig).registry(registryConfig).reference(reference).start();

        //获取泛化调用服务
        ReferenceConfigCache cache = ReferenceConfigCache.getCache();
        GenericService genericService = cache.get(reference);

        //创建并保存泛化工厂
        knownGenericReferences.put(methodName,new GenericReferenceProxyFactory(genericService));
    }

02完结( •̀ ω •́ )y


03_分治处理会话流程

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值