Erlang——嵌套字编程


套接字编程能让应用程序与互联网上的其他机器交互,比只进行本地操作有更大的空间。

套接字是一种通信信道,让不同的机器能用互联网协议(简称IP)在网上通信。

两个核心互联网协议:传输控制协议(简称TCP)和用户数据报协议(简称UDP)。

UDP能让应用程序相互发送简短的消息(称为数据报),但是并不保证这些消息能成功到达。
它们也可能会不按照发送顺序到达,更快但不可靠。

TCP能提供可靠的字节流,只要连接存在就会按顺序到达。用TCP发送数据的额外开销比用UDP发送数据更大,可靠但更慢。

套接字编程有两个主要的库:gen_tcp用于编写TCP应用程序,gen_udp用于编写UDP应用程序。

使用TCP

以一个简单的顺序型TCP服务器为例,会展示如何对应用程序的数据进行打包和编码。它接收一个请求,计算出一个回应,把回应送回,然后自行关闭。

从服务器上获取数据

代码演示(注意文件名称不能使用socket,会和系统内置模块名字冲突):

-module(socket1).
%%%=======================EXPORT=======================
-export([nano_get_url/0]).
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,打开一个TCP套接字,它会连接到http://www.baidu.com的80端口上。连接调用的参数binary告诉系统,以binary模式打开套接字。在这个模式下,应用程序之间的数据传输都是二进制格式的。{packet,0}意味着Erlang系统会把TCP数据原封不动的直接传送给应用程序。
(2)通过调用 gen_tcp:send将GET / HTTP/1.0\r\n\r\n发送到套接字。之后等待回应。回应消息通常不会在一个数据包内全部返回,而是一帧一帧地返回,每帧一部分数据,程序以顺序行消息的方式收到这些数据帧,并把它转发给打开套接字的进程。
(3)程序会收到一个{tcp,Socket,Bin}消息。Bin是二进制类型,这就是我们为什么以二进制模型打开套接字。这个消息只是外部服务器发给程序的众多数据分之一,程序会把它添加到一个列表之中,这个列表汇聚程序目前接收到的所有数据,然后程序继续等待接收下一帧。
(4)当收到一个{tcp_closed, Socket}消息时,表明外部服务器已停止向程序发送消息。
(5)因为接收时我们是以错误的顺序来存储这些数据帧的,所以在所有的数据帧都接收完毕之后,需要调整顺序并拼接所有数据帧。

receive_data函数在数据帧到达时,程序只是简单的把数据帧加入列表SoFar的头部,程序接收完所有帧并且嵌套字也被关掉之后,程序再反转列表将它们拼接起来。

重整数据帧部分的代码,我们不用如下这种拼接的方式,因为要持续的向一个缓冲区中追加新的二进制数据,这个行为会产生很多无用的数据副本。

receive_data(Socket, SoFar) ->
  receive
    {tcp,Socket,Bin} ->
      receive_data(Socket, list_to_binary([SoFar,Bin]));
    {tcp_closed, Socket} ->
      SoFar
  end.

所以这是最好的方法:在一个列表中收集所有的数据帧,注意数据帧都添加完时顺序是逆序的,然后翻转整个列表,把所有数据在一个操作内拼接起来。

执行结果:

3> c(socket1).
{ok,socket1}
4> B = socket1:nano_get_url().
<<"HTTP/1.0 200 OK\r\nAccept-Ranges: bytes\r\nCache-Control: no-cache\r\nContent-Length: 9508\r\nContent-Type: text/html\r\nDate:"...>>
5> io:format("~p~n",[B]).    
<<72,84,84,80,47,49,46,48,32,50,48,48,32,79,75,13,10,65,99,99,101,112,116,45,
  ...
  47,104,116,109,108,62>>
