netty學習之手寫RPC框架

RPC簡介

RPC,Remote Procedure Call,遠程過程調用,是一種通過網絡從遠程計算機上請求服務,而不需要了解底層網絡技術的協議。在 OSI 網絡通信模型中,RPC 跨越了傳輸層(第四層,傳輸協議 TCP/UDP,即通過 ip+port 進行通信)和應用層(第七層,傳輸協議有 HTTP、HTTPS、FTP 等)。RPC 使得開發分佈式系統應用變得更加容易。
RPC 採用 C/S 模式。請求程序就是 Client,而服務提供程序就是 Server。首先,Client 發送一個帶有請求參數的調用請求到 Server,然後等待響應。在 Server 端,進程一直處於睡眠狀態直到接收到 Client 的調用請求。當一個調用請求到達,Server 會根據請求參數進行計算,並將計算結果發送給 Client,然後等待下一個調用請求。Client 接收到響應信息,即獲取到調用結果,然後根據情況繼續發出下一次調用。

RPC框架需求分析

我們這裏要定義一個 RPC 框架,這個框架提供給用戶後,用戶只需要按照使用步驟就可以完成 RPC 遠程調用。我們現在給出用戶對於該 RPC 框架的使用步驟:

  • 用戶需要將業務接口通知到 Server 與 Client,因爲業務接口是服務名稱。
  • 用戶只需將業務接口的實現類寫入到 Server 端的指定包下,那麼這個包下的實現類就
    會被 Server 發佈。
  • Client 端只需根據業務接口名就可獲取到 Server 端發佈的服務提供者,然後就可以調用
    到遠程 Server 端的實現類方法的執行。

實現步驟

創建API工程

該 api 工程中用於存放業務接口、常量類、工具類等將來服務端與客戶端均會使用到的一個接口與類。

創建工程並導入依賴

創建工程rpc-api,並導入

<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">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.lhl</groupId>
	<artifactId>rpc-api</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>rpc-api</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.6</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>
</project>

定義業務接口

定義業務接口

package com.lhl.rpc_api.service;

import com.lhl.rpc_api.service.databean.User;

/**
 * 用戶處理業務Service
 * 
 * @author LIUHL
 *
 */
public interface UserService {

	/**
	 * 新增用戶接口
	 * 
	 * @param user
	 *            用戶信息實體類
	 * @return 添加結果
	 */
	public String addUser(User user);

}

定義實體類信息

package com.lhl.rpc_api.service.databean;

import lombok.Data;

/**
 * 用戶信息
 * 
 * @author LIUHL
 *
 */
@Data
public class User {

	// 注意一定要實現Serializable接口否則netty會報xception in thread "main" io.netty.handler.codec.EncoderException: java.io.NotSerializableException: com.lhl.rpc_api.service.databean.User
	/*
	 * 用戶名稱
	 */
	private String userName;

}

定義常量類

這是定義了客戶端請求服務端的調用信息

package com.lhl.rpc_api.invoke;

import java.io.Serializable;

/**
 * 客戶端調用信息
 * 
 * @author LIUHL
 *
 */
public class InvokeMessage implements Serializable {
	/**
	 * 接口名,即微服務名稱
	 */
	private String className;
	/**
	 * 要遠程調用的方法名
	 */
	private String methodName;
	/**
	 * 參數類型列表
	 */
	private Class<?>[] paramTypes;
	/**
	 * 參數值列表
	 */
	private Object[] paramValues;
}

創建Server工程

該工程實現了API中定義的業務接口,並且創建netty服務器。

導入依賴

<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">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.lhl</groupId>
	<artifactId>rpc-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>rpc-server</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.6</version>
			<scope>provided</scope>
		</dependency>
		<!-- 導入API工程 -->
		<dependency>
			<groupId>com.lhl</groupId>
			<artifactId>rpc-api</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<!-- 導入netty -->
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.36.Final</version>
		</dependency>
	</dependencies>
</project>

實現業務接口

package com.lhl.rpc_server.provider;

import com.lhl.rpc_api.service.UserService;
import com.lhl.rpc_api.service.databean.User;

/**
 * 用戶服務實現
 * 
 * @author LIUHL
 *
 */
public class UserServiceImpl implements UserService {

	/**
	 * 添加用戶並返回結果
	 */
	public String addUser(User user) {
		return user.getUserName() + "註冊成功";
	}

}

定義服務端消息處理器

package com.lhl.rpc_server.server;

import java.util.Map;

