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 注意事项
⚠️ 常见陷阱
- TCP 粘包:需要自己处理消息边界(使用
{packet, N}选项) - 资源泄漏:确保 Socket 和文件 Handle 在错误时也被关闭
- 阻塞 IO:被动模式下
recv会阻塞,使用 active 模式或多进程 - 大文件:不要用
read_file读取大文件,使用流式读取
💡 最佳实践
- 使用 binary 模式处理数据,比 list 模式更高效
- 服务端使用
{active, once}控制流量 - 用
{packet, 2}简化二进制协议的消息分帧 - 文件操作后始终关闭 Handle(或使用 try/after)
15.7 扩展阅读
上一章:14 - Mnesia 数据库 下一章:16 - 错误处理