【2】文件编程(二)、套接字编程

本文深入探讨Erlang中的文件编程,包括写入文件的不同方法、目录操作和文件操作。接着转向套接字编程,介绍TCP和UDP服务器的实现,涵盖主动和被动套接字模式及其优缺点,以及错误处理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 文件编程(二)

写入文件的各种方式

1.把各行写入文件

2.写入整个文件

3.写入随机访问文件

目录操作

文件操作

 套接字编程

一个简单的TCP服务器

顺序和并行服务器

主动和被动套接字

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

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

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

套接字错误处理

一个UDP阶乘服务器

对多台机器广播


 文件编程(二)

写入文件的各种方式

1.把各行写入文件

io:format承担了创建格式化输出的重任。要生成格式化输出,我们会做以下调用。

-spec io:format(IoDevice, Format, Args) -> ok.
%% 其中ioDevice是一个I/O对象(必须以write模式打开),Format是一个包含格式代码的字符串,Args是待输出的项目列表。

Args里的每一项都必须对应格式字符串里的某个格式命令。格式命令以一个波浪字符(~)开头。

1> {ok, S} = file:open("test.dat", write).
{ok,<0.87.0>}
2> io:format(S, "~s~n", ["hello"]).
ok
3> io:format(S, "~s~n", [hello]).
ok
4> io:format(S, "~p~n", [123]).
ok
5> io:format(S, "~p", [7]).
ok
6> io:format(S, "~p~n", [8]).
ok
7>

 得到文件test.dat

hello
hello
123
78

2.写入整个文件

file:write_file(File, IO)会把IO里的数据(一个I/O列表)写入File。

I/O列表是一个元素为I/O列表、二进制型或0到255整数的列表。

I/O列表在输出时会被自动“扁平化”,意思是所有的列表括号都会被移除。

3.写入随机访问文件

file:pwrite(IoDev, Position, Bin) 和随机访问读相似。

1> {ok, S} = file:open("test.dat", [raw, write, binary]).
{ok,{file_descriptor,prim_file,
                     #{handle => #Ref<0.3645283694.4081713185.82801>,
                       owner => <0.85.0>,
                       r_buffer => #Ref<0.3645283694.4081713160.83160>,
                       r_ahead_size => 0}}}
2> file:pwrite(S, 3, <<"new">>).
ok
3>file:pwrite(S, 10, <<"10">>).
ok

在刚才有内容的文件中写入:第一次写会清除原文件中所有内容(试出来是这样)

在idea中显示:

 复制到文本实际上其中有空格:

   new    10

目录操作

list_dir(Dir) 用来生成一个Dir里的文件列表

make_dir(Dir) 创建一个新目录

del_dir(Dir) 删除一个目录。

6> file:list_dir(".").
{ok,["data1.dat","demo1.erl","test.dat"]}
7> file:make_dir("abc").
ok
8> file:list_dir(".").
{ok,["abc","data1.dat","demo1.erl","test.dat"]}
9> file:del_dir("abc").
ok
10> file:list_dir(".").
{ok,["data1.dat","demo1.erl","test.dat"]}
11> file:del_dir("test.dat").
{error,enotdir}

文件操作

file:read_file_info(F) F是一个合法的文件或目录名,返回{ok, Info}。Info是一个#file_info类型的记录。

filelib:file_size(File) 获取文件或目录大小

filelib:is_dir(X) 判断是否是目录

file:copy(Source, Destination) 把文件Source复制到Destination(包含复制后文件名)里。返回{ok, 文件大小}

file:delete(File) 删除File。

3> file:list_dir(".").
{ok,["a","data1.dat","demo1.erl","test.dat"]}
4> file:read_file_info("data1.dat").
{ok,{file_info,185,regular,read_write,
               {{2023,7,7},{20,14,44}},
               {{2023,7,6},{22,5,34}},
               {{2023,7,6},{21,54,57}},
               33206,1,4,0,0,0,0}}
5> file:read_file_info("a").
{ok,{file_info,0,directory,read_write,
               {{2023,7,11},{20,23,23}},
               {{2023,7,11},{20,23,23}},
               {{2023,7,11},{20,23,23}},
               16895,1,4,0,0,0,0}}
6> filelib:file_size("data1.dat").
185
7> filelib:file_size("a").
0
8> filelib:is_dir("a").
true
9> file:copy("data1.dat","a").
{error,eisdir}
10> file:copy("data1.dat","a/data").
{ok,185}
11> file:list_dir("a").
{ok,["data"]}
12> file:read_file_info("a/data").
{ok,{file_info,185,regular,read_write,
               {{2023,7,11},{20,27,4}},
               {{2023,7,11},{20,26,44}},
               {{2023,7,11},{20,26,44}},
               33206,1,4,0,0,0,0}}
13> file:delete("a").
{error,eperm}
14> file:delete("a/data").
ok

 套接字编程

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

 一个小试验:使用TCP从服务器获取数据

socket.erl

nano_get_url() ->
  nano_get_url("www.baidu.com").
