RPC--Netty客户端实现

组件一、channelProvider

为netty客户端提供可用的通道的组件,本质上就是维护了一个channelMap,为了线程安全,这里用的是concurrentHashMap(有点连接池的意思,有状态感知获取channel时判断channel状态)

1、get方法:

根据传入的服务地址(如127.0.0.1:9980)查找对应的channel,如果channel已经关闭或者不可用就清理缓存避免使用无效连接发生异常

2、set方法:

新建连接时调用,将建立的channel连接缓存起来,避免频繁连接导致性能问题

3、remove:

一般是断开或者异常时调用,确保下次连接时重新建立连接而不是复用异常的连接

存在不足:

1、可以添加心跳机制来检测channel是否健康,自动清理死连接

@Slf4j

public class ChannelProvider {

private final Map<String, Channel> channelMap;

public ChannelProvider() {

channelMap = new ConcurrentHashMap<>();

}

public Channel get(InetSocketAddress inetSocketAddress) {

String key = inetSocketAddress.toString();

// determine if there is a connection for the corresponding address

if (channelMap.containsKey(key)) {

Channel channel = channelMap.get(key);

// if so, determine if the connection is available, and if so, get it directly

if (channel != null && channel.isActive()) {

return channel;

} else {

channelMap.remove(key);

}

}

return null;

}

public void set(InetSocketAddress inetSocketAddress, Channel channel) {

String key = inetSocketAddress.toString();

channelMap.put(key, channel);

}

public void remove(InetSocketAddress inetSocketAddress) {

String key = inetSocketAddress.toString();

channelMap.remove(key);

log.info("Channel map size :[{}]", channelMap.size());

}

}

组件二、UnprocessedRequests

这个组件是用来缓存尚未被处理的completableFuture的,本质上也是在维护一个ConcurrentHashMap,实现了一个异步非阻塞的通信模型,

1、put方法,客户端发送请求后调用这个方法,把请求ID和CompletableFuture缓存起来,等待服务端响应

2、complete方法,当客户端收到服务端响应时调用这个方法,根据请求ID找到对应的future方法,调用future的complete方法来触发一个回调,同时从这个缓存中移除future防止内存泄露

存在不足:

1、未添加超时机制,如果服务端一直不响应,future就会一直阻塞在那里,导致内存泄露和线程的阻塞

public class UnprocessedRequests {

private static final Map<String, CompletableFuture<RpcResponse<Object>>> UNPROCESSED_RESPONSE_FUTURES = new ConcurrentHashMap<>();

public void put(String requestId, CompletableFuture<RpcResponse<Object>> future) {

UNPROCESSED_RESPONSE_FUTURES.put(requestId, future);

}

public void complete(RpcResponse<Object> rpcResponse) {

CompletableFuture<RpcResponse<Object>> future = UNPROCESSED_RESPONSE_FUTURES.remove(rpcResponse.getRequestId());

if (null != future) {

future.complete(rpcResponse);

} else {

throw new IllegalStateException();

}

}

}

组件三、Netty客户端

netty客户端,负责初始化、连接服务端、发送请求、管理连接复用,并处理异步调用逻辑。

1、doconnect:

异步连接服务端,监听连接成功事件,成功后将channel放入CompletableFuture中

2、sendRpcRequest

从请求中获取服务地址,根据服务发现拿到服务的channel(自动连接或者复用已有的连接),将请求ID和channel信息存入map等待响应

如果找到了连接的channel就发送rpcMessage,下面的写法是典型的netty异步发送消息+回调处理的写法,异步发送一个消息到channel并刷新缓冲区(即发送数据到服务端),监听到发送成功了就打印记录日志,如果失败就发送失败原因

3、getchannel:

从map中获取活跃的连接,具体看之前channelProvider组件的实现

4、close:

优雅地关闭eventLoopGroup,这个函数不会立即中断正在执行的任务,netty会将每个eventLoop标记为"立即关闭",不再接受新的任务并继续执行队列中剩余的异步任务,最终关闭所有eventLoop及背后的线程,并释放相关资源如selector和socket连接等

存在不足:

1、没有实现重试机制,比如我通信连接失败了,那是不是可以重试几次这个channel再去删除

@Slf4j

public final class NettyRpcClient implements RpcRequestTransport {

private final ServiceDiscovery serviceDiscovery;

private final UnprocessedRequests unprocessedRequests;

private final ChannelProvider channelProvider;

private final Bootstrap bootstrap;

private final EventLoopGroup eventLoopGroup;

public NettyRpcClient() {

// initialize resources such as EventLoopGroup, Bootstrap

eventLoopGroup = new NioEventLoopGroup();

bootstrap = new Bootstrap();

bootstrap.group(eventLoopGroup)

.channel(NioSocketChannel.class)

.handler(new LoggingHandler(LogLevel.INFO))

// The timeout period of the connection.

// If this time is exceeded or the connection cannot be established, the connection fails.

.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)

.handler(new ChannelInitializer<SocketChannel>() {

@Override

protected void initChannel(SocketChannel ch) {

ChannelPipeline p = ch.pipeline();

// If no data is sent to the server within 15 seconds, a heartbeat request is sent

p.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));

p.addLast(new RpcMessageEncoder());

p.addLast(new RpcMessageDecoder());

p.addLast(new NettyRpcClientHandler());

}

});

this.serviceDiscovery = ExtensionLoader.getExtensionLoader(ServiceDiscovery.class).getExtension(ServiceDiscoveryEnum.ZK.getName());