import com.lhl.rpc_api.invoke.InvokeMessage;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * RPC框架服務端處理器,負責接收和處理客戶端發來的調度 <br>
 * SimpleChannelInboundHandler<InvokeMessage> 泛型中定義的是服務端-客戶端之間的消息通信類型
 * 
 * @author LIUHL
 *
 */
public class RpcServerHandler extends SimpleChannelInboundHandler<InvokeMessage> {

	// 掃描過後的的服務名與對應的處理類
	private Map<String, Object> registerMap;

	public RpcServerHandler(Map<String, Object> registerMap) {
		this.registerMap = registerMap;
	}

	/**
	 * 接受客戶端發送過來的消息,並通過服務名稱找到對應的實現類,通過反射機制執行對應業務,並將結果返回到客戶端
	 */
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, InvokeMessage msg) throws Exception {
		Object result = "沒有該提供者,或沒有該方法";
		if (registerMap.containsKey(msg.getClassName())) {
			// 從註冊表中獲取接口對應的實現類實例
			Object invoker = registerMap.get(msg.getClassName());
			result = invoker.getClass().getMethod(msg.getMethodName(), msg.getParamTypes()).invoke(invoker,
					msg.getParamValues());
		}
		// 將運算結果返回給client
		ctx.writeAndFlush(result);
		ctx.close();
	}

}

定義服務器類

package com.lhl.rpc_server.server;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

/**
 * 該類有以下兩種功能 <br>
 * 1.業務實現類的解析器 2.定義netty的服務端信息並提供啓動方法
 * 
 * @author LIUHL
 *
 */
public class RpcServer {
	// 註冊表
	private Map<String, Object> registerMap = new HashMap<String, Object>();
	// 用於存放指定包中的業務接口的實現類名
	private List<String> classCache = new ArrayList<String>();

	// 發佈服務:將指定包中的業務接口實現類實例寫入到註冊表
	public void publish(String basePackage) throws Exception {
		// 將指定包中的業務接口實現類名寫入到classCache中
		cacheClassCache(basePackage);
		// 將指定包中的業務接口實現類實例寫入到註冊表
		doRegister();
	}

	// 將指定包中的業務接口實現類名寫入到classCache中
	private void cacheClassCache(String basePackage) {
		// 獲取指定包目錄中的資源
		URL resource = this.getClass().getClassLoader()
				// com.lhl.rpc_server.provider => com/lhl/rpc_server/provider
				.getResource(basePackage.replaceAll("\\.", "/"));

		// 若指定的目錄中沒有資源,則直接返回
		if (resource == null) {
			return;
		}

		File dir = new File(resource.getFile());
		// 遍歷指定目錄中的所有文件
		for (File file : dir.listFiles()) {
			if (file.isDirectory()) {
				// 若當前file爲目錄,則遞歸
				cacheClassCache(basePackage + "." + file.getName());
			} else if (file.getName().endsWith(".class")) {
				// 去掉文件名後的.class後輟
				String fileName = file.getName().replace(".class", "").trim();
				// 將類的全限定性類名寫入到classCache
				classCache.add(basePackage + "." + fileName);
			}
		}
		// System.out.println(classCache);
	}

	// 將指定包中的業務接口實現類實例寫入到註冊表
	// 註冊表是一個map
	// key爲業務接口名,即微服務名稱
	// value爲該業務接口對應的實現類實例
	private void doRegister() throws Exception {
		if (classCache.size() == 0) {
			return;
		}

		for (String className : classCache) {
			// 將當前遍歷的類加載到內存
			Class<?> clazz = Class.forName(className);
			registerMap.put(clazz.getInterfaces()[0].getName(), clazz.newInstance());
		}
	}

	// 啓動服務器
	public void start() throws InterruptedException {
		EventLoopGroup parentGroup = new NioEventLoopGroup();
		EventLoopGroup childGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap bootstrap = new ServerBootstrap();
			bootstrap.group(parentGroup, childGroup)
					// 用於指定當Server的連接請求處理線程全被佔用時,
					// 臨時存放已經完成了三次握手的請求的隊列的長度。
					// 默認是50
					.option(ChannelOption.SO_BACKLOG, 1024)
					// 指定使用心跳機制來保證TCP長連接的存活性
					.childOption(ChannelOption.SO_KEEPALIVE, true).channel(NioServerSocketChannel.class)
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline pipeline = ch.pipeline();
							pipeline.addLast(new ObjectEncoder());
							pipeline.addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
							pipeline.addLast(new RpcServerHandler(registerMap));
						}
					});
			// 不使用同步的話 這一點不會阻塞會執行下面的關閉方法
			ChannelFuture future = bootstrap.bind(8888).sync();
			System.out.println("服務端已啓動,監聽的端口爲:8888");
			// 不使用同步的話 這一點不會阻塞會執行下面的關閉方法
			future.channel().closeFuture().sync();
		} finally {
			parentGroup.shutdownGracefully();
			childGroup.shutdownGracefully();
		}
	}
}