6> string:tokens(binary_to_list(B),"\r\n").
["HTTP/1.0 200 OK","Accept-Ranges: bytes",
 "Cache-Control: no-cache","Content-Length: 9508",
 "Content-Type: text/html",
 "Date: Wed, 20 Sep 2023 15:04:03 GMT",
 "P3p: CP=\" OTI DSP COR IVA OUR IND COM \"",
 "P3p: CP=\" OTI DSP COR IVA OUR IND COM \"",
 "Pragma: no-cache","Server: BWS/1.1",
 "Set-Cookie: BAIDUID=3F53CD0CD1E0D2C394D1256F08954962:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.b
aidu.com",
 "Set-Cookie: BIDUPSID=3F53CD0CD1E0D2C394D1256F08954962; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu
.com",
 "Set-Cookie: PSTM=1695222243; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com",
 "Set-Cookie: BAIDUID=3F53CD0CD1E0D2C326284A0E92F9B1AD:FG=1; max-age=31536000; expires=Thu, 19-Sep-24 15:04:03 GMT; domain=.baidu.com; 
path=/; version=1; comment=bd",
 "Traceid: 169522224304226053226872194239021866982",
 "Vary: Accept-Encoding","X-Ua-Compatible: IE=Edge,chrome=1",
 [60,33,68,79,67,84,89,80,69,32,104|...]]

一个简单的TCP服务器

TCP套接字的数据是一种无差异的节流数据,在传输中可以被切分为任意尺寸的数据帧,所以我们要做一些转换才能让程序知道单个请求或响应到底还有多少数据。

在Erlang中,我们使用的转换方法就是在每一个逻辑请求或响应前都带上一个N(1或2或4)字节长的长度计数,这就是gen_tcp:connect和gen_tcp:listen函数中参数的含义。服务器和客户端使用的packet参数必须一致,如果服务器嵌套字以 {packet, 2}方式打开,而客户端以 {packet, 4}打开程序就不能收到任何有用的消息。

使用{packet, N}选项打开一个套接字后,我们就无需担心数据帧了。Erlang的内置驱动器会保证在把数据发送到应用程序之前,所有数据消息的数据帧都会按照其长度正确接收。

我们主要用term_to_binary函数来对Erlang数据项进行编码,用它的反函数binary_to_term来对数据进行解码。

仅需两行代码就能实现客户端和服务器通信所需的数据包规则和编码规则。所以我们整个流程就是使用{packet, 4}选项来打开一个套接字,然后使用term_to_binary和它的反函数来对呀数据进行编码和解码。这种操作在处理基于文本的协议,如HTTP或XML之类的领域中,有极大的优势。在小规模数据处理上,速度要快出一个量级。

socket_examples.erl文件内
代码演示:

-module(socket_examples).

-export([start_nano_server/0, 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 recieved binary = ~p~n", [Bin]),
			Str = binary_to_term(Bin),
			io:format("Server (unpacked) ~p~n", [Str]),
			Reply = 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.

string2value(Str) ->
	{ok, Tokens, _} = erl_scan:string(Str ++ "."),
	{ok, Exprs} = erl_parse:parse_exprs(Tokens),
	Bindings = erl_eval:new_bindings(),
	{value, Value, _} = erl_eval:exprs(Exprs, Bindings),
	Value.
%% 客户端
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 recieved 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, Socket}或{error, Why},但我们关心的只是可以返回给我们一个已打开套接字的情况。
所以写为{ok, Listen} = gen_tcp:listen()。如果返回{error, Why}的话,这句话会导致一个模式匹配的异常。正常情况下,Listen变量与监听到的套接字绑定起来。接下来我们把套接字作为一个参数送入一个gen_tcp:accept。
(2)调用gen_tcp:accept(Listen)时,程序在这里暂停并等待一个连接。一个新连接建立起来是,函数会返回变量Socket,变量绑定到新建连接的套接字上,通过这个套接字,服务器就可以与那个发起连接的客户机进行通信。
(3)当accept函数返回之后,立刻调用gen_tcp:close(Listen)。来关闭监听套接字,之后服务器不会继续处于监听状态,也无法建立任何新连接。此操作不会影响已建立起来的连接,他只是阻止新连接的建立。
(4)程序对输入数据进行解码(反整编)。
(5)对字符串进行求值。
(6)对回应消息进行编码(整编)然后把它发回套接字。
此程序只会接收单个请求,一旦程序运行完成,就不会继续接收新的连接。

要在一台机器上同时运行客户端和服务端,我们要打开两个终端窗口,每个窗口上都启动一个Erlang shell。

服务端:

3> c(socket_examples).
{ok,socket_examples}
4> socket_examples:start_nano_server().

服务端没有输出,因为还没有接收到任何数据,等待接收消息。

客户端:

1> c(socket_examples).
{ok,socket_examples}
2> socket_examples:nano_client_eval("list_to_tuple([1+2*2,1212+923])").
Client recieved binary = <<131,104,2,97,5,98,0,0,8,87>>
Client result = {5,2135}
ok

服务端接收到消息,就会看到如下显示:

4> socket_examples:start_nano_server().
Server recieved binary = <<131,107,0,31,108,105,115,116,95,116,111,95,116,117,
                           112,108,101,40,91,49,43,50,42,50,44,49,50,49,50,43,
                           57,50,51,93,41>>                                   
Server (unpacked) "list_to_tuple([1+2*2,1212+923])"
Server replying = {5,2135}                         
Server socket closed                               
ok

改进服务器

顺序型服务器(一次接收一个连接)

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

我们不仅只服务一个请求,这就要让监听端口继续打开而不是调用gen_tcp:close(Listen)。另外不同的就是当运行完成后,我们会再次调用seq_loop(Listen),他会继续等待下一个客户端的连接。
服务器忙于服务一个现存的连接时,如果又有新的客户机尝试连接服务器,这个连接就会在服务器上排队,直到服务器完成对所有现有连接的服务为止。如果等待队列中连接数目超过监听套接字的能力,那么这个连接会被拒绝。
关闭服务器只要终止已经启动的服务器进程或服务器进程组,gen_tcp会在自己的控制进程之间建立连接,如果控制进程消亡,也会关闭对应的套接字.

并行服务器(同时接收多个并行连接)

start_parallel_server() ->
  {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]),
  spawn(fun() -> par_connect(Listen) end).
 
