ekfa/src/efka_transport.erl
2026-04-20 12:50:58 +08:00

154 lines
5.5 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 20. 4月 2025 18:47
%%%-------------------------------------------------------------------
-module(efka_transport).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_monitor/3]).
-export([connect/1, send/2, stop/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {
parent_pid :: pid(),
host :: string(),
port :: integer(),
socket :: undefined | ssl:sslsocket()
}).
-spec connect(Pid :: pid()) -> no_return().
connect(Pid) when is_pid(Pid) ->
gen_server:cast(Pid, connect).
-spec send(Pid :: pid(), Packet :: binary()) -> no_return().
send(Pid, Packet) when is_pid(Pid), is_binary(Packet) ->
gen_server:cast(Pid, {send, Packet}).
%% 关闭的时候不一定能成功可能关闭的时候transport进程已经退出了
-spec stop(Pid :: pid() | undefined) -> ok.
stop(undefined) ->
ok;
stop(Pid) when is_pid(Pid) ->
catch gen_server:stop(Pid, normal, 2000).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_monitor(ParentPid :: pid(), Host :: string(), Port :: integer()) ->
{ok, {Pid :: pid(), MRef :: reference()}} | ignore | {error, Reason :: term()}).
start_monitor(ParentPid, Host, Port) when is_pid(ParentPid), is_list(Host), is_integer(Port) ->
gen_server:start_monitor(?MODULE, [ParentPid, Host, Port], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([ParentPid, Host, Port]) ->
{ok, #state{parent_pid = ParentPid, host = Host, port = Port, socket = undefined}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Req, _From, State = #state{}) ->
{reply, ok, State#state{}}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
%% 建立到目标服务器的连接
handle_cast(connect, State = #state{host = Host, port = Port, parent_pid = ParentPid}) ->
SslOptions = [
binary,
{packet, 4},
{verify, verify_none}
],
case ssl:connect(Host, Port, SslOptions, 5000) of
{ok, Socket} ->
ok = ssl:controlling_process(Socket, self()),
ParentPid ! {connect_reply, ok},
ping_ticker(),
{noreply, State#state{socket = Socket}};
{error, Reason} ->
ParentPid ! {connect_reply, {error, Reason}},
{noreply, State#state{socket = undefined}}
end;
handle_cast({send, Packet}, State = #state{socket = Socket}) ->
ok = ssl:send(Socket, Packet),
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
%% 服务器主动推送的数据
handle_info({ssl, Socket, PacketBin}, State = #state{socket = Socket, parent_pid = ParentPid}) ->
ParentPid ! {server_packet, PacketBin},
{noreply, State};
handle_info({ssl_error, Socket, Reason}, State = #state{socket = Socket}) ->
logger:debug("[efka_transport] ssl error: ~p", [Reason]),
{stop, normal, State};
handle_info({ssl_closed, Socket}, State = #state{socket = Socket}) ->
{stop, normal, State};
handle_info({timeout, _, ping_ticker}, State) ->
ping_ticker(),
{noreply, State};
handle_info(Info, State = #state{}) ->
logger:notice("[efka_transport] get unknown info: ~p", [Info]),
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(Reason, #state{}) ->
logger:notice("[efka_transport] terminate with reason: ~p", [Reason]),
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
ping_ticker() ->
erlang:start_timer(5000, self(), ping_ticker).