序章
RPC(RemoteProcedureCall)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。
在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
在本模块中,我们试图实现client通过指定id,通过RPC框架,从server拿到id对应的User对象信息。
本项目实现参考自MyRPCFromZero
环境
IntelliJ IDEA 2021.2.3 (Community Edition)
实现
项目创建
先new一个project
创建一个maven工程
命名为SimpleRPC
创建
右键SimpleRPC文件,再new 一个 module,方便管理:
命名为simpleRPC-01
这个module simpleRPC-01就是我们这次要编写的位置。
simpleRPC-01将会初步实现一个极简单的RPC调用,之后还会有其他module:simpleRPC-02,simpleRPC-03,simpleRPC-04 ....
我们将会一步步改进我们的RPC。
在本模块中,我们试图实现一个简单地调用:client通过给定一个id,通过RPC框架,从server拿到id对应的User对象信息。
在project的java目录下,创建package
名为com.rpc
依赖配置
pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SimpleRPC</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simpleRPC-01</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
复制代码
common
创建一个package common,然后再common中创建User对象:
package com.rpc.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author zwy
*
* 定义简单User信息,要使用lombok,IDEA必须也安装lombok插件,否则用不了。
*
* '@Builder' 创建者模式又叫建造者模式。简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但却可以精细地控制对象的构造过程。
* '@NoArgsConstructor' 生成一个无参构造方法
* '@AllArgsConstructor' 使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数
* '@Data' 相当于 @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
// 客户端和服务端共有的
private Integer id;
private String userName;
private Boolean sex;
}
复制代码
service
创建package service,创建UserService接口和UserSercviceImpl的实现类:
UserService接口
package com.rpc.service;
import com.rpc.common.User;
/**
* @author zwy
*
* 服务器端提供服务的方法的接口
*/
public interface UserService {
// 客户端通过这个接口调用服务器端的实现类
User getUserByUserId(Integer id);
}
复制代码
UserServiceImpl.java
package com.rpc.service;
import com.rpc.common.User;
import java.util.Random;
import java.util.UUID;
/**
* @author zwy
*
* 服务器端提供服务的方法
* UserServiceImpl服务:接收一个id,返回一个User对象,
* 提供属于这个ID(Integer)的User,User中包含他的ID(Integer),
* 名字Name(String)和性别sex(Boolean)。
*
* lombok.Builder构造器构造方式详情可见:https://blog.csdn.net/weixin_41540822/article/details/86606562
*
* 这里举个例子,构造格式为:目标类.builder()....build():比如
* User.builder().id(id).build();
* 则实际上是给User构造了:
* public User.UserBuilder id(int id) {
* this.id = id;
* return this;
* }
*/
public class UserServiceImpl implements UserService {
@Override
public User getUserByUserId(Integer id) {
System.out.println("客户端查询了ID:" + id + "的用户");
// 模拟数据库中取用户的行为
Random random = new Random();
User user = User.builder()
.userName(UUID.randomUUID().toString())
.id(id)
.sex(random.nextBoolean()).build(); // 随机给User对象赋值
return user; // 返回User对象
}
}
复制代码
server
创建package server,创建RPCServer.java
RPCServer.java:
package com.rpc.server;
import com.rpc.common.User;
import com.rpc.service.UserServiceImpl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author zwy
*
* RPC服务器端
* 服务器端:创建ServerSocket对象监听客户端的连接(BIO),监听到连接之后,开启一个线程来处理,socket对象的获取输入
* 输出流作为targat,初始化输入输出流,读取从客户端传过来的id,用输入流的readInt()读取出来,
* 调用getUserByUserId,给User赋值之后返回User对象,把Client想要的User对象通过输出流返回给客户端Client,
* 输出流刷新(flush)。
*
* ServerSocket:用于服务器端,监听客户端连接
* ServerSocket.accept():是一个阻塞方法,方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,
* 并将它返回。如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。
* java.io.ObjectInputStream.readInt():方法读取一个32位的int
* java.io.ObjectOutputStream.writeObject(Object obj): 此方法将指定的对象写入ObjectOutputStream。该对象的类,类的签名,
* 以及类及其所有超类型的非瞬态和非静态字段的值被写入。
*/
public class RPCServer {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
try {
// 创建ServerSocket对象,端口号要和Client一致
ServerSocket serverSocket = new ServerSocket(8899);
System.out.println("服务器启动!");
// BIO的方式监听Socket,监听到之后返回Socket对象
while (true) {
Socket socket = serverSocket.accept();
// 监听到连接之后,开启一个线程来处理
new Thread(new Runnable() {
@Override
public void run() {
try {
// socket对象的获取输入输出流作为targat,初始化输入输出流
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
// 读取从客户端传过来的id,用readInt读取出来,调用getUserByUserId,给User赋值之后返回User对象
Integer id = ois.readInt();
User userByUserId = userService.getUserByUserId(id); // 这!就是Client想要Server调用的方法!
// 把Client想要的User对象返回给客户端Client
oos.writeObject(userByUserId);
oos.flush();
} catch (IOException e) {
e.printStackTrace();
System.out.println("从IO中读取数据错误");
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败");
}
}
}
复制代码
client
创建package client,创建RPCClient.java
RPCClient.java:
package com.rpc.client;
import com.rpc.common.User;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Random;
/**
* @author zwy
*
* RPC客户端:调用服务器端的方法
* 客户端建立socket连接,标定主机ip地址,指定程序使用的端口号,
* 将socket作为输入输出流的target来创建输入输出流对象,
* 客户端通过输出流传id给服务器,刷新流。
* 客户端通过输入流获取服务器的的返回对象,打印。
*
* host:主机名,用于回送地址。主机名对应的IP地址,可以和别人通信(一个主机是一栋楼,这栋楼的名字是这个主机的ip地址)
* port:端口号(一栋楼有很多个房间可以使用,这就是端口。一个程序就是一个人,如果要跟另外一个主机通信,
* 需要开一个房间给他的程序使用)
* Socket.getInputStream():方法得到一个输入流,客户端的Socket对象上的getInputStream()方法
* 得到的输入流其实就是从服务器端发回的数据流。
* Socket.GetOutputStream():方法得到一个输出流,客户端Socket对象上的getOutputStream()方法
* 返回的输出流,就是将要发送到服务器端的数据流(其实是一个缓冲区,暂时存储将要发送过去的数据)。
* java.io.ObjectOutputStream.flush():此方法刷新流。这将写入所有缓冲的输出字节并刷新到基础流。
* java.io.ObjectInputStream.readObject():方法从ObjectInputStream中读取对象。读取该对象的类,类签名,类及其所有超类型的
* 非瞬态和非静态字段的值。默认的反序列化的类可以使用writeObject和readObject方法被重写。
* 由此对象引用的对象被传递地读,这样对象的完全等价的图形是由readObject重建。
*/
public class RPCClient {
public static void main(String[] args) {
try {
// 建立socket连接,标定主机ip地址,指定程序使用的端口号
Socket socket = new Socket("127.0.0.1", 8899);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
// 传id给服务器
objectOutputStream.writeInt(new Random().nextInt());
objectOutputStream.flush();
// 服务器查询数据,返回对应的对象,输入流读取对象,打印返回的user
User user = (User) objectInputStream.readObject();
System.out.println("返回的User: " + user);
}
catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
System.out.println("客户端启动失败");
}
}
}
复制代码
文件结构
到此我们simpleRPC-01的所有文件结构如下:
运行
先启动RPCServer.java:
然后启动RPCClient.java
成功~