par_connect(Listen) ->
  {ok, Socket} = gen_tcp:accept(Listen),
  spawn(fun() -> par_connect(Listen) end),
  loop(Socket).

与顺序型服务器不同的是,添加了启动进程的代码,它会确保程序为每一个新建的套接字连接,创建一个新的进程。

控制逻辑

Erlang的套接字三种打开模式:active、active once或被动passive。
通过在gen_tcp:connect(Address, Port, Options)或gen_tcp:listen(Port, Options) 的Options参数里加入{active, true | false | once}选项实现。
{active, true}:创建一个主动套接字;
{active, false}:创建一个被动套接字;
{active, once}:创建一个主动套接字,但这个套接字只接受一条消息,接收完这条消息之后,如果想让他接收下一条消息,必须再次激活他;

1.主动消息接收(非阻塞式)

{ok, Listen} = gen_tcp:listen(Port, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]),
{ok, Socket} =gen_tcp:accept(listen),
loop(Socket).
loop(Socket) ->
  receive
    {tcp, Socket, Data} ->
      ... 
    {tcp_closed, Socket} ->
      ...
  end.

这个进程无法控制服务器循环中的消息流,如果客户端发送的数据快过服务器可以处理的速度,系统就会被消息淹没,消息缓冲区会被塞满,系统可能会莫名其妙的崩溃。
这个类型的服务器叫异步服务器,它不会阻塞客户端,只有我们可以确信服务端的性能,能够跟上客户机的需求时才应该选择使用异步服务器。

2.被动型消息接收(阻塞式)

