前言
在1024程序员节前夕,学习技术是对节日最好的庆祝。
手写一个简易版的RPC,可以把模糊抽象的概念具像化,属于落地层面了。
1. RPC基本原理
RPC原理
2. 四个版本的迭代
注:api表示服务端和客户端都有的包,server表示服务端专有的包,client表示客户端专有的包。
后一个版本在前一个版本基础上改进。
(1)版本一:根据商品id列表查询商品列表
// api
@Data
public class Product implements Serializable {
private final static long serialVersionUID = 8552999810899286984L;
private int id;
private String desc;
private double price;
public Product() {
}
public Product(int id, String desc, double price) {
this.id = id;
this.desc = desc;
this.price = price;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", desc='" + desc + '\'' +
", price=" + price +
'}';
}
}
// api
public interface ProductListService {
List<Product> getProductList(List<Integer> ids);
}
// server
public class ProductListServiceImpl implements ProductListService {
Map<Integer, Product> productMap = new HashMap<>();
public ProductListServiceImpl() {
productMap.put(1, new Product(1, "精致茶壶", 59.9));
productMap.put(2, new Product(2, "机械键盘", 119));
productMap.put(3, new Product(3, "ipad专用键盘", 29.8));
productMap.put(4, new Product(4, "DIY手工填涂画布", 23.5));
productMap.put(6, new Product(6, "机器人DIY", 399));
productMap.put(8, new Product(8, "无人机", 1289));
productMap.put(9, new Product(9, "洗发水", 49.9));
productMap.put(10, new Product(10, "沐浴露", 29.9));
}
@Override
public List<Product> getProductList(List<Integer> ids) {
List<Product> resList = new ArrayList<>();
ids.forEach(id->resList.add(productMap.get(id)));
return resList;
}
}
// server
public class Server extends Thread {
private ProductListService productListService;
public Server(int port) throws IOException, ClassNotFoundException {
productListService = new ProductListServiceImpl();
ServerSocket serverSocket = new ServerSocket(port);
while(true) {
System.out.println("等待连接,ip=127.0.0.1, port=9999");
Socket socket = serverSocket.accept();
process(socket);
socket.close();
}
}
private void process(Socket socket) throws IOException, ClassNotFoundException {
System.out.println("ip Address:"+socket.getInetAddress());
System.out.println("port:"+socket.getPort());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
List<Integer> list = (List<Integer>) ois.readObject();
List<Product> productList = productListService.getProductList(list);
oos.writeObject(productList);
oos.flush();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
new Server(9999).start();
}
}
// client
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Socket socket = new Socket("127.0.0.1", 9999);
System.out.println("客户端:连接成功!");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream dos = new ObjectOutputStream(baos);
dos.writeObject(Arrays.asList(1,2,3,4));
socket.getOutputStream().write(baos.toByteArray());
dos.flush();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
List<Product> list = (List<Product>) ois.readObject();
System.out.println(list);
socket.close();
}
}
先启动server端,再启动client端,运行结果:
客户端:连接成功!
[Product{id=1, desc='精致茶壶', price=59.9}, Product{id=2, desc='机械键盘', price=119.0}, Product{id=3, desc='ipad专用键盘', price=29.8}, Product{id=4, desc='DIY手工填涂画布', price=23.5}]
分析:客户端只是为了调用一个getProductList()方法,就要编写大量无关的socket代码,明显做了很多无关的事情,改进空间巨大。
(2)版本二:使用动态代理帮助客户端处理网络相关细节
// client
public class Stub {
public ProductListService getSub() {
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = new Socket("127.0.0.1", 9999);
System.out.println("客户端:连接成功!");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream dos = new ObjectOutputStream(baos);
dos.writeObject(args[0]);
socket.getOutputStream().write(baos.toByteArray());
dos.flush();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
List<Product> list = (List<Product>) ois.readObject();
socket.close();
return list;
}
};
return (ProductListService) Proxy.newProxyInstance(ProductListService.class.getClassLoader(), new Class[]{ProductListService.class}, invocationHandler);
}
}
// client
public class Client {
public static void main(String[] args) {
ProductListService sub = new Stub().getSub();
System.out.println(sub.getProductList(Arrays.asList(1,2,4,9)));
}
}
先启动server端,再启动client端,运行结果:
客户端:连接成功!
[Product{id=1, desc='精致茶壶', price=59.9}, Product{id=2, desc='机械键盘', price=119.0}, Product{id=4, desc='DIY手工填涂画布', price=23.5}, Product{id=9, desc='洗发水', price=49.9}]
分析:这样一写,客户端代码只有简洁的两行。但仍有不足,这限定死了只能调用getProductList方法,参数只能是ArrayList<Integer>类型。比如想调用其他的方法,就还得再封装一次网络底层。
(3)版本三:网络传输时将方法名、参数类型、参数值一一存放到Map中
// api
public interface ProductListService {
List<Product> getProductList(List<Integer> ids);
Product getProduct(int id);
}
// server
public class ProductListServiceImpl implements ProductListService {
Map<Integer, Product> productMap = new HashMap<>();
public ProductListServiceImpl() {
productMap.put(1, new Product(1, "精致茶壶", 59.9));
productMap.put(2, new Product(2, "机械键盘", 119));
productMap.put(3, new Product(3, "ipad专用键盘", 29.8));
productMap.put(4, new Product(4, "DIY手工填涂画布", 23.5));
productMap.put(6, new Product(6, "机器人DIY", 399));
productMap.put(8, new Product(8, "无人机", 1289));
productMap.put(9, new Product(9, "洗发水", 49.9));
productMap.put(10, new Product(10, "沐浴露", 29.9));
}
@Override
public List<Product> getProductList(List<Integer> ids) {
List<Product> resList = new ArrayList<>();
ids.forEach(id->resList.add(productMap.get(id)));
return resList;
}
@Override
public Product getProduct(int id) {
return productMap.get(id);
}
}
// server
public class Server3 extends Thread {
private ProductListService productListService;
public Server3(int port) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
productListService = new ProductListServiceImpl();
ServerSocket serverSocket = new ServerSocket(port);
while(true) {
System.out.println("等待连接,ip=127.0.0.1, port=9999");
Socket socket = serverSocket.accept();
process(socket);
socket.close();
}
}
private void process(Socket socket) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
System.out.println("ip Address:"+socket.getInetAddress());
System.out.println("port:"+socket.getPort());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
Map<String, Object> map = (Map<String, Object>) ois.readObject();
/**
* map.put("args", args);
* map.put("argsTypes", method.getParameterTypes());
* map.put("methodName", method.getName());
*/
Object[] args = (Object[]) map.get("args");
String methodName = (String) map.get("methodName");
Class[] argsTypes = (Class[]) map.get("argsTypes");
Method method = ProductListService.class.getMethod(methodName, argsTypes);
Object res = method.invoke(productListService, args);
oos.writeObject(res);
oos.flush();
}
public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
new Server3(1999).start();
}
}
// client
public class Stub {
public ProductListService getSub() {
InvocationHandler invocationHandler = (proxy, method, args) -> {
Socket socket = new Socket("127.0.0.1", 1999);
System.out.println("客户端:连接成功!");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream dos = new ObjectOutputStream(baos);
Map<String, Object> map = new HashMap<>();
map.put("args", args);
map.put("argsTypes", method.getParameterTypes());
map.put("methodName", method.getName());
dos.writeObject(map);
socket.getOutputStream().write(baos.toByteArray());
dos.flush();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Object obj = ois.readObject();
socket.close();
return obj;
};
return (ProductListService) Proxy.newProxyInstance(ProductListService.class.getClassLoader(), new Class[]{ProductListService.class}, invocationHandler);
}
}
// client
public class Client {
// 可调用任意方法
public static void main(String[] args) {
ProductListService sub = new Stub().getSub();
System.out.println(sub.getProductList(Arrays.asList(7,8,9,10)));
System.out.println(sub.getProduct(1));
}
}
先启动server端,再启动client端,运行结果:
客户端:连接成功!
[null, Product{id=8, desc='无人机', price=1289.0}, Product{id=9, desc='洗发水', price=49.9}, Product{id=10, desc='沐浴露', price=29.9}]
客户端:连接成功!
Product{id=1, desc='精致茶壶', price=59.9}
分析:现在可以调用ProductListService服务的各种方法啦!假如再新增一个方法deleteProduct(id)
// api
public interface ProductListService {
List<Product> getProductList(List<Integer> ids);
Product getProduct(int id);
int deleteProduct(int id);
}
// server
public class ProductListServiceImpl implements ProductListService {
Map<Integer, Product> productMap = new HashMap<>();
public ProductListServiceImpl() {
productMap.put(1, new Product(1, "精致茶壶", 59.9));
productMap.put(2, new Product(2, "机械键盘", 119));
productMap.put(3, new Product(3, "ipad专用键盘", 29.8));
productMap.put(4, new Product(4, "DIY手工填涂画布", 23.5));
productMap.put(6, new Product(6, "机器人DIY", 399));
productMap.put(8, new Product(8, "无人机", 1289));
productMap.put(9, new Product(9, "洗发水", 49.9));
productMap.put(10, new Product(10, "沐浴露", 29.9));
}
@Override
public List<Product> getProductList(List<Integer> ids) {
List<Product> resList = new ArrayList<>();
ids.forEach(id->resList.add(productMap.get(id)));
return resList;
}
@Override
public Product getProduct(int id) {
return productMap.get(id);
}
@Override
public int deleteProduct(int id) {
Product remove = productMap.remove(id);
return remove==null?0:1;
}
}
先启动server端,再启动client端,运行结果:
客户端:连接成功!
[null, Product{id=8, desc='无人机', price=1289.0}, Product{id=9, desc='洗发水', price=49.9}, Product{id=10, desc='沐浴露', price=29.9}]
客户端:连接成功!
0
客户端:连接成功!
null
分析:似乎ProductListService服务的方法新增删除不会影响网络底层的变化,但细心的读者可能发现了,如果需要再新增一个UserService呢,网络底层仍得变。
迭代四:先找服务,再确定方法
// api
public interface UserService {
String sayHello(String name);
User getUserById();
}
// server
public class Server4 extends Thread {
private Map<String, Class> routeClassMap;
private Map<String, Object> serviceMap;
public void init() {
routeClassMap = new HashMap<>();
routeClassMap.put("rpc.ProductListService", ProductListService.class);
routeClassMap.put("service.UserService", UserService.class);
serviceMap = new HashMap<>();
serviceMap.put("rpc.ProductListService", new ProductListServiceImpl());
serviceMap.put("service.UserService", new UserServiceImpl());
}
public Server4(int port) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
init();
ServerSocket serverSocket = new ServerSocket(port);
while(true) {
System.out.println("等待连接,ip=127.0.0.1, port="+port);
Socket socket = serverSocket.accept();
process(socket);
socket.close();
}
}
private void process(Socket socket) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
System.out.println("ip Address:"+socket.getInetAddress());
System.out.println("port:"+socket.getPort());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
Map<String, Object> map = (Map<String, Object>) ois.readObject();
/**
* map.put("args", args);
* map.put("argsTypes", method.getParameterTypes());
* map.put("methodName", method.getName());
*/
Object[] args = (Object[]) map.get("args");
String methodName = (String) map.get("methodName");
Class[] argsTypes = (Class[]) map.get("argsTypes");
String serviceName = (String) map.get("serviceName");
Method method = routeClassMap.get(serviceName).getMethod(methodName, argsTypes);
Object res = method.invoke(serviceMap.get(serviceName), args);
oos.writeObject(res);
oos.flush();
}
public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
new Server4(2999).start();
}
}
// client
public class Stub<T> {
Class<T> serviceClazz;
public Stub(Class<T> serviceClazz) {
this.serviceClazz = serviceClazz;
}
public T getSub() {
InvocationHandler invocationHandler = (proxy, method, args) -> {
Socket socket = new Socket("127.0.0.1", 2999);
System.out.println("客户端:连接成功!");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream dos = new ObjectOutputStream(baos);
Map<String, Object> map = new HashMap<>();
map.put("serviceName", method.getDeclaringClass().getName());
map.put("args", args);
map.put("argsTypes", method.getParameterTypes());
map.put("methodName", method.getName());
dos.writeObject(map);
socket.getOutputStream().write(baos.toByteArray());
dos.flush();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Object obj = ois.readObject();
socket.close();
return obj;
};
return (T) Proxy.newProxyInstance(serviceClazz.getClassLoader(), new Class[]{serviceClazz}, invocationHandler);
}
}
// client
public class Client {
// 可调用任意服务、任意方法
public static void main(String[] args) {
ProductListService sub = new Stub<>(ProductListService.class).getSub();
System.out.println(sub.getProductList(Arrays.asList(7,8,9,10)));
UserService sub1 = new Stub<>(UserService.class).getSub();
System.out.println(sub1.getUserById());
System.out.println(sub1.sayHello("jj"));
}
}
先启动server端,再启动client端,运行结果:
客户端:连接成功!
[null, Product{id=8, desc='无人机', price=1289.0}, Product{id=9, desc='洗发水', price=49.9}, Product{id=10, desc='沐浴露', price=29.9}]
客户端:连接成功!
User(id=1, name=Jack)
客户端:连接成功!
hello:jj
分析:这样写就可以支持各种不同的服务、不同方法、不同参数调用了。
只不过本文使用了原始的BIO同步阻塞模型,其实可以用netty提供的nio模型来做rpc的底层,出于学习的目的,才有了本文的四次迭代演进。
Reference
1_RPC基本理论_哔哩哔哩_bilibili 马士兵老师亲自授课