Socket编程之TCP

一个Socket是一个允许机器与Internet上另一端使用IP通信的端点

gen_tcp 用于TCP编程、 gen_udp 用于UDP编程。

TCP:
例子:从服务器获取数据

nano_get_url() ->
nano_get_url("www.google.com").

nano_get_url(Host) ->
{ok,Socket} = gen_tcp:connect(Host,80,[binary, {packet, 0}]), %% (1)
ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"), %% (2)
receive_data(Socket, []).
receive_data(Socket, SoFar) ->
receive
{tcp,Socket,Bin} -> %% (3)
receive_data(Socket, [Bin|SoFar]);
{tcp_closed,Socket} -> %% (4)
list_to_binary(reverse(SoFar)) %% (5)
end.

1.通过 gen_tcp:connect 在 http://www.google.com 打开TCP协议80端口。connect的参数binary告知系统以binary模式打开socket,并且以二进制方式传递数据到应用。 {packet,0} 意味着无需遵守特定格式即可将数据传递到应用。
2.用 gen_tcp:send 并发送消息 GET / HTTP/1.0\r\n\r\n 到socket。然后我们等待响应。响应并不会一次性得到,而是分片的、分时间的。这些分片是按照一系列报文的方式接收的,并且通过打开的socket发送到进程。
3.接收一个 {tcp,Socket,Bin} 报文。第三个参数是binary。这是因为我们已经使用二进制方式打开了socket。这是从WEB服务器发到我们的一个消息报文。我们把这个报文加到分片列表并等待下一个报文
4.我们接收到 {tcp_closed,Socket} 报文,这在服务器发送完所有数据时发生
5.当我们收到了所有的分片,存储顺序是错误的,所以我们重新对分片排序和连接。


一个简单的TCP服务器:

start_nano_server() ->
{ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4},
{reuseaddr, true},
{active, true}]),
{ok, Socket} = gen_tcp:accept(Listen),
gen_tcp:close(Listen),
loop(Socket).

loop(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("Server received binary = ~p~n",[Bin]),
Str = binary_to_term(Bin),
io:format("Server (unpacked) ~p~n",[Str]),
Reply = lib_misc:string2value(Str),
io:format("Server replying = ~p~n",[Reply]),
gen_tcp:send(Socket, term_to_binary(Reply)),
loop(Socket);
{tcp_closed, Socket} ->
io:format("Server socket closed~n")
end.

1.首先,我们调用 gen_tcp:listen 来监听2345端口,并且设置报文转换格式为 {packet,4} ,意味着每个包有4个字节的包头,代表长度。然后 gen_tcp:listen(..) 会返回 {ok,Socket} 或者 {error,Why}
2.调用 gen_tcp:accept(Listen) 。在这里,程序会挂起以等待连接,当我们获得连接时,这个函数返回已经绑定的Socket.
3.当 gen_tcp:accept 返回,我们立即调用 gen_tcp:close(Listen) 。这就关闭了监听的socket,服务器也就不会继续接受新的连接了。而这不会影响已有的连接,只是针对新连接.
4.解码输入数据
5.对字符串求值
6.编码返回数据并且通过socket发送
上面代码服务器只接收一个请求。

测试服务器:

nano_client_eval(Str) ->
{ok,Socket}=get_tcp:connect("localhost",2345,[binary,{packet,4}]),
ok=gen_tcp:send(Socket,term_to_binary(Str)),
receive
{tcp,Socket,Bin} ->
io:format("Client received binary = ~p~n",[Bin]),
Val=binary_to_term(Bin),
io:format("Client result = ~p~n",[Val]),
gen_tcp:close(Socket)
end.


服务器接收多个请求:

start_seq_server() ->
{ok,Listen} = gen_tcp:listen(...),
seq_loop(Listen).

seq_loop(Listen) ->
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket),
seq_loop(Listen).

