• emqtt 3 (我要subscribe 这个topic)


    这一次,主要分析client subscribe 某个topic 的处理流程。

    由protocol开始

    是的,还是要从protocol开始,至于为什么,之前就说过了。

    subscribe 类型的packet的处理是:

     1 %% 直接过滤掉topic 为空的情况
     2 process(?SUBSCRIBE_PACKET(PacketId, []), State) ->
     3     send(?SUBACK_PACKET(PacketId, []), State);
     4 
     5 process(?SUBSCRIBE_PACKET(PacketId, TopicTable), State = #proto_state{session = Session}) ->
     6     %% 组装client 信息
     7     Client = client(State),
     8     %% 检查ACL
     9     ...
    10     %% session 为clientid 对应的session pid
    11     %% TopicTable 为 [{TopicName, QoS}] 
    12     emqttd_session:subscribe(Session, PacketId, TopicTable)
    13     ...
    14     ;
    15     

    1、过滤掉topictable 为空的情况

    2、组装必要的client 信息,完成ACL检查

    3、获取clientid 对应的session pid,并调用emqttd_session:subscribe/3 函数

    emqttd_session 模块处理

    emqttd_session:subscribe/3 只是一个接口函数,实际的处理逻辑是在emqttd_session 模块的handle_cast callback 中实现。

    -spec(subscribe(pid(), mqtt_packet_id(), [{binary(), mqtt_qos()}]) -> ok).
    subscribe(SessPid, PacketId, TopicTable) ->
        From   = self(), %%这里的self 是client process id
        AckFun = fun(GrantedQos) ->
                   From ! {suback, PacketId, GrantedQos}
                 end,
    gen_server2:cast(SessPid, {subscribe, TopicTable, AckFun}).

    接口函数的定义如上。

    handle_cast callback 的实现如下:

    handle_cast({subscribe, TopicTable0, AckFun},
                Session = #session{client_id     = ClientId,
                       %% subscription 是dict subscriptions
    = Subscriptions}) -> %% rewrite topic name 对topic name 做一些处理 Subscriptions1 = lists:foldl( fun({Topic, Qos}, SubDict) -> case dict:find(Topic, SubDict) of {ok, Qos} -> %% 已经存在,并且QoS 未更新,所以什么都不需要做 SubDict; {ok, OldQos} -> %% 已经存在,但是QoS 更新,所以,需要更新一下 emqttd_server:update_subscription(ClientId, Topic, OldQos, Qos), dict:store(Topic, Qos, SubDict); error -> %% 不存在,直接添加 emqttd:subscribe(ClientId, Topic, Qos), dict:store(Topic, Qos, SubDict) end end, Subscriptions, TopicTable),

    更新subscribe

    更新subscribe,也就是调用emqttd_server:update_subscription/4 。

    emqttd_server 也是由pool 组织的gen_server进程,主要作用是subscription 的增删改查,subscription 信息是保存在 subscription mnesia table 中的,subscription mnesia table的字段信息如下:

    -record(mqtt_subscription,
            {subid   :: binary() | atom(),
             topic   :: binary(),
             qos = 0 :: 0 | 1 | 2
            }).

    其中,subid 即为subscriber id,也就是clientid,topic 即为topic的名称。

    而,update subscription 的逻辑:

     1 %% 外部接口
     2 update_subscription(ClientId, Topic, OldQos, NewQos) ->
     3     call(server(self()), {update_subscription, ClientId, Topic, ?QOS_I(OldQos), ?QOS_I(NewQos)}).
     4 
     5 handle_call({update_subscription, ClientId, Topic, OldQos, NewQos}, _From, State) ->
     6     if_subsciption(State, fun() ->
     7         OldSub = #mqtt_subscription{subid = ClientId, topic = Topic, qos = OldQos},
     8         NewSub = #mqtt_subscription{subid = ClientId, topic = Topic, qos = NewQos},
     9         %% 使用事物
    10         mnesia:transaction(fun update_subscription_/2, [OldSub, NewSub]),
    11         set_subscription_stats()
    12     end), ok(State);
    13 
    14 update_subscription_(OldSub, NewSub) ->
    15     %% 删除旧的 subscription
    16     mnesia:delete_object(subscription, OldSub, write),
    17     %% 写入新的 subscription
    18     mnesia:write(subscription, NewSub, write).

    因为 subscription mnesia table的类型为bag,也就是一个clientid 可能会和多个topic 相对应,所以,不能依据key 进行delete,必须使用delete_object的方式。

    创建subscribe

    create subscribe的处理略微有些绕,不知道是作者有意而为之,还是其他什么原因。

    首先,create subscribe的入口函数在emqttd module中,

    -spec(subscribe(binary(), binary(), mqtt_qos()) -> {ok, mqtt_qos()}).
    subscribe(ClientId, Topic, Qos) ->
        emqttd_server:subscribe(ClientId, Topic, Qos).

    在此调用emqttd_server:subscribe/3 函数,并请求emqttd_server 进程,emqttd_server 进程调用handle_call callback 函数,处理请求。

     1 %% 外部接口
     2 -spec(subscribe(binary(), binary(), mqtt_qos()) -> ok).
     3 subscribe(ClientId, Topic, Qos) ->
     4     %% 这里的self 是emqttd_session 进程,这个调用是在emqttd_session 
     5     %% module 中的 handle_cast callback 发起的
     6     From = self(),
     7     call(server(From), {subscribe, From, ClientId, Topic, ?QOS_I(Qos)}).
     8 
     9 handle_call({subscribe, SubPid, ClientId, Topic, Qos}, _From, State) ->
    10     %% call pubsub process
    11     pubsub_subscribe_(SubPid, Topic),
    12     if_subsciption(State, fun() ->
    13         %% 将subscription 信息写入到 subscription mnesia table 中
    14         add_subscription_(ClientId, Topic, Qos),
    15         set_subscription_stats()
    16     end),
    17     %% monitor session pid,当起DOWN 之后,去掉subscribe 并移除相关信息
    18     ok(monitor_subscriber_(ClientId, SubPid, State));
    19 
    20 %% @private
    21 %% @doc Call pubsub to subscribe
    22 pubsub_subscribe_(SubPid, Topic) ->
    23     case ets:match(subscribed, {SubPid, Topic}) of
    24         [] ->
    25             emqttd_pubsub:async_subscribe(Topic, SubPid),
    26             ets:insert(subscribed, {SubPid, Topic});
    27         [_] ->
    28             false
    29     end.

    L26处,用了subscribed ets table,记录session pid subscribe 的所有topic,这样在 session pid DOWN的时候,就可以移除所有的topic 中session pid 相关的信息了。

    而,emqttd_pubsub 同样是由pool 组织的gen_server 进程。

     1 %% 外部接口,发起请求
     2 -spec(async_subscribe(binary(), pid()) -> ok).
     3 async_subscribe(Topic, SubPid) when is_binary(Topic) ->
     4     cast(pick(Topic), {subscribe, Topic, SubPid}).
     5 
     6 handle_cast({subscribe, Topic, SubPid}, State) ->
     7     %% 实际的处理函数
     8     add_subscriber_(Topic, SubPid),
     9     {noreply, setstats(State)};
    10 
    11 add_subscriber_(Topic, SubPid) ->
    12     %% 检查该Topic 是否已经存在
    13     %% 若不存在,则先增加{Topic,Node}信息,为多node 场景服务
    14     ...
    15     ets:insert(subscriber, {Topic, SubPid}). %% 这里的subscriber 是一张ets table,接下来的publish 主要就是用的这张表 ********

    至此,subscribe 操作的处理逻辑就ok了。

    总结

    应该也不需要,只是这代码贴的有点多了。(示意图待补)

  • 相关阅读:
    Python3标准库:fnmatch UNIX式glob模式匹配
    Python3标准库:glob文件名模式匹配
    Python3标准库:pathlib文件系统路径作为对象
    Python3标准库:os.path平台独立的文件名管理
    Python3标准库:statistics统计计算
    36-Docker 的两类存储资源
    第四章-操作列表
    35-外部世界如何访问容器?
    34-容器如何访问外部世界?
    33-容器间通信的三种方式
  • 原文地址:https://www.cnblogs.com/--00/p/emqtt_learning_subscribe_topic.html
Copyright © 2020-2023  润新知