MeteorCat / Erlang进阶(三)

Created Mon, 15 Jul 2024 22:23:46 +0800 Modified Wed, 29 Oct 2025 23:24:53 +0800
2401 Words

Erlang进阶(三)

这个篇章会稍微加速说明比较多知识点, 涵盖了 字符串/二进制/Erlang进程 方面.

string|字符串

Erlang 实际上来说是没有字符串类型, 所谓的字符串实际上是 List 类型的对象:

%%% 测试入口方法
strings_main() ->
  Str1 = "ThisIsString",
  if erlang:is_list(Str1) -> io:format("String is List~n") end,

  % 这里看起来很美好的获取长度
  io:format("StringText: ~ts,StringLen: ~w~n", [Str1, length(Str1)]),

  %% 但是涉及到非英语的情况会自动切换成utf8识别获取长度, 同时非纯英文需要采用 ~ts 进行格式化输出
  Str2 = "中文内容", % 注意按照中文来说这里只有4个字符
  io:format("ChineseText: ~ts,StringLen: ~w~n", [Str2, length(Str2)]),

  %% Erlang内部工具库集成字符串处理, 下面是有bug内容代码
  Str3 = string:replace(Str2, "内容", ""), % 注意这里替换两个中文字符, 实际上长度是2
  io:format("ChineseText: ~ts,StringLen: ~w~n", [Str3, length(Str3)]),
  % 很遗憾, 这里输出的是 = ChineseText: 中文,StringLen: 3
  ok.

请注意, 因为 字符串的特殊性 导致 List 类型功能都能对字符串使用, 但是 length != string:length.

对于字符串数据绝对必须要用 string 库功能做处理:

%%% 测试入口方法
strings_main() ->
  %% length /= string:length, 对于字符串操作应当全部采用 string 库当中处理
  Str2 = "中文内容", % 注意按照中文来说这里只有4个字符
  Str3 = string:replace(Str2, "内容", ""),
  Str3Length = string:length(Str3),
  Str3Size = length(Str3), % 不带前缀的 length 是获取列表长度
  io:format("Str3Size: ~w, Str3Length ~w~n", [Str3Size, Str3Length]),
  % 现在已经成功获取正确的长度 = Str3Size: 3, Str3Length 2
  ok.

对于字符串处理绝对要使用 string 库内部处理, 而不要使用到涉及 List 所有功能.

位串和二进制

Erlang 当中实际上都是靠位串处理二进制数据, 同时包含网络协议数据封包解包也是靠位串实现; 并且对于 Erlang 来说, 二进制处理完全没有什么心智负担, 数据封包简单程度让人惊喜.

注意: Erlang 位串需要有二进制相关处理经验, 了解到其他语言当中不同类型字节计算, 比如常规 int32 其他语言占位 4 字节

Erlang 作为可以和 C/C++ 做连接的语言, 内部实际上也带有个自身的类型声明:

  • 常规类型: integer, float, binary, bytes, bitstring, bits, utf8, utf16, utf32
  • 数值符号: signed, unsigned
  • 网络序列: big, little, native

以上 bits 是 bitstring 的别名, bytes 是 binary 的别名, 按照自身习惯选择声明定义

三种类型声明可以不按顺序组合, 比如 unsigned-big-integer 或者 little-unsigned-integer.

这里模拟编写网络传输的二进制协议数据的程序:

bytes() ->
  %% 以日常最简单的客户端封包数据协议来看:
  %%% [后续二进制长度(4字节)] [推送协议ID(4字节)] [推送消息数据二进制内容]
  %%% 以上面简单的消息发包机制来测试传递的位串二进制数据
  ProtocolText = "Hello.World", % 需要推送字符串
  ProtocolTextBytes = erlang:list_to_binary(ProtocolText), % 字符串转二进制数据
  ProtocolId = 100001, % 需要推送的服务端协议ID

  %% 开始构建打包协议数据
  %% 这里打印出来最后二进制内容: [0,0,0,11,0,1,134,161,72,101,108,108,111,46,87,111,114,108,100]
  %%% big-integer-unsigned 代表打包的数据类型是采用网络序(大端)的 uint32 类型
  %%% binary 代表打包内容是二进制数据
  %%% 最后通过 <<xx,yyy>> 合并成一段完成二进制数据
  ProtocolBodyLen = byte_size(ProtocolTextBytes), % 获取打包协议体长度, 也就是传递字符串二进制数据长度
  % 这里就是关键的声明 uint32 长度封包写入位串当中的定义
  Protocol = <<ProtocolBodyLen:32/big-integer-unsigned, ProtocolId:32/big-integer-unsigned, ProtocolTextBytes/binary>>,
  io:format("Protocol Body(~w): ~w~n", [byte_size(Protocol), erlang:binary_to_list(Protocol)]),


  %% 数据解包内容 Erlang 更加方便, 在和其他语言做网络交互的时候, 首先确定数据长度不能小于两个 uint32 位值
  %% 也就是 ([uint32] [uint32], 验证必须要有这两个uint32长度) [bytes]

  %%% 那么声明解构的数据如下
  <<Len:32/big-integer-unsigned, Id:32/big-integer-unsigned, Data/binary>> = Protocol,
  io:format("Message Body Length: ~w~n", [Len]),
  io:format("Message Protocol Id: ~w~n", [Id]),
  io:format("Message Data: ~ts~n", [erlang:binary_to_list(Data)]), % 用 erlang 内置方法将 binary 转 List
  %% 这就是简单数据消息封包解包的流程
  %% 当然正式的游戏服务端开发当中还有 CheckSum 位值校验认证和长度比较, 这里先不做深入只需要知道位串封包二进制的特性
  %% <<Value:32/big-integer-unsigned>> 这种位串声明实际上约等于 byte[] Value = new byte[32], Value 按照封包|解构不同做写入|读取处理
  ok.

