檢視原始碼 Socket 使用
簡介
socket 介面 (模組) 基本上是 OS socket 介面之上的一個「薄」層。除非您有特殊需求,否則假設 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}
如果由於任何原因中止操作 (例如,如果 socket 被「其他人」關閉)。Info
部分包含中止原因 (在此情況下為 socket 已關閉 Info = {SelectHandle, closed}
)。
'socket' 訊息的一般形式為
{'$socket', Sock :: socket(), Tag :: atom(), Info :: term()}
其中 Info
的格式是 Tag
的函式
標籤 | Info 值類型 |
---|---|
select | select_handle() |
completion | {completion_handle(), CompletionStatus} |
abort | {select_handle(), Reason :: term()} |
表格:socket 訊息 info 值類型
select_handle()
與 SelectInfo
中傳回的相同。
completion_handle()
與 CompletionInfo
中傳回的相同。
Socket 註冊表
socket 註冊表是我們追蹤 socket 的方式。有兩個函式可用於互動:socket:number_of/0
和 socket:which_sockets/1
。
在動態建立和刪除許多 socket 的系統中,它 (socket 註冊表) 可能會成為瓶頸。對於此類系統,有幾種方法可以控制 socket 註冊表的使用。
首先,可以使用兩個設定選項在從原始碼建置 OTP 時影響全域預設值
--enable-esock-socket-registry (default) | --disable-esock-socket-registry
第二,可以在啟動 erlang 之前設定環境變數 ESOCK_USE_SOCKET_REGISTRY
(布林值) 來影響全域預設值。
第三,可以透過呼叫函式 use_registry/1
在執行階段變更全域預設值。
最後,可以在建立 socket 時 (使用 open/2
和 open/4
) 透過在其 Opts
參數中提供屬性 use_registry
(布林值) 來覆寫全域預設值 (這會影響該特定 socket)。
範例
此範例旨在展示如何建立簡單的 (echo) 伺服器 (和用戶端)。
-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.
Socket 選項
層級 otp
的選項
選項名稱 | 值類型 | 設定 | 取得 | 其他需求和註解 |
---|---|---|---|---|
assoc_id | integer() | 否 | 是 | type = seqpacket,protocol = sctp,是一個關聯 |
debug | 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() | 否 | 是 | 該值在建立 socket 時,透過呼叫 open/2 或 open/4 來設定。 |
表格:選項層級
層級 socket
的選項
選項名稱 | 值類型 | 設定 | 取得 | 其他需求和註解 |
---|---|---|---|---|
acceptconn | boolean() | 否 | 是 | 無 |
bindtodevice | string() | 是 | 是 | 在 Linux 3.8 之前,可以設定此 socket 選項,但不能取得。僅適用於某些 socket 類型 (例如 inet )。如果設定空值,則會移除繫結。 |
broadcast | boolean() | 是 | 是 | type = dgram |
bsp_state | map() | 否 | 是 | 僅限 Windows |
debug | 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() | 是 | 是 | 通常不支援此選項 (請參閱以下原因)。必須使用 --enable-esock-rcvsndtime 設定選項明確建置 OTP,才能使其可用。由於我們的實作是非阻塞的,因此不知道此選項是否有效,甚至是否可能導致故障。因此,我們不建議設定此選項。請改為使用 recv/3 函式的 Timeout 參數。 |
reuseaddr | boolean() | 是 | 是 | 無 |
reuseport | boolean() | 是 | 是 | domain = inet | inet6 |
sndbuf | non_neg_integer() | 是 | 是 | 無 |
sndlowat | non_neg_integer() | 是 | 是 | 在 Linux 上不可變更 |
sndtimeo | timeval() | 是 | 是 | 通常不支援此選項 (請參閱以下原因)。必須使用 --enable-esock-rcvsndtime 設定選項明確建置 OTP,才能使其可用。由於我們的實作是非阻塞的,因此不知道此選項是否有效,甚至是否可能導致故障。因此,我們不建議設定此選項。請改為使用 send/3 函式的 Timeout 參數。 |
timestamp | boolean() | 是 | 是 | 無 |
type | type() | 否 | 是 | 無 |
表格:socket 選項
層級 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 socket |
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() | 是 | 是 | 取得:僅在 socket 連線後 |
mtu_discover | ipv6_pmtudisc() | 是 | 是 | 無 |
multicast_hops | default | uint8() | 是 | 是 | 無 |
multicast_if | integer() | 是 | 是 | type = dgram | raw |
multicast_loop | boolean() | 是 | 是 | 無 |
recverr | boolean() | 是 | 是 | 無 |
recvhoplimit | boolean() | 是 | 是 | type = dgram | raw。在某些平台 (例如 Linux) 上,設定 recvhoplimit 以取得 hoplimit |
recvpktinfo | pktinfo | boolean() | 是 | 是 | type = dgram | raw。在某些平台 (例如 FreeBSD) 上,用於設定以取得 hoplimit 作為控制訊息標頭。在其他平台 (例如 Linux) 上,設定 recvhoplimit 以取得 hoplimit 。 |
recvtclass | boolean() | 是 | 是 | type = 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 選項