RPC(Remote Procedure Call):远程过程调用。客户端能够像调用本地方法一样去调用服务器的服务。
常见的RPC框架有阿里的Dubbo、Google的gRPC、Twitter 的Finagle,Facebook 的 Thrift,等等。
那么RPC中有一些什么技术呢?
框架:Netty
通信协议:Socket、RMI。
服务发布与订阅:Zookeeper
Spring:使用Spring配置服务,加载Bean,扫描注解
消息编码与解码:使用Protostuff序列化与反序列化消息。
实现的流程图:
核心流程:
1、服务消费方(client)调用以本地调用方式调用服务;
2、client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3、client stub 找到服务地址,并将消息发送给服务端;
4、server stub收到消息后进行解码;
5、server stub 根据解码结果调用本地服务;
6、本地服务执行并将结果返回给serverstub;
7、server stub 将返回的结果打包成消息发送至消费方;
8、client stub 接收到消息,并进行解码;
9、服务消费方得到最终结果
RPC的目标就是将2~8这些步骤封装起来,让用户不需要了解这些细节。Java一般使用动态代理的方式实现远程调用。
RMI(Java Remote Method Invocation)实现方式
是Java编程语言里面,一种用于实现远程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。
实现步骤:
1、编写远程服务接口,该接口必须继承java.rmi.Remote接口,方法必须抛出java.rmi.RemoteException异常;
2、编写远程接口实现类,该类必须继承java.rmi.server.UnicastRemoteObject类。
3、运行RMI编译器,创建客户端stub类和服务端skeleton类;
4、启动一个RMI注册表,以便驻留这些服务;
5、在RMI注册表中注册服务;
6、客服端查找远程对象,并调用远程方法;
1:创建远程接口,继承java.rmi.Remote接口
public interface GreetServer extends java.rmi.Remote{
String sayHello(String name) throws RemoteException;
}
2:实现远程接口,继承java.rmi.server.UnicastRemoteObject类
public class GreeServerImpl extends java.rmi.UnicastRemoteObject
implements GreetServer{
private static final long serilVersionUID = 343406015238720004l;
public GreetServer() throws RemoteException{
super();
}
@Override
public String sayHello(String name) throws RemoteException{
return "Hello"+name;
}
}
3、生成Stub和Skeleton;
4、执行rmiregistry命令注册服务
5、启动服务
LocateRegistry.createRegistry(1098);
Naming.bind("rmi://10.108.1.1.138:1098/GreetServer",New GreetServerImpl());
6、客服端调用
GreetServer greetServer = (GreetServer)Naming.out.println(greetServer.sayHello("Jobs"));
Zookeeper
Zookeeper是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。Zookeeper提供了类似于Linux文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。
Zookeeper角色
Zookeeper集群是一个基于主从复制的高可用集群,每个服务器承担如下三种角色的一种
Leader:
1、一个Zookeeper集群同一时间只会只有一个实际工作的leader,它会发起并维护与各Follwer及Observer间的心跳。
2、所有的写操作必须通过Leader完成再由Leader将写操作广播给其他服务器。只要有超过半数节点(不包括observeer节点)写入成功。该请求就会被提交。
Follower:
1、一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳
2、Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理
3、并且负责在Leader处理写请求时对请求进行投票。
Netty架构:
服务端通讯流程
1、ServerSocketChannel
2、绑定InetSocketAdress
3、创建Selector,启动线程
4、将ServerSocketChannel注册到Selector监听
5、Selector轮询就绪的Key
6、handleAccept()处理新的客户端接入
7、设置新建客户连接的Socket
8、向selector注册监听读操作SelectionKey.OP_READ
9、handleRead()异步读请求消息到ByteBuffer
10、decode请求
11、异步写ByteBuffer到SocketChannel
package NettyTest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
//服务器端(端口号为63932,192.168.1.4)
public class NIOServer {
private Selector selector;
/*
* 获得一个ServerSocket通道,并对通道进行一些初始化的工作
* @param port 绑定端口号
* @Throws IOException
*/
public void initServer(int port) throws IOException{
//获得一个ServerSocket的通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
//设置通道为非阻塞
serverChannel.configureBlocking(false);
//将对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
//获得一个管道处理器
this.selector = Selector.open();
//将通道管理器与该通道绑定,并为该通道注册SelectorKey.OP_ACCEPT事件,注册该事件后
//当该事件到达时,selector.select()会返回,如果事件没有到达,则selector.select()会一直阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
/*
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有则进行处理
* @throws IOException
*/
}
public void listen() throws IOException{
System.out.println("服务端启动成功!");
//轮询selector
while(true){
//当注册事件到来时返回方法,否则一直阻塞
selector.select();
//获得selector中的迭代器,选中的项为注册事件
Iterator ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()){
SelectionKey key = (SelectionKey) ite.next();
//删除已选用的Key,防止重复处理
ite.remove();
//客户端连接请求事件
if(key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//获得和客户端连接通道
SocketChannel channel = server.accept();
//设置成非阻塞
channel.configureBlocking(false);
//在这可以给客户端发送消息
channel.write(ByteBuffer.wrap(new String("向客户端发送的一条消息").getBytes()));
//在和客户端连接成功后,为了可以接收客户端的信息,需要给通道设置成读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
//获得可读事件
}else if(key.isReadable()){
read(key);
}
}
}
}
private void read(SelectionKey key)throws IOException {
//服务器可读取消息:得到的消息发生的socket通道
SocketChannel channel = (SocketChannel) key.channel();
//创建读取缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务器端收到的消息"+msg);
ByteBuffer outbuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outbuffer);//将消息送回给客户端
}
//启动服务端测试
public static void main(String[] args) throws IOException{
NIOServer server = new NIOServer();
server.initServer(63932);
server.listen();
}
}
代码中缺少handle的处理
客户端通信
1、打开SocketChannel
2、设置SocketChannel为非阻塞模式,同时设置TCP参数
3、异步连接服务端
4、判断连接结果,如果连接成功,则调到步骤10,否则执行步骤5
5、向Reactor线程多路复用器注册OP CONNECT事件
6、创建Selector启动线程
7、Selector轮询就绪的Key
8、handerConnect()
9、判断连接完成,则执行步骤10
10、向多路复用器注册读事件OP_READ
11、handerConnect()
12、decode请求
13、异步写ByteBuffer到SocketChannel
package NettyTest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOClient {
//通道管理器
private Selector selector;
/*
* 获得一个Socket通道,并对通道进行一些初始化工作
* @param ip 连接客户端IP
* @param port 连接服务器的端口
* @throws IOException
*/
public void initClient(String ip,int port) throws IOException{
//获取一个socket通道
SocketChannel channel = SocketChannel.open();
//设置通道位非阻塞
channel.configureBlocking(false);
//获取一个通道管理器
this.selector=Selector.open();
//客户端连接服务器,其实方法执行并没有连接服务器,需要在listen()方法中调用
//用channel.finishConnet();才能完成连接
channel.connect(new InetSocketAddress(port));
//将通道管理器与通道绑定,并为该通道注册SelectionKey.OP_CONNET事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/*
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有则进行处理
* @throws IOException
*/
public void listen() throws IOException{
//轮询访问selector
selector.select();
Iterator ite = this.selector.selectedKeys().iterator();
while(ite.hasNext()){
SelectionKey key = (SelectionKey) ite.next();
ite.remove();
//连接该事件发生
if(key.isConnectable()){
SocketChannel channel = (SocketChannel) key.channel();
if(channel.isConnectionPending()){
channel.finishConnect();
}
//设置成非阻塞
channel.configureBlocking(false);
channel.write(ByteBuffer.wrap(new String("向服务器发送了一条消息").getBytes()));
//在服务器连接成功后,为了可以接收到服务端消息,需要设置管道的读取权限
channel.register(this.selector, SelectionKey.OP_READ);
//获取可读事件
}else if(key.isReadable()){
read(key);
}
}
}
private void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
//创建读取缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务器端收到的消息"+msg);
ByteBuffer outbuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outbuffer);//将消息送回给客户端
}
//启动客户端测试
public static void main(String[] args) throws IOException{
NIOClient client = new NIOClient();
client.initClient("localhost", 63932);
client.listen();
}
}
Netty的IO线程由于聚合了 多路复用器Selector,可以同时并发处理成百上千个客户端Channel,由于读写操作都是非阻塞的,这就可以充分提升IO的运行效率,避免频繁的IO阻塞导致线程的挂起。
零拷贝:
1、Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFER)进行Socket的读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2、Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便对组合buffer进行操作,避免了传统通过内存拷贝的方式将几个小的Buffer合并成一个大的Buffer。
3、Netty的文件传输采用了transferTo的方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过write方式导致内存拷贝问题。