add ssl 心跳机制

This commit is contained in:
anlicheng 2026-05-10 23:10:17 +08:00
parent 11df258dfd
commit a85b38748f
2 changed files with 64 additions and 10 deletions

View File

@ -219,6 +219,22 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
`iot` 会为每个 `{UUID, TaskId}` 维护一个独立的任务进程,用于缓存最近的部署日志、支持多个 SSE listener并在收到 close 事件后结束事件流。 `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 ### iot -> efka: pub
```erlang ```erlang
@ -235,6 +251,7 @@ GET /event_stream?uuid=<host_uuid>&task_id=<task_id>
- `efka` 鉴权超时时间5 秒。 - `efka` 鉴权超时时间5 秒。
- `iot` command inflight 超时时间60 秒。 - `iot` command inflight 超时时间60 秒。
- `iot` SSL channel 空闲超时时间120 秒。120 秒内没有收到任何 TLS 包,包括 `ping`、业务 `message``request``command_response``iot` 会主动关闭该连接。
- `iot` 管理多个 `efka` 时,每个连接有独立 `ssl_channel` 和独立 inflight 表。 - `iot` 管理多个 `efka` 时,每个连接有独立 `ssl_channel` 和独立 inflight 表。
- command 超时后,`iot` 删除 inflight 记录;之后如果迟到的 `command_response` 到达,会被视为未预期响应。 - command 超时后,`iot` 删除 inflight 记录;之后如果迟到的 `command_response` 到达,会被视为未预期响应。

View File

@ -11,6 +11,7 @@
-behaviour(ranch_protocol). -behaviour(ranch_protocol).
-define(INFLIGHT_TIMEOUT, 60000). -define(INFLIGHT_TIMEOUT, 60000).
-define(SSL_IDLE_TIMEOUT, 120000).
%% API %% API
-export([pub/4, container_call/4, cancel_command_call/2]). -export([pub/4, container_call/4, cancel_command_call/2]).
@ -29,7 +30,9 @@
host_pid = undefined, host_pid = undefined,
%% iot command command_response %% iot command command_response
inflight = #{} inflight = #{},
idle_timer_ref :: undefined | reference()
}). }).
-record(inflight_command, { -record(inflight_command, {
@ -72,7 +75,8 @@ init(Ref, Transport, _Opts = []) ->
{ok, Socket} = ranch:handshake(Ref), {ok, Socket} = ranch:handshake(Ref),
logger:debug("[ssl_channel] get a new connection: ~p", [Socket]), logger:debug("[ssl_channel] get a new connection: ~p", [Socket]),
Transport:setopts(Socket, [binary, {active, true}, {packet, 4}]), Transport:setopts(Socket, [binary, {active, true}, {packet, 4}]),
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = Socket}). IdleTimerRef = start_idle_timer(),
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = Socket, idle_timer_ref = IdleTimerRef}).
handle_call({cancel_command_call, Ref}, _From, State = #state{inflight = Inflight}) -> handle_call({cancel_command_call, Ref}, _From, State = #state{inflight = Inflight}) ->
case maps:take(Ref, Inflight) of case maps:take(Ref, Inflight) of
@ -120,6 +124,12 @@ handle_info({timeout, TimerRef, {command_timeout, Ref}}, State = #state{inflight
{noreply, State} {noreply, State}
end; end;
handle_info({timeout, TimerRef, ssl_idle_timeout}, State = #state{idle_timer_ref = TimerRef}) ->
logger:notice("[ssl_channel] ssl channel idle timeout"),
{stop, ssl_idle_timeout, State#state{idle_timer_ref = undefined}};
handle_info({timeout, _TimerRef, ssl_idle_timeout}, State) ->
{noreply, State};
%% %%
handle_info({stop, Reason}, State) -> handle_info({stop, Reason}, State) ->
{stop, Reason, State}; {stop, Reason, State};
@ -130,19 +140,20 @@ handle_info({'DOWN', _, process, HostPid, Reason}, State = #state{uuid = UUID, h
{stop, Reason, State}; {stop, Reason, State};
handle_info({ssl, Socket, PacketBin}, State = #state{socket = Socket}) when is_binary(PacketBin) -> handle_info({ssl, Socket, PacketBin}, State = #state{socket = Socket}) when is_binary(PacketBin) ->
State1 = reset_idle_timer(State),
try binary_to_term(PacketBin, [safe]) of try binary_to_term(PacketBin, [safe]) of
{<<"request">>, Ref, Body} -> {<<"request">>, Ref, Body} ->
handle_request_frame(Ref, Body, State); handle_request_frame(Ref, Body, State1);
{<<"message">>, Body} -> {<<"message">>, Body} ->
handle_message_frame(Body, State); handle_message_frame(Body, State1);
{<<"command_response">>, Ref, Response} -> {<<"command_response">>, Ref, Response} ->
handle_command_response_frame(Ref, Response, State); handle_command_response_frame(Ref, Response, State1);
Other -> Other ->
logger:warning("[ssl_channel] unsupported packet: ~p", [Other]), logger:warning("[ssl_channel] unsupported packet: ~p", [Other]),
{stop, bad_packet, State} {stop, bad_packet, State1}
catch error:Error -> catch error:Error ->
logger:warning("[ssl_channel] binary_to_term get error: ~p, packet_size: ~p", [Error, byte_size(PacketBin)]), logger:warning("[ssl_channel] binary_to_term get error: ~p, packet_size: ~p", [Error, byte_size(PacketBin)]),
{stop, bad_packet, State} {stop, bad_packet, State1}
end; end;
handle_info({ssl_closed, Socket}, State = #state{socket = Socket}) -> handle_info({ssl_closed, Socket}, State = #state{socket = Socket}) ->
@ -161,7 +172,8 @@ handle_info(Info, State) ->
logger:warning("[ssl_channel] get a unknown message: ~p, state: ~p", [Info, State]), logger:warning("[ssl_channel] get a unknown message: ~p, state: ~p", [Info, State]),
{noreply, State}. {noreply, State}.
terminate(Reason, #state{inflight = Inflight, transport = Transport, socket = Socket}) -> terminate(Reason, #state{inflight = Inflight, transport = Transport, socket = Socket, idle_timer_ref = IdleTimerRef}) ->
cancel_timer(IdleTimerRef),
maps:foreach(fun(Ref, CommandInfo) -> reply_command_closed(Ref, CommandInfo, Reason) end, Inflight), maps:foreach(fun(Ref, CommandInfo) -> reply_command_closed(Ref, CommandInfo, Reason) end, Inflight),
Transport:close(Socket), Transport:close(Socket),
logger:warning("[ssl_channel] stop with reason: ~p", [Reason]), logger:warning("[ssl_channel] stop with reason: ~p", [Reason]),
@ -205,8 +217,17 @@ handle_request_frame(Ref, Body, State) ->
logger:warning("[ws_channel] unsupported request body, ref: ~p, body: ~p", [Ref, Body]), logger:warning("[ws_channel] unsupported request body, ref: ~p, body: ~p", [Ref, Body]),
{stop, normal, State}. {stop, normal, State}.
-spec handle_message_frame(tuple(), #state{}) -> -spec handle_message_frame(term(), #state{}) ->
{noreply, #state{}}. {noreply, #state{}} | {stop, term(), #state{}}.
handle_message_frame(<<"ping">>, State = #state{transport = Transport, socket = Socket}) ->
Packet = term_to_binary({<<"message">>, <<"pong">>}),
case Transport:send(Socket, Packet) of
ok ->
{noreply, State};
{error, Reason} ->
logger:warning("[ssl_channel] send pong failed, reason: ~p", [Reason]),
{stop, {send_failed, Reason}, State}
end;
handle_message_frame({<<"data">>, #{<<"route_key">> := RouteKey, <<"metric">> := Metric}}, State = #state{host_pid = HostPid}) when is_pid(HostPid) -> handle_message_frame({<<"data">>, #{<<"route_key">> := RouteKey, <<"metric">> := Metric}}, State = #state{host_pid = HostPid}) when is_pid(HostPid) ->
iot_host:handle(HostPid, {data, RouteKey, Metric}), iot_host:handle(HostPid, {data, RouteKey, Metric}),
{noreply, State}; {noreply, State};
@ -329,3 +350,19 @@ safe_term(Value) when is_tuple(Value) ->
list_to_tuple([safe_term(Item) || Item <- tuple_to_list(Value)]); list_to_tuple([safe_term(Item) || Item <- tuple_to_list(Value)]);
safe_term(Value) -> safe_term(Value) ->
Value. Value.
-spec start_idle_timer() -> reference().
start_idle_timer() ->
erlang:start_timer(?SSL_IDLE_TIMEOUT, self(), ssl_idle_timeout).
-spec reset_idle_timer(#state{}) -> #state{}.
reset_idle_timer(State = #state{idle_timer_ref = TimerRef}) ->
cancel_timer(TimerRef),
State#state{idle_timer_ref = start_idle_timer()}.
-spec cancel_timer(undefined | reference()) -> ok.
cancel_timer(undefined) ->
ok;
cancel_timer(TimerRef) ->
_ = erlang:cancel_timer(TimerRef),
ok.