iot_cloud/docs/efka_iot_protocol.md
2026-05-30 15:25:40 +08:00

7.3 KiB
Raw Blame History

EFKA 与 IOT 交互协议

本文档描述 efkaiot 之间的 TLS 长连接协议。当前协议由 Erlang term 直接序列化,发送端使用 term_to_binary/1,接收端使用 binary_to_term(PacketBin, [safe])

协议帧只使用 safe external term顶层 label、业务 label、map key 使用 binaryRef 使用 crypto:strong_rand_bytes(16) 生成,是 16 字节 binary。网络协议里不发送 Erlang reference(),也不依赖动态创建 atom。

传输层

  • efka 作为 TLS client 连接 iot
  • iot 作为 TLS server 接收多个 efka 连接,一个连接对应一个 ssl_channel 进程。
  • socket 使用 {packet, 4},每个 Erlang term binary 作为一个完整包发送。
  • Ref 使用 crypto:strong_rand_bytes(16) 生成,只在当前连接的 inflight 表内匹配。

顶层帧

协议顶层 tuple 用来表达交互语义:

{<<"request">>, Ref, Body}
{<<"response">>, Ref, Reply}
{<<"command">>, Ref, {Domain, Payload}}
{<<"command_response">>, Ref, {Domain, Reply}}
{<<"message">>, Body}

语义说明:

方向 语义
{<<"request">>, Ref, Body} efka -> iot efka 发起请求,需要 iot 回复
{<<"response">>, Ref, Reply} iot -> efka iot 对 efka request 的回复
{<<"command">>, Ref, {Domain, Payload}} iot -> efka iot 下发命令,需要 efka 回复
{<<"command_response">>, Ref, {Domain, Reply}} efka -> iot efka 对 iot command 的回复
{<<"message">>, Body} 双向 异步消息,不要求回复

commandcommand_responseDomain 表示业务域,目前支持:

  • <<"container">>

鉴权请求

初始连接由 efka 发起鉴权 request。每条 TLS 连接只允许一次鉴权;iot 侧鉴权成功后会在 ssl_channel 标记该连接已鉴权,如果同一连接再次发送 auth_requestiot 会直接关闭连接。

{<<"request">>, Ref, {<<"auth_request">>, #{
    <<"uuid">> => UUID,
    <<"token">> => Token,
    <<"timestamp">> => Timestamp
}}}

iot 回复:

{<<"response">>, Ref, {<<"auth_response">>, <<"ok">>}}
{<<"response">>, Ref, {<<"auth_response">>, {<<"error">>, {<<"failed">>, Reason}}}}

处理语义:

  • <<"ok">>efka 进入 activated 状态。
  • {<<"error">>, {<<"failed">>, Reason}}:鉴权失败,iot 返回失败响应后关闭连接;efka 进入重连流程。

授权控制

/host/activate 只修改 iot 本地和持久化的 host 授权状态,不再向 efka 下发 auth command。efka 可以继续保持连接并发送数据,是否处理这些数据由 iot_host 当前状态决定。

因此当前协议没有 {<<"command">>, Ref, {<<"auth">>, ...}}{<<"command_response">>, Ref, {<<"auth">>, ...}}。授权关闭时,iot_host 保持 channel 在线,但不处理上报数据;授权重新打开后,已在线的 channel 可以继续使用。

容器管理命令

iotefka 的容器管理使用 command 语义:

{<<"command">>, Ref, {<<"container">>, CommandMap}}

efka 回复:

{<<"command_response">>, Ref, {<<"container">>, Reply}}

Reply 取值:

<<"ok">>
{<<"ok">>, Result}
{<<"error">>, Reason}

CommandMap 使用 binary key 和 binary actionefka 接收后直接按 binary key/action 匹配Docker 参数链路继续使用 binary-key map不再转换成 atom-key map。

list

#{<<"action">> => <<"list">>}

返回当前 efka 主机上的容器列表。

deploy