这里比较常见的数据强类型对象:

  • 32/big-integer: 大端(网络序列)的 int32 数据
  • 32/big-integer-unsigned: 大端(网络序列)的 uint32 数据
  • 32/little-integer: 小端(字节序列)的 int32 数据
  • 32/unsigned-little-integer: 小端(字节序列)的 uint32 数据
  • binary: 二进制数据流

这里在后续游戏数据传输章节会专门讲解, 现在只是作为位串学习来看.

文件读写

这里作为小章节来补充本地文件读写:

file_main() ->
  % 这里本身作为小篇章说明文件IO操作, Erlang 内部负责文件IO处理有两个库: filelib 和 file
  % 判断文件夹是否存在, 如果是文件就采用 is_file
  case filelib:is_dir("_build") of
    ?true -> io:format("_build exists~n");
    ?false -> io:format("_build not found~n")
  end,

  % 获取文件大小, 单位为字节
  FileSize = filelib:file_size("Emakefile"),
  io:format("Emakefile FileSize: ~w~n", [FileSize]),

  % 正则扫描文件
  Beams = filelib:fold_files("_build/dev", ".*.beam", ?true, fun(F, Acc) -> [F | Acc] end, []),
  io:format("Beams : ~p~n", [Beams]),


  % 上面大多都是文件状况, 之后就是处理文件准确读写
  {?ok, File} = file:open("proto.bin", [append, write, raw, binary]), % 以原生二进制数据打开文件, 不存在就创建文件
  file:write(File, Protocol), % 将之前协议的二进制数据写入本地
  file:close(File),
  ok.

这里的文件IO和 C 风格很类似, 所有如果之前有了解和学习过 C 方面应该很容易上手, 就是原子量配置比较有混淆性.

Erlang进程

Erlang最强优势就是并发和分布式设计, 能够通过集群节点将服务分赛在不同线程, 虽然自称为 “进程” 但本质上还是多线程调度.

注意: Erlang 的进程和电脑的进程概念是不同的, 后续 ErlangProcess 都是指代 Erlang 进程.

% 设计进程调用程序, 注意方法必须得 export
proc_echo(Msg) ->
  io:format("Proc(~p): ~p~n", [erlang:self(), Msg]),
  done.

% 程序入口
proc_main() ->
  % 首先打印目前Erlang进程id, pid 格式都是 <x.y,z> 格式的 Map 类型
  % erlang:self() 返回目前所在进程id, 类型为 pid()
  Pid = erlang:self(),
  io:format("Procce Running By ~p~n", [Pid]),
  case erlang:is_pid(Pid) of
    ?true -> io:format("Is Pid~n");
    _ -> io:format("Is Not Pid~n")
  end,

  % 这里首先学习如何构建 Erlang, spawn 调用方式:
  % spawn(Module名称,Export函数,ListOfArgs)
  % 这里调用之后可以对照下 Pid 是完全不同, 代表了两者运行进程的不同
  erlang:spawn(?MODULE, proc_echo, ["Hello.World"]),
  ok.

虽然 spawn 构建新的任务进程, 但是目前还没有涉及到数据推送交互, 这里开始介绍怎么推送消息给其他进程:

% ping-pong 程序
proc_ping_pong() ->
  % 拦截 ping 原子量返回 pong 原子量, 反过来取反
  Pid = erlang:self(),
  io:format("PingPong Running By ~p~n", [Pid]),
  receive
    {ping, OwnerPid} -> io:format("Ping(~p)->Pong(~p)~n", [OwnerPid, Pid]);
    {pong, OwnerPid} -> io:format("Pong(~p)->Ping(~p)~n", [OwnerPid, Pid])
  end,
  done.


% 另外进程推送给指定线程
proc_send(SendPid, RecPid) when erlang:is_pid(SendPid) andalso erlang:is_pid(RecPid) ->
  SendPid ! {ping, RecPid}.


% 程序入口
proc_main() ->
  % 注意: Pid是标识当前运行 Erlang的唯一标识, 也依赖这个标识来传递推送数据
  % Spawn构建的进程返回 Pid 就是构建进程对象 Pid
  PingPongId = erlang:spawn(?MODULE, proc_ping_pong, []),


  %% Erlang进程要接收消息传递只需要采用 receive-end 拦截即可
  %% 进程消息推送方式以 '接受者PID ! 推送消息' 构成,
  Pid = erlang:self(),
  erlang:spawn(?MODULE, proc_send, [PingPongId, Pid]),


  %% 以上就是构建独立Erlang进程并将消息传递过程, 但是正式游戏服务器没有那么简单
  %% 首先正式项目应该手动去管理线程分配调度, 且会生成管理任务池分配调度任务给指定CPU线程防止线程任务不平衡
  %% 如果在其他语言就要开始设计自己 Actor 模型, 但是 Erlang 天生支持 Actor 省去这个过程.
  ok.

这里只是说下基础进程推送, 可以采用 ProccessId ! Message 推送数据, 而 receive-end 拦截推送过来的进程数据.