文章目录
更多博客内容见个人博客: https://itboyer.github.io
引言
最近研究了Netty的相关技术,用于实施高并发场景下的消息通信,期间搜集了大量资料,围绕着netty的channel连接池的设计,这个稍微有些复杂的主题,做了大量功课,其中牵扯到蛮多技术点,要想在网上找到相关的又相对完整的参考文章,确实不太容易。在此记录一下实现的方案,用于技术沉淀。
首先,阅读本文之前需要具备一些基础知识:
- socket通信和长短连接
- 知道Netty的执行流程和相关API操作
- 理解什么是TCP半包,了解Netty提供的粘包和拆包解码器
在此贴出一些学习过程中遇到的优秀Blog
官方文档
分隔符解码器处理半包问题
netty实战-netty client连接池设计(Netty官方新版本中已经实现了简单的连接池,可以学习连接池的设计思想)
线程模型
首先,整个系统架构的线程模型如下:
同步通信机制
其次我们需要关注单线程内的同步请求和响应
抛出问题:
Q1:如何实现基于Netty的“请求-响应”同步通信机制
Netty提供了异步IO和同步IO的统一实现,但是我们的需求其实和IO的同步异步并无关系。我们的关键是要实现请求-响应这种典型的一问一答交互方式。用于实现微服务之间的调用和返回结果获取,要实现这个需求,需要解决两个问题:
a. 请求和响应的正确匹配。
当服务端返回响应结果的时候,怎么和客户端的请求正确匹配起来呢?解决方式:通过客户端唯一的RequestId,服务端返回的响应中需要包含该RequestId,这样客户端就可以通过RequestId来正确匹配请求响应。
b. 请求线程和响应线程的通信。
因为请求线程会在发出请求后,同步等待服务端的返回。因此,就需要解决,Netty在接受到响应之后,怎么通知请求线程结果。
方案:使用LinkedBlockingQueue
阻塞任务队列,使用take()获取相应的返回结果
首先需要对每一个请求标识一个全局唯一的标识,下面贴出核心代码:
NettyChannelPoolHandler.java
@Slf4j
public class ChannelTaskThread implements Callable<String> {
/**
* netty channel池
*/
final NettyClientPool nettyClientPool = NettyClientPool.getInstance();
private String message;
public ChannelTaskThread(String message){
this.message = message;
}
@Override
public String call(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
//同一个线程使用同一个全局唯一的随机数,保证从同一个池中获取和释放资源,同时使用改随机数作为Key获取返回值,时间戳+6位随机数
long random = Long.valueOf(sdf.format(new Date())) * 1000000 + Math.round(Math.random() * 1000000);
Channel channel = nettyClientPool.getChannel(random);
log.debug("在链接池池中取到的Channel: "+ channel.id());
UnpooledByteBufAllocator allocator = new UnpooledByteBufAllocator(false);
ByteBuf buffer = allocator.buffer(20);
//使用固定分隔符的半包解码器
String msg = message + DataBusConstant.DELIMITER;
buffer.writeBytes(msg.getBytes());
NettyClientHandler tcpClientHandler = channel.pipeline().get(NettyClientHandler.class);
ChannelId id = channel.id();
log.info("SEND SEQNO[{}] MESSAGE AND CHANNEL id [{}]",random,id);
String serverMsg = tcpClientHandler.sendMessage(buffer, channel);
NettyClientPool.release(channel);
return "请求SEQNO["+random+"] "+ serverMsg;
}
}
NettyClientHandler.java
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 使用阻塞式LinkedBlockingQueue,对响应结果保存
* 用于记录通道响应的结果集
*/
private static final Map<Long, LinkedBlockingQueue<String>> RESULT_MAP = new ConcurrentHashMap<>();
public String sendMessage(ByteBuf message,Channel ch) {
LinkedBlockingQueue<String> linked = new LinkedBlockingQueue<>(1);
//获取channel中存储的全局唯一随机值
Long randomId = ch.attr(AttributeKey.<Long>valueOf(DataBusConstant.RANDOM_KEY)).get();
RESULT_MAP.put(randomId,linked);
ch.writeAndFlush(message);
String res = null;
try {
//设置3分钟的获取超时时间或者使用take()--获取不到返回结果一直阻塞
res = RESULT_MAP.get(randomId).poll(3,TimeUnit.MINUTES);
RESULT_MAP.remove(randomId);
}catch (Exception e){
e.printStackTrace();
}
return res;
}
@Override
public void channelRead