先簡單說明一下,對於mqtt是個雙向通信的過程,也就是說,他既允許client向broker發佈消息,同時也允許broker向client發佈消息
public void processPublish(Channel channel, MqttPublishMessage msg) {
final MqttQoS qos = msg.fixedHeader().qosLevel();
final String clientId = NettyUtils.clientID(channel);
LOG.info("Processing PUBLISH message. CId={}, topic={}, messageId={}, qos={}", clientId,
msg.variableHeader().topicName(), msg.variableHeader().packetId(), qos);
switch (qos) {
case AT_MOST_ONCE:
this.qos0PublishHandler.receivedPublishQos0(channel, msg);
break;
case AT_LEAST_ONCE:
this.qos1PublishHandler.receivedPublishQos1(channel, msg);
break;
case EXACTLY_ONCE:
this.qos2PublishHandler.receivedPublishQos2(channel, msg);
break;
default:
LOG.error("Unknown QoS-Type:{}", qos);
break;
}
}
根據發佈的消息的qos,用不同的QosPublishHandler來處理,QosPublishHandler有三個具體實現,分別是Qos0PublishHandler,Qos1PublishHandler,Qos2PublishHandler.
這裏面先講解Qos1PublishHandler的處理
void receivedPublishQos1(Channel channel, MqttPublishMessage msg) {
// verify if topic can be write
final Topic topic = new Topic(msg.variableHeader().topicName());
String clientID = NettyUtils.clientID(channel);
String username = NettyUtils.userName(channel);
if (!m_authorizator.canWrite(topic, username, clientID)) {
LOG.error("MQTT client is not authorized to publish on topic. CId={}, topic={}", clientID, topic);
return;
}
final int messageID = msg.variableHeader().packetId();
// route message to subscribers
IMessagesStore.StoredMessage toStoreMsg = asStoredMessage(msg);
toStoreMsg.setClientID(clientID);
this.publisher.publish2Subscribers(toStoreMsg, topic, messageID);
sendPubAck(clientID, messageID);
if (msg.fixedHeader().isRetain()) {
if (!msg.payload().isReadable()) {
m_messagesStore.cleanRetained(topic);
} else {
// before wasn't stored
m_messagesStore.storeRetained(topic, toStoreMsg);
}
}
// 修改publish消息,slice出的ByteBuf對象,原文中存在內存泄漏
MoquetteMessage moquetteMessage = new MoquetteMessage(msg.fixedHeader(), msg.variableHeader(), msg.content());
m_interceptor.notifyTopicPublished(moquetteMessage, clientID, username);
msg.content().release();
}
1.鑑權,該client下的username是否有對該topic發佈消息(對topic寫)的權限。
2.創建一個IMessagesStore.StoredMessage,同時把消息推送給所有該對該消息的訂閱者。
if (LOG.isTraceEnabled()) {
LOG.trace("Sending publish message to subscribers. ClientId={}, topic={}, messageId={}, payload={}, " +
"subscriptionTree={}", pubMsg.getClientID(), topic, messageID, DebugUtils.payload2Str(pubMsg.getPayload()),
subscriptions.dumpTree());
} else if(LOG.isInfoEnabled()){
LOG.info("Sending publish message to subscribers. ClientId={}, topic={}, messageId={}", pubMsg.getClientID(), topic,
messageID);
}
publish2Subscribers(pubMsg, topic);
判斷是否是跟蹤模式,如果是的話,會把當前所有的訂閱關係打印到日誌,由於這個需要遍歷topic樹,消耗比較大,所以是可配置的,在moquette.cof裏面配置。
核心的處理邏輯在下面,接着往下看
void publish2Subscribers(IMessagesStore.StoredMessage pubMsg, Topic topic) {
List<Subscription> topicMatchingSubscriptions = subscriptions.matches(topic);
final String topic1 = pubMsg.getTopic();
final MqttQoS publishingQos = pubMsg.getQos();
final ByteBuf origPayload = pubMsg.getPayload();
for (final Subscription sub : topicMatchingSubscriptions) {
MqttQoS qos = lowerQosToTheSubscriptionDesired(sub, publishingQos);
ClientSession targetSession = m_sessionsStore.sessionForClient(sub.getClientId());
boolean targetIsActive = this.connectionDescriptors.isConnected(sub.getClientId());
//TODO move all this logic into messageSender, which puts into the flightZone only the messages that pull out of the queue.
if (targetIsActive) {
if(LOG.isDebugEnabled()){
LOG.debug("Sending PUBLISH message to active subscriber. CId={}, topicFilter={}, qos={}",
sub.getClientId(), sub.getTopicFilter(), qos);
}
// we need to retain because duplicate only copy r/w indexes and don't retain() causing
// refCnt = 0
ByteBuf payload = origPayload.retainedDuplicate();
MqttPublishMessage publishMsg;
if (qos != MqttQoS.AT_MOST_ONCE) {
// QoS 1 or 2
int messageId = targetSession.inFlightAckWaiting(pubMsg);
// set the PacketIdentifier only for QoS > 0
publishMsg = notRetainedPublishWithMessageId(topic1, qos, payload, messageId);
} else {
publishMsg = notRetainedPublish(topic1, qos, payload);
}
this.messageSender.sendPublish(targetSession, publishMsg);
} else {
if (!targetSession.isCleanSession()) {
if(LOG.isDebugEnabled()){
LOG.debug("Storing pending PUBLISH inactive message. CId={}, topicFilter={}, qos={}",
sub.getClientId(), sub.getTopicFilter(), qos);
}
// store the message in targetSession queue to deliver
targetSession.enqueue(pubMsg);
}
}
}
}
大概分爲以下幾步
2.1.根據topic找出匹配的訂閱集合list,這裏面由於涉及到比較大的計算,所以單獨講解
public List<Subscription> matches(Topic topic) {
Queue<Token> tokenQueue = new LinkedBlockingDeque<>(topic.getTokens());
List<ClientTopicCouple> matchingSubs = new ArrayList<>();
subscriptions.get().matches(tokenQueue, matchingSubs);
// 客戶端使用帶通配符的主題過濾器請求訂閱時,客戶端的訂閱可能會重複,因此發佈的消息可能會匹配多個過濾器。對於這種情
//況,服務端必須將消息分發給所有訂閱匹配的QoS等級最高的客戶端。服務端之後可以按照訂閱的QoS等級,分發消息的副本給每
//一個匹配的訂閱者。
Map<String, Subscription> subsForClient = new HashMap<>();
for (ClientTopicCouple matchingCouple : matchingSubs) {
Subscription existingSub = subsForClient.get(matchingCouple.clientID);
Subscription sub = this.subscriptionsStore.getSubscription(matchingCouple);
if (sub == null) {
// if the m_sessionStore hasn't the sub because the client disconnected
continue;
}
// update the selected subscriptions if not present or if has a greater qos
if (existingSub == null || existingSub.getRequestedQos().value() < sub.getRequestedQos().value()) {
subsForClient.put(matchingCouple.clientID, sub);//注意這裏最終存入的從session裏面獲取的訂閱,而不是從topic目錄裏面獲取的,是因爲,有可能client當時並不在線或者該訂閱的qos等級變化了
}
}
return new ArrayList<>(subsForClient.values());
}
可以看的出來,會先創建一個隊列,存儲topic的層級比如/a/b/c,隊列裏面就會有三個運輸[c,b,a] 這裏面之所以要用到隊列而不是,list就是因爲後面進行匹配的時候需要確保先從第一個層級開始匹配,而不是最後一個
void matches(Queue<Token> tokens, List<ClientTopicCouple> matchingSubs) {
Token t = tokens.poll();
// check if t is null <=> tokens finished
if (t == null) {
matchingSubs.addAll(m_subscriptions);
// check if it has got a MULTI child and add its subscriptions
for (TreeNode n : m_children) {
if (n.getToken() == Token.MULTI || n.getToken() == Token.SINGLE) {
matchingSubs.addAll(n.subscriptions());
}
}
return;
}
// we are on MULTI, than add subscriptions and return
if (m_token == Token.MULTI) {
matchingSubs.addAll(m_subscriptions);
return;
}
for (TreeNode n : m_children) {
if (n.getToken().match(t)) {
// Create a copy of token, else if navigate 2 sibling it
// consumes 2 elements on the queue instead of one
n.matches(new LinkedBlockingQueue<>(tokens), matchingSubs);
// TODO don't create a copy n.matches(tokens, matchingSubs);
}
}
}
這段代碼不好理解,涉及到迭代topic樹io.moquette.spi.impl.subscriptions.TreeNode,下面以一個圖說明
另外對topic匹配規則不熟悉的同學可以看一下這裏https://github.com/mcxiaoke/mqtt/blob/master/mqtt/04-OperationalBehavior.md
假如有A,B,C,D, E五個client,其中A訂閱了/test/#,B訂閱了/test/hello/#,C 訂閱了/test/hello/beijing,
D訂閱了/test/+/hello,現在E向topic-name爲/test/hello/shanghai發佈了一條消息請問哪幾個client應該收到這條消息。
向畫出topic樹如下(請原諒我手畫)
先分析 E發佈消息的整個過程:
2.1.1./test/hello/shanghai被放入queue,取出來的順序依次爲test,hello,shanghai
2.1.2.第一輪先匹配test,test不爲空,RootNode(其實是null)不爲#,執行到遍歷RootNode下的子節點,RootNode先的子節點只有一個,test,test.equals(test),然後當前treenode變爲test,
2.1.3.從queue裏面取出hello,hello不爲空,test不爲#,遍歷test這個treenode的子節點,test有三個子節點,分別是(#,+,hello)
2.1. 3.1 子節點是#,# .mathcs(hello),當前節點是#,然後從隊列裏面取出shanghai,shanghai不爲空,#爲#,當前迭代終止,節點A匹配放入匹配的list,
2.1.3.2 子節點是+,+ .mathcs(hello)當前節點是+,然後從隊列裏面取出shanghai(這裏可能有的同學有疑問,爲什麼還能取出shanghai呢,因爲進行下一級迭代的時候是new的新的queue),上海不爲空,+不爲#所以不匹配,接着匹配+這個treenode的子節點,+只有一個子節點
2.1. 3.2.1 當前節點是hello,hello.mathcs(shanghai)不成立,迭代終止,所以D不會放入匹配的list
1.3.3 子節點是hello,hello.equals(hello),當前節點變成hello,然後從隊列裏面取出shanghai,shanghai不爲空,hello不爲#,遍歷hello這個treenode下的子節點,hello先有兩個子節點,分別是(#,beijing),
2.1.3.3.1 子節點是#,# .mathcs(hello),當前節點是#,從隊列裏面取出的是空,所以直接會走第一個if分支,並且把B放入匹配的list,並且退出方法。
2.1. 3.3.2 子節點是beijing,beijing.equals(shanghai)不成立,退出迭代
最終能夠匹配成功的只有A,B這兩個client
也就說說能夠被成功匹配,要麼/test/hello/shanghai的每一層級都能成功匹配,“+”能夠夠任意的單個層級,或者某一個層級是“#”(分別對應上面的兩個if分支)
到此位置,有topic-name匹配由topic-filters組成的topic樹的整個過程分析完成了。下面接着回到上面的publish2Subscribers的這個方法講解匹配出client之後的動作
2.2 遍歷找出的匹配的client,確定qos,qos取訂閱請求要求的qos和發佈的消息自身的qos的最小值,這個是mqtt協議自身規定的,之所以這樣規定是因爲,訂閱的時候其基本單位是某個topic,訂閱者只能訂閱一個topic,而不能訂閱一個消息,而發佈消息的基本單位是一個消息,一個topic下可以有很多個消息,不同的消息可以有不同的qos,所以這裏面,真正的qos是由訂閱方和發佈方共同決定的,出於性能的考慮,去最小的qos
2.3 根據連接描述符,判斷是否client依然在線,如果不在線,且客戶端要求保留會話,則把消息保存到該client的session的BlockingQueue<StoredMessage>,以待client再次上線之後,再次發送給該client,這個對應着在建立連接的時候有一個republish的動作,具體看http://blog.51cto.com/13579730/2073630的 第10步
2.3.如果在線,根據qos做不同的處理,如果qos是0,比較簡單,之間發送,qos是1或者2,則會先把消息放入outboundFlightZone,產生一個messageId,再通過PersistentQueueMessageSender進行發送
io.moquette.spi.impl.PersistentQueueMessageSender#sendPublish,具體的分發邏輯比較簡單,這裏不詳細講解。類之間的關係是
ProtocolProcessor--》qos1PublishHandler--》MessagesPublisher--》PersistentQueueMessageSender,基本的邏輯就是通過ConnectionDescriptorStore進行發送,對於發送失敗的要求保存會話的qos1或者2消息,將會繼續保留,知道重複成功
到此publish2Subscribers方法即發送的核心邏輯講解完了,讓我們回到io.moquette.spi.impl.Qos1PublishHandler#receivedPublishQos1這個方法,
3.發送PUBACK消息,
4.如果是retain消息,但是有沒有paylod,將該topic下的retain消息清除掉,可以理解成是客戶端主動要求清除的,因爲它發送了一個空的消息,如果有payload,則存儲retain消息,對於保留消息,詳細看這裏https://github.com/mcxiaoke/mqtt/blob/master/mqtt/0303-PUBLISH.md 我簡單總結一下,1.每個topic下永遠只會存儲一條retain消息,如果發送了兩條,那麼後面的將會將前面的覆蓋;2.如果客戶端發送了一個零字節的retain消息,broker將會清理調該topic下的retain消息,因爲broker不會存儲零字節的retain消息;3.服務端的保留消息不是會話狀態的組成部分。服務端應該保留那種消息直到客戶端刪除它。4.當client收到broker發送的一個retain消息是,可以理解成這是client新建立的一個訂閱的第一條消息。
5.喚醒攔截器
到此moquette對PUBLISH報文的處理講解完了,這裏面只講解了qos1的處理,是因爲qos0處理比較簡單,而qos2我們沒什麼應用場景。另外這裏說明一下比較容易混淆的概念cleanSession與retain消息
1.retain消息並不是session的一部分,它不與client掛鉤,而是與topic-filter掛鉤。也就是說當發佈這個retain消息的clientsession不存在了,但是retain消息依然存在,除非有client主動刪除它,
2.對於要求保留會話的client,會存在一個broker主動重新發送消息的過程,這個動作實在client重新建立連接的時候,具體看這裏http://blog.51cto.com/13579730/2073630 的第10步,這是因爲broker有責任對要求保留會話的client重新發送qos1與qos2消息
3.對於client發佈訂閱的時候,broker也會有一個主動發送消息的過程,具體看這裏http://blog.51cto.com/13579730/2073914 的第8步
4.qos1消息和qos2是在消息發送失敗或者,client不在線的時候,存儲到clientsession裏面的一個BlockingQueue<StoredMessage>裏面的,而retain消息是在,broker收到的時候直接存儲到IMessagesStore進行存儲的,其底層是個Map<Topic, StoredMessage>