#{
    <<"action">> => <<"deploy">>,
    <<"task_id">> => TaskId,
    <<"params">> => Params
}

触发容器部署。部署过程中的流式日志不通过该 command response 返回,而是通过 messagetask_event 上报。

start

#{
    <<"action">> => <<"start">>,
    <<"target">> => Target
}

stop

#{
    <<"action">> => <<"stop">>,
    <<"target">> => Target,
    <<"timeout_seconds">> => TimeoutSeconds
}

kill

#{
    <<"action">> => <<"kill">>,
    <<"target">> => Target,
    <<"signal">> => Signal
}

remove

#{
    <<"action">> => <<"remove">>,
    <<"target">> => Target,
    <<"force">> => Force,
    <<"remove_volumes">> => RemoveVolumes
}

config

#{
    <<"action">> => <<"config">>,
    <<"target">> => Target,
    <<"config">> => Config
}

更新容器配置文件。

Target

容器目标使用 map 表示:

#{
    <<"name">> => ContainerName,
    <<"id">> => ContainerId
}

nameid 至少一个非空;优先使用 namename 为空时使用 id

异步消息

message 不带 Ref,不要求对端回复。

efka -> iot: data

{<<"message">>, {<<"data">>, #{
    <<"route_key">> => RouteKey,
    <<"metric">> => Metric
}}}

用于 efka 上报业务指标数据。

efka -> iot: task_event

{<<"message">>, {<<"task_event">>, #{
    <<"task_id">> => TaskId,
    <<"type">> => Type,
    <<"stream">> => Stream
}}}

任务事件流关闭时:

{<<"message">>, {<<"task_event">>, #{
    <<"task_id">> => TaskId,
    <<"type">> => <<"close">>,
    <<"stream">> => Reason
}}}

task_event 本身不携带 uuidiot 在接收该消息时使用当前已鉴权 ssl_channel 绑定的 host UUID把事件路由到内部任务进程 {UUID, TaskId}。HTTP 页面通过 SSE 订阅时也必须使用同一组参数:

GET /event_stream?uuid=<host_uuid>&task_id=<task_id>

iot 会为每个 {UUID, TaskId} 维护一个独立的任务进程,用于缓存最近的部署日志、支持多个 SSE listener并在收到 close 事件后结束事件流。

efka -> iot: ping

{<<"message">>, <<"ping">>}

用于 TLS 长连接的应用层保活。efka 在鉴权成功后周期发送,当前发送间隔为 30 秒。

iot 收到后回复:

{<<"message">>, <<"pong">>}

ping/pong 只表示 TLS 连接仍可读写,不参与 host online/offline 判定。host 上下线仍由 UDP 心跳和 iot_host 本地连接状态共同维护。

iot -> efka: pub

{<<"message">>, {<<"pub">>, #{
    <<"topic">> => Topic,
    <<"qos">> => Qos,
    <<"content">> => Content
}}}

用于 iotefka 本地订阅系统发布 topic 消息。

状态与超时

  • efka 鉴权超时时间5 秒。
  • iot command inflight 超时时间60 秒。
  • iot SSL channel 空闲超时时间120 秒。120 秒内没有收到任何 TLS 包,包括 ping、业务 messagerequestcommand_responseiot 会主动关闭该连接。
  • iot 管理多个 efka 时,每个连接有独立 ssl_channel 和独立 inflight 表。
  • command 超时后,iot 删除 inflight 记录;之后如果迟到的 command_response 到达,会被视为未预期响应。

兼容性

当前协议不兼容旧 tuple

  • 旧容器管理:{request, Ref, {container_request, ...}}
  • 旧容器回复:{response, Ref, {container_response, ...}}
  • 旧授权控制:{message, {auth_control, Command}}
  • 已移除的 auth command{command, Ref, {auth, activate | deactivate}}
  • 旧 RefErlang reference(),例如 make_ref() 生成的值。

如果需要滚动升级,应先增加临时兼容分支或引入协议版本协商。