查看源码 套接字使用
简介
套接字接口(模块)基本上是操作系统套接字接口之上的一个“薄”层。假设除非您有特殊需求,否则 gen_[tcp|udp|sctp] 应该足够(当它们可用时)。
请注意,仅仅因为我们有一个已记录和描述的选项,并不意味着操作系统支持它。因此,建议用户阅读所使用选项的特定于平台的文档。
异步调用
某些函数允许异步调用(accept/2
、connect/3
、recv/3,4
、recvfrom/3,4
、recvmsg/2,3,5
、send/3,4
、sendmsg/3,4
和 sendto/4,5
)。这是通过将 Timeout
参数设置为 nowait
来实现的。例如,如果使用设置为 nowait
的 Timeout 调用 recv/3
函数(即 recv(Sock, 0, nowait)
),而实际上没有任何内容可读,它将返回
在 Unix 上 -
{select,
SelectInfo
}
SelectInfo
包含SelectHandle
。在 Windows 上 -
{completion,
CompletionInfo
}
CompletionInfo
包含CompletionHandle
。
当数据最终到达时,将向调用者发送 'select' 或 'completion' 消息
在 Unix 上 -
{'$socket', socket(), select, SelectHandle}
然后,调用者可以再次调用 recv 函数,并期望现在有数据。
请注意,所有其他用户都将被锁定,直到“当前用户”调用该函数(本例中为 recv)。因此,要么立即调用该函数,要么使用
cancel
取消。在 Windows 上 -
{'$socket', socket(), completion, {CompletionHandle, CompletionStatus}}
CompletionStatus
包含操作(读取)的结果。
用户还必须准备好接收中止消息
{'$socket', socket(), abort, Info}
如果操作因任何原因中止(例如,如果套接字被“其他人”关闭)。Info
部分包含中止原因(在这种情况下,套接字已关闭 Info = {SelectHandle, closed}
)。
'socket' 消息的通用形式是
{'$socket', Sock :: socket(), Tag :: atom(), Info :: term()}
其中 Info
的格式是 Tag
的函数
标记 | 信息值类型 |
---|---|
选择 | select_handle() |
完成 | {completion_handle(), CompletionStatus} |
中止 | {select_handle(), Reason :: term()} |
表格:套接字消息信息值类型
select_handle()
与 SelectInfo
中返回的相同。
completion_handle()
与 CompletionInfo
中返回的相同。
套接字注册表
套接字注册表是我们跟踪套接字的方式。可以使用两个函数进行交互:socket:number_of/0
和 socket:which_sockets/1
。
在动态创建和删除许多套接字的系统中,它(套接字注册表)可能会成为瓶颈。对于此类系统,有几种方法可以控制套接字注册表的使用。
首先,可以使用两个配置选项,在从源代码构建 OTP 时影响全局默认值
--enable-esock-socket-registry (default) | --disable-esock-socket-registry
其次,可以通过在启动 erlang 之前设置环境变量 ESOCK_USE_SOCKET_REGISTRY
(布尔值)来影响全局默认值。
第三,可以通过调用函数 use_registry/1
在运行时更改全局默认值。
最后,可以在创建套接字时(使用 open/2
和 open/4
)通过在其 Opts
参数中提供属性 use_registry
(布尔值)来覆盖全局默认值(这会影响该特定套接字)。
示例
此示例旨在展示如何创建简单的(回显)服务器(和客户端)。
-module(example).
-export([client/2, client/3]).
-export([server/0, server/1, server/2]).
%% ======================================================================
%% === Client ===
client(#{family := Family} = ServerSockAddr, Msg)
when is_list(Msg) orelse is_binary(Msg) ->
{ok, Sock} = socket:open(Family, stream, default),
ok = maybe_bind(Sock, Family),
ok = socket:connect(Sock, ServerSockAddr),
client_exchange(Sock, Msg);
client(ServerPort, Msg)
when is_integer(ServerPort) andalso (ServerPort > 0) ->
Family = inet, % Default
Addr = get_local_addr(Family), % Pick an address
SockAddr = #{family => Family,
addr => Addr,
port => ServerPort},
client(SockAddr, Msg).
client(ServerPort, ServerAddr, Msg)
when is_integer(ServerPort) andalso (ServerPort > 0) andalso
is_tuple(ServerAddr) ->
Family = which_family(ServerAddr),
SockAddr = #{family => Family,
addr => ServerAddr,
port => ServerPort},
client(SockAddr, Msg).
%% Send the message to the (echo) server and wait for the echo to come back.
client_exchange(Sock, Msg) when is_list(Msg) ->
client_exchange(Sock, list_to_binary(Msg));
client_exchange(Sock, Msg) when is_binary(Msg) ->
ok = socket:send(Sock, Msg, infinity),
{ok, Msg} = socket:recv(Sock, byte_size(Msg), infinity),
ok.
%% ======================================================================
%% === Server ===
server() ->
%% Make system choose port (and address)
server(0).
%% This function return the port and address that it actually uses,
%% in case server/0 or server/1 (with a port number) was used to start it.
server(#{family := Family, addr := Addr, port := _} = SockAddr) ->
{ok, Sock} = socket:open(Family, stream, tcp),
ok = socket:bind(Sock, SockAddr),
ok = socket:listen(Sock),
{ok, #{port := Port}} = socket:sockname(Sock),
Acceptor = start_acceptor(Sock),
{ok, {Port, Addr, Acceptor}};
server(Port) when is_integer(Port) ->
Family = inet, % Default
Addr = get_local_addr(Family), % Pick an address
SockAddr = #{family => Family,
addr => Addr,
port => Port},
server(SockAddr).
server(Port, Addr)
when is_integer(Port) andalso (Port >= 0) andalso
is_tuple(Addr) ->
Family = which_family(Addr),
SockAddr = #{family => Family,
addr => Addr,
port => Port},
server(SockAddr).
%% --- Echo Server - Acceptor ---
start_acceptor(LSock) ->
Self = self(),
{Pid, MRef} = spawn_monitor(fun() -> acceptor_init(Self, LSock) end),
receive
{'DOWN', MRef, process, Pid, Info} ->
erlang:error({failed_starting_acceptor, Info});
{Pid, started} ->
%% Transfer ownership
socket:setopt(LSock, otp, owner, Pid),
Pid ! {self(), continue},
erlang:demonitor(MRef),
Pid
end.
acceptor_init(Parent, LSock) ->
Parent ! {self(), started},
receive
{Parent, continue} ->
ok
end,
acceptor_loop(LSock).
acceptor_loop(LSock) ->
case socket:accept(LSock, infinity) of
{ok, ASock} ->
start_handler(ASock),
acceptor_loop(LSock);
{error, Reason} ->
erlang:error({accept_failed, Reason})
end.
%% --- Echo Server - Handler ---
start_handler(Sock) ->
Self = self(),
{Pid, MRef} = spawn_monitor(fun() -> handler_init(Self, Sock) end),
receive
{'DOWN', MRef, process, Pid, Info} ->
erlang:error({failed_starting_handler, Info});
{Pid, started} ->
%% Transfer ownership
socket:setopt(Sock, otp, owner, Pid),
Pid ! {self(), continue},
erlang:demonitor(MRef),
Pid
end.
handler_init(Parent, Sock) ->
Parent ! {self(), started},
receive
{Parent, continue} ->
ok
end,
handler_loop(Sock, undefined).
%% No "ongoing" reads
%% The use of 'nowait' here is clearly *overkill* for this use case,
%% but is intended as an example of how to use it.
handler_loop(Sock, undefined) ->
case socket:recv(Sock, 0, nowait) of
{ok, Data} ->
echo(Sock, Data),
handler_loop(Sock, undefined);
{select, SelectInfo} ->
handler_loop(Sock, SelectInfo);
{completion, CompletionInfo} ->
handler_loop(Sock, CompletionInfo);
{error, Reason} ->
erlang:error({recv_failed, Reason})
end;
%% This is the standard (asyncronous) behaviour.
handler_loop(Sock, {select_info, recv, SelectHandle}) ->
receive
{'$socket', Sock, select, SelectHandle} ->
case socket:recv(Sock, 0, nowait) of
{ok, Data} ->
echo(Sock, Data),
handler_loop(Sock, undefined);
{select, NewSelectInfo} ->
handler_loop(Sock, NewSelectInfo);
{error, Reason} ->
erlang:error({recv_failed, Reason})
end
end;
%% This is the (asyncronous) behaviour on platforms that support 'completion',
%% currently only Windows.
handler_loop(Sock, {completion_info, recv, CompletionHandle}) ->
receive
{'$socket', Sock, completion, {CompletionHandle, CompletionStatus}} ->
case CompletionStatus of
{ok, Data} ->
echo(Sock, Data),
handler_loop(Sock, undefined);
{error, Reason} ->
erlang:error({recv_failed, Reason})
end
end.
echo(Sock, Data) when is_binary(Data) ->
ok = socket:send(Sock, Data, infinity),
io:format("** ECHO **"
"~n~s~n", [binary_to_list(Data)]).
%% ======================================================================
%% === Utility functions ===
maybe_bind(Sock, Family) ->
maybe_bind(Sock, Family, os:type()).
maybe_bind(Sock, Family, {win32, _}) ->
Addr = get_local_addr(Family),
SockAddr = #{family => Family,
addr => Addr,
port => 0},
socket:bind(Sock, SockAddr);
maybe_bind(_Sock, _Family, _OS) ->
ok.
%% The idea with this is extract a "usable" local address
%% that can be used even from *another* host. And doing
%% so using the net module.
get_local_addr(Family) ->
Filter =
fun(#{addr := #{family := Fam},
flags := Flags}) ->
(Fam =:= Family) andalso (not lists:member(loopback, Flags));
(_) ->
false
end,
{ok, [SockAddr|_]} = net:getifaddrs(Filter),
#{addr := #{addr := Addr}} = SockAddr,
Addr.
which_family(Addr) when is_tuple(Addr) andalso (tuple_size(Addr) =:= 4) ->
inet;
which_family(Addr) when is_tuple(Addr) andalso (tuple_size(Addr) =:= 8) ->
inet6.
套接字选项
级别 otp
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
assoc_id | integer() | 否 | 是 | type = seqpacket,protocol = sctp,是一个关联 |
调试 | boolean() | 是 | 是 | 无 |
iow | boolean() | 是 | 是 | 无 |
controlling_process | pid() | 是 | 是 | 无 |
rcvbuf | default | pos_integer() | {pos_integer(), pos_ineteger()} | 是 | 是 | 元组格式在 Windows 上不允许。'default' 仅对设置有效。元组形式仅对类型 'stream' 和协议 'tcp' 有效。 |
rcvctrlbuf | default | pos_integer() | 是 | 是 | default 仅对设置有效 |
sndctrlbuf | default | pos_integer() | 是 | 是 | default 仅对设置有效 |
fd | integer() | 否 | 是 | 无 |
use_registry | boolean() | 否 | 是 | 该值在创建套接字时通过调用 open/2 或 open/4 设置。 |
表格:选项级别
级别 socket
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
acceptconn | boolean() | 否 | 是 | 无 |
bindtodevice | string() | 是 | 是 | 在 Linux 3.8 之前,可以设置此套接字选项,但无法获取。仅适用于某些套接字类型(例如,inet )。如果设置为空值,则会删除绑定。 |
broadcast | boolean() | 是 | 是 | type = dgram |
bsp_state | map() | 否 | 是 | 仅限 Windows |
调试 | integer() | 是 | 是 | 可能需要管理员权限 |
domain | domain() | 否 | 是 | 例如,在 FreeBSD 上不适用 |
dontroute | boolean() | 是 | 是 | 无 |
exclusiveaddruse | boolean() | 是 | 是 | 仅限 Windows |
keepalive | boolean() | 是 | 是 | 无 |
linger | abort | linger() | 是 | 是 | 无 |
maxdg | integer() | 否 | 是 | 仅限 Windows |
max_msg_size | integer() | 否 | 是 | 仅限 Windows |
oobinline | boolean() | 是 | 是 | 无 |
peek_off | integer() | 是 | 是 | domain = local (unix)。目前由于第二次调用 recv([peek]) 时可能出现的无限循环而被禁用。 |
priority | integer() | 是 | 是 | 无 |
protocol | protocol() | 否 | 是 | 例如,在(某些)Darwin 上不适用 |
rcvbuf | non_neg_integer() | 是 | 是 | 无 |
rcvlowat | non_neg_integer() | 是 | 是 | 无 |
rcvtimeo | timeval() | 是 | 是 | 此选项通常不受支持(请参阅下面的原因)。OTP 必须使用 --enable-esock-rcvsndtime 配置选项显式构建才能使用。由于我们的实现是非阻塞的,因此不清楚此选项是否有效以及如何工作,甚至是否可能导致故障。因此,我们不建议设置此选项。相反,请使用 Timeout 参数,例如 recv/3 函数。 |
reuseaddr | boolean() | 是 | 是 | 无 |
reuseport | boolean() | 是 | 是 | domain = inet | inet6 |
sndbuf | non_neg_integer() | 是 | 是 | 无 |
sndlowat | non_neg_integer() | 是 | 是 | 在 Linux 上不可更改 |
sndtimeo | timeval() | 是 | 是 | 此选项通常不受支持(请参阅下面的原因)。OTP 必须使用 --enable-esock-rcvsndtime 配置选项显式构建才能使用。由于我们的实现是非阻塞的,因此不清楚此选项是否有效以及如何工作,甚至是否可能导致故障。因此,我们不建议设置此选项。相反,请使用 Timeout 参数,例如 send/3 函数。 |
timestamp | boolean() | 是 | 是 | 无 |
type | type() | 否 | 是 | 无 |
表格:套接字选项
级别 ip
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
add_membership | ip_mreq() | 是 | 否 | 无 |
add_source_membership | ip_mreq_source() | 是 | 否 | 无 |
block_source | ip_mreq_source() | 是 | 否 | 无 |
drop_membership | ip_mreq() | 是 | 否 | 无 |
drop_source_membership | ip_mreq_source() | 是 | 否 | 无 |
freebind | boolean() | 是 | 是 | 无 |
hdrincl | boolean() | 是 | 是 | type = raw |
minttl | integer() | 是 | 是 | type = raw |
msfilter | null | ip_msfilter() | 是 | 否 | 无 |
mtu | integer() | 否 | 是 | type = raw |
mtu_discover | ip_pmtudisc() | 是 | 是 | 无 |
multicast_all | boolean() | 是 | 是 | 无 |
multicast_if | any | ip4_address() | 是 | 是 | 无 |
multicast_loop | boolean() | 是 | 是 | 无 |
multicast_ttl | uint8() | 是 | 是 | 无 |
nodefrag | boolean() | 是 | 是 | type = raw |
pktinfo | boolean() | 是 | 是 | type = dgram |
recvdstaddr | boolean() | 是 | 是 | type = dgram |
recverr | boolean() | 是 | 是 | 无 |
recvif | boolean() | 是 | 是 | type = dgram | raw |
recvopts | boolean() | 是 | 是 | type =/= stream |
recvorigdstaddr | boolean() | 是 | 是 | 无 |
recvttl | boolean() | 是 | 是 | type =/= stream |
retopts | boolean() | 是 | 是 | type =/= stream |
router_alert | integer() | 是 | 是 | type = raw |
sendsrcaddr | boolean() | 是 | 是 | 无 |
tos | ip_tos() | 是 | 是 | 某些高优先级级别可能需要超级用户权限 |
transparent | boolean() | 是 | 是 | 需要管理员权限 |
ttl | integer() | 是 | 是 | 无 |
unblock_source | ip_mreq_source() | 是 | 否 | 无 |
表格:ip 选项
级别 ipv6
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
addrform | inet | 是 | 否 | 仅允许用于已连接并绑定到 v4 映射到 v6 地址的 IPv6 套接字 |
add_membership | ipv6_mreq() | 是 | 否 | 无 |
authhdr | boolean() | 是 | 是 | type = dgram | raw,已过时? |
drop_membership | ipv6_mreq() | 是 | 否 | 无 |
dstopts | boolean() | 是 | 是 | type = dgram | raw,需要超级用户权限才能更新 |
flowinfo | boolean() | 是 | 是 | type = dgram | raw,需要超级用户权限才能更新 |
hoplimit | boolean() | 是 | 是 | type = dgram | raw。在某些平台(例如 FreeBSD)上,用于设置以获取 hoplimit 作为控制消息标头。在其他平台(例如 Linux)上,设置 recvhoplimit 以获取 hoplimit 。 |
hopopts | boolean() | 是 | 是 | type = dgram | raw,需要超级用户权限才能更新 |
mtu | boolean() | 是 | 是 | 获取:仅在套接字连接后 |
mtu_discover | ipv6_pmtudisc() | 是 | 是 | 无 |
multicast_hops | default | uint8() | 是 | 是 | 无 |
multicast_if | integer() | 是 | 是 | type = dgram | raw |
multicast_loop | boolean() | 是 | 是 | 无 |
recverr | boolean() | 是 | 是 | 无 |
recvhoplimit | boolean() | 是 | 是 | 类型 = dgram | raw。在某些平台(例如 Linux)上,设置 recvhoplimit 以获取 hoplimit 。 |
recvpktinfo | pktinfo | boolean() | 是 | 是 | type = dgram | raw。在某些平台(例如 FreeBSD)上,用于设置以获取 hoplimit 作为控制消息标头。在其他平台(例如 Linux)上,设置 recvhoplimit 以获取 hoplimit 。 |
recvtclass | boolean() | 是 | 是 | 类型 = dgram | raw。在某些平台上,设置为 (=true) 以获取 tclass 控制消息头。在其他平台上,设置 tclass 以获取 tclass 控制消息头。 |
router_alert | integer() | 是 | 是 | type = raw |
rthdr | boolean() | 是 | 是 | type = dgram | raw,需要超级用户权限才能更新 |
tclass | integer() | 是 | 是 | 设置与传出数据包关联的流量类别。RFC3542。 |
unicast_hops | default | uint8() | 是 | 是 | 无 |
v6only | boolean() | 是 | 否 | 无 |
表:ipv6 选项
级别 tcp
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
congestion | string() | 是 | 是 | 无 |
cork | boolean() | 是 | 是 | 在某些平台(FreeBSD)上为 'nopush' |
keepcnt | integer() | 是 | 是 | 在 Windows(至少)上,设置为大于 255 的值是非法的。 |
keepidle | integer() | 是 | 是 | 无 |
keepintvl | integer() | 是 | 是 | 无 |
maxseg | integer() | 是 | 是 | 并非所有平台都允许设置。 |
nodelay | boolean() | 是 | 是 | 无 |
nopush | boolean() | 是 | 是 | 在某些平台(Linux)上为 'cork'。在 Darwin 上,它的含义与 FreeBSD 等平台不同。 |
表:tcp 选项
级别 udp
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
cork | boolean() | 是 | 是 | 无 |
表:udp 选项
级别 sctp
的选项
选项名称 | 值类型 | 设置 | 获取 | 其他要求和注释 |
---|---|---|---|---|
associnfo | sctp_assocparams() | 是 | 是 | 无 |
autoclose | non_neg_integer() | 是 | 是 | 无 |
disable_fragments | boolean() | 是 | 是 | 无 |
events | sctp_event_subscribe() | 是 | 否 | 无 |
initmsg | sctp_initmsg() | 是 | 是 | 无 |
maxseg | non_neg_integer() | 是 | 是 | 无 |
nodelay | boolean() | 是 | 是 | 无 |
rtoinfo | sctp_rtoinfo() | 是 | 是 | 无 |
表:sctp 选项