定義啓動類

package com.lhl.rpc_server.server;

/**
 * 服務器啓動類
 * 
 * @author LIUHL
 *
 */
public class RpcStarter {
	public static void main(String[] args) throws Exception {
		RpcServer server = new RpcServer();
		// 指定業務實現類包路徑
		server.publish("com.lhl.rpc_server.provider");
		server.start();
	}
}

創建client工程

<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">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.lhl</groupId>
	<artifactId>rpc-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>rpc-client</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.6</version>
			<scope>provided</scope>
		</dependency>
		<!-- 導入API工程 -->
		<dependency>
			<groupId>com.lhl</groupId>
			<artifactId>rpc-api</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<!-- 導入netty -->
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.36.Final</version>
		</dependency>
	</dependencies>
</project>

客戶端處理類

package com.lhl.rpc_client.client;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * 客戶端處理類
 * 
 * @author LIUHL
 *
 */
public class RpcClientHandler extends SimpleChannelInboundHandler<Object> {
	private Object result;

	public Object getResult() {
		return result;
	}

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
		this.result = msg;
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		ctx.close();
	}
}

客戶端動態代理類

package com.lhl.rpc_client.client;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import com.lhl.rpc_api.invoke.InvokeMessage;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

/**
 * RPC框架的核心<br>
 * 使用動態代理生成業務接口的實現類,遠程調用服務端取得結果
 * 
 * @author LIUHL
 *
 */
public class RpcProxy {
	@SuppressWarnings("unchecked")
	public static <T> T create(final Class<?> clazz) {
		return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, new InvocationHandler() {

			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				// 若調用的是Object的方法,則直接進行本地調用
				if (Object.class.equals(method.getDeclaringClass())) {
					return method.invoke(this, args);
				}
				// 遠程調用在這裏發生
				return rpcInvoke(clazz, method, args);
			}

		});
	}

	/**
	 * 服務端遠程調用
	 * 
	 * @param clazz
	 * @param method
	 * @param args
	 * @return
	 * @throws InterruptedException
	 */
	private static Object rpcInvoke(Class<?> clazz, Method method, Object[] args) throws InterruptedException {
		final RpcClientHandler handler = new RpcClientHandler();
		NioEventLoopGroup loopGroup = new NioEventLoopGroup();
		try {
			Bootstrap bootstrap = new Bootstrap();
			bootstrap.group(loopGroup).channel(NioSocketChannel.class)
					// Nagle算法開關
					.option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline pipeline = ch.pipeline();
							pipeline.addLast(new ObjectEncoder());
							pipeline.addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
							pipeline.addLast(handler);
						}
					});
			ChannelFuture future = bootstrap.connect("localhost", 8888).sync();

			// 形成遠程調用的參數實例
			InvokeMessage invocation = new InvokeMessage();
			invocation.setClassName(clazz.getName());
			invocation.setMethodName(method.getName());
			invocation.setParamTypes(method.getParameterTypes());
			invocation.setParamValues(args);

			// 將參數實例發送給Server
			future.channel().writeAndFlush(invocation).sync();

			future.channel().closeFuture().sync();
		} finally {
			loopGroup.shutdownGracefully();
		}
		return handler.getResult();
	}
}

定義消費者類

package com.lhl.rpc_client.consumer;

import com.lhl.rpc_api.service.UserService;
import com.lhl.rpc_api.service.databean.User;
import com.lhl.rpc_client.client.RpcProxy;

/**
 * 用戶Service的消費者
 * @author LIUHL
 *
 */
public class UserConsumer {
    public static void main(String[] args) {
        UserService service = RpcProxy.create(UserService.class);
        User user = new User();
        user.setUserName("張三");
        System.out.println(service.addUser(user));
    }
}

執行測試

運行client工程的UserConsumer的main方法,出現下面結果
在這裏插入圖片描述
就算是成功了。

總結

  1. api工程定義業務接口。
  2. server端實現業務接口,並提供netty服務器
  3. client端使用動態代理產生遠程調用的實例,訪問server端返回結果

通過這個RPC框架也爲了後續學習Dubbo打下基礎

源碼下載地址

https://download.csdn.net/download/baidu_29609961/12323282

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章