前景回顧
【mq】從零開始實現 mq-02-如何實現生產者呼叫消費者?
【mq】從零開始實現 mq-03-引入 broker 中間人
上一節我們學習瞭如何實現生產者給消費者傳送訊息,但是是通過直連的方式。
那麼如何才能達到解耦的效果呢?
答案就是引入 broker,訊息的中間人。
MqBroker 實現
核心啟動類
類似我們前面 consumer 的啟動實現:
package com.github.houbb.mq.broker.core;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqBroker extends Thread implements IMqBroker {
// 省略
private ChannelHandler initChannelHandler() {
MqBrokerHandler handler = new MqBrokerHandler();
handler.setInvokeService(invokeService);
handler.setRegisterConsumerService(registerConsumerService);
handler.setRegisterProducerService(registerProducerService);
handler.setMqBrokerPersist(mqBrokerPersist);
handler.setBrokerPushService(brokerPushService);
handler.setRespTimeoutMills(respTimeoutMills);
return handler;
}
@Override
public void run() {
// 啟動服務端
log.info("MQ 中間人開始啟動服務端 port: {}", port);
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(DelimiterUtil.DELIMITER);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(workerGroup, bossGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
.addLast(initChannelHandler());
}
})
// 這個引數影響的是還沒有被accept 取出的連線
.option(ChannelOption.SO_BACKLOG, 128)
// 這個引數只是過一段時間內客戶端沒有響應,服務端會傳送一個 ack 包,以判斷客戶端是否還活著。
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 繫結埠,開始接收進來的連結
ChannelFuture channelFuture = serverBootstrap.bind(port).syncUninterruptibly();
log.info("MQ 中間人啟動完成,監聽【" + port + "】埠");
channelFuture.channel().closeFuture().syncUninterruptibly();
log.info("MQ 中間人關閉完成");
} catch (Exception e) {
log.error("MQ 中間人啟動異常", e);
throw new MqException(BrokerRespCode.RPC_INIT_FAILED);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
initChannelHandler
中有不少新面孔,我們後面會詳細介紹。
MqBrokerHandler 處理邏輯
package com.github.houbb.mq.broker.handler;
import java.util.List;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqBrokerHandler extends SimpleChannelInboundHandler {
private static final Log log = LogFactory.getLog(MqBrokerHandler.class);
/**
* 呼叫管理類
* @since 1.0.0
*/
private IInvokeService invokeService;
/**
* 消費者管理
* @since 0.0.3
*/
private IBrokerConsumerService registerConsumerService;
/**
* 生產者管理
* @since 0.0.3
*/
private IBrokerProducerService registerProducerService;
/**
* 持久化類
* @since 0.0.3
*/
private IMqBrokerPersist mqBrokerPersist;
/**
* 推送服務
* @since 0.0.3
*/
private IBrokerPushService brokerPushService;
/**
* 獲取響應超時時間
* @since 0.0.3
*/
private long respTimeoutMills;
//set 方法
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
RpcMessageDto rpcMessageDto = null;
try {
rpcMessageDto = JSON.parseObject(bytes, RpcMessageDto.class);
} catch (Exception exception) {
log.error("RpcMessageDto json 格式轉換異常 {}", new String(bytes));
return;
}
if (rpcMessageDto.isRequest()) {
MqCommonResp commonResp = this.dispatch(rpcMessageDto, ctx);
if(commonResp == null) {
log.debug("當前訊息為 null,忽略處理。");
return;
}
// 寫回響應,和以前類似。
writeResponse(rpcMessageDto, commonResp, ctx);
} else {
final String traceId = rpcMessageDto.getTraceId();
// 丟棄掉 traceId 為空的資訊
if(StringUtil.isBlank(traceId)) {
log.debug("[Server Response] response traceId 為空,直接丟棄", JSON.toJSON(rpcMessageDto));
return;
}
// 新增訊息
invokeService.addResponse(traceId, rpcMessageDto);
}
}
/**
* 非同步處理訊息
* @param mqMessage 訊息
* @since 0.0.3
*/
private void asyncHandleMessage(MqMessage mqMessage) {
List<Channel> channelList = registerConsumerService.getSubscribeList(mqMessage);
if(CollectionUtil.isEmpty(channelList)) {
log.info("監聽列表為空,忽略處理");
return;
}
BrokerPushContext brokerPushContext = new BrokerPushContext();
brokerPushContext.setChannelList(channelList);
brokerPushContext.setMqMessage(mqMessage);
brokerPushContext.setMqBrokerPersist(mqBrokerPersist);
brokerPushContext.setInvokeService(invokeService);
brokerPushContext.setRespTimeoutMills(respTimeoutMills);
brokerPushService.asyncPush(brokerPushContext);
}
}
訊息分發
broker 接收到訊息以後,dispatch 實現如下:
/**
* 訊息的分發
*
* @param rpcMessageDto 入參
* @param ctx 上下文
* @return 結果
*/
private MqCommonResp dispatch(RpcMessageDto rpcMessageDto, ChannelHandlerContext ctx) {
try {
final String methodType = rpcMessageDto.getMethodType();
final String json = rpcMessageDto.getJson();
String channelId = ChannelUtil.getChannelId(ctx);
final Channel channel = ctx.channel();
log.debug("channelId: {} 接收到 method: {} 內容:{}", channelId,
methodType, json);
// 生產者註冊
if(MethodType.P_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerProducerService.register(registerReq.getServiceEntry(), channel);
}
// 生產者登出
if(MethodType.P_UN_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerProducerService.unRegister(registerReq.getServiceEntry(), channel);
}
// 生產者訊息傳送
if(MethodType.P_SEND_MSG.equals(methodType)) {
MqMessage mqMessage = JSON.parseObject(json, MqMessage.class);
MqMessagePersistPut persistPut = new MqMessagePersistPut();
persistPut.setMqMessage(mqMessage);
persistPut.setMessageStatus(MessageStatusConst.WAIT_CONSUMER);
MqCommonResp commonResp = mqBrokerPersist.put(persistPut);
this.asyncHandleMessage(mqMessage);
return commonResp;
}
// 生產者訊息傳送-ONE WAY
if(MethodType.P_SEND_MSG_ONE_WAY.equals(methodType)) {
MqMessage mqMessage = JSON.parseObject(json, MqMessage.class);
MqMessagePersistPut persistPut = new MqMessagePersistPut();
persistPut.setMqMessage(mqMessage);
persistPut.setMessageStatus(MessageStatusConst.WAIT_CONSUMER);
mqBrokerPersist.put(persistPut);
this.asyncHandleMessage(mqMessage);
return null;
}
// 消費者註冊
if(MethodType.C_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerConsumerService.register(registerReq.getServiceEntry(), channel);
}
// 消費者登出
if(MethodType.C_UN_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerConsumerService.unRegister(registerReq.getServiceEntry(), channel);
}
// 消費者監聽註冊
if(MethodType.C_SUBSCRIBE.equals(methodType)) {
ConsumerSubscribeReq req = JSON.parseObject(json, ConsumerSubscribeReq.class);
return registerConsumerService.subscribe(req, channel);
}
// 消費者監聽登出
if(MethodType.C_UN_SUBSCRIBE.equals(methodType)) {
ConsumerUnSubscribeReq req = JSON.parseObject(json, ConsumerUnSubscribeReq.class);
return registerConsumerService.unSubscribe(req, channel);
}
// 消費者主動 pull
if(MethodType.C_MESSAGE_PULL.equals(methodType)) {
MqConsumerPullReq req = JSON.parseObject(json, MqConsumerPullReq.class);
return mqBrokerPersist.pull(req, channel);
}
throw new UnsupportedOperationException("暫不支援的方法型別");
} catch (Exception exception) {
log.error("執行異常", exception);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.FAIL.getCode());
resp.setRespMessage(MqCommonRespCode.FAIL.getMsg());
return resp;
}
}
訊息推送
this.asyncHandleMessage(mqMessage);
是 broker 接收到訊息之後的處理邏輯。
/**
* 非同步處理訊息
* @param mqMessage 訊息
* @since 0.0.3
*/
private void asyncHandleMessage(MqMessage mqMessage) {
List<Channel> channelList = registerConsumerService.getSubscribeList(mqMessage);
if(CollectionUtil.isEmpty(channelList)) {
log.info("監聽列表為空,忽略處理");
return;
}
BrokerPushContext brokerPushContext = new BrokerPushContext();
brokerPushContext.setChannelList(channelList);
brokerPushContext.setMqMessage(mqMessage);
brokerPushContext.setMqBrokerPersist(mqBrokerPersist);
brokerPushContext.setInvokeService(invokeService);
brokerPushContext.setRespTimeoutMills(respTimeoutMills);
brokerPushService.asyncPush(brokerPushContext);
}
推送的核心實現如下:
package com.github.houbb.mq.broker.support.push;
/**
* @author binbin.hou
* @since 0.0.3
*/
public class BrokerPushService implements IBrokerPushService {
private static final Log log = LogFactory.getLog(BrokerPushService.class);
private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
@Override
public void asyncPush(final BrokerPushContext context) {
EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
log.info("開始非同步處理 {}", JSON.toJSON(context));
final List<Channel> channelList = context.getChannelList();
final IMqBrokerPersist mqBrokerPersist = context.getMqBrokerPersist();
final MqMessage mqMessage = context.getMqMessage();
final String messageId = mqMessage.getTraceId();
final IInvokeService invokeService = context.getInvokeService();
final long responseTime = context.getRespTimeoutMills();
for(Channel channel : channelList) {
try {
String channelId = ChannelUtil.getChannelId(channel);
log.info("開始處理 channelId: {}", channelId);
//1. 呼叫
mqMessage.setMethodType(MethodType.B_MESSAGE_PUSH);
MqConsumerResultResp resultResp = callServer(channel, mqMessage,
MqConsumerResultResp.class, invokeService, responseTime);
//2. 更新狀態
mqBrokerPersist.updateStatus(messageId, resultResp.getConsumerStatus());
//3. 後期新增重試策略
log.info("完成處理 channelId: {}", channelId);
} catch (Exception exception) {
log.error("處理異常");
mqBrokerPersist.updateStatus(messageId, ConsumerStatus.FAILED.getCode());
}
}
log.info("完成非同步處理");
}
});
}
}
此處在訊息推送之後,需要更新訊息的 ACK 狀態。
訊息生產者處理類
IBrokerProducerService
介面定義如下:
package com.github.houbb.mq.broker.api;
/**
* <p> 生產者註冊服務類 </p>
*
* @author houbinbin
* @since 0.0.3
*/
public interface IBrokerProducerService {
/**
* 註冊當前服務資訊
* (1)將該服務通過 {@link ServiceEntry#getGroupName()} 進行分組
* 訂閱了這個 serviceId 的所有客戶端
* @param serviceEntry 註冊當前服務資訊
* @param channel channel
* @since 0.0.8
*/
MqCommonResp register(final ServiceEntry serviceEntry, Channel channel);
/**
* 登出當前服務資訊
* @param serviceEntry 註冊當前服務資訊
* @param channel 通道
* @since 0.0.8
*/
MqCommonResp unRegister(final ServiceEntry serviceEntry, Channel channel);
/**
* 獲取服務地址資訊
* @param channel channel
* @return 結果
* @since 0.0.3
*/
ServiceEntry getServiceEntry(final Channel channel);
}
實現如下:
本地基於 map 儲存請求過來的基本資訊。
package com.github.houbb.mq.broker.support.api;
/**
* <p> 生產者註冊服務類 </p>
*
* @author houbinbin
* @since 0.0.3
*/
public class LocalBrokerProducerService implements IBrokerProducerService {
private static final Log log = LogFactory.getLog(LocalBrokerProducerService.class);
private final Map<String, BrokerServiceEntryChannel> registerMap = new ConcurrentHashMap<>();
@Override
public MqCommonResp register(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
registerMap.put(channelId, entryChannel);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp unRegister(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
registerMap.remove(channelId);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public ServiceEntry getServiceEntry(Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
return registerMap.get(channelId);
}
}
訊息消費者處理類
介面定義如下:
package com.github.houbb.mq.broker.api;
/**
* <p> 消費者註冊服務類 </p>
*
* @author houbinbin
* @since 0.0.3
*/
public interface IBrokerConsumerService {
/**
* 註冊當前服務資訊
* (1)將該服務通過 {@link ServiceEntry#getGroupName()} 進行分組
* 訂閱了這個 serviceId 的所有客戶端
* @param serviceEntry 註冊當前服務資訊
* @param channel channel
* @since 0.0.3
*/
MqCommonResp register(final ServiceEntry serviceEntry, Channel channel);
/**
* 登出當前服務資訊
* @param serviceEntry 註冊當前服務資訊
* @param channel channel
* @since 0.0.3
*/
MqCommonResp unRegister(final ServiceEntry serviceEntry, Channel channel);
/**
* 監聽服務資訊
* (1)監聽之後,如果有任何相關的機器資訊發生變化,則進行推送。
* (2)內建的資訊,需要傳送 ip 資訊到註冊中心。
*
* @param serviceEntry 客戶端明細資訊
* @param clientChannel 客戶端 channel 資訊
* @since 0.0.3
*/
MqCommonResp subscribe(final ConsumerSubscribeReq serviceEntry,
final Channel clientChannel);
/**
* 取消監聽服務資訊
* (1)監聽之後,如果有任何相關的機器資訊發生變化,則進行推送。
* (2)內建的資訊,需要傳送 ip 資訊到註冊中心。
*
* @param serviceEntry 客戶端明細資訊
* @param clientChannel 客戶端 channel 資訊
* @since 0.0.3
*/
MqCommonResp unSubscribe(final ConsumerUnSubscribeReq serviceEntry,
final Channel clientChannel);
/**
* 獲取所有匹配的消費者
* 1. 同一個 groupName 只返回一個,注意負載均衡
* 2. 返回匹配當前訊息的消費者通道
*
* @param mqMessage 訊息體
* @return 結果
*/
List<Channel> getSubscribeList(MqMessage mqMessage);
}
預設實現:
package com.github.houbb.mq.broker.support.api;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class LocalBrokerConsumerService implements IBrokerConsumerService {
private final Map<String, BrokerServiceEntryChannel> registerMap = new ConcurrentHashMap<>();
/**
* 訂閱集合
* key: topicName
* value: 對應的訂閱列表
*/
private final Map<String, Set<ConsumerSubscribeBo>> subscribeMap = new ConcurrentHashMap<>();
@Override
public MqCommonResp register(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
registerMap.put(channelId, entryChannel);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp unRegister(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
registerMap.remove(channelId);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp subscribe(ConsumerSubscribeReq serviceEntry, Channel clientChannel) {
final String channelId = ChannelUtil.getChannelId(clientChannel);
final String topicName = serviceEntry.getTopicName();
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(set == null) {
set = new HashSet<>();
}
ConsumerSubscribeBo subscribeBo = new ConsumerSubscribeBo();
subscribeBo.setChannelId(channelId);
subscribeBo.setGroupName(serviceEntry.getGroupName());
subscribeBo.setTopicName(topicName);
subscribeBo.setTagRegex(serviceEntry.getTagRegex());
set.add(subscribeBo);
subscribeMap.put(topicName, set);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp unSubscribe(ConsumerUnSubscribeReq serviceEntry, Channel clientChannel) {
final String channelId = ChannelUtil.getChannelId(clientChannel);
final String topicName = serviceEntry.getTopicName();
ConsumerSubscribeBo subscribeBo = new ConsumerSubscribeBo();
subscribeBo.setChannelId(channelId);
subscribeBo.setGroupName(serviceEntry.getGroupName());
subscribeBo.setTopicName(topicName);
subscribeBo.setTagRegex(serviceEntry.getTagRegex());
// 集合
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(CollectionUtil.isNotEmpty(set)) {
set.remove(subscribeBo);
}
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public List<Channel> getSubscribeList(MqMessage mqMessage) {
final String topicName = mqMessage.getTopic();
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(CollectionUtil.isEmpty(set)) {
return Collections.emptyList();
}
//2. 獲取匹配的 tag 列表
final List<String> tagNameList = mqMessage.getTags();
Map<String, List<ConsumerSubscribeBo>> groupMap = new HashMap<>();
for(ConsumerSubscribeBo bo : set) {
String tagRegex = bo.getTagRegex();
if(hasMatch(tagNameList, tagRegex)) {
//TODO: 這種設定模式,統一新增處理
String groupName = bo.getGroupName();
List<ConsumerSubscribeBo> list = groupMap.get(groupName);
if(list == null) {
list = new ArrayList<>();
}
list.add(bo);
groupMap.put(groupName, list);
}
}
//3. 按照 groupName 分組之後,每一組只隨機返回一個。最好應該調整為以 shardingkey 選擇
final String shardingKey = mqMessage.getShardingKey();
List<Channel> channelList = new ArrayList<>();
for(Map.Entry<String, List<ConsumerSubscribeBo>> entry : groupMap.entrySet()) {
List<ConsumerSubscribeBo> list = entry.getValue();
ConsumerSubscribeBo bo = RandomUtils.random(list, shardingKey);
BrokerServiceEntryChannel entryChannel = registerMap.get(bo.getChannelId());
channelList.add(entryChannel.getChannel());
}
return channelList;
}
private boolean hasMatch(List<String> tagNameList,
String tagRegex) {
if(CollectionUtil.isEmpty(tagNameList)) {
return false;
}
Pattern pattern = Pattern.compile(tagRegex);
for(String tagName : tagNameList) {
if(RegexUtils.match(pattern, tagName)) {
return true;
}
}
return false;
}
}
getSubscribeList
的邏輯可能稍微複雜點,其實就是訊息過來,找到匹配的訂閱消費者而已。
因為同一個 groupName 的消費者訊息只消費一次,所以需要一次分組。
訊息持久化
介面如下:
package com.github.houbb.mq.broker.support.persist;
/**
* @author binbin.hou
* @since 0.0.3
*/
public interface IMqBrokerPersist {
/**
* 儲存訊息
* @param mqMessage 訊息
* @since 0.0.3
*/
MqCommonResp put(final MqMessagePersistPut mqMessage);
/**
* 更新狀態
* @param messageId 訊息唯一標識
* @param status 狀態
* @return 結果
* @since 0.0.3
*/
MqCommonResp updateStatus(final String messageId,
final String status);
/**
* 拉取訊息
* @param pull 拉取訊息
* @return 結果
*/
MqConsumerPullResp pull(final MqConsumerPullReq pull, final Channel channel);
}
本地預設實現:
package com.github.houbb.mq.broker.support.persist;
/**
* 本地持久化策略
* @author binbin.hou
* @since 1.0.0
*/
public class LocalMqBrokerPersist implements IMqBrokerPersist {
private static final Log log = LogFactory.getLog(LocalMqBrokerPersist.class);
/**
* 佇列
* ps: 這裡只是簡化實現,暫時不考慮併發等問題。
*/
private final Map<String, List<MqMessagePersistPut>> map = new ConcurrentHashMap<>();
//1. 接收
//2. 持久化
//3. 通知消費
@Override
public synchronized MqCommonResp put(MqMessagePersistPut put) {
log.info("put elem: {}", JSON.toJSON(put));
MqMessage mqMessage = put.getMqMessage();
final String topic = mqMessage.getTopic();
List<MqMessagePersistPut> list = map.get(topic);
if(list == null) {
list = new ArrayList<>();
}
list.add(put);
map.put(topic, list);
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}
@Override
public MqCommonResp updateStatus(String messageId, String status) {
// 這裡效能比較差,所以不可以用於生產。僅作為測試驗證
for(List<MqMessagePersistPut> list : map.values()) {
for(MqMessagePersistPut put : list) {
MqMessage mqMessage = put.getMqMessage();
if(mqMessage.getTraceId().equals(messageId)) {
put.setMessageStatus(status);
break;
}
}
}
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}
@Override
public MqConsumerPullResp pull(MqConsumerPullReq pull, Channel channel) {
//TODO... 待實現
return null;
}
}
ps: 後續將會基於 springboot+mysql 進行持久化策略實現。
消費者啟動調整
我們將生產者、消費者的啟動都進行調整,連線到 broker 中。
二者是類似的,此處以消費者為例。
核心啟動類
package com.github.houbb.mq.consumer.core;
/**
* 推送消費策略
*
* @author binbin.hou
* @since 1.0.0
*/
public class MqConsumerPush extends Thread implements IMqConsumer {
// 屬性&設定
@Override
public void run() {
// 啟動服務端
log.info("MQ 消費者開始啟動服務端 groupName: {}, brokerAddress: {}",
groupName, brokerAddress);
//1. 引數校驗
this.paramCheck();
try {
// channel handler
ChannelHandler channelHandler = this.initChannelHandler();
//channel future
this.channelFutureList = ChannelFutureUtils.initChannelFutureList(brokerAddress, channelHandler);
// register to broker
this.registerToBroker();
// 標識為可用
enableFlag = true;
log.info("MQ 消費者啟動完成");
} catch (Exception e) {
log.error("MQ 消費者啟動異常", e);
throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
}
}
//訂閱&取消訂閱
@Override
public void registerListener(IMqConsumerListener listener) {
this.mqListenerService.register(listener);
}
}
初始化 handler
private ChannelHandler initChannelHandler() {
final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(DelimiterUtil.DELIMITER);
final MqConsumerHandler mqConsumerHandler = new MqConsumerHandler(invokeService, mqListenerService);
// handler 實際上會被多次呼叫,如果不是 @Shareable,應該每次都重新建立。
ChannelHandler handler = new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
.addLast(mqConsumerHandler);
}
};
return handler;
}
註冊到服務端
/**
* 註冊到所有的服務端
* @since 0.0.3
*/
private void registerToBroker() {
for(RpcChannelFuture channelFuture : this.channelFutureList) {
ServiceEntry serviceEntry = new ServiceEntry();
serviceEntry.setGroupName(groupName);
serviceEntry.setAddress(channelFuture.getAddress());
serviceEntry.setPort(channelFuture.getPort());
serviceEntry.setWeight(channelFuture.getWeight());
BrokerRegisterReq brokerRegisterReq = new BrokerRegisterReq();
brokerRegisterReq.setServiceEntry(serviceEntry);
brokerRegisterReq.setMethodType(MethodType.C_REGISTER);
brokerRegisterReq.setTraceId(IdHelper.uuid32());
log.info("[Register] 開始註冊到 broker:{}", JSON.toJSON(brokerRegisterReq));
final Channel channel = channelFuture.getChannelFuture().channel();
MqCommonResp resp = callServer(channel, brokerRegisterReq, MqCommonResp.class);
log.info("[Register] 完成註冊到 broker:{}", JSON.toJSON(resp));
}
}
訂閱與取消訂閱
消費者對於關心的訊息,實現也比較簡單:
public void subscribe(String topicName, String tagRegex) {
ConsumerSubscribeReq req = new ConsumerSubscribeReq();
String messageId = IdHelper.uuid32();
req.setTraceId(messageId);
req.setMethodType(MethodType.C_SUBSCRIBE);
req.setTopicName(topicName);
req.setTagRegex(tagRegex);
req.setGroupName(groupName);
Channel channel = getChannel();
MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
throw new MqException(ConsumerRespCode.SUBSCRIBE_FAILED);
}
}
取消訂閱:
public void unSubscribe(String topicName, String tagRegex) {
ConsumerUnSubscribeReq req = new ConsumerUnSubscribeReq();
String messageId = IdHelper.uuid32();
req.setTraceId(messageId);
req.setMethodType(MethodType.C_UN_SUBSCRIBE);
req.setTopicName(topicName);
req.setTagRegex(tagRegex);
req.setGroupName(groupName);
Channel channel = getChannel();
MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
throw new MqException(ConsumerRespCode.UN_SUBSCRIBE_FAILED);
}
}
測試
broker 啟動
MqBroker broker = new MqBroker();
broker.start();
啟動日誌:
[DEBUG] [2022-04-21 20:36:27.158] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 20:36:27.186] [Thread-0] [c.g.h.m.b.c.MqBroker.run] - MQ 中間人開始啟動服務端 port: 9999
[INFO] [2022-04-21 20:36:29.060] [Thread-0] [c.g.h.m.b.c.MqBroker.run] - MQ 中間人啟動完成,監聽【9999】埠
consumer 啟動
final MqConsumerPush mqConsumerPush = new MqConsumerPush();
mqConsumerPush.start();
mqConsumerPush.subscribe("TOPIC", "TAGA");
mqConsumerPush.registerListener(new IMqConsumerListener() {
@Override
public ConsumerStatus consumer(MqMessage mqMessage, IMqConsumerListenerContext context) {
System.out.println("---------- 自定義 " + JSON.toJSONString(mqMessage));
return ConsumerStatus.SUCCESS;
}
});
啟動日誌:
...
[INFO] [2022-04-21 20:37:40.985] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.registerToBroker] - [Register] 完成註冊到 broker:{"respMessage":"成功","respCode":"0000"}
啟動時會註冊到 broker。
producer 啟動
MqProducer mqProducer = new MqProducer();
mqProducer.start();
String message = "HELLO MQ!";
MqMessage mqMessage = new MqMessage();
mqMessage.setTopic("TOPIC");
mqMessage.setTags(Arrays.asList("TAGA", "TAGB"));
mqMessage.setPayload(message);
SendResult sendResult = mqProducer.send(mqMessage);
System.out.println(JSON.toJSON(sendResult));
日誌:
...
[INFO] [2022-04-21 20:39:17.885] [Thread-0] [c.g.h.m.p.c.MqProducer.registerToBroker] - [Register] 完成註冊到 broker:{"respMessage":"成功","respCode":"0000"}
...
此時消費者消費到我們傳送的訊息。
---------- 自定義 {"methodType":"B_MESSAGE_PUSH","payload":"HELLO MQ!","tags":["TAGA","TAGB"],"topic":"TOPIC","traceId":"2237bbfe55b842328134e6a100e36364"}
小結
到這裡,我們就實現了基於中間人的生產者與消費者通訊。
希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。
我是老馬,期待與你的下次重逢。
開源地址
The message queue in java.(java 簡易版本 mq 實現) https://github.com/houbb/mq