nano_get_url(Host) ->
  %% 打开一个到Host 80端口的TCP套接字。
  %% binary:以“二进制”模式打开套接字,并把所有数据用二进制型传给应用程序。
  %% {packet,0}:把未经修改的TCP数据直接传给应用程序。
  {ok, Socket} = gen_tcp:connect(Host, 80, [binary, {packet, 0}]),
  %% 把消息”GET / HTTP/1.0\r\n\r\n“发送给套接字
  ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"),
  %% 接受返回的消息,消息分成多个片段,一次发送一点。
  receive_data(Socket, []).

receive_data(Socket, SoFar) ->
  receive
    %% Web服务器发送给我们的数据片段之一。把它添加到目前已收到的片段列表中,然后等待下一个片段。
    {tcp,Socket,Bin} ->
      receive_data(Socket, [Bin | SoFar]);
    %% 服务器完成数据发送
    {tcp_closed, Socket} ->
      list_to_binary(lists:reverse(SoFar))
  end.

运行: 

3> B = socket:nano_get_url().
<<"HTTP/1.0 200 OK\r\nAccept-Ranges: bytes\r\nCache-Control: no-cache\r\nContent-Length: 9508\r\nContent-Security-Policy: frame"...>>
4> 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,
  82,97,110,103,101,115,58,32,98,121,116,101,115,13,10,67,97,99,104,101,45,67,
  111,110,116,114,111,108,58,32,110,111,45,99,97,99,104,101,13,10,67,111,110,
  ...省略一堆数字...
ok
5> string:tokens(binary_to_list(B),"\r\n").
["HTTP/1.0 200 OK","Accept-Ranges: bytes",
 "Cache-Control: no-cache","Content-Length: 9508",
 "Content-Security-Policy: frame-ancestors 'self' https://chat.baidu.com http://mirror-chat.baidu.com https://fj-chat.baidu.com https://hba-chat.baidu.com https://hbe-chat.baidu.com ht
tps://njjs-chat.baidu.com https://nj-chat.baidu.com https://hna-chat.baidu.com https://hnb-chat.baidu.com http://debug.baidu-int.com;",
 "Content-Type: text/html",
 "Date: Tue, 11 Jul 2023 13:13:34 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=21792DD930B801337402A950E3E25364:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com",
 "Set-Cookie: BIDUPSID=21792DD930B801337402A950E3E25364; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com",
 "Set-Cookie: PSTM=1689081214; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com",
 "Set-Cookie: BAIDUID=21792DD930B80133BC7A433F2D6F74AD:FG=1; max-age=31536000; expires=Wed, 10-Jul-24 13:13:34 GMT; domain=.baidu.com; path=/; version=1; comment=bd",
 "Traceid: 168908121428493012587665399977752381857",
 "Vary: Accept-Encoding","X-Ua-Compatible: IE=Edge,chrome=1",
 [60,33,68,79,67,84,89,80,69,32|...]]
7>

一个简单的TCP服务器

TCP套接字数据在传输过程中可以被打散成任意大小的片段,gen_tcp:connect和gen_tcp:listen函数里参数{packet, N},就表示多少数据代表一个请求或响应。

packet这个词在这里指的是应用程序请求或响应消息的长度,而不是网络上的实际数据包。客户端和服务器使用的packet参数必须一致。如果启动服务器时用了{packet,2},客户端用了{packet,4},程序就会失败。

用{packet,N}选项打开一个套接字后,无需担心数据碎片的问题。Erlang驱动会确保所有碎片化的数据消息首先被重组成正确的长度,然后才会传给应用程序。

这个最简单的服务器演示了如何打包和编码应用程序数据。它接收一个请求,计算出回复, 发送回复,然后终止。

socket.erl

-export([start_nano_server/0, nano_client_eval/1]).

%% 服务端
start_nano_server() ->
  %% gen_tcp:listen来监听2345端口的连接,并设置消息的打包约定。
  %% {packet, 4}的意思是每个应用程序消息前部都有一个4字节的长度包头。
  {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]),
  %% 调用gen_tcp:accept(Listen)。在这个阶段,程序会挂起并等待一个连接。
  %% 收到连接时,这个函数就会返回变量Socket,它绑定了可以与连接客户端通信的套接字。
  {ok, Socket} = gen_tcp:accept(Listen),
  %% 返回后立即调用gen_tcp:close(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> socket:start_nano_server().

客户端:执行后收到服务端的回复,并打印了结果

1> socket:nano_client_eval("list_to_tuple([2+3*4, 10+20])").
Client recieved binary = <<131,104,2,97,14,97,30>>
Client result = {14,30}
ok

服务端:接收到了客户端的信息

1> socket:start_nano_server().
Server recieved binary = <<131,107,0,29,108,105,115,116,95,116,111,95,116,117,
                           112,108,101,40,91,50,43,51,42,52,44,32,49,48,43,50,
                           48,93,41>>
Server (unpacked) "list_to_tuple([2+3*4, 10+20])"
Server replying = {14,30}
Server socket closed
ok
2>

顺序和并行服务器

顺序服务器:一次接收一个连接

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

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

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

在代码中的区别就是顺序服务器的下一次接收是在loop之后,而并行服务器是在loop之前开启一个线程继续接收。

主动和被动套接字

Erlang的套接字可以有三种打开模式:主动(active)、单次主动(active once)或被动(passive)。

通过在gen_tcp:connect(Address, Port, Options)或gen_tcp:listen(Port, Options) 的Options参数里加入{active, true | false | once}选项实现。

true为主动,false为被动,once表示主动接收一个消息,接收完后必须重新启用才能接收下一个消息。

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

当一个主动套接字被创建后,它会在收到数据时向控制进程发送{tcp, Socket, Data}消息。控制进程无法控制这些消息流。如果客户端生成数据的速度快于服务器处理数据的速度,系统就会遭受数据洪流的冲击:消息缓冲区会被塞满,系统可能会崩溃或表现异常。

之前的例子就是这种。

{ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]), %% 主动消息接收