this.unprocessedRequests = SingletonFactory.getInstance(UnprocessedRequests.class);

this.channelProvider = SingletonFactory.getInstance(ChannelProvider.class);

}

/**

* connect server and get the channel ,so that you can send rpc message to server

*

* @param inetSocketAddress server address

* @return the channel

*/

@SneakyThrows

public Channel doConnect(InetSocketAddress inetSocketAddress) {

CompletableFuture<Channel> completableFuture = new CompletableFuture<>();

bootstrap.connect(inetSocketAddress).addListener((ChannelFutureListener) future -> {

if (future.isSuccess()) {

log.info("The client has connected [{}] successful!", inetSocketAddress.toString());

completableFuture.complete(future.channel());

} else {

throw new IllegalStateException();

}

});

return completableFuture.get();

}

@Override

public Object sendRpcRequest(RpcRequest rpcRequest) {

// build return value

CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();

// get server address

InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcRequest);

// get server address related channel

Channel channel = getChannel(inetSocketAddress);

if (channel.isActive()) {

// put unprocessed request

unprocessedRequests.put(rpcRequest.getRequestId(), resultFuture);

RpcMessage rpcMessage = RpcMessage.builder().data(rpcRequest)

.codec(SerializationTypeEnum.HESSIAN.getCode())

.compress(CompressTypeEnum.GZIP.getCode())

.messageType(RpcConstants.REQUEST_TYPE).build();

channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {

if (future.isSuccess()) {

log.info("client send message: [{}]", rpcMessage);

} else {

future.channel().close();

resultFuture.completeExceptionally(future.cause());

log.error("Send failed:", future.cause());

}

});

} else {

throw new IllegalStateException();

}

return resultFuture;

}

public Channel getChannel(InetSocketAddress inetSocketAddress) {

Channel channel = channelProvider.get(inetSocketAddress);

if (channel == null) {

channel = doConnect(inetSocketAddress);

channelProvider.set(inetSocketAddress, channel);

}

return channel;

}

public void close() {

eventLoopGroup.shutdownGracefully();

}

}

组件四、NettyRpcClientHandler

这个组件是netty服务器的核心业务处理器,是 Netty 客户端的核心业务处理器,负责处理服务端返回的消息(响应或心跳)、触发异步回调、发送心跳包以及异常处理。能够接收服务端响应消息,解析RpcMessage并完成对应的CompletableFuture,支持心跳机制检测写空闲事件并自动发送心跳请求,同时通过ConcurrentHashMap保证线程安全

1、channelRead:

从channel中读取一个message,判断这个message如果是普通的心跳信息,如果是请求的响应就提取其中的RpcResponse对象,并从unprocessedRequest中完成这个请求对应的future,最后释放msg的内存防止泄露

2、userEventTriggered:

实现了一个心跳机制,当检测到空闲(5秒没数据时)就发送心跳请求到服务端,如果发送失败就断开这个连接

3、exceptionCaught:

异常捕获,捕获到异常后自动断开连接并打印异常信息

存在不足:

1、心跳请求参数硬编码 codec、compress、data 都是硬编码

2、ctx.close() 不够优雅,直接关闭连接会导致客户端需要重新建立连接,可以增加一些重试机制

3、可以在捕获到错误后进行服务熔断等等

@Slf4j

public class NettyRpcClientHandler extends ChannelInboundHandlerAdapter {

private final UnprocessedRequests unprocessedRequests;

private final NettyRpcClient nettyRpcClient;

public NettyRpcClientHandler() {

this.unprocessedRequests = SingletonFactory.getInstance(UnprocessedRequests.class);

this.nettyRpcClient = SingletonFactory.getInstance(NettyRpcClient.class);

}

/**

* Read the message transmitted by the server

*/

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg) {

try {

log.info("client receive msg: [{}]", msg);

if (msg instanceof RpcMessage) {

RpcMessage tmp = (RpcMessage) msg;

byte messageType = tmp.getMessageType();

if (messageType == RpcConstants.HEARTBEAT_RESPONSE_TYPE) {

log.info("heart [{}]", tmp.getData());

} else if (messageType == RpcConstants.RESPONSE_TYPE) {

RpcResponse<Object> rpcResponse = (RpcResponse<Object>) tmp.getData();

unprocessedRequests.complete(rpcResponse);

}

}

} finally {

ReferenceCountUtil.release(msg);

}

}

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

if (evt instanceof IdleStateEvent) {

IdleState state = ((IdleStateEvent) evt).state();

if (state == IdleState.WRITER_IDLE) {

log.info("write idle happen [{}]", ctx.channel().remoteAddress());

Channel channel = nettyRpcClient.getChannel((InetSocketAddress) ctx.channel().remoteAddress());

RpcMessage rpcMessage = new RpcMessage();

rpcMessage.setCodec(SerializationTypeEnum.PROTOSTUFF.getCode());

rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode());

rpcMessage.setMessageType(RpcConstants.HEARTBEAT_REQUEST_TYPE);

rpcMessage.setData(RpcConstants.PING);

channel.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);

}

} else {

super.userEventTriggered(ctx, evt);

}

}

/**

* Called when an exception occurs in processing a client message

*/

@Override

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {

log.error("client catch exception:", cause);

cause.printStackTrace();

ctx.close();

}

}

相关推荐
市场部需要一个软件开发岗位2 分钟前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
忆~遂愿6 分钟前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能
MZ_ZXD00111 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东13 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology19 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble23 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域31 分钟前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel
凡人叶枫2 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发