{ok, Listen} = gen_tcp:listen(Port, [binary, {packet, 4}, {reuseaddr, true}, {active, once}]), 
{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。调用recv这个函数时,客户端会被堵塞。不过调用recv这个函数前,操作系统还会做一些缓存,允许客户机继续发少量的数据,然后将才其阻塞。

3.混合消息接收(部分阻塞式)

{ok, Listen} = gen_tcp:listen(Port, [binary, {packet, 4}, {reuseaddr, true}, {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}选项打开套接字。这个模式中,套接字是主动的,但仅仅针对一个消息。控制进程发过一个消息后必须显式地调用inet:setopts函数来把它重新激活,以便接收到下一个消息。在此操作之前系统会处于封闭状态。所以这种模式兼具了主动型,被动型两种方式的优点。

连接

inet:peername(Socket) -> {ok, {Address, Port}} | {error, posix()}
函数可以返回该连接另外一端的IP地址和端口号,服务器就可以发现是谁发起了这些连接。Address使用一个整型元组{N1,N2,N3,N4}表示IPV4的IP地址,使用{K1,K2,K3,K4,K5,K6,K7,K8}表示IPV6的IP地址。Ni和Ki表示的都是0~255的整型数。

UDP

UDP(用户数据报协议)。任意两台计算机之间都可以使用UDP协议来发送叫报文的短消息。UDP报文并不可靠,客户端向服务器发送一串UDP报文,这些数据可能是乱序到达的,也可能完全不会出错,有时报文还会很频繁。若单个数据能到达目的地,他必定是完整的。大型的数据报会被切分成小块数据帧,底层IP协议会在这些数据报发送之前对其进行重组。
UDP是无连接协议,客户机在发送消息之前无需向服务器发起一个连接,无需费心于如何维护客户机与服务器的连接。UDP很适合有大量用户向服务器发送短消息的应用程序。

简单的UDP服务器和客户机

UDP服务器:

server(Port)->
  {ok, Socket} =gen_udp:open(Port, [binary]),
  loop(Socket).
loop(Socket) ->
  receive
    {udp, Socket, Host,Port,Bin} ->
      BinReply=...,
      gen_udp:send(Socket, Host,Port,BinReply),
      loop(Socket)
  end.

不关心进程是否会收到“socket closed”消息。注意打开一个套接字时使用的是二进制模式,意味着系统驱动程序向控制进程发送的消息都是二进制数据。

UDP客户机:

client(Request) ->
  {ok, Socket} = gen_udp:open(0, [binary]),
  ok = gen_udp:send(Socket, "localhost", 4000, Request),
  Value = receive
            {udp, Socket, _, _, Bin} = Msg ->
              {ok,Bin}
          after 2000 -> 
              error
          end,
  gen_udp:close(Socket),
  Value

客户机只是打开了一个UDP套接字,向服务器发送消息,等待一个回应(或者超时),然后关闭套接字返回由服务器传回的值。
必须在这个程序之中设置一个超时,因为UDP协议的传输是不可靠的,我们有可能得不到来自服务器的回应。

计算阶乘的UDP服务器

代码演示:

-module(udp_test).
%%%=======================EXPORT=======================
-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.

执行结果:

1> c(udp_test).
{ok,udp_test}
2> udp_test:start_server().
<0.84.0>                          
3> server opened socket:#Port<0.5>
3> udp_test:client(12).           
client opened socket=#Port<0.6>
server received:{udp,#Port<0.5>,{127,0,0,1},51074,<<131,97,12>>}
client received:{udp,#Port<0.6>,{127,0,0,1},4000,<<131,98,28,140,252,0>>}
479001600  

关于UDP的注意事项

UDP是无连接协议,服务器没有机会阻塞客户端,即拒绝从客户端接收数据,因为服务器无法得知谁是客户端。
大型数据报通过网络传输时,可能会被切分为多个帧。UDP数据报在网络传输中,如果UDP数据报大小超过他所经过路由器设定的最大传输单元(MTU)限制时,就会出现分帧的情况。在优化UDP的网络应用中,最常见的解决方案是从一个较小的数据报长度开始(如500字节左右),然后一边测试吞吐量一边逐渐地增大数据报的长度。若在某一时刻吞吐量明显下降,则可知此时的数据报可能是过大了。
一个UDP数据报可能被传输两次,必须小心地编写远程过程调用的代码。可能会有这种情况:在回应第二条消息的时候,实际上只回应了第一条消息的复制品。为了避免,我们应该修改客户端代码,为其加入一个唯一的引用,然后在服务器返回时去校验这个引用。为保证引用的唯一性,我们可以调用Erlang的BIF make_ref,这个函数保证会返回一个全球唯一的引用。增加这些逻辑之后:

client(N) ->
  {ok, Socket} = gen_udp:open(0, [binary]),
  io:format("client opened socket=~p~n",[Socket]),
  Ref = make_ref(),	%% 生成一个唯一引用
  B1 = term_to_binary({Ref, N}),
  ok = gen_udp:send(Socket, "localhost", 4000, B1),
  wait_for_ref(Socket, Ref).
 
wait_for_ref(Socket, Ref) ->
  receive
    {udp, Socket, _, _, Bin} ->
      case binary_to_term(Bin) of
        {Ref, Val} -> %% 正确的Ref,得到的时正确的值
          Val;
        {_SomeOtherRef, _} -> %% 其他则丢弃
          wait_for_ref(Socket, Ref)
      end
  after 1000 ->
    0
  end.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值