目录
1.基本原理
传输协议
网络IO
2. RPC介绍
RPC架构
RPC调用过程
3. RMI 介绍
1.客户端:
2.服务端
3.注册表(Registry):
远程调用过程:
结果返回过程
4. BIO、NIO、AIO
同步和异步
阻塞和非阻塞
4.1 BIO
4.2 NIO
4.3 AIO
在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java领域中有很多可实现远程通讯的技
术,例如:RMI、Hessian、SOAP、ESB和JMS等
在底层层面去看,网络通信需要做的就是将流从一台计算机传输到另外一台计算机,基于传输协议和网络IO来实现
所有的分布式应用通讯都基于这个原理而实现,只是为了应用的易用,各种语言通常都会提供一些更为贴近应用易用的应用层协议
RPC全称为remote procedure call,即远程过程调用
RPC并不是一个具体的技术,而是指整个网络远程调用过程
一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Client Stub,Server以及Server Stub,这个Stub可以理解为存根。
(1) 客户端(client)以本地调用方式(即以接口的方式)调用服务;
(2) 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制);
(3) 客户端通过sockets将消息发送到服务端;
(4) 服务端存根( server stub)收到消息后进行解码(将消息对象反序列化);
(5) 服务端存根( server stub)根据解码结果调用本地的服务;
(6) 本地服务执行并将结果返回给服务端存根( server stub);
(7) 服务端存根( server stub)将返回结果打包成消息(将结果消息对象序列化);
(8) 服务端(server)通过sockets将消息发送到客户端;
(9) 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息发序列化);
(10) 客户端(client)得到最终结果
注意:无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二进制流(序列化),而数据的接收方则需要把二进制流再恢复为对象(反序列化)
在java中RPC框架比较多,常见的有Hessian、gRPC、Thrix、HSF (High Speed Service Framework)、Dubbo 等,其实对 于RPC框架而言,核心模块 就是通讯和序列化
Java RMI 指的是远程方法调用 (Remote Method Invocation),是java原生支持的远程调用 ,采用JRMP(Java Remote Messageing protocol)作为通信协议,可以认为是纯java版本的分布式远程调用解决方案, RMI主要用于不同虚拟机之间的通信,这些虚拟机可以在不同的主机上、也可以在同一个主机上,这里的通信可以理解为一个虚拟机上的对象调用另一个虚拟机上对象的方法
1)存根/桩(Stub):远程对象在客户端上的代理;
2)远程引用层(Remote Reference Layer):解析并执行远程引用协议;
3)传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。
1)骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法,并接收方法执行后的返回值;
2)远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
3)传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。
以URL形式注册远程对象,并向客户端回复对远程对象的引用
1)客户端从远程服务器的注册表中查询并获取远程对象引用
2)桩对象与远程对象具有相同的接口和方法列表,当客户端调用远程对象时,实际上是由相应的桩对象代理完成的。
3 ) 远程引用层在将桩的本地引用转换为服务器上对象的远程引用后,再将调用传递给传输层(Transport),由传输层通过TCP协议发送调用;
4)在服务器端,传输层监听入站连接,它一旦接收到客户端远程调用后,就将这个引用转发给其上层的远程引用层
5)服务器端的远程引用层将客户端发送的远程应用转换为本地虚拟机的引用后,再将请求传递给骨架(Skeleton);
6)骨架读取参数,又将请求传递给服务器,最后由服务器进行实际的方法调用
1)如果远程方法调用后有返回值,则服务器将这些结果又沿着“骨架->远程引用层->传输层”向下传递;
2)客户端的传输层接收到返回值后,又沿着“传输层->远程引用层->桩”向上传递,然后由桩来反序列化这些返回值,并将最终的结果传递给客户端程序
代码示例:
服务端:
1)定义Remote子接口,在其内部定义要发布的远程方法,并且这些方法都要Throws RemoteException;
import com.yaron.pojo.User;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @description
*/
public interface IHelloService extends Remote {
String sayHello(User user) throws RemoteException;
}
2)定义实现远程接口,并且继承:UnicastRemoteObject
import com.yaron.pojo.User;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* @description 定义实现远程接口,并且继承:UnicastRemoteObject
*/
public class HelloServiceImpl extends UnicastRemoteObject implements IHelloService {
/**
* 手动实现父类的构造方法
* @throws RemoteException
*/
public HelloServiceImpl() throws RemoteException {
}
@Override
public String sayHello(User user) throws RemoteException {
System.out.println("this is server, say Hello to :"+ user.getName());
return "success";
}
}
3)启动服务器:依次完成注册表的启动和远程对象绑定
import com.yaron.pojo.User;
import com.yaron.service.IHelloService;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
* @description 启动服务器:依次完成注册表的启动和远程对象绑定
*/
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
// 1. 从注册表中获取远程对象, 强转接口 获取到的是代理对象
IHelloService service = (IHelloService)Naming.lookup("rmi://127.0.0.1:8888/hello");
// 2. 准备参数
User user = new User("JackMa", 55);
// 3. 调用远程方法
String res = service.sayHello(user);
System.out.println(res);
}
}
客户端:
1)通过符合JRMP规范的URL字符串在注册表中获取并强转成Remote子接口对象;
2)调用这个Remote子接口对象中的某个方法就是为一次远程方法调用行为
import com.yaron.pojo.User;
import com.yaron.service.IHelloService;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
* @description
*/
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
// 1. 从注册表中获取远程对象, 强转接口 获取到的是代理对象
IHelloService service = (IHelloService)Naming.lookup("rmi://127.0.0.1:8888/hello");
// 2. 准备参数
User user = new User("JackMa", 55);
// 3. 调用远程方法
String res = service.sayHello(user);
System.out.println(res);
}
}
同步(synchronize)、异步(asychronize)是指应用程序和内核的交互而言的
指用户进程触发IO操作等待或者轮训的方式查看IO操作是否就绪
当一个异步进程调用发出之后,调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态、通知来通知调用者,或者通过回调函数来处理这个调用。
使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,OS需要支持异步IO操作
阻塞和非阻塞是针对于进程访问数据的时候,根据IO操作的就绪状态来采取不同的方式,简单点说就是一种读写操作方法的实现方式. 阻塞方式下读取和写入将一直等待, 而非阻塞方式下,读取和写入方法会立即返回一个状态值
使用阻塞IO的时候,Java调用会一直阻塞到读写完成才返回
使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class IOServer {
public static void main(String[] args) throws IOException {
// 首先创建一个ServerSocket
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1",8081));
while (true){
Socket socket = serverSocket.accept();
new Thread(() -> {
byte[] bytes = new byte[1024];
try {
int len = socket.getInputStream().read(bytes); // 同步阻塞
System.out.println(new String(bytes,0,len));
socket.getOutputStream().write(bytes,0,len);
socket.getOutputStream().flush();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
import java.io.IOException;
import java.net.Socket;
/**
* @description
*/
public class IOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",8081);
socket.getOutputStream().write("hello".getBytes());
socket.getOutputStream().flush();
System.out.println("Server send back data ---------------------");
byte[] bytes = new byte[1024];
int len = socket.getInputStream().read(bytes);
System.out.println(new String(bytes,0,len));
socket.close();
}
}
同步非阻塞IO
服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理
一个连接创建后,不会需要对应一个线程,这个连接会被注册到多路复用器,所有的连接需要一个线程就可以操作,该线程的多路复用器会轮询,发现连接有请求时,才开启一个线程处理
NIO 新引入的最重要的抽象是通道的概念。Channel 数据连接的通道。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中
通道channel可以向缓冲区Buffer中写数据,也可以像buffer中存数据
使用选择器,借助单一线程,就可对数量庞大的活动 I/O 通道实时监控和维护
如下图所示,IO模型中,一个连接来了,会创建一个线程,对应一个while死循环,死循环的目的就是不断检测这条连接上是否有数据可以读,大多数情况下,1w个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为读不出啥数据
而在NIO模型中,多个while死循环变成一个死循环,这个死循环由一个线程控制,那么NIO是如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢?
这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* @description 服务端
*/
public class NIOServer extends Thread{
// 1. 声明多路利用器
private Selector selector;
// 2. 定义读写缓冲区
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
// 3. 定义构造方法初始化端口
public NIOServer(int port){
init(port);
}
// 4. 初始化
private void init(int port) {
System.out.println("服务器正在启动....");
try {
// 1. 开启多路复用器
this.selector = Selector.open();
// 2. 开启服务通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3 . 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4. 绑定端口
serverSocketChannel.bind(new InetSocketAddress(port));
/**
* SelectionKey.OP_ACCEPT 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
* SelectionKey.OP_CONNECT 连接就绪事件,表示客户与服务器的连接已经 建立 成功
* SelectionKey.OP_READ 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作)
* SelectionKey.OP_WRITE 写 就绪事件,表示已经 可以向通道 写数据了 (通道目前可以用于写操作)
*/
// 5. 注册,标记服务连接状态为ACCEPT状态
serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器 启动完毕....");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
// 1. 当有至少一个通道被选中,执行此方法, 看下哪个通道被选中
this.selector.select();
// ps 当所有的客户端 与 服务端建立 连接之后,每个channel通道都有一个编号,这个 selectionKeys 就是这些 通道的集合
// 2. 获取选中的通道编号集合
Set selectionKeys = this.selector.selectedKeys();
Iterator keys = selectionKeys.iterator();
// 3. 遍历Keys
while (keys.hasNext()){
SelectionKey key = keys.next();
// 4. 当前 key需要从集合中移出,如果不移出,下次循环会执行对应的逻辑,造成业务混乱
keys.remove();
// 5. 判断通道是否有效
if (key.isValid()){
try {
// 6.判断是否可以连接 对应 java.nio.channels.SelectionKey.OP_ACCEPT
if (key.isAcceptable()) {
accept(key);
}
}catch (CancelledKeyException e){
// 出现异常断开连接
key.cancel();
}
try {
// 7.判断是否可读 对应 java.nio.channels.SelectionKey.OP_READ
if (key.isReadable()) {
read(key);
}
}catch (CancelledKeyException e){
// 出现异常断开连接
key.cancel();
}
try {
// 8.判断是否可写 对应 java.nio.channels.SelectionKey.OP_WRITE
if (key.isWritable()) {
write(key);
}
}catch (CancelledKeyException e){
// 出现异常断开连接
key.cancel();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void accept(SelectionKey key) {
try {
// 1. 当前 通道在init方法中注册到了selector中的ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 2. 阻塞方法,客户端发起请求后 返回
SocketChannel channel = serverSocketChannel.accept();
// 3. serverSocketChannel 设置为非阻塞
channel.configureBlocking(false);
// 4. 设置对应客户端的通道标记,设置次通道 为可读时使用
// 此时 客户端channel已经 变为可读了
channel.register(this.selector,SelectionKey.OP_READ);
}catch (IOException ex){
ex.printStackTrace();
}
}
/**
* 使用通道读数据
*
* @param key
*/
private void read(SelectionKey key) {
try {
// 清空缓存
this.readBuffer.clear();
// 获取当前 通道对象
SocketChannel channel = (SocketChannel) key.channel();
// 将通道 的数据(客户发送的data) 读取到缓存 中
int readLen = channel.read(readBuffer);
// 如果通道 中没有数据
if (readLen == -1){
// 关闭通道
key.channel().close();
// 关闭连接
key.cancel();
return;
}
// Buffer中有游标,游标不会重置, 需要我们调用 flip重置,否则读取不一致
this.readBuffer.flip();
// 创建有效 字节长度数组
byte[] bytes = new byte[readBuffer.remaining()];
// 读取buffer中数据保存在字节数据中
readBuffer.get(bytes);
System.out.println("收到了从客户端 : "+ channel.getRemoteAddress()+":" + new String(bytes, StandardCharsets.UTF_8));
// 注册通道,标记为写操作
channel.register(this.selector,SelectionKey.OP_WRITE);
}catch (IOException ex){
ex.printStackTrace();
}
}
/**
* 给通道中写操作
* @param key
*/
private void write(SelectionKey key) {
// 清空缓存
this.readBuffer.clear();
// 获取当前 通道对象
SocketChannel channel = (SocketChannel)key.channel();
// 录入数据
Scanner scanner = new Scanner(System.in);
try {
System.out.println("即将发送数据到客户端...");
String line = scanner.nextLine();
// 把录入的数据写到buffer中
writeBuffer.put(line.getBytes(StandardCharsets.UTF_8));
// 重置缓存 游标
writeBuffer.flip();
channel.write(writeBuffer);
channel.register(this.selector, SelectionKey.OP_READ);
}catch (IOException ex){
ex.printStackTrace();
}
}
public static void main(String[] args) {
new NIOServer(8888).start();
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Scanner;
/**
* @description 客户端
*/
public class NIOClient {
public static void main(String[] args) {
// 创建远程地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1",8888);
// 当前 客户端 与服务端 的连接通道
SocketChannel channel = null;
// 定义缓存
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// 开启通道
channel = SocketChannel.open();
// 连接远程服务器
channel.connect(address);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("客户端即将给 服务器发送数据....");
String line = scanner.nextLine();
if ("exit".equals(line)){
break;
}
// 控制台输入数据写到缓存
buffer.put(line.getBytes(StandardCharsets.UTF_8));
// 重置buffer游标
buffer.flip();
// 发送数据
channel.write(buffer);
// 清空缓存数据
buffer.clear();
// 读取服务器返回的数据
int readLen = channel.read(buffer);
if (readLen == -1){
break;
}
// 重置buffer游标
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 读取数据到字节数据
buffer.get(bytes);
System.out.println("收到了服务器返回的数据: "+ new String(bytes,StandardCharsets.UTF_8));
buffer.clear();
}
}catch (IOException ex){
try {
if (Objects.nonNull(channel)) {
channel.close();
}
}catch (IOException ex1){
ex1.printStackTrace();
}
}
}
}
异步非阻塞IO。A代表asynchronize
当有流可以读时,操作系统会将可以读的流传入read方法的缓冲区,并通知应用程序,对于写操作,OS将write方法的流写入完毕是操作系统会主动通知应用程序。因此read和write都是异步 的,完成后会调用回调函数。
使用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器。重点调用了OS参与并发操作,编程比较复杂。Java7开始支持