防范错误数据

回想一下在第??章(程序??.5)中描述的那个用来分析电话号码的服务程序。它的主循环包含了以下代码:

  1. server(AnalTable) ->
  2. receive
  3. {From, {analyse,Seq}} ->
  4. Result = lookup(Seq, AnalTable),
  5. From ! {number_analyser, Result},
  6. server(AnalTable);
  7. {From, {add_number, Seq, Key}} ->
  8. From ! {number_analyser, ack},
  9. server(insert(Seq, Key, AnalTable))
  10. end.

以上的Seq是一个表示电话号码的数字序列,如[5,2,4,8,9]。在编写lookup/2insert/3这两个函数时,我们应检查Seq是否是一个电话拨号按键字符[1]的列表。若不做这个检查,假设Seq是一个原子项hello,就会导致运行时错误。一个简单些的做法是将lookup/2insert/3放在一个catch语句的作用域中求值:

  1. server(AnalTable) ->
  2. receive
  3. {From, {analyse,Seq}} ->
  4. case catch lookup(Seq, AnalTable) of
  5. {'EXIT', _} ->
  6. From ! {number_analyser, error};
  7. Result ->
  8. From ! {number_analyser, Result}
  9. end,
  10. server(AnalTable);
  11. {From, {add_number, Seq, Key}} ->
  12. From ! {number_analyser, ack},
  13. case catch insert(Seq, Key, AnalTable) of
  14. {'EXIT', _} ->
  15. From ! {number_analyser, error},
  16. server(AnalTable); % Table not changed
  17. NewTable ->
  18. server(NewTable)
  19. end
  20. end.

注意,借助catch我们的号码分析函数可以只处理正常情况,而让Erlang的错误处理机制去处理badmatchbadargfunction_clause等错误。

一般来说,设计服务器时应注意即使面对错误的输入数据,服务器也不会“崩溃”。很多情况下发送给服务器的数据都来自服务器的访问函数。在上面的例子中,号码分析服务器获悉的客户端进程标识From是从访问函数获得的,例如:

  1. lookup(Seq) ->
  2. number_analyser ! {self(), {analyse,Seq}},
  3. receive
  4. {number_analyser, Result} ->
  5. Result
  6. end.

服务器不需要检查From是否是一个进程标识。在这个案例中,我们(借助访问函数)来防范意外的错误情况。然而恶意程序仍然可以绕过访问函数,向服务器发送恶意数据致使服务器崩溃:

  1. number_analyser ! {55, [1,2,3]}

这样一来号码分析器将试图向进程55发送分析结果,继而崩溃。