我刚刚开始学习Erlang,这是我测试项目中的一个模块。我这样做是为了让我能更好地了解监督树的工作原理,练习快速失败的代码和一些编程最佳实践。
udp_listener
进程侦听UDP消息。它的作用是侦听来自网络中其他主机的通信请求,并使用UDP消息中定义的端口号通过TCP与它们联系。
每次套接字收到UDP消息时都会调用handle_info(...)
函数,它会解码UDP消息并将其传递给tcp_client
进程。
根据我的理解,我的代码中唯一的失败点是decode_udp_message(Data)
在handle_info(...)
内被调用。
当此函数失败时,整个udp_listener
进程是否重新启动?我应该阻止这种情况发生吗?
handle_info(...)
函数不应该在不影响udp_listener
进程的情况下自行死亡吗?
如何在decode_udp_message(Data)
上记录例外?我想在主机上注册,这是失败的消息。
-module(udp_listener).
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
%% ====================================================================
%% API functions
%% ====================================================================
-export([start_link/1]).
start_link(Port) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, Port, []).
%% ====================================================================
%% Behavioural functions
%% ====================================================================
%% init/1
%% ====================================================================
-spec init(Port :: non_neg_integer()) -> Result when
Result :: {ok, Socket :: port()}
| {stop, Reason :: term()}.
%% ====================================================================
init(Port) ->
SocketTuple = gen_udp:open(Port, [binary, {active, true}]),
case SocketTuple of
{ok, Socket} -> {ok, Socket};
{error, eaddrinuse} -> {stop, udp_port_in_use};
{error, Reason} -> {stop, Reason}
end.
% Handles "!" messages from the socket
handle_info({udp, Socket, Host, _Port, Data}, State) -> Socket = State,
handle_ping(Host, Data),
{noreply, Socket}.
terminate(_Reason, State) -> Socket = State,
gen_udp:close(Socket).
handle_cast(_Request, State) -> {noreply, State}.
handle_call(_Request, _From, State) -> {noreply, State}.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%% ====================================================================
%% Internal functions
%% ====================================================================
handle_ping(Host, Data) ->
PortNumber = decode_udp_message(Data),
contact_host(Host, PortNumber).
decode_udp_message(Data) when is_binary(Data) ->
% First 16 bits == Port number
<<PortNumber:16>> = Data,
PortNumber.
contact_host(Host, PortNumber) ->
tcp_client:connect(Host, PortNumber).
我已根据您的回答更改了我的代码,decode_udp_message
已消失,因为handle_ping
执行了我需要的操作。
handle_ping(Host, <<PortNumber:16>>) ->
contact_host(Host, PortNumber);
handle_ping(Host, Data) ->
%% Here I'll log the invalid datagrams but the process won't be restarted
我喜欢现在的方式,通过添加以下代码,我可以在将来处理协议更改,而不会失去与旧服务器的向后兼容性:
handle_ping(Host, <<PortNumber:16, Foo:8, Bar:32>>) ->
contact_host(Host, PortNumber, Foo, Bar);
handle_ping(Host, <<PortNumber:16>>) ->
...
@塞缪尔-Rivas的
tcp_client
是另一个拥有自己主管的gen_server,它将处理自己的失败。
-> Socket = State
现在只出现在terminate
函数中。 gen_udp:close(Socket).
在眼睛上更容易。
答案 0 :(得分:4)
我认为&#34;让它崩溃&#34;经常被误解为&#34;不处理错误&#34; (一个更强大和更奇怪的建议)。你问题的答案(&#34;我是否应该处理错误&#34;)是&#34;它取决于&#34;。
错误处理的一个问题是用户体验。您永远不会想要向您的用户抛出堆栈跟踪监督树。正如塞缪尔·里瓦斯所指出的那样,另一个问题是,从一个崩溃的过程进行调试可能会很痛苦(特别是初学者)。
Erlang的设计倾向于使用非本地客户端的服务器。在这种架构中,客户端必须能够处理服务器突然变得不可用(当您点击SO上的&#34; post&#34;按钮时,您的wifi连接只会 ),服务器必须能够处理来自客户的突然辍学。在这种情况下,我会翻译&#34;让它崩溃&#34; as&#34;因为所有各方都可以处理服务器消失并返回,为什么不将它用作错误处理程序?而不是编写大量代码行来从所有边缘情况中恢复(然后仍然缺少一些),只需删除所有连接并返回已知良好状态。&#34;
&#34;它取决于&#34;进来了也许知道谁发送了坏数据报对你来说真的很重要(因为你也在编写客户端)。也许客户总是想要回复(希望不是UDP)。
就个人而言,我首先编写&#34;成功路径&#34;,其中既包括成功的成功,也包括我想向客户展示的错误。我没有想到的或客户不需要了解的所有内容都会在重新启动过程中处理。
答案 1 :(得分:3)
您的decode_message
不是唯一的失败点。 contact_host
很可能也会失败,但您要么忽略错误元组,要么在tcp_client
实施中处理失败。
除此之外,只要管理员使用正确的策略启动udp_listener
,您就可以使用错误处理方法。如果Data
不完全是16位,则匹配将失败,并且进程将因badmatch
异常而崩溃。然后主管将开始一个新的。
许多在线风格指南都会宣传这种风格。我认为他们错了。即使在那里失败就是你想要的,但这并不意味着你不能提供比坏匹配更好的理由。所以我会在那里写一些更好的错误处理。通常,我会抛出一个信息丰富的元组,但对于gen服务器来说这很棘手,因为它们将每个调用都包裹在catch
内,这会将抛出变为有效值。这是不幸的,但这是一个其他长期解释的话题,所以出于实际目的,我会在这里抛出错误。第三种选择只是使用错误元组({ok, Blah} | {error, Reason}
),但这很快就会变得复杂。使用哪个选项也是一个长期解释/辩论的主题,所以现在我只是继续我自己的方法。
回到你的代码,如果你想要正确和提供信息的错误管理,我会用decode_udp_message
函数在这一行中做一些事情(保留你当前的语义,看看这个回复的结尾,因为我认为他们不是你想要的):
decode_udp_message(<<PortNumber:16>>) ->
PortNumber;
decode_udp_message(Ohter) ->
%% You could log here if you want or live with the crash message if that is good enough for you
erlang:error({invalid_udp_message, {length, byte_size(Other)}}).
正如您所说,这将采用整个UDP连接。如果进程由主管重新启动,那么它将重新连接(除非您使用reuseaddr
sockopt,否则可能会导致问题)。除非你计划每秒多次失败并打开连接成为负担,否则这将没有问题。如果是这种情况,您有几种选择。
要点:
有关与错误处理无关的代码的一些注意事项:
decode_udp_message
中的评论似乎暗示您要解析第一个 16位,但实际上您强迫Data
完全 16位。-> Socket = State
这样的事情,缩进可能是坏的风格,而且变量的重命名也有些不必要。您可以在函数头中更改State
的{{1}},或者,如果您想明确说明您的状态是套接字,请将您的函数头写为Socket