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 的进程和电脑的进程概念是不同的, 后续
Erlang的Process都是指代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 拦截推送过来的进程数据.