强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

15 - IO 与网络

第 15 章:IO 与网络

本章学习文件操作、TCP/UDP Socket 编程和二进制数据处理——构建网络应用的基础。


15.1 文件操作

15.1.1 读写文件

%% 写文件
file:write_file("hello.txt", "Hello, Erlang!").

%% 读文件
{ok, Data} = file:read_file("hello.txt").
%% Data = <<"Hello, Erlang!">>

%% 追加写入
file:write_file("log.txt", "New line\n", [append]).

%% 文件不存在时返回错误
{error, enoent} = file:read_file("nonexistent.txt").

15.1.2 逐行读取

%% 打开文件获取 Handle
{ok, Handle} = file:open("data.txt", [read, {encoding, utf8}]).

%% 逐行读取
read_lines(Handle) ->
    case file:read_line(Handle) of
        {ok, Line} ->
            io:format("Line: ~s", [Line]),
            read_lines(Handle);
        eof ->
            ok
    end.

%% 关闭文件
file:close(Handle).

%% 使用 file:consult 读取 Erlang terms
{ok, Terms} = file:consult("config.erl").

15.1.3 文件操作速查

函数作用
file:read_file(Path)读取整个文件为 binary
file:write_file(Path, Data)写入文件
file:open(Path, Modes)打开文件
file:close(Handle)关闭文件
file:read_line(Handle)读取一行
file:write(Handle, Data)写入数据
file:read(Handle, N)读取 N 字节
file:list_dir(Path)列出目录
file:make_dir(Path)创建目录
file:delete(Path)删除文件
file:copy(Source, Dest)复制文件
file:rename(Old, New)重命名
file:file_info(Path)文件信息
file:consult(Path)读取 Erlang terms
file:is_file(Path)是否是文件
file:is_dir(Path)是否是目录

15.1.4 文件操作模式

模式说明
read只读
write只写(创建或覆盖)
append追加
{encoding, utf8}UTF-8 编码
binary二进制模式
raw无缓冲(更快)
compressed压缩读写

15.1.5 目录操作

%% 列出目录
{ok, Files} = file:list_dir(".").

%% 递归遍历目录
-spec find_files(string()) -> [string()].
find_files(Dir) ->
    {ok, Files} = file:list_dir(Dir),
    lists:flatmap(fun(File) ->
        Path = filename:join(Dir, File),
        case filelib:is_dir(Path) of
            true  -> find_files(Path);
            false -> [Path]
        end
    end, Files).

%% 使用 filelib
filelib:wildcard("src/**/*.erl").  %% 通配符匹配
filelib:is_file("test.txt").       %% 文件存在
filelib:file_size("test.txt").     %% 文件大小

15.2 TCP Socket

15.2.1 TCP 服务器

%% tcp_server.erl
-module(tcp_server).
-export([start/1, acceptor/1]).

start(Port) ->
    {ok, ListenSocket} = gen_tcp:listen(Port, [
        binary,
        {packet, 0},
        {active, false},
        {reuseaddr, true}
    ]),
    io:format("Server listening on port ~p~n", [Port]),
    spawn(fun() -> acceptor(ListenSocket) end).

acceptor(ListenSocket) ->
    {ok, ClientSocket} = gen_tcp:accept(ListenSocket),
    spawn(fun() -> acceptor(ListenSocket) end),
    handle_client(ClientSocket).

handle_client(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            io:format("Received: ~p~n", [Data]),
            gen_tcp:send(Socket, Data),  %% 回显
            handle_client(Socket);
        {error, closed} ->
            io:format("Client disconnected~n"),
            ok
    end.

15.2.2 TCP 客户端

%% tcp_client.erl
-module(tcp_client).
-export([connect/2, send/2, close/1]).

connect(Host, Port) ->
    gen_tcp:connect(Host, Port, [binary, {packet, 0}, {active, false}]).

send(Socket, Data) ->
    gen_tcp:send(Socket, Data),
    gen_tcp:recv(Socket, 0, 5000).

close(Socket) ->
    gen_tcp:close(Socket).

15.2.3 Active vs Passive 模式

模式设置特点
Passive{active, false}阻塞 recv,可控流
Active{active, true}消息自动发送到进程
Active once{active, once}接收一条后自动关闭
%% Active 模式:消息自动发送到进程
{ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, true}]),
receive
    {tcp, Socket, Data} ->
        io:format("Received: ~p~n", [Data]);
    {tcp_closed, Socket} ->
        io:format("Connection closed~n")
end.

%% Active once:接收一条后需要重新激活
setopts(Socket, [{active, once}]),
receive
    {tcp, Socket, Data} ->
        process(Data),
        setopts(Socket, [{active, once}])  %% 重新激活
end.

15.2.4 gen_tcp 选项速查

选项说明推荐值
{packet, 0}原始数据通用
{packet, 2}2字节长度前缀二进制协议
{packet, line}行分隔文本协议
binary二进制模式性能更好
{active, false}被动模式服务端
{active, once}一次性激活流量控制
{reuseaddr, true}地址复用服务端必须
{backlog, 128}连接队列长度高并发
{nodelay, true}禁用 Nagle低延迟
{keepalive, true}保活探测长连接

