Gen_tcp
Gen_tcp模块遵循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),
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.
更多例子,见examples部分。
数据类型
option() = {active, true | false | once}
| {bit8, clear | set |on | off}
| {buffer, integer()>= 0}
| {delay_send,boolean()}
| {deliver, port | term}
| {dontroute, boolean()}
| {exit_on_close,boolean()}
| {header, integer()>= 0}
| {high_watermark, integer()>= 0}
| {keepalive, boolean()}
| {linger, {boolean(),integer() >= 0}}
| {low_watermark,integer() >= 0}
| {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,integer() >= 0}
| {priority, integer()>= 0}
| {raw,
Protocol ::integer() >= 0,
OptionNum ::integer() >= 0,
ValueBin ::binary()}
| {recbuf, integer()>= 0}
| {reuseaddr, boolean()}
| {send_timeout,integer() >= 0 | infinity}
| {send_timeout_close,boolean()}
| {sndbuf, integer()>= 0}
| {tos, integer() >=0}
option_name() = active
| bit8
| buffer
| delay_send
| deliver
| dontroute
| exit_on_close
| header
| high_watermark
| keepalive
| linger
| low_watermark
| mode
| nodelay
| packet
| packet_size
| priority
| {raw,
Protocol :: integer()>= 0,
OptionNum ::integer() >= 0,
ValueSpec ::(ValueSize :: integer() >= 0)
| (ValueBin ::binary())}
| recbuf
| reuseaddr
| send_timeout
| send_timeout_close
| sndbuf
| tos
connect_option() = {ip, inet:ip_address()}
| {fd, Fd :: integer() >= 0}
| {ifaddr,inet:ip_address()}
|inet:address_family()
| {port,inet:port_number()}
| {tcp_module,module()}
| option()
listen_option() = {ip, inet:ip_address()}
| {fd, Fd ::integer() >= 0}
| {ifaddr,inet:ip_address()}
|inet:address_family()
| {port,inet:port_number()}
| {backlog, B ::integer() >= 0}
| {tcp_module,module()}
| option()
socket() 是accept/1,2和connect/3,4的返回值。
导出的函数
connect(Address, Port, Options) -> {ok, Socket} | {error, Reason}
connect(Address, Port, Options, Timeout) ->
{ok, Socket} |{error, Reason}
类型:
Address = inet:ip_address() |inet:hostname()
Port = inet:port_number()
Options = [connect_option()]
Timeout = timeout()
Socket = socket()
Reason = inet:posix()
connect通过TCP端口Port和主机IP地址Address连接到服务器。参数Address可以是主机名字或者主机IP地址。
可用的选项:
list
数据包作为列表传输。
binary
数据包作为二进制数据传输。
{ip,ip_address()}
如果主机有多个网络接口,这个选项则指定使用哪一个接口。
{port,Port}
指定使用哪一个本地端口。
{fd,integer()>=0}
如果套接字没有使用gen_tcp建立连接,则通过这个选项给它传递文件描述符。
Inet6
使用IPv6建立套接字。
Inet
使用IPv4建立套接字。
Opt
参考inet:setopts/2
使用send/2向connect返回的Socket发送数据包。数据包的发送如同消息的传递:
{tcp, Socket, Data}
如果socket被关闭,将会发送下面的消息:
{tcp_closed,Socket}
如果socket出现错误,将会发送下面的消息:
{tcp_error,Socket,Reason}
如果在socket的选项列表中指定{active,false},这样就得使用recv/2接收数据包。
选项Timeout指定了一个毫秒级的超时,默认值是infinity。
注意:
内核配置参数inet_default_connect_options可能影响到connect选项的默认值。详情请参考inet(3)。
listen(Port, Options) -> {ok, ListenSocket} | {error, Reason}
类型:
Port = inet:port_number()
Options = [listen_option()]
ListenSocket = socket()
Reason = inet:posix()
建立一个socket在本地主机的Port端口上监听连接。
如果Port==0,底层操作系统将通过inet:port/1重新分配一个可用的端口给它。
可用的选项:
list
数据包作为列表传输。
binary
数据包作为二进制数据传输。
{backlog,B}
B是一个大于等于0的整数,默认值是5。B的值定义了等待连接的队列的最大长度。
{ip,ip_address()}
如果主机有多个网络接口,这个选项则指定使用哪一个接口。
{port,Port}
指定使用哪一个本地端口。
{fd,Fd}
如果套接字没有使用gen_tcp建立连接,则通过这个选项给它传递文件描述符。
Inet6
使用IPv6建立套接字。
Inet
使用IPv4建立套接字。
Opt
参考inet:setopts/2
返回的socket标示符ListenSocket只能用于函数accept/1,2。
注意:
内核配置参数inet_default_listen_options可能影响listen选项的默认值,详情请参考inet(3)。
accept(ListenSocket) -> {ok, Socket} | {error, Reason}
accept(ListenSocket, Timeout) -> {ok, Socket} | {error, Reason}
类型:
ListenSocket = socket()
Returned by listen/2.
Timeout = timeout()
Socket = socket()
Reason = closed | timeout |inet:posix()
通过socket监听接受到来的连接请求。ListenSocket必须是由listen/2返回的。Timeout指定了一个毫秒级的超时,默认为infinity。
如果连接建立则返回{ok,Socket},如果ListenSocket关闭了则返回{error,closed},如果在指定的时间内没有建立连接,则返回{error,timeout}。如果某些东西出错,也可能返回一个POSIX错误。可能的错误值请参考inet(3)。
使用send/2向accept返回的Socket发送数据包。数据包的发送如同消息的传递:
{tcp, Socket, Data}
如果在socket的选项列表中指定{active,false},这样就得使用recv/2接收数据包。
注意:
值得注意的是accept调用不必从socket进程传出,在5.5.3或更高版本中,许多同步接受调用从不同的进程传出,这是一个处理到来的连接请求的进程池。
send(Socket, Packet) -> ok | {error, Reason}
类型:
Socket = socket()
Packet = iodata()
Reason = inet:posix()
通过socket发送数据。
Send调用没有超时选项,如果需要超时,则使用send_timeout选项。具体参考examples部分。
recv(Socket, Length) -> {ok, Packet} | {error, Reason}
recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason}
类型:
Socket = socket()
Length = integer() >= 0
Timeout = timeout()
Packet = string() | binary() |HttpPacket
Reason = closed | inet:posix()
HttpPacket = term()
HttpPacket的描述请参考erlang:decode_packet/3。
这个函数以被动方式接收数据包。返回值{error,closed}表明socket已关闭。
当socket是raw模式时参数Length是唯一有意义的,并且Length表明接收字节的数量。如果Length=0,所有有效的字节都会被接收。如果Length>0,则只会接收Length长度的字节或者发生错误;当socket从另一端关闭,接收的数据长度可能会小于Length。
选项Timeout指定了一个毫秒级的超时,默认值是infinity。
controlling_process(Socket, Pid) -> ok | {error, Reason}
类型:
Socket = socket()
Pid = pid()
Reason = closed | not_owner |inet:posix()
为Socket分配一个新的控制进程Pid。控制进程就是从socket接收数据的进程。如果被控制进程以外的进程调用则会返回{error,eperm}。
close(Socket) -> ok
类型:
Socket = socket()
关闭TCP套接字。
shutdown(Socket, How) -> ok | {error, Reason}
类型:
Socket = socket()
How = read | write | read_write
Reason = inet:posix()
单向或双向的关闭socket。
How == write意味着关闭socket的写通道,读通道仍然可用。
为了能够通过写的一端处理相同的关闭,选项{exit_on_close,false}将会很有用。
Examples
下面这个例子阐述了选项{active,once}和使用单一的监听函数接受多个连接请求建立多个工作进程的socket用法。函数start/2得到工作进程的数量和监听连接请求的端口号。如果LPort指定为0,将使用一个暂时的端口号,所以函数start返回一个真实分配的端口号。
start(Num,LPort) ->
casegen_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) ->
casegen_tcp:accept(LS) of
{ok,S}->
loop(S),
server(LS);
Other->
io:format("acceptreturned ~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调用没有接收timeout选项,因为关于send的超时通过socket选项send_timeout来处理的。没有接收的send操作的等级非常高,这是由底层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中请求被处理,我们可以检测发送超时:
loop(Sock) ->
receive
{Client,send_data, Binary} ->
casegen_tcp:send(Sock,[Binary]) of
{error,timeout} ->
io:format("Sendtimeout, 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("Someother 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迟早会有用的。