gen_tcp模块¶
accept/1/2¶
用法:
accept(ListenSocket, TimeOut) -> {ok, Socket} | {error, Reason}
// ListenSocket 必须是由函数 gen_tcp:listen/2 建立返回
// 该函数会引起进程阻塞,直到有一个连接请求发送到监听的套接字
返回值:
{ok,Socket}: 连接已建立
{error,closed}: ListenSocket 已经关闭
{error,timeout}: 指定的时间内连接没有建立
{error, system_limit}: Erlang 虚拟机里可用的端口都被使用了
如果某些东西出错,也可能返回一个 POSIX 错误
close/1¶
关闭一个 TCP 套接字
用法:
gen_tcp:close(Socket) -> ok
connect/3_4¶
用法:
connect(Address, Port, Options, Timeout) -> {ok, Socket} | {error, Reason}
// 参数 Timeout 指定一个以毫秒为单位的超时值,默认值是 infinity
实例:
gen_tcp:connect("localhost", Port, [{active, false}, {packet, 0}], 5000).
controlling_process/2¶
改变一个套接字的控制进程
用法:
gen_tcp:controlling_process(Socket, Pid) -> ok | {error, Reason}
// 为 Socket 分配一个新的控制进程 Pid
// 控制进程就是接收发自套接字消息数据的进程
// 如果被当前控制进程以外的其他任何进程调用,则会返回 {error, not_owner} 的错误
实例:
{Rand, _RandSeed} = random:uniform_s(9999, erlang:now()),
Port = 40000 + Rand,
case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of
{ok, Socket} ->
gen_tcp:controlling_process(Socket, self());
_ ->
socket_listen_fail
end.
listen/2¶
使用方法:
listen(Port, Options) -> {ok, ListenSocket} | {error, Reason}
// 参数 Port 为 0,那么底层操作系统将赋值一个可用的端口号
// 可以使用 inet:port/1 来获取一个 socket 监听的端口
参数 Options 的一些常用选项:
- {active,true}¶
套接字设置为主动模式[默认] 所有套接字接收到的消息都作为 Erlang 消息转发到拥有这个套接字进程上 消息形式: {tcp, Socket, Data} | {tcp_closed, Socket}
- {active,false}¶
设置套接字为被动模式 套接字收到的消息被缓存起来,进程必须通过调用函数: gen_tcp:recv/2 或 gen_tcp:recv/3 来读取这些消息: gen_tcp:recv(Socket, Length) -> {ok, Data} | {error, Reason}
- {active,once}¶
将设置套接字为主动模式,但是一旦收到第一条消息,就将其设置为被动模式
- {keepalive,true}¶
当没有转移数据时,确保所连接的套接字发送保持活跃(keepalive)的消息。因为关闭套接字消息可能会丢失,如果没有接收到保持活跃消息的响应,那么该选项可确保这个套接字能被关闭。默认情况下,该标签是关闭的
- {nodelay,true}¶
数据包直接发送到套接字,不管它多么小。在默认情况下,此选项处于关闭状态,并且与之相反,数据被聚集而以更大的数据块进行发送。
- {packet_size,Size}¶
设置数据包允许的最大长度。如果数据包比 Size 还大,那么将认为这个数据包无效。
- {packet,PacketType}¶
raw | 0:
没有封包,即不管数据包头,而是根据Length参数接收数据, 表示 Erlang 系统会把 TCP 数据原封不动地直接传送给应用程序
1 | 2 | 4:
表示包头的长度,分别是1,2,4个字节(2,4以大端字节序,无符号表示) 当设置了此参数时,接收到数据后将自动剥离对应长度的头部,只保留Body
asn1 | cdr | sunrm | fcgi | tpkt | line:
设置以上参数时,应用程序将保证数据包头部的正确性 但是在gen_tcp:recv/2,3接收到的数据包中并不剥离头部 asn1 - ASN.1 BER, sunrm - Sun's RPC encoding, cdr - CORBA (GIOP 1.1), fcgi - Fast CGI, tpkt - TPKT format [RFC1006], line - Line mode, a packet is a line terminated with newline, lines longer than the receive buffer are truncated.
httph | httph_bin:
设置以上参数,收到的数据将被erlang:decode_packet/3格式化 在被动模式下将收到{ok, HttpPacket} 主动模式下将收到{http, Socket, HttpPacket}.
- {reuseaddr,true}¶
允许本地重复使用端口号
- {delay_send,true}¶
数据不是立即发送,而是存到发送队列里,等 socket 可写的时候再发送
- {backlog,1024}¶
缓冲区的长度
- {exit_on_close,false}¶
设置为 false,那么 socket 被关闭之后还能将缓冲区中的数据发送出去
- {send_timeout,15000}¶
设置一个时间去等待操作系统发送数据,如果底层在这个时间段后还没发出数据,那么就会返回 {error,timeout}
recv/3¶
用法:
recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason}
//当 Socket 是 raw 模式下,参数 Length 才有意义的,并且 Length 表示接收字节的大小
//如果 Length = 0,所有有效的字节数据都会被接收
//如果 Length > 0,则只会接收 Length 长度的字节,或发生错误
//当另一端 Socket 关闭时,接收的数据长度可能会小于 Length
//选项 Timeout 是一个以毫秒为单位的超时值,默认值是 infinity。
实例:
{Rand, _RandSeed} = random:uniform_s(9999, erlang:now()),
Port = 40000 + Rand,
case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of
{ok, ListenSocket} ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
gen_tcp:recv(Socket, 0, 5000);
{error, SocketAcceptFail} ->
SocketAcceptFail
end;
_ ->
socket_listen_fail
end.
send/2¶
在一个套接字 Socket 发送一个数据包
用法:
send(Socket, Packet) -> ok | {error, Reason}
shutdown/2¶
半关闭一个套接字 用法:
shutdown(Socket, How) -> ok | {error, Reason}
// How :: read | write | read_write
// 如果参数How为write的形式,则套接字socket会关闭数据写入,读取仍可以正常执行
// 参数 How 为 read,则反之
// 要实现套接字半打开, 那么套接字要设置 {exit_on_close, false} 这个参数
实例:
{Rand, _RandSeed} = random:uniform_s(9999, erlang:now()),
Port = 40000 + Rand,
case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of
{ok, ListenSocket} ->
case gen_tcp:accept(ListenSocket, 1500) of
{ok, Socket} ->
inet:setopts(Socket, [{exit_on_close, false}]),
gen_tcp:shutdown(Socket, write);
{error, SocketAcceptFail} ->
SocketAcceptFail
end;
_ ->
socket_listen_fail
end.
实例¶
1-module(tcp_test).
2-export([
3 start_server/0,
4 start_client_unpack/0, start_client_packed/0
5 ]).
6
7-define(PORT, 8888).
8-define(PORT2, 8889).
9
10start_server()->
11 {ok, ListenSocket} = gen_tcp:listen(?PORT, [binary,{active,false}]),
12 {ok, ListenSocket2} = gen_tcp:listen(?PORT2, [binary,{active,false},{packet,2}]),
13 spawn(fun() -> accept(ListenSocket) end),
14 spawn(fun() -> accept(ListenSocket2) end),
15 receive
16 _ -> ok
17 end.
18
19accept(ListenSocket)->
20 case gen_tcp:accept(ListenSocket) of
21 {ok, Socket} ->
22 spawn(fun() -> accept(ListenSocket) end),
23 loop(Socket);
24 _ ->
25 ok
26 end.
27
28loop(Socket)->
29 case gen_tcp:recv(Socket,0) of
30 {ok, Data}->
31 io:format("received message ~p~n", [Data]),
32 gen_tcp:send(Socket, "receive successful"),
33 loop(Socket);
34 {error, Reason}->
35 io:format("socket error: ~p~n", [Reason])
36 end.
37
38start_client_unpack()->
39 {ok,Socket} = gen_tcp:connect({127,0,0,1},?PORT,[binary,{active,false}]),
40 gen_tcp:send(Socket, "1"),
41 gen_tcp:send(Socket, "2"),
42 gen_tcp:send(Socket, "3"),
43 gen_tcp:send(Socket, "4"),
44 gen_tcp:send(Socket, "5"),
45 sleep(1000).
46
47start_client_packed()->
48 {ok,Socket} = gen_tcp:connect({127,0,0,1},?PORT2,[binary,{active,false},{packet,2}]),
49 gen_tcp:send(Socket, "1"),
50 gen_tcp:send(Socket, "2"),
51 gen_tcp:send(Socket, "3"),
52 gen_tcp:send(Socket, "4"),
53 gen_tcp:send(Socket, "5"),
54 sleep(1000).
55
56sleep(Count) ->
57 receive
58 after Count ->
59 ok
60 end.
操作:
C:\>erlc tcp_test.erl
C:\>erl -s tcp_test start_server
Eshell V5.10.2 (abort with ^G)
1> tcp_test:start_client_packed().
received message <<"1">>
received message <<"2">>
received message <<"3">>
received message <<"4">>
received message <<"5">>
ok
2> tcp_test:start_client_unpack().
received message <<"12345">>
ok
字节序¶
字节序分为两类:Big-Endian和Little-Endian,定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
其实还有一种网络字节序,为TCP/IP各层协议定义的字节序,为Big-Endian。
packet包头是以大端字节序(big-endian)表示。如果erlang与其他语言,比如C++,就要注意字节序问题了。如果机器的字节序是小端字节序(little-endian),就要做转换:
{packet, 2} :[L1,L0 | Data]
{packet, 4} :[L3,L2,L1,L0 | Data]
如何判断机器的字节序,以C++为例:
BOOL IsBigEndian()
{
int a = 0x1234;
char b = *(char *)&a; //通过将int强制类型转换成char单字节,通过判断起始存储位置
if( b == 0x12)
{
return TRUE;
}
return FALSE;
}
转换字节序,以C++为例:
// 32位字数据
#define LittletoBig32(A) ((( (UINT)(A) & 0xff000000) >> 24) | \
(( (UINT)(A) & 0x00ff0000) >> 8) | \
(( (UINT)(A) & 0x0000ff00) << 8) | \
(( (UINT)(A) & 0x000000ff) << 24))
// 16位字数据
#define LittletoBig16(A) (( ((USHORT)(A) & 0xff00) >> 8) | \
(( (USHORT)(A) & 0x00ff) << 8))