15.3 UDP Socket

15.3.1 UDP 基本用法

%% UDP 服务器
{ok, Socket} = gen_udp:open(12345, [binary, {active, false}]),

{ok, {Address, Port, Data}} = gen_udp:recv(Socket, 0),
io:format("From ~p:~p - ~p~n", [Address, Port, Data]),

gen_udp:send(Socket, Address, Port, <<"ACK">>).

%% UDP 客户端
{ok, Socket} = gen_udp:open(0, [binary]),
gen_udp:send(Socket, "127.0.0.1", 12345, <<"Hello">>),
{ok, {_Addr, _Port, Data}} = gen_udp:recv(Socket, 0, 5000).

15.4 二进制数据处理

15.4.1 Binary 构造

%% 基本 binary
<<1, 2, 3>>.
<<"hello">>.

%% 位语法
<<A:4, B:4>> = <<16#AB>>.  %% A=10, B=11

%% 字节序
<<Val:32/big>> = <<0, 0, 1, 0>>.
%% Val = 256

<<Val:32/little>> = <<0, 0, 1, 0>>.
%% Val = 65536

%% 浮点数
<<F:32/float>> = <<64, 72, 245, 195>>.
%% F = 3.14

15.4.2 位语法详解

%% 语法:<<Value:Size/TypeSpecifierList>>

%% Size: 位数
%% Type: integer | float | binary | bitstring | bytes
%% Sign: signed | unsigned
%% Endian: big | little | native
%% Unit: unit:N

%% 解析以太网帧头
parse_ethernet(<<
    DstMac:6/binary,     %% 目的 MAC
    SrcMac:6/binary,     %% 源 MAC
    EtherType:16/big,    %% 协议类型
    Payload/binary       %% 数据
>>) ->
    #{
        dst_mac => DstMac,
        src_mac => SrcMac,
        ether_type => EtherType,
        payload => Payload
    }.

%% 解析 IPv4 头
parse_ipv4(<<
    Version:4, IHL:4,
    TOS:8,
    TotalLength:16,
    Identification:16,
    Flags:3, FragmentOffset:13,
    TTL:8,
    Protocol:8,
    Checksum:16,
    SourceIP:32,
    DestIP:32,
    Rest/binary
>>) ->
    #{
        version => Version,
        ihl => IHL,
        total_length => TotalLength,
        protocol => Protocol,
        source_ip => format_ip(SourceIP),
        dest_ip => format_ip(DestIP)
    }.

format_ip(<<A:8, B:8, C:8, D:8>>) ->
    io_lib:format("~p.~p.~p.~p", [A, B, C, D]).

15.4.3 Binary 操作

%% 拼接
<<1, 2>> <<3, 4>>  %% <<1, 2, 3, 4>> 需要使用 binary 模块

%% 使用 binary 模块
binary:part(<<"hello">>, 0, 3).    %% <<"hel">>
binary:at(<<"hello">>, 1).         %% 101 (e 的 ASCII)
byte_size(<<"hello">>).            %% 5
binary:match(<<"hello">>, <<"ll">>).  %% {2, 2}
binary:split(<<"a,b,c">>, <<",">>). %% [<<"a">>, <<"b,c">>]
binary:replace(<<"hello">>, <<"l">>, <<"L">>, [global]). %% <<"heLLo">>

15.5 实战:简单 HTTP 服务器

%% mini_http.erl
-module(mini_http).
-export([start/1, accept/1]).

start(Port) ->
    {ok, Listen} = gen_tcp:listen(Port, [
        binary,
        {packet, http_bin},
        {active, false},
        {reuseaddr, true}
    ]),
    io:format("HTTP server on port ~p~n", [Port]),
    spawn(fun() -> accept(Listen) end).

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

handle_request(Socket, _Acc) ->
    case gen_tcp:recv(Socket, 0, 5000) of
        {ok, {http_request, 'GET', {abs_path, Path}, _}} ->
            Body = io_lib:format("You requested: ~s", [Path]),
            Response = [
                "HTTP/1.1 200 OK\r\n",
                "Content-Type: text/plain\r\n",
                io_lib:format("Content-Length: ~p\r\n", [iolist_size(Body)]),
                "\r\n",
                Body
            ],
            gen_tcp:send(Socket, Response),
            gen_tcp:close(Socket);
        {error, closed} ->
            ok
    end.

15.6 注意事项

⚠️ 常见陷阱

  1. TCP 粘包:需要自己处理消息边界(使用 {packet, N} 选项)
  2. 资源泄漏:确保 Socket 和文件 Handle 在错误时也被关闭
  3. 阻塞 IO:被动模式下 recv 会阻塞,使用 active 模式或多进程
  4. 大文件:不要用 read_file 读取大文件,使用流式读取

💡 最佳实践

  1. 使用 binary 模式处理数据,比 list 模式更高效
  2. 服务端使用 {active, once} 控制流量
  3. {packet, 2} 简化二进制协议的消息分帧
  4. 文件操作后始终关闭 Handle(或使用 try/after)

15.7 扩展阅读


上一章:14 - Mnesia 数据库 下一章:16 - 错误处理