檢視原始碼 Socket 使用

簡介

socket 介面 (模組) 基本上是 OS socket 介面之上的一個「薄」層。除非您有特殊需求,否則假設 gen_[tcp|udp|sctp] 應該就足夠了 (當它們可用時)。

請注意,僅僅因為我們有文件記錄和描述的選項,並不代表作業系統支援它。因此建議使用者閱讀所使用選項的特定平台文件。

非同步呼叫

某些函式允許非同步呼叫 (accept/2connect/3recv/3,4recvfrom/3,4recvmsg/2,3,5send/3,4sendmsg/3,4sendto/4,5)。這是透過將 Timeout 參數設定為 nowait 來實現的。例如,如果使用設定為 nowait 的 Timeout 呼叫 recv/3 函式 (即 recv(Sock, 0, nowait)),而實際上沒有任何資料可讀,它將返回

當資料最終到達時,將會向呼叫者傳送 '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 值類型
selectselect_handle()
completion{completion_handle(), CompletionStatus}
abort{select_handle(), Reason :: term()}

表格:socket 訊息 info 值類型

select_handle()SelectInfo 中傳回的相同。

completion_handle()CompletionInfo 中傳回的相同。

Socket 註冊表

socket 註冊表是我們追蹤 socket 的方式。有兩個函式可用於互動:socket:number_of/0socket: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/2open/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_idinteger()type = seqpacket,protocol = sctp,是一個關聯
debugboolean()
iowboolean()
controlling_processpid()
rcvbufdefault | pos_integer() | {pos_integer(), pos_ineteger()}在 Windows 上允許使用元組格式。 'default' 僅對設定有效。元組形式僅對類型 'stream' 和協定 'tcp' 有效。
rcvctrlbufdefault | pos_integer()default 僅對設定有效
sndctrlbufdefault | pos_integer()default 僅對設定有效
fdinteger()
use_registryboolean()該值在建立 socket 時,透過呼叫 open/2open/4 來設定。

表格:選項層級

層級 socket 的選項

選項名稱值類型設定取得其他需求和註解
acceptconnboolean()
bindtodevicestring()在 Linux 3.8 之前,可以設定此 socket 選項,但不能取得。僅適用於某些 socket 類型 (例如 inet)。如果設定空值,則會移除繫結。
broadcastboolean()type = dgram
bsp_statemap()僅限 Windows
debuginteger()可能需要管理員權限
domaindomain()在 FreeBSD (例如) 上適用
dontrouteboolean()
exclusiveaddruseboolean()僅限 Windows
keepaliveboolean()
lingerabort | linger()
maxdginteger()僅限 Windows
max_msg_sizeinteger()僅限 Windows
oobinlineboolean()
peek_offinteger()domain = local (unix)。目前已停用,因為第二次呼叫 recv([peek]) 時可能會出現無限迴圈。
priorityinteger()
protocolprotocol()在 (某些) Darwin (例如) 上適用
rcvbufnon_neg_integer()
rcvlowatnon_neg_integer()
rcvtimeotimeval()通常不支援此選項 (請參閱以下原因)。必須使用 --enable-esock-rcvsndtime 設定選項明確建置 OTP,才能使其可用。由於我們的實作是非阻塞的,因此不知道此選項是否有效,甚至是否可能導致故障。因此,我們不建議設定此選項。請改為使用 recv/3 函式的 Timeout 參數。
reuseaddrboolean()
reuseportboolean()domain = inet | inet6
sndbufnon_neg_integer()
sndlowatnon_neg_integer()在 Linux 上不可變更
sndtimeotimeval()通常不支援此選項 (請參閱以下原因)。必須使用 --enable-esock-rcvsndtime 設定選項明確建置 OTP,才能使其可用。由於我們的實作是非阻塞的,因此不知道此選項是否有效,甚至是否可能導致故障。因此,我們不建議設定此選項。請改為使用 send/3 函式的 Timeout 參數。
timestampboolean()
typetype()

表格:socket 選項

層級 ip 的選項

