概述
前言:所谓RPC是一种通过网络从远程计算机请求服务,而不必了解底层技术的协议,客户端不在乎传输层使用TCP或者UDP,不在意IO模型选择select还是epoll。现在典型的RPC框架有:Thrift,Dubbo等。接下来将参考一些dubbo的东西,展示如何基于Netty和zookeeper开发实现一个高性能RPC框架,同时结合问题分析解决方法
一,定义RPC请求和响应消息结构。
1,首先是请求类。必须包含的是:(1)类名(2)方法名(3)参数(4)参数结构。然后为了其他的一些考虑,我们可以加入其他的特性。例如:本次请求ID,服务的版本号等。实现如下:
public class RpcRequest {
private String requestId;
private String interfaceName;
private String serviceVersion;
private String methodName;
private Class<?>[] parameterTypes;
private Object[] parameters;
//省略get和set
}
2,响应类。包括(1)请求ID(2)异常信息(3)执行结果
public class RpcResponse {
private String requestId;
private Exception exception;
private Object result;
public boolean hasException() {
return exception != null;
}
//省略set和get
}
二,定义消息协议和消息encode和decode
1,消息协议:
一个消息报文分为两部分:(4bytes)消息长度+消息主体。
2,TCP半包和粘包问题
发生的原因:
(1)应用程序写入的字节大小大于套接字发送的缓冲区大小
(2)进行MSS大小的TCP分段
(3)以太网的payload大于MTU进行IP分片
(4)接收端读取速度小于接受速度
Netty内置了LengthFieldBasedFrameDecoder处理读半包问题,但是为了了解如何处理,选择了使用ByteToMessageDecoder。
3,Decoder
因为选择ByteToMessageDecoder,所以会出现读半包问题,那么我们可以这样来解决:由于我们定义了4个byte来存储消息长度,所以如果可读byte小于4和消息主体长度跟消息长度不吻合,那么就先不读,以为这是个半包,可以等到下次再来读取。
public class RpcDecoder extends ByteToMessageDecoder {
private Class<?> genericClass;
public RpcDecoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 4) {
return;
}
in.markReaderIndex();
int dataLength = in.readInt();
if (in.readableBytes() < dataLength) {
in.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
in.readBytes(data);
out.add(SerializationUtil.deserialize(data, genericClass));
}
}
4,Encoder
public class RpcEncoder extends MessageToByteEncoder {
private Class<?> genericClass;
public RpcEncoder(Class<?> genericClass) {
this.genericClass = genericClass;
}
@Override
public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) throws Exception {
if (genericClass.isInstance(in)) {
byte[] data = SerializationUtil.serialize(in);
out.writeInt(data.length);
out.writeBytes(data);
}
}
}
5,使用Protostuff序列化工具
原生的序列化性能效率较低,产生的码流较大,所以采用了Protostuff。
public class SerializationUtil {
private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<>();
private static Objenesis objenesis = new ObjenesisStd(true);
private SerializationUtil() {
}
/**
* 序列化(对象 -> 字节数组)
*/
@SuppressWarnings("unchecked")
public static <T> byte[] serialize(T obj) {
Class<T> cls = (Class<T>) obj.getClass();
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
Schema<T> schema = getSchema(cls);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
/**
* 反序列化(字节数组 -> 对象)
*/
public static <T> T deserialize(byte[] data, Class<T> cls) {
try {
T message = objenesis.newInstance(cls);
Schema<T> schema = getSchema(cls);
ProtostuffIOUtil.mergeFrom(data, message, schema);
return message;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
@SuppressWarnings("unchecked")
private static <T> Schema<T> getSchema(Class<T> cls) {
Schema<T> schema = (Schema<T>) cachedSchema.get(cls);
if (schema == null) {
schema = RuntimeSchema.createFrom(cls);
cachedSchema.put(cls, schema);
}
return schema;
}
}
三,zookeeper注册中心
1,Provider注册到ZK
Provider将自己的信息注册到ZK的节点,这点类似Dubbo,由于可能设计到多个服务提供方,所以需要提供负载均衡策略,这里借鉴了Dubbo的随机负载均衡策略。后续会继续增加不同的策略。
public class ZooKeeperServiceRegistry implements ServiceRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperServiceRegistry.class);
private final ZkClient zkClient;
public ZooKeeperServiceRegistry(String zkAddress) {
// 创建 ZooKeeper 客户端
zkClient = new ZkClient(zkAddress, Constant.ZK_SESSION_TIMEOUT, Constant.ZK_CONNECTION_TIMEOUT);
LOGGER.debug("connect zookeeper");
}
@Override
public void register(String serviceName, String serviceAddress, int weight) {
// 创建 registry 节点(持久)
String registryPath = Constant.ZK_REGISTRY_PATH;
if (!zkClient.exists(registryPath)) {
zkClient.createPersistent(registryPath);
LOGGER.debug("create registry node: {}", registryPath);
}
// 创建 service 节点(持久)
String servicePath = registryPath + "/" + serviceName;
if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath);
LOGGER.debug("create service node: {}", servicePath);
}
// 创建 address 节点(临时)
String addressPath = servicePath + "/address-";
String addressNode = zkClient.createEphemeralSequential(addressPath, serviceAddress);
LoadBalance.add(addressPath, weight);
LOGGER.debug("create address node: {}", addressNode);
}
}
2,消费方寻找ZK上的服务
public class ZooKeeperServiceDiscovery implements ServiceDiscovery {
private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperServiceDiscovery.class);
private String zkAddress;
public ZooKeeperServiceDiscovery(String zkAddress) {
this.zkAddress = zkAddress;
}
@Override
public String discover(String name) {
// 创建 ZooKeeper 客户端
ZkClient zkClient = new ZkClient(zkAddress, Constant.ZK_SESSION_TIMEOUT, Constant.ZK_CONNECTION_TIMEOUT);
LOGGER.debug("connect zookeeper");
try {
// 获取 service 节点
String servicePath = Constant.ZK_REGISTRY_PATH + "/" + name;
if (!zkClient.exists(servicePath)) {
throw new RuntimeException(String.format("can not find any service node on path: %s", servicePath));
}
List<String> addressList = zkClient.getChildren(servicePath);
if (CollectionUtil.isEmpty(addressList)) {
throw new RuntimeException(String.format("can not find any address node on path: %s", servicePath));
}
// 获取 address 节点
String address;
int size = addressList.size();
if (size == 1) {
// 若只有一个地址,则获取该地址
address = addressList.get(0);
LOGGER.debug("get only address node: {}", address);
} else {
// 若存在多个地址,则根据负载均衡算法获取一个地址
address = LoadBalance.Random_LoadBalance(addressList);
LOGGER.debug("get random address node: {}", address);
}
// 获取 address 节点的值
String addressPath = servicePath + "/" + address;
return zkClient.readData(addressPath);
} finally {
zkClient.close();
}
}
}
四,服务端接收消息后,通过反射调用
public class RpcServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
private static final Logger LOGGER = LoggerFactory.getLogger(RpcServerHandler.class);
private final Map<String, Object> handlerMap;
public RpcServerHandler(Map<String, Object> handlerMap) {
this.handlerMap = handlerMap;
}
@Override
public void channelRead0(final ChannelHandlerContext ctx, RpcRequest request) throws Exception {
// 创建并初始化 RPC 响应对象
RpcResponse response = new RpcResponse();
response.setRequestId(request.getRequestId());
try {
Object result = handle(request);
response.setResult(result);
} catch (Exception e) {
LOGGER.error("handle result failure", e);
response.setException(e);
}
// 写入 RPC 响应对象并自动关闭连接
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private Object handle(RpcRequest request) throws Exception {
// 获取服务对象
String serviceName = request.getInterfaceName();
String serviceVersion = request.getServiceVersion();
if (StringUtil.isNotEmpty(serviceVersion)) {
serviceName += "-" + serviceVersion;
}
Object serviceBean = handlerMap.get(serviceName);
if (serviceBean == null) {
throw new RuntimeException(String.format("can not find service bean by key: %s", serviceName));
}
// 获取反射调用所需的参数
Class<?> serviceClass = serviceBean.getClass();
String methodName = request.getMethodName();
Class<?>[] parameterTypes = request.getParameterTypes();
Object[] parameters = request.getParameters();
// 执行反射调用
// Method method = serviceClass.getMethod(methodName, parameterTypes);
// method.setAccessible(true);
// return method.invoke(serviceBean, parameters);
// 使用 CGLib 执行反射调用
FastClass serviceFastClass = FastClass.create(serviceClass);
FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, parameterTypes);
return serviceFastMethod.invoke(serviceBean, parameters);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOGGER.error("server caught exception", cause);
ctx.close();
}
}
六,结尾
现在项目还不够完善,没有考虑高并发的情况以及没有提供异步处理,客户端也没有提供服务断线后的处理策略,在接下来这阵子将进行升级。
Git
https://github.com/wacxt/NettyRpc
参考资料:
(1)《Netty权威编程》
最后
以上就是健康纸飞机为你收集整理的基于Netty和Zookeeper实现RPC框架的全部内容,希望文章能够帮你解决基于Netty和Zookeeper实现RPC框架所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复