Erlang/OTP 26 亮点

2023 年 5 月 16 日 · 作者:Björn Gustavsson

Erlang/OTP 26 终于发布了。这篇博客文章将介绍我们最兴奋的新功能。

所有更改的列表可以在 Erlang/OTP 26 自述文件中找到。或者,像往常一样,查看您感兴趣的应用程序的发行说明。例如:Erlang/OTP 26 - Erts 发行说明 - 版本 14.0

这篇博客文章中提到的今年亮点包括

shell #

OTP 26 对使用 Erlang shell 的体验带来了许多改进。

例如,现在可以直接在 shell 中定义函数

1> factorial(N) -> factorial(N, 1).
ok
2> factorial(N, F) when N > 1 -> factorial(N - 1, F * N);
.. factorial(_, F) -> F.
ok
3> factorial(5).
120

当前一行不是完整的 Erlang 结构时,shell 提示符会变为 ..

以这种方式定义的函数使用 erl_eval 模块进行评估,而不是由 Erlang 编译器编译。这意味着性能将无法与编译的 Erlang 代码相提并论。

还可以定义类型、规范和记录,从而可以将代码从模块直接粘贴到 shell 中进行测试。例如

1> -record(coord, {x=0.0 :: float(), y=0.0 :: float()}).
ok
2> -type coord() :: #coord{}.
ok
3> -spec add(coord(), coord()) -> coord().
ok
4> add(#coord{x=X1, y=Y1}, #coord{x=X2, y=Y2}) ->
..     #coord{x=X1+X2, y=Y1+Y2}.
ok
5> Origin = #coord{}.
#coord{x = 0.0,y = 0.0}
6> add(Origin, #coord{y=10.0}).
#coord{x = 0.0,y = 10.0}

shell 中的自动完成功能得到了极大的改进,支持变量、记录名称、记录字段名称、map 键、函数参数类型和文件名的自动完成。

例如,我只需键入 O 并按 TAB 将其展开为 Origin,而无需键入变量名 Origin,因为 shell 中唯一以字母 O 开头的变量是 Origin。这在博客文章中有点难以说明,所以我们再介绍一个以 O 开头的变量

7> Oxford = #coord{x=51.752022, y=-1.257677}.
#coord{x = 51.752022,y = -1.257677}

如果我现在按 O 和 TAB,shell 会显示可能的补全

8> O
bindings
Origin    Oxford

(单词 bindings 以粗体和下划线显示。)

如果我按 x 和 TAB,该单词将补全为 Oxford

8> Oxford.
#coord{x = 51.752022,y = -1.257677}

要键入 #coord{,只需键入 # 和 TAB(因为 shell 中目前只定义了一个记录)

9> #coord{

再次按 TAB 会打印记录中的字段名称

9> #coord{
fields
x=    y=

当尝试补全有许多可能扩展的内容时,shell 会尝试首先显示最可能的补全。例如,如果我键入 l 并按 TAB,shell 会显示以字母 l 开头的 BIF 列表

10> l
bifs
length(                   link(                     list_to_atom(
list_to_binary(           list_to_bitstring(        list_to_existing_atom(
list_to_float(            list_to_integer(          list_to_pid(
list_to_port(             list_to_ref(              list_to_tuple(
Press tab to see all 37 expansions

再次按 TAB,会显示更多 BIF,以及可能的 shell 命令和模块

10> l
bifs
length(                   link(                     list_to_atom(
list_to_binary(           list_to_bitstring(        list_to_existing_atom(
list_to_float(            list_to_integer(          list_to_pid(
list_to_port(             list_to_ref(              list_to_tuple(
load_module(
commands
l(     lc(    lm(    ls(
modules
lcnt:                      leex:                      lists:
local_tcp:                 local_udp:                 log_mf_h:
logger:                    logger_backend:            logger_config:
logger_disk_log_h:         logger_filters:            logger_formatter:
logger_h_common:           logger_handler_watcher:    logger_olp:
logger_proxy:              logger_server:             logger_simple_h:
logger_std_h:              logger_sup:

键入 ists:(以补全单词 lists)并按 TAB,会显示 lists 模块中函数的部分列表

10> lists:
functions
all(            any(            append(         concat(         delete(
droplast(       dropwhile(      duplicate(      enumerate(      filter(
filtermap(      flatlength(     flatmap(        flatten(        foldl(
foldr(          foreach(        join(           keydelete(      keyfind(
Press tab to see all 72 expansions

键入 m 并按 TAB,函数列表将缩小到仅以字母 m 开头的那些函数

10> lists:m
functions
map(            mapfoldl(       mapfoldr(       max(            member(
merge(          merge3(         min(            module_info(

显示 shell 功能的动画 #

map 的改进 #

原子键的排序更改 #

OTP 25 和更早的版本根据其键的项顺序打印带有原子键的小 map(最多 32 个元素)

1> AM = #{a => 1, b => 2, c => 3}.
#{a => 1,b => 2,c => 3}
2> maps:to_list(AM).
[{a,1},{b,2},{c,3}]

在 OTP 26 中,作为某些 map 操作(例如 maps:from_list/1)的优化,带有原子键的 map 现在以不同的顺序排序。新顺序是未定义的,并且可能会在 Erlang VM 的不同调用之间更改。在撰写本文时,我的计算机上的顺序如下

1> AM = #{a => 1, b => 2, c => 3}.
#{c => 3,a => 1,b => 2}
2> maps:to_list(AM).
[{c,3},{a,1},{b,2}]

格式字符串有一个新的修饰符 k,用于指定 map 应在打印之前根据其键的项顺序进行排序

3> io:format("~kp\n", [AM]).
#{a => 1,b => 2,c => 3}
ok

也可以使用 自定义排序函数。例如,根据键的相反顺序对 map 元素进行排序

4> io:format("~Kp\n", [fun(A, B) -> A > B end, AM]).
#{c => 3,b => 2,a => 1}
ok

还有一个新的 maps:iterator/2 函数,它支持以更直观的顺序迭代 map 的元素。示例将在下一节中显示。

Map 推导式 #

在 OTP 25 和更早的版本中,通常将 maps:from_list/1maps:to_list/1 与列表推导式结合使用。例如

1> M = maps:from_list([{I,I*I} || I <- lists:seq(1, 5)]).
#{1 => 1,2 => 4,3 => 9,4 => 16,5 => 25}

在 OTP 26 中,可以使用 map 推导式 更简洁地编写

1> M = #{I => I*I || I <- lists:seq(1, 5)}.
#{1 => 1,2 => 4,3 => 9,4 => 16,5 => 25}

使用 map 生成器,推导式现在可以迭代 map 的元素。例如

2> [K || K := V <- M, V < 10].
[1,2,3]

使用带有 map 生成器的 map 推导式,以下示例显示了如何交换键和值

3> #{V => K || K := V <- M}.
#{1 => 1,4 => 2,9 => 3,16 => 4,25 => 5}

Map 生成器接受 map 迭代器和 map。特别有用的是从新的 maps:iterator/2 函数返回的排序迭代器

4> AM = #{a => 1, b => 2, c => 1}.
#{c => 1,a => 1,b => 2}
5> [{K,V} || K := V <- maps:iterator(AM, ordered)].
[{a,1},{b,2},{c,1}]
6> [{K,V} || K := V <- maps:iterator(AM, reversed)].
[{c,1},{b,2},{a,1}]
7> [{K,V} || K := V <- maps:iterator(AM, fun(A, B) -> A > B end)].
[{c,1},{b,2},{a,1}]

Map 推导式最早在 EEP 58 中提出。

内联 maps:get/3 #

在 OTP 26 中,编译器将内联调用 maps:get/3,使其效率略高。

改进的 maps:merge/2 #

合并两个 map 时,maps:merge/2 函数现在将尝试重用其中一个 map 的 键元组,以减少 map 的内存使用量。

例如

1> maps:merge(#{x => 13, y => 99, z => 100}, #{x => 0, z => -7}).
#{y => 99,x => 0,z => -7}

结果 map 与第一个 map 具有相同的三种键,因此可以重用第一个 map 的键元组。

如果其中一个 map 具有另一个 map 中不存在的任何键,则无法进行此优化。例如

2> maps:merge(#{x => 1000}, #{y => 2000}).
#{y => 2000,x => 1000}

改进的 map 更新 #

使用 => 运算符更新 map 已得到改进,以避免更新不更改 map 的值或其 键元组。例如

1> M = #{a => 42}.
#{a => 42}
2> M#{a => 42}.
#{a => 42}

更新操作不会更改 map 的值,因此为了节省内存,将返回原始 map。

针对 := 运算符的类似优化已在 5 年前实现。)

当使用 => 运算符更新 map 中已存在的键的值时,现在将重用键元组。例如

3> M#{a => 100}.
#{a => 100}

map 改进的拉取请求 #

对于任何想要深入了解的人,以下是 OTP 26 中 map 的主要拉取请求

lists 模块的改进 #

新函数 lists:enumerate/3 #

在 OTP 25 中,引入了 lists_enumerate()。例如

1> lists:enumerate([a,b,c]).
[{1,a},{2,b},{3,c}]
2> lists:enumerate(0, [a,b,c]).
[{0,a},{1,b},{2,c}]

在 OTP 26 中,lists:enumerate/3 通过允许指定增量来完成函数系列

3> lists:enumerate(0, 10, [a,b,c]).
[{0,a},{10,b},{20,c}]
4> lists:enumerate(0, -1, [a,b,c]).
[{0,a},{-1,b},{-2,c}]

zip 系列函数的新选项 #

lists 模块中的 zip 系列函数将两个或三个列表组合成一个元组列表。例如

1> lists:zip([a,b,c], [1,2,3]).
[{a,1},{b,2},{c,3}]

如果列表的长度不相同,则现有的 zip 函数会失败

2> lists:zip([a,b,c,d], [1,2,3]).
** exception error: no function clause matching . . .

在 OTP 26 中,zip 函数现在采用一个额外的 How 参数,该参数确定当列表长度不相等时应发生的情况。

对于 zip 的某些用例,忽略较长列表中的多余元素可能是有意义的。可以使用 trim 选项来完成此操作

3> lists:zip([a,b,c,d], [1,2,3], trim).
[{a,1},{b,2},{c,3}]

对于其他用例,将较短的列表扩展到最长列表的长度可能更有意义。可以使用 {pad, Defaults} 选项来完成此操作,其中 Defaults 应该是一个元组,其元素数量与列表的数量相同。对于 lists:zip/3,这意味着 Defaults 元组应具有两个元素

4> lists:zip([a,b,c,d], [1,2,3], {pad, {zzz, 999}}).
[{a,1},{b,2},{c,3},{d,999}]
5> lists:zip([a,b,c], [1,2,3,4,5], {pad, {zzz, 999}}).
[{a,1},{b,2},{c,3},{zzz,4},{zzz,5}]

对于 lists:zip3/3Defaults 元组应具有三个元素

6> lists:zip3([], [a], [1,2,3], {pad, {0.0, zzz, 999}}).
[{0.0,a,1},{0.0,zzz,2},{0.0,zzz,3}]

无需在运行时系统中启用 maybe 特性 #

在 OTP 25 中,引入了 特性概念maybe 特性。为了在 OTP 25 中使用 maybe,需要在编译器和运行时系统中启用它。例如

$ cat t.erl
-module(t).
-feature(maybe_expr, enable).
-export([listen_port/2]).
listen_port(Port, Options) ->
    maybe
        {ok, ListenSocket} ?= inet_tcp:listen(Port, Options),
        {ok, Address} ?= inet:sockname(ListenSocket),
        {ok, {ListenSocket, Address}}
    end.
$ erlc t.erl
$ erl
Erlang/OTP 25 . . .

Eshell V13.1.1  (abort with ^G)
1> t:listen_port(50000, []).
=ERROR REPORT==== 6-Apr-2023::12:01:20.373223 ===
Loading of . . ./t.beam failed: {features_not_allowed,
                                 [maybe_expr]}

** exception error: undefined function t:listen_port/2
2> q().
$ erl -enable-feature maybe_expr
Erlang/OTP 25 . . .

Eshell V13.1.1  (abort with ^G)
1> t:listen_port(50000, []).
{ok,{#Port<0.5>,{{0,0,0,0},50000}}}

在 OTP 26 中,不再需要在运行时系统中启用特性才能加载使用它的模块。只需在模块中使用 -feature(maybe_expr, enable). 即可。

例如

$ erlc t.erl
$ erl
Erlang/OTP 26 . . .

Eshell V14.0 (press Ctrl+G to abort, type help(). for help)
1> t:listen_port(50000, []).
{ok,{#Port<0.4>,{{0,0,0,0},50000}}}

Erlang 编译器和 JIT 的改进 #

OTP 26 改进了去年引入的 JIT 中基于类型的优化,但最明显的改进是使用位语法匹配和构造二进制文件。这些改进与 base64 模块本身的变化相结合,使得编码为 Base64 的速度快了约 4 倍,而从 Base64 解码的速度快了 3 倍以上。

有关这些改进的更多详细信息,请参见博客文章 编译器和 JIT 中的更多优化

这里值得一提的还有重新引入了在 OTP 24 中引入 JIT 时丢失的优化

erts:重新引入字面函数优化

事实证明,此优化对于 jason 库很重要。没有它,JSON 解码速度会慢 10%

Dialyzer 的增量模式 #

Dialyzer 有一个由 Tom Davies 实现的新的增量模式。当对代码库进行少量更改时,增量模式可以大大加快分析速度。

让我们直接进入一个示例。假设我们想为 stdlib 应用程序准备一个拉取请求,以下是我们如何使用 Dialyzer 的增量模式来显示 stdlib 中任何问题的警告

$ dialyzer --incremental --apps erts kernel stdlib compiler crypto --warning_apps stdlib
Proceeding with incremental analysis... done in 0m14.91s
done (passed successfully)

让我们分解命令行

  • --incremental 选项告诉 Dialyzer 使用增量模式。

  • --warning_apps stdlib 列出了我们想要警告的应用程序。在本例中,它是 stdlib 应用程序。

  • --apps erts kernel stdlib compiler crypto 选项列出了应该分析但不会生成任何警告的应用程序。

Dialyzer 分析了 --apps--warning_apps 选项给出的所有模块。在我的计算机上,分析大约在 15 秒内完成。

如果我立即使用相同的参数运行 Dialyzer,它几乎立即完成,因为没有任何更改

$ dialyzer --incremental --warning_apps stdlib --apps erts kernel stdlib compiler crypto
done (passed successfully)

如果我对 lists 模块进行任何更改(例如,添加一个新函数),Dialyzer 将重新分析所有直接或间接依赖于它的模块。

$ dialyzer --incremental --warning_apps stdlib --apps erts kernel stdlib compiler crypto
There have been changes to analyze
    Of the 270 files being tracked, 1 have been changed or removed,
    resulting in 270 requiring analysis because they depend on those changes
Proceeding with incremental analysis... done in 0m14.95s
done (passed successfully)

结果发现,被分析的应用程序中的所有模块都直接或间接地依赖于 lists 模块。

如果我更改 base64 模块中的内容,重新分析的速度会快得多,因为依赖关系更少。

$ dialyzer --incremental --warning_apps stdlib --apps erts kernel stdlib compiler crypto
There have been changes to analyze
    Of the 270 files being tracked, 1 have been changed or removed,
    resulting in 3 requiring analysis because they depend on those changes
Proceeding with incremental analysis... done in 0m1.07s
done (passed successfully)

在这种情况下,只需要重新分析三个模块,大约在一秒钟内完成。

使用 dialyzer.config 文件 #

请注意,以上所有示例都使用了相同的命令行。

当以增量模式运行 Dialyzer 时,每次调用 Dialyzer 时都必须提供要分析的应用程序列表和要生成警告的应用程序列表。

为了避免在命令行中提供应用程序列表,可以将它们放入名为 dialyzer.config 的配置文件中。要查找 Dialyzer 将在哪个目录中查找配置文件,请运行以下命令

$ dialyzer --help
  .
  .
  .
Configuration file:
     Dialyzer's configuration file may also be used to augment the default
     options and those given directly to the Dialyzer command. It is commonly
     used to avoid repeating options which would otherwise need to be given
     explicitly to Dialyzer on every invocation.

     The location of the configuration file can be set via the
     DIALYZER_CONFIG environment variable, and defaults to
     within the user_config location given by filename:basedir/3.

     On your system, the location is currently configured as:
       /Users/bjorng/Library/Application Support/erlang/dialyzer.config

     An example configuration file's contents might be:

       {incremental,
         {default_apps,[stdlib,kernel,erts]},
         {default_warning_apps,[stdlib]}
       }.
       {warnings, [no_improper_lists]}.
       {add_pathsa,["/users/samwise/potatoes/ebin"]}.
       {add_pathsz,["/users/smeagol/fish/ebin"]}.

  .
  .
  .

接近结尾处有关于配置文件的信息以及 Dialyzer 将在哪里查找它。

为了缩短先前示例的命令行,可以将以下术语存储在 dialyzer.config

{incremental,
 {default_apps, [erts,kernel,stdlib,compiler,crypto]},
 {default_warning_apps, [stdlib]}
}.

现在,只需向 Dialyzer 提供 --incremental 选项即可

$ dialyzer --incremental
done (passed successfully)

在 PropER 上运行 Dialyzer #

作为最后一个示例,让我们在 PropER 上运行 Dialyzer。

为此,必须将配置文件中的 default_warnings_apps 选项更改为 proper。还需要添加 add_pathsa 选项,将 proper 应用程序的路径添加到代码路径的前面。

{incremental,
 {default_apps, [erts,kernel,stdlib,compiler,crypto]},
 {default_warning_apps, [proper]}
}.
{add_pathsa, ["/Users/bjorng/git/proper/_build/default/lib/proper"]}.

运行 Dialyzer

$ dialyzer --incremental
There have been changes to analyze
    Of the 296 files being tracked,
    26 have been changed or removed,
    resulting in 26 requiring analysis because they depend on those changes
Proceeding with incremental analysis...
proper.erl:2417:13: Unknown function cover:start/1
proper.erl:2426:13: Unknown function cover:stop/1
proper_symb.erl:249:9: Unknown function erl_syntax:atom/1
proper_symb.erl:250:5: Unknown function erl_syntax:revert/1
proper_symb.erl:250:23: Unknown function erl_syntax:application/3
proper_symb.erl:257:51: Unknown function erl_syntax:nil/0
proper_symb.erl:259:49: Unknown function erl_syntax:cons/2
proper_symb.erl:262:5: Unknown function erl_syntax:revert/1
proper_symb.erl:262:23: Unknown function erl_syntax:tuple/1
 done in 0m2.36s
done (warnings were emitted)

Dialyzer 发现了 26 个新文件要分析(proper 应用程序中的 BEAM 文件)。这些文件大约在两秒半内分析完成。

Dialyzer 发出了未知函数的警告,因为 proper 调用了未被分析的应用程序中的函数。要消除这些警告,可以将 toolssyntax_tools 应用程序添加到 default_apps 列表中的应用程序列表中。

{incremental,
 {default_apps, [erts,kernel,stdlib,compiler,crypto,tools,syntax_tools]},
 {default_warning_apps, [proper]}
}.
{add_pathsa, ["/Users/bjorng/git/proper/_build/default/lib/proper"]}.

修改配置文件后,将不再打印警告。

$ dialyzer --incremental
There have been changes to analyze
    Of the 319 files being tracked,
    23 have been changed or removed,
    resulting in 38 requiring analysis because they depend on those changes
Proceeding with incremental analysis... done in 0m6.47s

也可以在配置文件中包含警告选项,例如禁用非正确列表的警告或启用不匹配返回值的警告。让我们启用不匹配返回值的警告

{incremental,
 {default_apps, [erts,kernel,stdlib,compiler,crypto,tools,syntax_tools]},
 {default_warning_apps, [proper]}
}.
{warnings, [unmatched_returns]}.
{add_pathsa, ["/Users/bjorng/git/proper/_build/default/lib/proper"]}.

当警告选项更改时,Dialyzer 将重新分析所有模块。

$ dialyzer --incremental
PLT was built for a different set of enabled warnings,
so an analysis must be run for 319 modules to rebuild it
Proceeding with incremental analysis... done in 0m19.43s
done (passed successfully)

拉取请求 #

dialyzer:添加增量分析模式

argparse:Erlang 的命令行解析器 #

OTP 26 中的新功能是 argparse 模块,它简化了 escripts 中命令行的解析。argparse 由 Maxim Fedorov 实现。

为了展示一些功能,让我们为名为 ehead 的 escript 实现命令行解析,该 escript 的灵感来自于 Unix 命令 head

#!/usr/bin/env escript
%% -*- erlang -*-

main(Args) ->
    argparse:run(Args, cli(), #{progname => ehead}).

cli() ->
    #{
      arguments =>
          [#{name => lines, type => {integer, [{min, 1}]},
             short => $n, long => "-lines", default => 10,
             help => "number of lines to print"},
           #{name => files, nargs => nonempty_list, action => extend,
             help => "lists of files"}],
      handler => fun(Args) ->
                         io:format("~p\n", [Args])
                 end
     }.

目前编写的 ehead 脚本将简单地打印 argparse 收集的参数并退出。

如果 ehead 在没有任何参数的情况下运行,则会显示错误消息

$ ehead
error: ehead: required argument missing: files
Usage:
  ehead [-n <lines>] [--lines <lines>] <files>...

Arguments:
  files       lists of files

Optional arguments:
  -n, --lines number of lines to print (int >= 1, 10)

该消息告诉我们必须至少提供一个文件名。

$ ehead foo bar baz
#{lines => 10,files => ["foo","bar","baz"]}

由于命令行有效,argparse 将参数收集到一个 map 中,然后由 handler 函数打印出来。

要从每个文件中打印的行数默认为 10,但可以使用 -n--lines 选项进行更改。

$ ehead -n 42 foo bar baz
#{lines => 42,files => ["foo","bar","baz"]}
$ ehead foo --lines=42 bar baz
#{lines => 42,files => ["foo","bar","baz"]}
$ ehead --lines 42 foo bar baz
#{lines => 42,files => ["foo","bar","baz"]}
$ ehead foo bar --lines 42 baz
#{lines => 42,files => ["foo","bar","baz"]}

尝试将行数设置为 0 会导致错误消息。

$ ehead -n 0 foobar
error: ehead: invalid argument for lines: 0 is less than accepted minimum
Usage:
  ehead [-n <lines>] [--lines <lines>] <files>...

Arguments:
  files       lists of files

Optional arguments:
  -n, --lines number of lines to print (int >= 1, 10)

拉取请求 #

[argparse] Erlang 的命令行解析器

SSL:更安全的默认值 #

在 OTP 25 中,ssl:connect/3 的默认选项允许建立连接而不验证服务器的真实性(即,不检查服务器的证书链)。例如

Erlang/OTP 25 . . .

Eshell V13.1.1  (abort with ^G)
1> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
2> ssl:connect("www.erlang.org", 443, []).
=WARNING REPORT==== 6-Apr-2023::12:29:20.824457 ===
Description: "Authenticity is not established by certificate path validation"
     Reason: "Option {verify, verify_peer} and cacertfile/cacerts is missing"

{ok,{sslsocket,{gen_tcp,#Port<0.6>,tls_connection,undefined},
               [<0.122.0>,<0.121.0>]}}

会生成警告报告,但会建立连接。

在 OTP 26 中,verify 选项的默认值现在是 verify_peer 而不是 verify_none。主机验证需要使用 cacertscacertsfile 选项之一提供受信任的 CA 证书。因此,在 OTP 26 中,使用空选项列表进行连接尝试将失败。

Erlang/OTP 26 . . .

Eshell V14.0 (press Ctrl+G to abort, type help(). for help)
1> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
2> ssl:connect("www.erlang.org", 443, []).
{error,{options,incompatible,
                [{verify,verify_peer},{cacerts,undefined}]}}

cacerts 选项的默认值为 undefined,这与 {verify,verify_peer} 选项不兼容。

为了使连接成功,建议使用 cacerts 选项来提供用于验证的 CA 证书。例如

1> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
2> ssl:connect("www.erlang.org", 443, [{cacerts, public_key:cacerts_get()}]).
{ok,{sslsocket,{gen_tcp,#Port<0.5>,tls_connection,undefined},
               [<0.137.0>,<0.136.0>]}}

或者,可以显式禁用主机验证。例如

1> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
2> ssl:connect("www.erlang.org", 443, [{verify,verify_none}]).
{ok,{sslsocket,{gen_tcp,#Port<0.6>,tls_connection,undefined},
               [<0.143.0>,<0.142.0>]}}

OTP 26 更安全的另一种方式是,默认情况下不再允许 SHA1 和 DSA 等传统算法。

SSL:改进的选项检查 #

在 OTP 26 中,选项检查得到加强,对于以前被默默忽略的不正确选项会返回错误。例如,如果将 fail_if_no_peer_cert 选项用于客户端,ssl 现在会拒绝它。

1> application:ensure_all_started(ssl).
{ok,[crypto,asn1,public_key,ssl]}
2> ssl:connect("www.erlang.org", 443, [{fail_if_no_peer_cert, true}, {verify, verify_peer}, {cacerts, public_key:cacerts_get()}]).
{error,{option,server_only,fail_if_no_peer_cert}}

在 OTP 25 中,该选项将被默默忽略。

OTP 26 中的 ssl 还返回更清晰的错误原因。在前一节的示例中,显示了以下连接尝试

2> ssl:connect("www.erlang.org", 443, []).
{error,{options,incompatible,
                [{verify,verify_peer},{cacerts,undefined}]}}

在 OTP 25 中,相应的错误返回不太清晰。

2> ssl:connect("www.erlang.org", 443, [{verify,verify_peer}]).
{error,{options,{cacertfile,[]}}}