查看源代码 gen_tcp (kernel v10.2)
TCP/IP 套接字的接口。
此模块提供通过 TCP/IP 协议套接字进行通信的函数。
以下代码片段是一个简单的客户端示例,它连接到端口 5678 上的服务器,传输二进制数据并关闭连接。
client() ->
SomeHostInNet = "localhost", % to make it runnable on one machine
{ok, Sock} = gen_tcp:connect(SomeHostInNet, 5678,
[binary, {packet, 0}]),
ok = gen_tcp:send(Sock, "Some Data"),
ok = gen_tcp:close(Sock).
另一端,服务器正在端口 5678 上监听,接受连接并接收二进制数据。
server() ->
{ok, LSock} = gen_tcp:listen(5678, [binary, {packet, 0},
{active, false}]),
{ok, Sock} = gen_tcp:accept(LSock),
{ok, Bin} = do_recv(Sock, []),
ok = gen_tcp:close(Sock),
ok = gen_tcp:close(LSock),
Bin.
do_recv(Sock, Bs) ->
case gen_tcp:recv(Sock, 0) of
{ok, B} ->
do_recv(Sock, [Bs, B]);
{error, closed} ->
{ok, list_to_binary(Bs)}
end.
有关更多示例,请参阅 示例 部分。
注意
创建套接字的函数可以接受一个可选的选项;
{inet_backend, Backend}
,如果指定,则必须是第一个选项。 这会选择平台套接字 API 的实现后端。这是一个临时选项,将在未来的版本中被忽略。
默认值为
Backend = inet
,它选择传统的inet_drv.c
驱动程序。另一个选择是Backend = socket
,它选择新的socket
模块及其 NIF 实现。当节点使用应用程序
kernel
的配置变量inet_backend
启动时,可以更改系统默认值。对于
gen_tcp
与inet_backend = socket
,我们尝试尽可能地保持“兼容”,但这有时是不可能的。以下是 inet 后端inet
(默认)和socket
的行为不同的情况列表:
如果用户调用
gen_tcp:send/2
且inet_backend = inet
,尝试发送的数据量超过了操作系统缓冲区中的空间,则“剩余数据”会由 inet 驱动程序缓冲(稍后在后台发送)。用户看到的效果是调用是非阻塞的。当
inet_backend = socket
时,效果不是这样,因为没有缓冲。相反,用户会一直挂起,直到所有数据都已发送或达到send_timeout
超时为止。
shutdown/2
可能会隐藏错误该调用不涉及接收进程状态,直接在底层套接字上完成。例如,在 Linux 上,众所周知的一个错误行为是它会跳过一些检查,因此在监听套接字上执行关闭操作会返回
ok
,而逻辑结果应该是{error, enotconn}
。inet_drv.c
驱动程序执行了额外的检查并模拟了正确的错误,但是使用Backend = socket
会引入涉及接收进程的开销。选项 nodelay 是一个 TCP 特定选项,它与
domain = local
不兼容。当使用
inet_backend = socket
时,尝试创建具有domain = local
的套接字(通过监听或连接)(例如使用选项 {ifaddr, {local,"/tmp/test"}})将失败,并返回{error, enotsup}
。这对于
inet_backend = inet
实际上也不起作用,但在那种情况下,错误会被简单地忽略,这是一个坏主意。我们选择对于inet_backend = socket
不忽略此错误。在以
inet_backend = socket
创建的套接字上调用 gen_tcp:shutdown(Socket, write | read_write) 将会立即生效,这与以inet_backend = inet
创建的套接字不同。有关更多信息,请参阅 异步关闭写入。
Windows 要求套接字(domain =
inet | inet6
)进行绑定。目前,在 Windows 上使用
inet_backend = socket
创建的所有套接字都将被绑定。如果用户未提供地址,则 gen_tcp 将尝试自行“计算出”地址。
示例
以下示例说明了如何使用选项 {active,once}
和多个接受操作,通过将服务器实现为多个工作进程,对单个侦听套接字执行接受操作。 函数 start/2
接受工作进程的数量和要侦听传入连接的端口号。如果 LPort
指定为 0
,则使用临时端口号,这就是为什么启动函数返回实际分配的端口号的原因。
start(Num,LPort) ->
case gen_tcp:listen(LPort,[{active, false},{packet,2}]) of
{ok, ListenSock} ->
start_servers(Num,ListenSock),
{ok, Port} = inet:port(ListenSock),
Port;
{error,Reason} ->
{error,Reason}
end.
start_servers(0,_) ->
ok;
start_servers(Num,LS) ->
spawn(?MODULE,server,[LS]),
start_servers(Num-1,LS).
server(LS) ->
case gen_tcp:accept(LS) of
{ok,S} ->
loop(S),
server(LS);
Other ->
io:format("accept returned ~w - goodbye!~n",[Other]),
ok
end.
loop(S) ->
inet:setopts(S,[{active,once}]),
receive
{tcp,S,Data} ->
Answer = process(Data), % Not implemented in this example
gen_tcp:send(S,Answer),
loop(S);
{tcp_closed,S} ->
io:format("Socket ~w closed [~w]~n",[S,self()]),
ok
end.
一个简单客户端的示例
client(PortNo,Message) ->
{ok,Sock} = gen_tcp:connect("localhost",PortNo,[{active,false},
{packet,2}]),
gen_tcp:send(Sock,Message),
A = gen_tcp:recv(Sock,0),
gen_tcp:close(Sock),
A.
send
调用不接受超时选项,因为发送超时是通过套接字选项 send_timeout
处理的。在没有接收器的情况下,发送操作的行为主要由底层 TCP 堆栈和网络基础设施定义。 要编写代码来处理挂起的接收器,该接收器最终可能导致发送方在 send
上挂起,请执行以下操作。
考虑一个进程,该进程从客户端进程接收数据,以便转发到网络上的服务器。该进程通过 TCP/IP 连接到服务器,并且不接收对其发送的每个消息的任何确认,但必须依靠发送超时选项来检测另一端无响应。连接时可以使用选项 send_timeout
。
...
{ok,Sock} = gen_tcp:connect(HostAddress, Port,
[{active,false},
{send_timeout, 5000},
{packet,2}]),
loop(Sock), % See below
...
在处理请求的循环中,现在可以检测到发送超时
loop(Sock) ->
receive
{Client, send_data, Binary} ->
case gen_tcp:send(Sock,[Binary]) of
{error, timeout} ->
io:format("Send timeout, closing!~n",
[]),
handle_send_timeout(), % Not implemented here
Client ! {self(),{error_sending, timeout}},
%% Usually, it's a good idea to give up in case of a
%% send timeout, as you never know how much actually
%% reached the server, maybe only a packet header?!
gen_tcp:close(Sock);
{error, OtherSendError} ->
io:format("Some other error on socket (~p), closing",
[OtherSendError]),
Client ! {self(),{error_sending, OtherSendError}},
gen_tcp:close(Sock);
ok ->
Client ! {self(), data_sent},
loop(Sock)
end
end.
通常,检测接收超时就足够了,因为大多数协议都包含来自服务器的某种确认,但是如果协议是严格单向的,则选项 send_timeout
会派上用场。
概要
函数
接受监听套接字上的传入连接请求。
关闭 TCP 套接字。
创建一个连接到指定地址的套接字。
创建一个连接到指定地址的套接字。
更改套接字的控制进程(所有者)。
创建监听套接字。
从被动模式下的套接字接收数据包。
在套接字上发送数据包。
在一个或两个方向关闭套接字。
类型
-type connect_option() :: {fd, Fd :: non_neg_integer()} | inet:address_family() | {ifaddr, socket:sockaddr_in() | socket:sockaddr_in6() | inet:socket_address()} | {ip, inet:socket_address()} | {port, inet:port_number()} | {tcp_module, module()} | {netns, file:filename_all()} | {bind_to_device, binary()} | option().
-type listen_option() :: {fd, Fd :: non_neg_integer()} | inet:address_family() | {ifaddr, socket:sockaddr_in() | socket:sockaddr_in6() | inet:socket_address()} | {ip, inet:socket_address()} | {port, inet:port_number()} | {backlog, B :: non_neg_integer()} | {tcp_module, module()} | {netns, file:filename_all()} | {bind_to_device, binary()} | option().
-type option() :: {active, true | false | once | -32768..32767} | {buffer, non_neg_integer()} | {debug, boolean()} | {delay_send, boolean()} | {deliver, port | term} | {dontroute, boolean()} | {exit_on_close, boolean()} | {exclusiveaddruse, boolean()} | {header, non_neg_integer()} | {high_msgq_watermark, pos_integer()} | {high_watermark, non_neg_integer()} | {keepalive, boolean()} | {linger, {boolean(), non_neg_integer()}} | {low_msgq_watermark, pos_integer()} | {low_watermark, non_neg_integer()} | {mode, list | binary} | list | binary | {nodelay, boolean()} | {packet, 0 | 1 | 2 | 4 | raw | sunrm | asn1 | cdr | fcgi | line | tpkt | http | httph | http_bin | httph_bin} | {packet_size, non_neg_integer()} | {priority, non_neg_integer()} | {raw, Protocol :: non_neg_integer(), OptionNum :: non_neg_integer(), ValueBin :: binary()} | {recbuf, non_neg_integer()} | {reuseaddr, boolean()} | {reuseport, boolean()} | {reuseport_lb, boolean()} | {send_timeout, non_neg_integer() | infinity} | {send_timeout_close, boolean()} | {show_econnreset, boolean()} | {sndbuf, non_neg_integer()} | {tos, non_neg_integer()} | {tclass, non_neg_integer()} | {ttl, non_neg_integer()} | {recvtos, boolean()} | {recvtclass, boolean()} | {recvttl, boolean()} | {ipv6_v6only, boolean()}.
-type option_name() :: active | buffer | debug | delay_send | deliver | dontroute | exit_on_close | exclusiveaddruse | header | high_msgq_watermark | high_watermark | keepalive | linger | low_msgq_watermark | low_watermark | mode | nodelay | packet | packet_size | priority | {raw, Protocol :: non_neg_integer(), OptionNum :: non_neg_integer(), ValueSpec :: (ValueSize :: non_neg_integer()) | (ValueBin :: binary())} | recbuf | reuseaddr | reuseport | reuseport_lb | send_timeout | send_timeout_close | show_econnreset | sndbuf | tos | tclass | ttl | recvtos | recvtclass | recvttl | pktoptions | ipv6_v6only.
-type pktoptions_value() :: {pktoptions, inet:ancillary_data()}.
来自套接字选项 pktoptions
的值。
如果平台为套接字实现了 IPv4 选项 IP_PKTOPTIONS
,或 IPv6 选项 IPV6_PKTOPTIONS
或 IPV6_2292PKTOPTIONS
;当使用选项名称 pktoptions
调用时,此值从 inet:getopts/2
返回。
注意
此选项似乎是特定于 Linux 的,并且它在未来的 Linux 内核版本中的存在也令人担忧,因为该选项是 RFC 2292 的一部分,该 RFC 早在 (2003) 就被 RFC 3542 废弃,而 RFC 3542 明确删除了从流套接字获取数据包信息的可能性。 为了比较:它曾经在 FreeBSD 中存在,但现在已被删除,至少从 FreeBSD 10 开始。
-type socket() :: inet:socket().
由 accept/1,2
和 connect/3,4
返回。
函数
-spec accept(ListenSocket) -> {ok, Socket} | {error, Reason} when ListenSocket :: socket(), Socket :: socket(), Reason :: closed | system_limit | inet:posix().
-spec accept(ListenSocket, Timeout) -> {ok, Socket} | {error, Reason} when ListenSocket :: socket(), Timeout :: timeout(), Socket :: socket(), Reason :: closed | timeout | system_limit | inet:posix().
接受监听套接字上的传入连接请求。
Socket
必须是从 listen/2
返回的套接字。Timeout
指定以毫秒为单位的超时值。 默认为 infinity
。
返回
{ok, Socket}
如果建立连接{error, closed}
如果ListenSocket
已关闭{error, timeout}
如果在Timeout
内未建立连接{error, system_limit}
如果 Erlang 模拟器中的所有可用端口都在使用中- 如果发生其他错误,则返回 POSIX 错误值,有关可能的值,请参阅
inet
。
要在返回的 Socket
上发送数据包(出站),请使用 send/2
。从对等方发送的数据包(入站)作为消息传递给套接字所有者; 创建套接字的进程。除非在创建 监听套接字时,在选项列表中指定了 {active, false}
。
有关活动模式套接字消息和被动模式,请参阅 connect/4
。
注意
accept
调用不必从套接字所有者进程发出。使用 5.5.3 及更高版本的模拟器,可以从不同的进程发出多个同时接受调用,这允许使用一个接受器进程池来处理传入的连接。
-spec close(Socket) -> ok when Socket :: socket().
关闭 TCP 套接字。
请注意,在大多数 TCP 实现中,执行 close
并不能保证发送的数据已传递给接收方。 保证接收方在关闭之前看到所有发送的数据,但发送方没有收到任何指示。
如果发送方需要知道接收方已接收到所有数据,则有两种常用的方法可以实现此目的
- 使用
gen_tcp:shutdown(Sock, write)
发出信号,表示不再发送数据,并等待另一方确认看到其读取端已关闭,方法是关闭其写入端,这在此端显示为套接字关闭。 - 在 TCP 之上的协议中实现一个双方连接都遵守的确认,指示已看到所有数据。套接字选项
{packet, N}
可能会很有用。
-spec connect(SockAddr, Opts) -> {ok, Socket} | {error, Reason} when SockAddr :: socket:sockaddr_in() | socket:sockaddr_in6(), Opts :: [inet:inet_backend() | connect_option()], Socket :: socket(), Reason :: inet:posix().
-spec connect(Address, Port, Opts) -> {ok, Socket} | {error, Reason} when Address :: inet:socket_address() | inet:hostname(), Port :: inet:port_number(), Opts :: [inet:inet_backend() | connect_option()], Socket :: socket(), Reason :: inet:posix(); (SockAddr, Opts, Timeout) -> {ok, Socket} | {error, Reason} when SockAddr :: socket:sockaddr_in() | socket:sockaddr_in6(), Opts :: [inet:inet_backend() | connect_option()], Timeout :: timeout(), Socket :: socket(), Reason :: timeout | inet:posix().
创建一个连接到指定地址的套接字。
使用参数 Address
和 Port
等效于 connect(Address, Port, Opts, infinity)
。
使用参数 SockAddr
(自 OTP 24.3 起)
连接到 SockAddr
指定的远程监听套接字,例如,socket:sockaddr_in6/0
允许指定链接本地 IPv6 地址的 scope_id
。
除了目标地址的格式之外,等效于 connect/4
。
-spec connect(Address, Port, Opts, Timeout) -> {ok, Socket} | {error, Reason} when Address :: inet:socket_address() | inet:hostname(), Port :: inet:port_number(), Opts :: [inet:inet_backend() | connect_option()], Timeout :: timeout(), Socket :: socket(), Reason :: timeout | inet:posix().
创建一个连接到指定地址的套接字。
创建一个套接字,并将其连接到主机上 TCP 端口 Port
上的服务器,该主机可以使用 IP 地址 Address
,也可以使用主机名。
Opts
(连接选项)
{ip, Address}
- 如果本地主机有多个 IP 地址,此选项指定要使用的 IP 地址。{ifaddr, Address}
- 与{ip, Address}
相同。但是,如果
Address
实际上是socket:sockaddr_in/0
或socket:sockaddr_in6/0
,则此选项优先于之前使用ip
和port
选项设置的任何值。如果这些选项(ip
或/和port
)出现在此选项之后,则它们可以用于更新此选项的相应字段(对于ip
,更新addr
字段,对于port
,更新port
字段)。{fd, integer() >= 0}
- 如果套接字在没有使用gen_tcp
的情况下被连接,则使用此选项传递其文件描述符。如果{ip, Address}
和/或{port, port_number()}
与此选项组合使用,则在连接之前,fd
将绑定到指定的接口和端口。如果未指定这些选项,则假定fd
已正确绑定。inet
- 为 IPv4 设置套接字。inet6
- 为 IPv6 设置套接字。local
- 设置 Unix 域套接字。请参阅inet:local_address/0
{port, Port}
- 指定要使用的本地端口号。{tcp_module, module()}
- 覆盖要使用的回调模块。IPv4 默认为inet_tcp
,IPv6 默认为inet6_tcp
。option/0
- 请参阅inet:setopts/2
。
套接字数据
可以使用 send(Socket, Packet)
向对等方(出站)发送数据包。来自对等方(入站)发送的数据包将作为消息传递给套接字所有者;除非在 Options
列表中指定了 {active, false}
,否则将传递给创建套接字的进程。
活动模式套接字消息
{tcp, Socket, Data}
- 来自套接字的入站数据。{tcp_passive, Socket}
- 套接字处于{active, N}
模式(有关详细信息,请参阅inet:setopts/2
),并且其消息计数器达到0
,表明该套接字已转换为被动 ({active, false}
) 模式。{tcp_closed, Socket}
- 套接字已关闭。{tcp_error, Socket, Reason}
- 发生套接字错误。
被动模式
如果在套接字的选项列表中指定了 {active, false}
,则通过调用 recv/2,3
来检索数据包和错误(send/2
也可能返回错误)。
超时
可选的 Timeout
参数以毫秒为单位指定连接超时时间。默认为 infinity
。
注意
请记住,如果底层操作系统
connect()
调用返回超时,即使指定了更大的Timeout
(例如infinity
),gen_tcp:connect
也会返回超时(即{error, etimedout}
)。
注意
为
connect
指定的选项的默认值可能会受到内核配置参数inet_default_connect_options
的影响。有关详细信息,请参阅inet
。
-spec controlling_process(Socket, Pid) -> ok | {error, Reason} when Socket :: socket(), Pid :: pid(), Reason :: closed | not_owner | badarg | inet:posix().
更改套接字的控制进程(所有者)。
将新的控制进程 Pid
分配给 Socket
。控制进程是套接字向其发送消息的进程。如果此函数是由当前控制进程以外的任何其他进程调用的,则返回 {error, not_owner}
。
如果 Pid
标识的进程不是现有的本地 pid/0
,则返回 {error, badarg}
。在某些情况下,当 Socket
在此函数执行期间关闭时,也可能返回 {error, badarg}
。
如果套接字处于活动模式,则此函数会将调用者的邮箱中来自套接字的任何消息传输到新的控制进程。
如果在传输期间有任何其他进程与套接字交互,则可能无法正常工作,并且消息可能会保留在调用者的邮箱中。例如,在传输期间更改套接字的活动模式可能会导致这种情况。
-spec listen(Port, Options) -> {ok, ListenSocket} | {error, Reason} when Port :: inet:port_number(), Options :: [inet:inet_backend() | listen_option()], ListenSocket :: socket(), Reason :: system_limit | inet:posix().
创建监听套接字。
创建一个套接字,并将其设置为侦听本地主机上的端口 Port
。
如果 Port == 0
,则底层操作系统会分配一个可用的(临时)端口号,请使用 inet:port/1
来检索它。
以下选项可用
list
- 收到的Packet
以字节列表的形式传递,[
byte/0
]
。binary
- 收到的Packet
以binary/0
的形式传递。{backlog, B}
-B ::
non_neg_integer/0
。积压值定义了挂起连接队列可以增长到的最大长度。默认为5
。inet6
- 为 IPv6 设置套接字。inet
- 为 IPv4 设置套接字。{fd, Fd}
- 如果套接字在没有使用gen_tcp
的情况下被创建,则使用此选项传递其文件描述符。{ip, Address}
- 如果主机有多个 IP 地址,此选项指定要侦听的 IP 地址。{port, Port}
- 指定要使用的本地端口号。{ifaddr, Address}
- 与{ip, Address}
相同。但是,如果它是一个
socket:sockaddr_in/0
或socket:sockaddr_in6/0
,则此选项优先于之前使用ip
和port
选项设置的任何值。如果这些选项(ip
或/和port
)出现在此选项之后,则它们可以用于更新此选项的相应字段(对于ip
,更新addr
字段,对于port
,更新port
字段)。{tcp_module, module()}
- 覆盖要使用的回调模块。IPv4 默认为inet_tcp
,IPv6 默认为inet6_tcp
。option/0
- 请参阅inet:setopts/2
。
在调用 accept/1,2
以接受传入的连接请求时,应使用返回的套接字 ListenSocket
。
注意
为
listen
指定的选项的默认值可能会受到内核配置参数inet_default_listen_options
的影响。有关详细信息,请参阅inet
。
-spec recv(Socket, Length) -> {ok, Packet} | {error, Reason} when Socket :: socket(), Length :: non_neg_integer(), Packet :: string() | binary() | HttpPacket, Reason :: closed | inet:posix(), HttpPacket :: term().
-spec recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason} when Socket :: socket(), Length :: non_neg_integer(), Timeout :: timeout(), Packet :: string() | binary() | HttpPacket, Reason :: closed | timeout | inet:posix(), HttpPacket :: term().
从被动模式下的套接字接收数据包。
关闭的套接字由返回值 {error, closed}
指示。如果套接字未处于被动模式,则返回值为 {error, einval}
。
仅当套接字处于 raw
模式时,参数 Length
才具有意义,并且表示要读取的字节数。如果 Length
为 0
,则返回所有可用的字节。如果 Length > 0
,则返回正好 Length
个字节,或者返回错误;除非套接字从另一侧关闭,然后返回 {error, closed}
之前的最后一次读取可能会返回少于 Length
个字节的数据。
可选的 Timeout
参数以毫秒为单位指定超时时间。默认为 infinity
。
任何进程都可以从被动套接字接收数据,即使该进程不是该套接字的控制进程。但是,在任何给定时间,只有一个进程可以对套接字调用此函数。不建议同时调用 recv
,因为该行为取决于套接字实现,并且可能会返回诸如 {error, ealready}
之类的错误。
-spec send(Socket, Packet) -> ok | {error, Reason} when Socket :: socket(), Packet :: iodata(), Reason :: closed | {timeout, RestData} | inet:posix(), RestData :: binary() | erlang:iovec().
在套接字上发送数据包。
没有带有超时选项的 send/2
调用;如果需要超时,请使用套接字选项 send_timeout
。请参阅“示例”部分。
只有当 inet_backend = socket
时,才能返回返回值 {error, {timeout, RestData}}
。
注意
非阻塞发送。
如果用户尝试发送的数据量超过了操作系统发送缓冲区中的可用空间,则“剩余数据”将存储在(inet 驱动程序)内部缓冲区中,并在后台发送。该函数立即返回 ok(不通知调用者某些日期尚未发送)。在发送“剩余数据”时出现的任何问题可能会在稍后返回。
当使用
inet_backend = socket
时,行为是不同的。没有缓冲,而是调用者将“挂起”,直到所有数据都已发送,或者发送超时(由send_timeout
选项指定)到期(即使在使用inet
后端时,如果内部缓冲区已满,该函数也可能“挂起”)。如果在使用
packet =/= raw
时发生这种情况,则已写入部分数据包。因此,此时不能写入新数据包,因为对等方无法将其与当前数据包中的数据区分开。相反,请将数据包设置为 raw,发送剩余数据(作为 raw 数据),然后再次将数据包设置为正确的数据包类型。
-spec shutdown(Socket, How) -> ok | {error, Reason} when Socket :: socket(), How :: read | write | read_write, Reason :: inet:posix().
在一个或两个方向关闭套接字。
How == write
表示关闭套接字的写入,仍然可以从中读取。
如果 How == read
或者 Socket
端口中没有缓冲的传出数据,则立即执行关闭,并且在 Reason
中返回遇到的任何错误。
如果套接字端口中有数据缓冲,则在将缓冲的数据写入操作系统协议栈之前,不会对套接字执行关闭。如果遇到任何错误,则会关闭套接字,并且下一个 recv/2
或 send/2
调用将返回 {error, closed}
。
如果对等方执行其写入端的关闭,则选项 {exit_on_close, false}
很有用。然后,在接收指示套接字已关闭后,套接字仍保持打开状态以进行写入。
注意
异步关闭写入 (
How :: write | read_write
)。如果在 inet 驱动程序在后台发送缓冲数据时尝试关闭,则关闭将推迟到发送完所有缓冲数据为止。此函数立即返回
ok
,并且不通知调用者(关闭已被推迟)。当使用
inet_backend = socket
时,行为是不同的。使用How :: write | read_write
进行关闭将始终立即执行。