这段代码我们让一直让监听的socket开着而不是关闭
如果客户端在服务器忙于处理一个已经存在的连接时尝试连接服务器,连接请求会被缓存,直到服务器完成那个已经存在的请求

并行服务器:

start_parallel_server() ->
{ok,Listen} = gen_tcp:listen(...),
spawn(fun() -> par_connect(Listen) end).

par_connect(Listen) ->
{ok,Socket} = gen_tcp:accept(Listen),
spawn(fun() -> par_connect(Listen) end),
loop(Socket).



Erlang中socket可以以3种模式打开:active、active once、passive。其设置可以通过 gen_tcp:connect(Address,Port,Options) 或 gen_tcp:listen(Port,Options) 中的Options参数来设置为 {active,true|false|once}
如果指定了 {active,true} ,就会创建一个主动(active)的socket; {active,false} 会创建一个被动的(passive)的socket; {active,once} 创建主动的socket,但是只接受一条消息,接收到消息后,必须手动重新开启(reenable)才能继续接受消息。
active和passive的socket的区别在于消息到来时的处理方式:
一旦一个active的socket被创建了,控制进程会发送收到的数据,以 {tcp,Socket,Data} 消息的形式。而控制进程无法控制消息流。一个无赖的客户端可以发送无数的消息到系统,而这些都会被发送到控制进程。而控制进程无法停止这个消息流。
如果socket在passive模式,控制进程需要调用 gen_tcp:recv(Socket,N) 来获取数据。它会尝试获取N字节的数据,如果N=0,就会尽可能的拿到所有可以取得的数据。这种情况下,服务器可通过选择是否调用 gen_tcp:recv 来控制消息流


主动消息获取:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,true}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
receive
{tcp,Socket,Data} ->
... 输出处理 ...
{tcp_closed,Socket} ->
...
end.

这个过程无法控制发到服务器循环的消息流,如果客户端产生数据的速度大于服务器消费数据的速度,系统就会收到洪水般地消息-消息缓冲区溢出,系统将会crash并表现怪异。

被动消息获取:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,false}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
case gen_tcp:recv(Socket,N) of
{ok,B} ->
... 数据处理 ...
loop(Socket);
{error,closed}
...
end.

服务器以被动模式打开socket,通过 {active,false} 选项。这个服务器不会被危险的客户端洪水袭击。

混合消息获取:

{ok,Listen} = gen_tcp:listen(Port,[...,{active,once}...]),
{ok,Socket} = gen_tcp:accept(Listen),
loop(Socket).

loop(Socket) ->
receive
{tcp,Socket,Data} ->
... 数据处理 ...
%%准备好启用下一条消息时
inet:setopts(Socket,[{active,once}]),
loop(Socket);
{tcp_closed,Socket} ->
...
end.

我们以一次主动(active once)模式打开socket。在这个模式中,socket是主动的,但是只能接收一条消息。在控制进程发出一条消息之后,他必须明确的调用 inet:setopts 以便让socket恢复并接收下一条消息。系统在这发生之前会一直阻塞。
使用 {active,once} 选项,用户可以实现高层次的数据流控制(有时叫交通管制),同时又防止了服务器被过多的消息洪水所淹没。

inet:peername(Socket) -> {ok,{IP_Address,Port}} | {error,Why}
返回连接另一端的IP地址和端口号,以便服务器找到对方的地址。IP_Address是一个元组的整数形如 {N1,N2,N3,N4} ,而 {K1,K2,K3,K4,K5,K6,K7,K8} 则是IPv6的地址。这里的整数取值范围是0到255。

Socket错误处理:
每个socket拥有控制进程(就是创建socket的进程)。如果控制进程死掉了,那么socket也会自动关闭。例如,一个客户端和服务器,而服务器因为编程错误死掉了。那么服务器拥有的socket会自动关闭,而客户端会收到 {tcp_closed,Socket} 消息。

你可能感兴趣的:(Erlang)