Netty作为一个异步事件驱动的网络应用框架,可以用于快速开发可维护的高性能服务器和客户端。国内著名的RPC框架Dubbo底层使用的是Netty作为网络通信的。本篇文章我们来探索一下RPC框架的本质以及使用Netty来实现一个简单地RPC框架。
RPC(Remote Procedure Call)— 远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图)
通过上面的描述,好像RPC与Socket非常像,都是调用远程的方法,都是client/server模式。但是值得注意的是,RPC并不等同于Socket。Socket是RPC经常采用的通信手段之一,RPC是在Socket的基础上实现的,它比socket需要更多的网络和系统资源。除了Socket,RPC还有其他的通信方法,比如:http、操作系统自带的管道等技术来实现对于远程程序的调用。微软的Windows系统中,RPC就是采用命名管道进行通信。需要了解Socket相关概念的,可以参考之前的这篇文章golang socket编程。
本地方法调用使我们开发中最常见的,如下定义一个方法:
public String sayHello(String name) {
return "hello, " + name;
}
只需要传入一个参数,调用sayHello方法就可以得到一个输出,入参、出参以及方法体都在同一个进程空间中,这就是本地方法调用
那有没有办法实现不同进程之间通信呢?调用方在进程A,需要调用方法B,但是方法B在进程B中。
最容易想到的实现方式
就是使用Socket通信,使用Socket可以完成跨进程调用,我们需要约定一个进程通信协议,来进行传参,调用函数,出参。写过Socket应该都知道,Socket是比较原始的方式,我们需要更多的去关注一些细节问题,比如参数和函数需要转换成字节流进行网络传输,也就是序列化操作,然后出参时需要反序列化。
假如RPC就是让我们在客户端直接使用Socket远程调用,那无疑是个灾难。所以有没有什么简单方法,让我们的调用方不需要关注细节问题,让调用方像调用本地函数一样,只要传入参数,调用方法,然后坐等返回结果就可以了呢?而这个诉求的解决方案就是RPC框架——为使用方屏蔽底层网络通信的细节。
RPC框架就是用来解决上面的问题的,它能够让调用方像调用本地函数一样调用远程服务,底层通讯细节对调用方是透明的,将各种复杂性都给屏蔽掉,给予调用方极致体验。
当server需要对方法内实现修改时,client完全感知不到,不用做任何变更。这种方式在跨部门,跨公司合作的时候是非常方便的。
常见的 RPC 框架有: 比较知名的如阿里的Dubbo、google的gRPC、Go语言的rpcx、Apache的thrift, Spring 旗下的 Spring Cloud。
小结:RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。
前面就已经说到RPC框架,可以让调用像调用本地函数一样调用远程服务。原理就是RPC框架屏蔽Socket通信的相关细节,使调用方可以向调用本地方法一样调用远程方法。
在使用的时候,调用方是直接调用本地函数,传入相应参数,其他细节它不用管,至于通讯细节交给RPC框架来实现。实际上RPC框架采用代理类的方式,具体来说是动态代理的方式,在运行时动态创建新的类,也就是代理类,在该类中实现通讯的细节问题,比如与服务端的连接、参数序列化、结果反序列化等。
除了上述动态代理,还需要约定一个双方通信的协议格式,规定好协议格式,比如请求方法的类名、请求的方法名、请求参数的数据类型,请求的参数等,这样根据格式进行序列化后进行网络传输,然后服务端收到请求对象后按照指定格式进行解码,这样服务端才知道具体该调用哪个方法,传入什么样的参数。
刚才又提到网络传输,RPC框架重要的一环也就是网络传输,服务是部署在不同主机上的,如何高效的进行网络传输,尽量不丢包,保证数据完整无误的快速传递出去?实际上,就是利用我们今天的主角——Netty,Netty是一个高性能的网络通讯框架,它足以胜任我们的任务。
前面说了这么多,再次总结下一个RPC框架需要重点关注哪几个点:
当然一个优秀的RPC框架需要关注的不止上面几点,只不过本篇文章旨在做一个简易的RPC框架,理解了上面关键的几点就够了
dubbo 底层使用了 Netty 作为网络通讯框架,要求用 Netty 实现一个简单的 RPC 框架 模仿 dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.20
创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据
RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。
//这个是接口,是服务提供方和 服务消费方都需要
public interface HelloService {
String hello(String mes);
}
public class HelloServiceImpl implements HelloService {
private static int count = 0;
//当有消费方调用该方法时, 就返回一个结果
@Override
public String hello(String mes) {
System.out.println("收到客户端消息=" + mes);
//根据mes 返回不同的结果
if(mes != null) {
return "你好客户端, 我已经收到你的消息 [" + mes + "] 第" + (++count) + " 次";
} else {
return "你好客户端, 我已经收到你的消息 ";
}
}
}
public class NettyServer {
public static void startServer(String hostName, int port) {
startServer0(hostName,port);
}
//编写一个方法,完成对NettyServer的初始化和启动
private static void startServer0(String hostname, int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(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(new NettyServerHandler()); //业务处理器
}
}
);
ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
System.out.println("服务提供方开始提供服务~~");
channelFuture.channel().closeFuture().sync();
}catch (Exception e) {
e.printStackTrace();
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
//服务器这边handler比较简单
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//获取客户端发送的消息,并调用服务
System.out.println("msg=" + msg);
//客户端在调用服务器的api 时,我们需要定义一个协议
//比如我们要求 每次发消息是都必须以某个字符串开头 "HelloService#hello#你好"
if(msg.toString().startsWith(ClientBootstrap.providerName)) {
String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
ctx.writeAndFlush(result);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
//ServerBootstrap 会启动一个服务提供者,就是 NettyServer
public class ServerBootstrap {
public static void main(String[] args) {
//代码代填..
NettyServer.startServer("127.0.0.1", 7000);
}
}
public class NettyClient {
//创建线程池
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static NettyClientHandler client;
private int count = 0;
//编写方法使用代理模式,获取一个代理对象
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) {
System.out.println("Handler为空");
initClient();
}
System.out.println("Handler不为空");
//设置要发给服务器端的信息
//providerName 协议头 args[0] 就是客户端调用api hello(???), 参数
client.setPara(providerName + args[0]);
//
return executor.submit(client).get();
});
}
public NettyClient() {
initClient();
}
//初始化客户端
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();
}
}
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {
private ChannelHandlerContext context;//上下文
private String result; //返回的结果
private String para; //客户端调用方法时,传入的参数
//与服务器的连接创建后,就会被调用, 这个方法是第一个被调用(1)
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println(" channelActive 被调用 ");
context = ctx; //因为我们在其它方法会使用到 ctx
System.out.println("初始化ctx....");
System.out.println(ctx);
}
//收到服务器的数据后,调用方法 (4)
//
@Override
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(" channelRead 被调用 ");
result = msg.toString();
notify(); //唤醒等待的线程
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
//被代理对象调用, 发送数据给服务器,-> wait -> 等待被唤醒(channelRead) -> 返回结果 (3)-》5
@Override
public synchronized Object call() throws Exception {
System.out.println(" call1 被调用 ");
context.writeAndFlush(para);
//进行wait
wait(); //等待channelRead 方法获取到服务器的结果后,唤醒
System.out.println(" call2 被调用 ");
return result; //服务方返回的结果
}
//(2)
void setPara(String para) {
System.out.println(" setPara ");
this.para = para;
}
}
public class ClientBootstrap {
//这里定义协议头
public static final String providerName = "HelloService#hello#";
public static void main(String[] args) throws Exception{
//创建一个消费者
NettyClient customer = new NettyClient();
//创建代理对象
HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
for (int i = 0; i < 3; i++) {
Thread.sleep(2 * 1000);
//通过代理对象调用服务提供者的方法(服务)
String res = service.hello("你好 dubbo~");
System.out.println("调用的结果 res= " + res);
}
}
}
F:\java1.8\bin\java.exe "-javaagent:F:\IDEA2020.2\IntelliJ IDEA 2020.2\lib\idea_rt.jar=53999:F:\IDEA2020.2\IntelliJ IDEA 2020.2\bin" -Dfile.encoding=UTF-8 -classpath F:\java1.8\jre\lib\charsets.jar;F:\java1.8\jre\lib\deploy.jar;F:\java1.8\jre\lib\ext\access-bridge-64.jar;F:\java1.8\jre\lib\ext\cldrdata.jar;F:\java1.8\jre\lib\ext\dnsns.jar;F:\java1.8\jre\lib\ext\jaccess.jar;F:\java1.8\jre\lib\ext\jfxrt.jar;F:\java1.8\jre\lib\ext\localedata.jar;F:\java1.8\jre\lib\ext\nashorn.jar;F:\java1.8\jre\lib\ext\sunec.jar;F:\java1.8\jre\lib\ext\sunjce_provider.jar;F:\java1.8\jre\lib\ext\sunmscapi.jar;F:\java1.8\jre\lib\ext\sunpkcs11.jar;F:\java1.8\jre\lib\ext\zipfs.jar;F:\java1.8\jre\lib\javaws.jar;F:\java1.8\jre\lib\jce.jar;F:\java1.8\jre\lib\jfr.jar;F:\java1.8\jre\lib\jfxswt.jar;F:\java1.8\jre\lib\jsse.jar;F:\java1.8\jre\lib\management-agent.jar;F:\java1.8\jre\lib\plugin.jar;F:\java1.8\jre\lib\resources.jar;F:\java1.8\jre\lib\rt.jar;D:\IDEAWorkSpace\Netty\Spring\target\classes;E:\Maven\warehouse\maven_repository\org\springframework\spring-core\5.3.1\spring-core-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-jcl\5.3.1\spring-jcl-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-beans\5.3.1\spring-beans-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-context\5.3.1\spring-context-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-aop\5.3.1\spring-aop-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\springframework\spring-expression\5.3.1\spring-expression-5.3.1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter\5.8.0-M1\junit-jupiter-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter-api\5.8.0-M1\junit-jupiter-api-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\apiguardian\apiguardian-api\1.1.1\apiguardian-api-1.1.1.jar;E:\Maven\warehouse\maven_repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;E:\Maven\warehouse\maven_repository\org\junit\platform\junit-platform-commons\1.8.0-M1\junit-platform-commons-1.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter-params\5.8.0-M1\junit-jupiter-params-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\jupiter\junit-jupiter-engine\5.8.0-M1\junit-jupiter-engine-5.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\junit\platform\junit-platform-engine\1.8.0-M1\junit-platform-engine-1.8.0-M1.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib-jdk8\1.4.20\kotlin-stdlib-jdk8-1.4.20.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib\1.4.20\kotlin-stdlib-1.4.20.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib-common\1.4.20\kotlin-stdlib-common-1.4.20.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\annotations\13.0\annotations-13.0.jar;E:\Maven\warehouse\maven_repository\org\jetbrains\kotlin\kotlin-stdlib-jdk7\1.4.20\kotlin-stdlib-jdk7-1.4.20.jar;E:\Maven\warehouse\maven_repository\io\netty\netty-all\4.1.18.Final\netty-all-4.1.18.Final.jar;E:\Maven\warehouse\maven_repository\com\alibaba\fastjson\1.2.28\fastjson-1.2.28.jar com.pjh.Example.dubborpc.customer.ClientBootstrap
channelActive 被调用
初始化ctx....
ChannelHandlerContext(NettyClientHandler#0, [id: 0x7a836196, L:/127.0.0.1:58448 - R:/127.0.0.1:7000])
(proxy, method, args) 进入....1 次
Handler不为空
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息 [你好 dubbo~] 第1 次
(proxy, method, args) 进入....2 次
Handler不为空
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息 [你好 dubbo~] 第2 次
(proxy, method, args) 进入....3 次
Handler不为空
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息 [你好 dubbo~] 第3 次
msg=HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
msg=HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
msg=HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
后续更新。。。。