..

loop(Socket) ->
  receive
    {tcp, Socket, Bin} ->
      ... %% 接收 对数据进行操作
    {tcp_closed, Socket} ->
      ...
  end.

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

如果一个套接字是用被动模式打开的,控制进程就必须调用gen_tcp:recv(Socket, N)来从这个套接字接收数据。然后它会尝试从套接字接收N个字节。如果N = 0,套接字就会返回所有可用的字节。

服务器循环里的代码会在每次想要接收数据时调用gen_tcp:recv。客户端会一直被阻塞,直到服务器调用recv为止。

操作系统有自己的缓冲设置,即使没有调用recv,客户端也能在阻塞前发送少量数据。

这个服务器不会因为某个过激的客户端试图用过量数据冲击它而崩溃。

{ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, false}]), %% 被动消息接收

...

loop(Socket) ->
  case gen_tcp:recv(Socket, 10) of %% 接收10个字节
    {ok, B} ->
      ... %% 对数据进行操作
      loop(Socket);
    {error, closed} ->
      ...
  end

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

套接字在这个模式下虽然是主动的,但只针对一个消息。当控制进程收到一个消息后,必须显式调用inet:setopts才能重启下一个消息的接收,在此之前系统会处于阻塞状态。这种方法集合了前两种模式的优点。

{ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, once}]), %% 混合消息接收

...

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

套接字错误处理

如果服务器因为程序错误挂了,那么服务器支配的套接字就会被自动关闭,同时向客户端发送一个{tcp_closed, Socket} 消息。


一个UDP阶乘服务器

服务器无需担心如何让进程接收“套接字关闭”的消息。

客户端只是打开一个UDP套接字,向服务器发送一个消息,等待回复(或者超时),然后关闭套接字并返回服务器的返回值。必须设置一个超时,因为UDP是不可靠的,可能会得不到回复。

-export([start_server/0, client/1]).

start_server() ->
  %% 服务器端口为4000
  spawn(fun() -> server(4000) end).

%% 服务器
server(Port) ->
  %% 用binary模式打开套接字,它告诉驱动要把所有消息以二进制数据的形式发送给控制进程。
  {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]),
  %% 向 localhost:4000 发送消息
  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> udp_test:start_server().
server opened socket:#Port<0.3>
<0.99.0>
2> udp_test:client(10).
client opened socket=#Port<0.4>
server received:{udp,#Port<0.3>,{127,0,0,1},50377,<<131,97,10>>}
client received:{udp,#Port<0.4>,{127,0,0,1},4001,<<131,98,0,55,95,0>>}
3628800

UDP数据包可以传输两次,所以在编写远程过程调用代码时一定要小心。第二次查询得到的回复可能只是第一次查询回复的复制。为防止这类问题,可以修改客户端代码来加入一个唯一的引用,然后检查服务器是否返回了这个引用。要生成一个唯一的引用,需要调用Erlang的内置函数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", 4001, 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 2000 ->
    0
  end.

对多台机器广播

这里使用5010端口来发送广播请求,6000端口用来监听广播。只有发送广播的进程才会打开5010端口,broadcast:send(IoList)会对局域网里的所有机器广播IoList。而网络上的所有机器都会调用broadcast:listen()来打开6000端口并监听广播消息。

-compile(export_all).

send(IoList) ->
  %% 接口名必须正确,而且系统必须支持广播。iMac用"en0"
  case inet:ifget("eth0", [broadaddr]) of
    {ok, [{broadaddr, Ip}]} ->
      {ok, S} =  gen_udp:open(5010, [{broadcast, true}]),
      gen_udp:send(S, Ip, 6000, IoList),
      gen_udp:close(S);
    _ ->
      io:format("Bad interface name, or\n"
      "broadcasting not supported\n")
  end.

listen() ->
  {ok, _} = gen_udp:open(6000),
  loop().
loop() ->
  receive
    Any ->
      io:format("received:~p~n", [Any]),
      loop()
  end.

这里我运行不了,inet:ifget("eth0", [broadaddr]) 的结果为 {error,einval}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值