7.3 KiB
EFKA 与 IOT 交互协议
本文档描述 efka 与 iot 之间的 TLS 长连接协议。当前协议由 Erlang term 直接序列化,发送端使用 term_to_binary/1,接收端使用 binary_to_term(PacketBin, [safe])。
协议帧只使用 safe external term:顶层 label、业务 label、map key 使用 binary;Ref 使用 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} |
双向 | 异步消息,不要求回复 |
command 和 command_response 的 Domain 表示业务域,目前支持:
<<"container">>
鉴权请求
初始连接由 efka 发起鉴权 request。每条 TLS 连接只允许一次鉴权;iot 侧鉴权成功后会在 ssl_channel 标记该连接已鉴权,如果同一连接再次发送 auth_request,iot 会直接关闭连接。
{<<"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 可以继续使用。
容器管理命令
iot 对 efka 的容器管理使用 command 语义:
{<<"command">>, Ref, {<<"container">>, CommandMap}}
efka 回复:
{<<"command_response">>, Ref, {<<"container">>, Reply}}
Reply 取值:
<<"ok">>
{<<"ok">>, Result}
{<<"error">>, Reason}
CommandMap 使用 binary key 和 binary action;efka 接收后直接按 binary key/action 匹配,Docker 参数链路继续使用 binary-key map,不再转换成 atom-key map。
list
#{<<"action">> => <<"list">>}
返回当前 efka 主机上的容器列表。
deploy
#{
<<"action">> => <<"deploy">>,
<<"task_id">> => TaskId,
<<"params">> => Params
}
触发容器部署。部署过程中的流式日志不通过该 command response 返回,而是通过 message 的 task_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
}
name 和 id 至少一个非空;优先使用 name,name 为空时使用 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 本身不携带 uuid。iot 在接收该消息时使用当前已鉴权 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
}}}
用于 iot 向 efka 本地订阅系统发布 topic 消息。
状态与超时
efka鉴权超时时间:5 秒。iotcommand inflight 超时时间:60 秒。iotSSL channel 空闲超时时间:120 秒。120 秒内没有收到任何 TLS 包,包括ping、业务message、request、command_response,iot会主动关闭该连接。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}} - 旧 Ref:Erlang
reference(),例如make_ref()生成的值。
如果需要滚动升级,应先增加临时兼容分支或引入协议版本协商。