選項名稱值類型設定取得其他需求和註解
add_membershipip_mreq()
add_source_membershipip_mreq_source()
block_sourceip_mreq_source()
drop_membershipip_mreq()
drop_source_membershipip_mreq_source()
freebindboolean()
hdrinclboolean()type = raw
minttlinteger()type = raw
msfilternull | ip_msfilter()
mtuinteger()type = raw
mtu_discoverip_pmtudisc()
multicast_allboolean()
multicast_ifany | ip4_address()
multicast_loopboolean()
multicast_ttluint8()
nodefragboolean()type = raw
pktinfoboolean()type = dgram
recvdstaddrboolean()type = dgram
recverrboolean()
recvifboolean()type = dgram | raw
recvoptsboolean()type =/= stream
recvorigdstaddrboolean()
recvttlboolean()type =/= stream
retoptsboolean()type =/= stream
router_alertinteger()type = raw
sendsrcaddrboolean()
tosip_tos()某些高優先級可能需要超級使用者權限
transparentboolean()需要管理員權限
ttlinteger()
unblock_sourceip_mreq_source()

表格:ip 選項

層級 ipv6 的選項

選項名稱值類型設定取得其他需求和註解
addrforminet僅適用於已連線且繫結至 v4 對應 v6 位址的 IPv6 socket
add_membershipipv6_mreq()
authhdrboolean()type = dgram | raw,已過時?
drop_membershipipv6_mreq()
dstoptsboolean()type = dgram | raw,需要超級使用者權限才能更新
flowinfoboolean()type = dgram | raw,需要超級使用者權限才能更新
hoplimitboolean()type = dgram | raw。在某些平台 (例如 FreeBSD) 上,用於設定以取得 hoplimit 作為控制訊息標頭。在其他平台 (例如 Linux) 上,設定 recvhoplimit 以取得 hoplimit
hopoptsboolean()type = dgram | raw,需要超級使用者權限才能更新
mtuboolean()取得:僅在 socket 連線後
mtu_discoveripv6_pmtudisc()
multicast_hopsdefault | uint8()
multicast_ifinteger()type = dgram | raw
multicast_loopboolean()
recverrboolean()
recvhoplimitboolean()type = dgram | raw。在某些平台 (例如 Linux) 上,設定 recvhoplimit 以取得 hoplimit
recvpktinfo | pktinfoboolean()type = dgram | raw。在某些平台 (例如 FreeBSD) 上,用於設定以取得 hoplimit 作為控制訊息標頭。在其他平台 (例如 Linux) 上,設定 recvhoplimit 以取得 hoplimit
recvtclassboolean()type = dgram | raw。在某些平台上,用於設定 (=true) 以取得 tclass 控制訊息標頭。在其他平台上,設定 tclass 以取得 tclass 控制訊息標頭。
router_alertinteger()type = raw
rthdrboolean()type = dgram | raw,需要超級使用者權限才能更新
tclassinteger()設定與輸出封包相關聯的流量類別。RFC3542。
unicast_hopsdefault | uint8()
v6onlyboolean()

表格:ipv6 選項

層級 tcp 的選項

選項名稱值類型設定取得其他需求和註解
congestionstring()
corkboolean()在某些平台 (FreeBSD) 上為 'nopush'
keepcntinteger()在 Windows (至少) 上,設定為大於 255 的值是非法的。
keepidleinteger()
keepintvlinteger()
maxseginteger()並非所有平台都允許設定。
nodelayboolean()
nopushboolean()在某些平台 (Linux) 上為 'cork'。在 Darwin 上,其含義與 FreeBSD 等平台不同。

表格:tcp 選項

層級 udp 的選項

選項名稱值類型設定取得其他需求和註解
corkboolean()

表格:udp 選項

層級 sctp 的選項

選項名稱值類型設定取得其他需求和註解
associnfosctp_assocparams()
autoclosenon_neg_integer()
disable_fragmentsboolean()
eventssctp_event_subscribe()
initmsgsctp_initmsg()
maxsegnon_neg_integer()
nodelayboolean()
rtoinfosctp_rtoinfo()

表格:sctp 選項