fix ssl 心跳机制

This commit is contained in:
anlicheng 2026-05-10 23:11:19 +08:00
parent 084be2654c
commit 0f57df4726
2 changed files with 61 additions and 5 deletions

View File

@ -219,6 +219,22 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
`iot` 会为每个 `{UUID, TaskId}` 维护一个独立的任务进程,用于缓存最近的部署日志、支持多个 SSE listener并在收到 close 事件后结束事件流。
### efka -> iot: ping
```erlang
{<<"message">>, <<"ping">>}
```
用于 TLS 长连接的应用层保活。`efka` 在鉴权成功后周期发送,当前发送间隔为 30 秒。
`iot` 收到后回复:
```erlang
{<<"message">>, <<"pong">>}
```
`ping/pong` 只表示 TLS 连接仍可读写,不参与 host online/offline 判定。host 上下线仍由 UDP 心跳和 `iot_host` 本地连接状态共同维护。
### iot -> efka: pub
```erlang
@ -235,6 +251,7 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
- `efka` 鉴权超时时间5 秒。
- `iot` command inflight 超时时间60 秒。
- `iot` SSL channel 空闲超时时间120 秒。120 秒内没有收到任何 TLS 包,包括 `ping`、业务 `message``request``command_response``iot` 会主动关闭该连接。
- `iot` 管理多个 `efka` 时,每个连接有独立 `ssl_channel` 和独立 inflight 表。
- command 超时后,`iot` 删除 inflight 记录;之后如果迟到的 `command_response` 到达,会被视为未预期响应。

View File

@ -28,11 +28,13 @@
-define(STATE_AUTH, auth).
%%
-define(STATE_ACTIVATED, activated).
-define(SSL_PING_INTERVAL, 30000).
-record(state, {
socket :: undefined | ssl:sslsocket(),
%% auth请求的refauth请求和响应的对应关系
auth_ref = undefined :: undefined | binary(),
ping_timer_ref = undefined :: undefined | reference(),
dropped_message_count = 0 :: non_neg_integer()
}).
@ -143,6 +145,20 @@ handle_event(state_timeout, auth_timeout, ?STATE_AUTH, State = #state{socket = S
schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
handle_event(info, {timeout, TimerRef, ssl_ping}, ?STATE_ACTIVATED, State = #state{socket = Socket, ping_timer_ref = TimerRef}) ->
Packet = term_to_binary({<<"message">>, <<"ping">>}),
case ssl:send(Socket, Packet) of
ok ->
{keep_state, schedule_ssl_ping(State)};
{error, Reason} ->
logger:warning("[efka_client] send ssl ping failed, reason: ~p", [Reason]),
disconnect(Socket),
schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}}
end;
handle_event(info, {timeout, _TimerRef, ssl_ping}, _StateName, State) ->
{keep_state, State};
%%
handle_event(info, flush_cache, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
case efka_client_cache:fetch_next() of
@ -165,19 +181,22 @@ handle_event(info, {ssl, Socket, PacketBin}, _, State = #state{socket = Socket})
error:Error ->
logger:warning("[efka_client] binary_to_term get error: ~p, packet_size: ~p", [Error, byte_size(PacketBin)]),
disconnect(Socket),
cancel_ssl_ping(State),
schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}}
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}}
end;
handle_event(info, {ssl_error, Socket, Reason}, _, State = #state{}) ->
logger:debug("[efka_client] ssl error: ~p", [Reason]),
disconnect(Socket),
cancel_ssl_ping(State),
schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}};
handle_event(info, {ssl_closed, Socket}, _, State = #state{}) ->
logger:debug("[efka_client] ssl closed"),
disconnect(Socket),
cancel_ssl_ping(State),
schedule_reconnect(),
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}};
%%% ssl收到的消息会先 binary_to_term
@ -193,7 +212,8 @@ handle_event(internal, {<<"command">>, Ref, {<<"container">>, Request}}, _StateN
%% response
handle_event(internal, {<<"response">>, AuthRef, {<<"auth_response">>, <<"ok">>}}, ?STATE_AUTH, State = #state{auth_ref = AuthRef}) ->
logger:debug("[efka_client] auth success"),
{next_state, ?STATE_ACTIVATED, State#state{auth_ref = undefined}, [{next_event, info, flush_cache}]};
State1 = schedule_ssl_ping(State#state{auth_ref = undefined}),
{next_state, ?STATE_ACTIVATED, State1, [{next_event, info, flush_cache}]};
handle_event(internal, {<<"response">>, AuthRef, {<<"auth_response">>, {<<"error">>, Reason}}}, ?STATE_AUTH, State = #state{socket = Socket, auth_ref = AuthRef}) ->
logger:debug("[efka_client] auth failed, reason: ~p", [Reason]),
disconnect(Socket),
@ -207,6 +227,8 @@ handle_event(internal, {<<"command_response">>, _Ref, Reply}, StateName, State)
{keep_state, State};
%% Pub/Sub机制
handle_event(internal, {<<"message">>, <<"pong">>}, ?STATE_ACTIVATED, State) ->
{keep_state, State};
handle_event(internal, {<<"message">>, {<<"pub">>, #{<<"topic">> := Topic, <<"qos">> := Qos, <<"content">> := Content}}}, ?STATE_ACTIVATED, State) ->
logger:debug("[efka_client] get pub topic: ~p, qos: ~p, content: ~p", [Topic, Qos, Content]),
efka_subscription:publish(Topic, Qos, Content),
@ -254,7 +276,8 @@ handle_container_command(Ref, Request, Socket) ->
ok.
-spec terminate(term(), atom(), #state{}) -> ok.
terminate(Reason, _StateName, _State = #state{socket = Socket}) ->
terminate(Reason, _StateName, State = #state{socket = Socket}) ->
cancel_ssl_ping(State),
disconnect(Socket),
efka_client_cache:close(),
logger:notice("[efka_client] terminate with reason: ~p", [Reason]),
@ -305,6 +328,22 @@ disconnect(Socket) ->
schedule_reconnect() ->
erlang:start_timer(5000, self(), create_transport).
-spec schedule_ssl_ping(#state{}) -> #state{}.
schedule_ssl_ping(State = #state{ping_timer_ref = TimerRef}) ->
cancel_timer(TimerRef),
State#state{ping_timer_ref = erlang:start_timer(?SSL_PING_INTERVAL, self(), ssl_ping)}.
-spec cancel_ssl_ping(#state{}) -> ok.
cancel_ssl_ping(#state{ping_timer_ref = TimerRef}) ->
cancel_timer(TimerRef).
-spec cancel_timer(undefined | reference()) -> ok.
cancel_timer(undefined) ->
ok;
cancel_timer(TimerRef) ->
_ = erlang:cancel_timer(TimerRef),
ok.
-spec request_ref() -> binary().
request_ref() ->
crypto:strong_rand_bytes(16).