主页

索引

模块索引

搜索页面

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))

主页

索引

模块索引

搜索页面