1.使用TCP从服务器获取数据
%%socket_examples.erl
-module(socket_examples).
-export([nano_get_url/0,nano_get_url/1,receive_data/2]).
nano_get_url() ->
nano_get_url("www.baidu.com").
nano_get_url(Host) ->
{ok,Socket}=gen_tcp:connect(Host,80,[binary,{packet,0}]),
ok=gen_tcp:send(Socket,"Get / HTTP/1.0\r\n\r\n"),
receive_data(Socket,[]).
receive_data(Socket,SoFar) ->
receive
{tcp,Socket,Bin} ->
receive_data(Socket,[Bin|SoFar]);
{tcp_closed,Socket} ->
list_to_binary(lists:reverse(SoFar))
end.
- 工作方式:
- 1)调用gen_tcp:connect来打开一个到www.baidu.com80端口的TCP套接字。连接调用里的binary参数告诉系统要以“二进制”模式打开套接字,并把所有数据用二进制型传给应用程序。{packet,0}的意思是把未经修改的TCP数据直接传给应用程序。
- 2) 调用gen_tcp:send,把消息GET / HTTP/1.0\r\n\r\n发送给套接字,然后等待回复。这个回复并不是放在一个数据包里,而是分成多个片段,一次发送一点。这些片段会被接收成为消息序列,发送给打开(或控制)套接字的进程。
- 3)收到一个{tcp,Socket,Bin}消息。这个元组的第三个参数是一个二进制型,原因是打开套接字时使用了二进制模式。这个消息是Web服务器发送给我们的数据片段之一。把它添加到目前已收到的片段列表中,然后等待下一个片段。
- 4)收到一个{tcp_closed, Socket}消息。这会在服务器完成数据发送时发生。
- 5)当所有片段都到达后,因为它们的保存顺序是错误的,所以反转它们并连接所有片段。
- 调用示例:B=socket_examples:nano_get_url().
- 转换输出格式: io:format("~p~n",[B]). 或是 string:tokens(binary_to_list(B),"\r\n").
2.一个简单的TCP服务器
%%socket_server.erl:
-module(socket_server).
-export([start_nano_server/0,loop/1,nano_client_eval/1]).
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.
nano_client_eval(Str) ->
{ok,Socket}=
gen_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.
- 工作方式:
- 1)首先,调用gen_tcp:listen来监听2345端口的连接,并设置消息的打包约定。{packet,4}的意思是每个应用程序消息前部都有一个4字节的长度包头。然后gen_tcp:listen(..)会返回{ok, Listen}或{error, Why},但我们只关心能够打开套接字的返回值。因此,编写如下代码 {ok,Listen}=gen_tcp:listen(....).
- 这会让程序在gen_tcp:listen返回{error, ...}时抛出一个模式匹配异常错误。在成功的情况下,这个语句会绑定Listen到刚监听的套接字上。我们只能对监听端口做一件事,那就是把它用作gen_tcp:accept的参数。
- 2)现在调用gen_tcp:accept(Listen)。在这个阶段,程序会挂起并等待一个连接。当我们收到连接时,这个函数就会返回变量Socket,它绑定了可以与连接客户端通信的套接字。
- 3)在accept返回后立即调用gen_tcp:close(Listen)。这样就关闭了监听套接字,使服务器不再接收任何新连接。这么做不会影响现有连接,只会阻止新连接。
- 4)解码输入数据
- 5)然后执行字符串
- 6)然后编码回复数据并把它发回套接字
- 7)同时定义一个客户端.
- 应用示例:(注意要开启两个shell窗口)
- socket_server:start_nano_server().
- socket_server:nano_client_eval("lsit_to_tuple([2+3*4,10+20])").
3.顺序和并行服务器
- 1)顺序服务器
将源代码: start_nano_server() -> {ok,Listen}=gen_tcp:listen(....), {ok,Socket}=gen_tcp:accept(Listen), loop(Socket).... 修改为: 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). loop(....) .... %和以前一样
- 2)并行服务器:每当gen_tcp:accept收到一个新连接时就立即分裂一个新进程
将源码: 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). loop(...).....%和之前一样
4.注意点:
- 1)创建某个套接字(通过调用gen_tcp:accept或gen_tcp:connect)的进程被称为该套接字的控制进程。所有来自套接字的消息都会被发送到控制进程。如果控制进程挂了,套接字就会被关闭。某个套接字的控制进程可以通过调用gen_tcp:controlling_process(Socket, NewPid)修改成NewPid
- 2)我们的并行服务器可能会创建出几千个连接,所以可以限制最大同时连接数。实现的方法可以是维护一个计数器来统计任一时刻有多少活动连接。每当收到一个新连接时就让计数器加1,每当一个连接结束时就让它减1。可以用它来限制系统里的同时连接总数。
- 3)接受一个连接后,显式设置必要的套接字选项是一种很好的做法,就像这样:
{ok,Socket}=gen_tcp:accept(Listen), inet:setopts(Socket,[{packet,4},binary,{nodelay,true},{active,true}]), loop(Socket)
- 4)Erlang 的 R11B-3 版开始允许多个 Erlang 进程对同一个监听套接字调用 gen_tcp:accept/1。这让编写并行服务器变得简单了,因为你可以生成一个预先分裂好的进程池,让它们都处在gen_tcp:accept/1的等待状态。
5.主动和被动套接字
- 1)三种打开模式:主动(active),单次主动(active once) ,被动(passive) . 通过在gen_tcp:connect(Address, Port, Options)或gen_tcp:listen(Port, Options)的Options参数里加入{active, true | false | once}选项实现的。
- 2)主动信息接收(非阻塞式)
{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. %当客户端生成数据的速度快于服务器处理数据的速度,系统就会遭受数据洪流的冲击
- 3)被动信息接收(阻塞式)
{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. %每次想要接收数据就调用gen_tcp:recv,否则客户端会一直阻塞.
- 4)混合信息接收(部分阻塞式)
{ok,Listen}=gen_tcp:listen(Port,[....,{active,once}...]), {ok,Socket}=gen_tcp:accept(Listen), loop(Socket). loop(Socket) -> receive {tcp,Socket,Data} -> .......对数据进行操作.... %当你准备好启动下一个信息的接收时 inet:setopts(Sock,[{active,once}]), loop(Socket); {tcp_closed,Socket}-> ..... end. %当控制进程收到一个消息后,必须显式调用inet:setopts才能重启下一个消息的接收,否则系统处于阻塞状态.
6.套接字错误处理
- 当服务器因为程序错误挂了,那么服务器支配的套接字就会被自动关闭,同时向客户端发送一个{tcp_closed,Socket}消息
7.UDP
%应用示例:一个UDP阶乘服务器
-module(udp_test).
-export([start_server/0,client/1]).
start_server() ->
spawn(fun() -> server(4000) end).
%服务器
server(Port) ->
{ok,Socket} =gen_udp:open(Port,[binary]),
io:format("server opened socket:~p~n",[Socket]),
loop(Socket).
loop(Socket) ->
receive
{udp,Socket,Host,Port,Bin}=Msg ->
io:format("server received:~p~n",[Msg]),
N=binary_to_term(Bin),
Fac=fac(N),
gen_udp:send(Socket,Host,Port,term_to_binary(Fac)),
loop(Socket)
end.
fac(0)->1;
fac(N) ->N* fac(N-1).
%客户端
client(N) ->
{ok,Socket}=gen_udp:open(0,[binary]),
io:format("client opened socket=~p~n",[Socket]),
ok=gen_udp:send(Socket,"localhost",4000,term_to_binary(N)),
Value=receive
{udp,Socket,_,_,Bin}=Msg ->
io:format("client received:~p~n",[Msg]),
binary_to_term(Bin)
after 2000 ->
0
end,
gen_udp:close(Socket),
value.
8.UDP数据包可能会二次传输,所以可调用Erlang的内置函数make_ref,可以确保返回一个全局唯一的引用.