一、基于Netty自定义RPC
RPC又称远程过程调用,我们所知的远程调用分为两种,现在在服务间通信的方式也基本以这两种为主
-
是基于HTTP的restful形式的广义远程调用,以spring could的feign和restTemplate为代表,采用的协议是HTTP的7层调用协议,并且协议的参数和响应序列化基本以JSON格式和XML格式为主。
-
是基于TCP的狭义的RPC远程调用,以阿里的Dubbo为代表,主要通过netty来实现4层网络协议,NIO来异步传输,序列化也可以是JSON或者hessian2以及java自带的序列化等,可以配置。
接下来我们主要以第二种的RPC远程调用来自己实现
需求:模仿 dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty
步骤:
- 创建一个公共的接口项目以及创建接口及方法,用于消费者和提供者之间的约定。 rpc-common
- 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。 rpc-provider
- 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据. rpc-consumer
1.1 、公共模块
1.1.1 工程构建
创建一个maven工程
首先,在公共模块中添加netty的maven依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.16.Final</version>
</dependency>
提供者及消费者工程都需依赖公共模块,这样提供者来实现接口并且提供网络调用,消费者直接通过接口来进行TCP通信及一定的协议定制获取提供者的实现返回值
接口的定义
package com.lagou.service;
public interface UserService {
String sayHello(String word);
}
只是一个普通的接口,参数是支持序列化的String类型,返回值同理
1.1.2、提供者实现
1.先将 上面的公共模块导入
<dependencies>
<dependency>
<groupId>com.lagou</groupId>
<artifactId>rpc-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.是接口的实现,这一点和普通接口实现是一样的
package com.lagou.service;
import com.lagou.handler.UserServiceHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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 UserServiceImpl implements UserService {
// 将来用户 要远程调用的方法
@Override
public String sayHello(String msg) {
String word = "客户端 发的数据--->" + msg;
System.out.println(word);
return "服务端返回的数据:"+msg;
}
// 创建一个方法启动服务器
public static void startServer(String ip , int port) throws InterruptedException {
// 1、 创建两个线程池对象
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
// 2、 创建服务器的启动引导对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 3、 配置启动引导对象
serverBootstrap.group(bossGroup,workGroup)
// 设置通道为Nio
.channel(NioServerSocketChannel.class)
// 创建监听
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 获取管道对象
ChannelPipeline pipeline = nioSocketChannel.pipeline();
// 设置编码
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
// 把我们自定义 一个ChannelHandler 添加到通道中
pipeline.addLast(new UserServiceHandler());
}
});
// 绑定 服务器和端口
serverBootstrap.bind(ip,port).sync();
}
}
3.在实现中加入了netty的服务器启动程序,上面的代码中添加了 String类型的编解码 handler,添加了一个自定义
handler
自定义 handler 逻辑如下:
package com.lagou.handler;
import com.lagou.service.UserServiceImpl;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class UserServiceHandler extends ChannelInboundHandlerAdapter {
// 当客户端读取数据是,该方法会被调用
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 注意:客户端将来发送请求的时候回传递一个参数 UserService#sayHello#are you ok
// 判断当时的当前的请求是否 符合规则
if(msg.toString().startsWith("UserService")){
//2.如果符合规则,调用实现类货到一个result
UserServiceImpl service = new UserServiceImpl();
String result = service.sayHello(msg.toString().substring(msg.toString().lastIndexOf("#")+1));
//3.把调用实现类的方法获得的结果写到客户端
ctx.writeAndFlush(result);
}
}
}
4.这里显示判断了是否符合约定(并没有使用复杂的协议,只是一个字符串判断),然后创建一个具体实现类,并调
用方法写回客户端。
还需要一个启动类:
package com.lagou.boot;
import com.lagou.service.UserServiceImpl;
public class ServiceBoot {
public static void main(String[] args) throws InterruptedException {
UserServiceImpl.startServer("127.0.0.1",8999);
}
}
关于提供者的代码就写完了,主要就是创建一个 netty 服务端,实现一个自定义的 handler,自定义 handler 判断是否符合之间的约定(协议),如果符合,就创建一个接口的实现类,并调用他的方法返回字符串。
1.1.3 消费者
服务端与客户端梳理
消费者有一个需要注意的地方,就是调用需要透明,也就是说,框架使用者不用关心底层的网络实现。这里我们可以使用 JDK 的动态代理来实现这个目的。
思路:
客户端调用代理方法,返回一个实现了 HelloService 接口的代理对象,调用代理对象的方法,返回结果。我们需要在代理中做手脚,当调用代理方法的时候,我们需要初始化 Netty 客户端,还需要向服务端请求数据,并返回数据。
1.先将 上面的公共模块导入
<dependencies>
<dependency>
<groupId>com.lagou</groupId>
<artifactId>rpc-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.创建代理相关的类:
package com.lagou.consumer;
import com.lagou.handler.UserClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//消费者
public class RPCConsumer {
// 一、创建一个线程池对象 --- 它要处理 我们自定义的事件
private static ExecutorService executorService =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 二、声明一个 自动应以事件处理器 userClientHandler
private static UserClientHandler userClientHandler ;
// 三、 编写方法,初始化客户端 (创建连接池 bootStrap 设置bootStrap ,连接服务器)
public static void initClient() throws InterruptedException {
// 1) 初始化UserClientHandler
userClientHandler = new UserClientHandler();
// 2)创建连接池对象
NioEventLoopGroup group = new NioEventLoopGroup();
// 3)创建客户端 的引导对象
Bootstrap bootstrap = new Bootstrap();
// 4)配置引导对象
bootstrap.group(group)
//设置通道为NIO
.channel(NioSocketChannel.class)
//设置请求协议为TCP
.option(ChannelOption.TCP_NODELAY,true)
//监听channel 并初始化
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
ChannelPipeline pipeline = nioSocketChannel.pipeline();
// 设置编码
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
// 添加自定义事件 处理器
pipeline.addLast(userClientHandler);
}
});
//5)连接服务端
bootstrap.connect("127.0.0.1",8999).sync();
}
// 四、编写一个 方法 ,事件JDK 的动态代理创建对象
// serviceClass 接口类型,根据 那个接口生成子类代理对象 ; providerParam : "UserService#sayHello#"
public static Object createProxy(Class<?> serviceClass, final String providerParam){
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{
serviceClass}, new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
//1) 初始化 客户端
if( userClientHandler == null){
initClient();
}
//2)给UserClientHandler 设置param参数
userClientHandler.setParam(providerParam+objects[0]);
//3).使用线程池,开启一个线程处理处理call() 写操作,并返回结果
Object result = executorService.submit(userClientHandler).get();
return result;
}
});
}
}
该类有 2 个方法,创建代理和初始化客户端
创建代理逻辑:使用 JDK 的动态代理技术,代理对象中的 invoke 方法实现如下: 如果 client 没有初始化,则初始化 client,这个 client 既是 handler ,也是一个 Callback。将参数设置进 client ,使用线程池调用 client 的 call 方法并阻塞等待数据返回
初始化客户端逻辑: 创建一个 Netty 的客户端,并连接提供者,并设置一个自定义 handler,和一些 String 类型的序列化方式。
UserClientHandler 的实现:
package com.lagou.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.concurrent.Callable;
public class UserClientHandler extends ChannelInboundHandlerAdapter implements Callable {
// 1、定义成员变量
private ChannelHandlerContext context; // 事件处理器上下文对象(存储handler信息,写操作)
private String result; // 记录服务器返回的数据
private String param; // 记录将要传送给 服务器的数据
// 2、 实现channelActive 客户端和服务区连接时 ,该方法自动执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.context = ctx;
}
// 3、实现channelRead 当我们读到服务器数据,该方法自动执行
@Override
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
result = msg.toString();
notify();
}
// 4、将客户端的数据写的服务器
public synchronized Object call() throws InterruptedException {
context.writeAndFlush(param);
wait();
return result;
}
//5.设置参数的方法
public void setParam(String param){
this.param = param;
}
}
该类缓存了 ChannelHandlerContext,用于下次使用,有两个属性:返回结果和请求参数。
当成功连接后,缓存 ChannelHandlerContext,当调用 call 方法的时候,将请求参数发送到服务端,等待。当服务端收到并返回数据后,调用 channelRead 方法,将返回值赋值个 result,并唤醒等待在 call 方法上的线程。此时,代理对象返回数据。
再看看消费者调用方式,一般的TCP的RPC只需要这样调用即可,无需关心具体的协议和通信方式:
package com.lagou.boot;
import com.lagou.consumer.RPCConsumer;
import com.lagou.service.UserService;
public class ConsumerBoot {
//参数定义 的规则
private static final String PROVIDER_NAME = "UserService#sayHello#";
public static void main(String[] args) throws InterruptedException {
//1.创建代理对象
UserService service = (UserService) RPCConsumer.createProxy(UserService.class, PROVIDER_NAME);
//2.循环给服务器写数据
while (true){
String result = service.sayHello("are you ok !!");
System.out.println(result);
Thread.sleep(2000);
}
}
}