Erlang进阶(四)
Erlang 内置 gen_tcp|gen_udp 可以作为 tcp|udp 网络监听模块,
这里主要讲解的是 gen_tcp 模块, udp 方面相对来说接触的比较少.
% 网络请求入口
net_main() ->
ListenPort = 6000, % 访问端口
ListenOpts = [
binary, % 二进制流
% 如果为 true 为主动模式, 所有接收数据都转发到声明监听的进程当中, 默认为主动模式
% 如果为 false 为被动模式, 接收到消息之后需要手动调用 gen_tcp:recv 读取信息
% 适合定制化 acceptor 处理, 一般游戏服务器都需要构建管理自己连接池, 所以该配置会关掉
{active, false},
% 系统级别帮你保证客户端和服务端的连接活跃性, 默认是关闭的
% 游戏服务一般都是自己做心跳维护的, 所以不需要系统做连接保活
{keepalive, false},
% 数据包直接推送给客户端而不会等待凑足待发送队列一次性推送
% 一般非高频网络服务会写入待发送队列之后一次性推送来提高效率, 游戏服务之类不需要
{nodelay, true},
% Erlang 会保证将TCP数据原封不动推送回客户端
{packet, 0},
% 允许对本地端口进行复用
{reuseaddr, true},
% 设置是否做待发送队列一次性推送, 同上, 游戏服务之类不需要
{delay_send, false},
% 缓冲待处理队列长度, 也就是数据连接之后消息队列长度
{backlog, 5120},
% Socket 被关闭之后数据是否还能将缓冲区数据推送, 游戏服务最好关闭连接之后一起推送
{exit_on_close, true},
% 系统发送数据的超时时间, 超过指定时间会返回 {error, timeout}
{send_timeout, 12800}
],
ok.
这里的 ListenOpts 就是 Socket 监听需要配置原子量声明:
{active, true}:套接字设置为主动模式。所有套接字接收到的消息都作为 Erlang 消息转发到拥有这个套接字进程上。当开启一个套接字时,默认是主动模式。{active, false}:设置套接字为被动模式。套接字收到的消息被缓存起来,进程必须通过调用函数gen_tcp:recv/2或gen_tcp:recv/3来读取这些消息。{active, once}:将设置套接字为主动模式,但是一旦收到第一条消息,就将其设置为被动模式,并使用gen_tcp:recv/2或gen_tcp:recv/3函数来读取后续消息。{keepalive, true}:当没有转移数据时,确保所连接的套接字发送保持活跃(keepalive)的消息。因为关闭套接字消息可能会丢失,如果没有接收到保持活跃消息的响应,那么该选项可确保这个套接字能被关闭。默认情况下,该标签是关闭的。{nodelay, true}:数据包直接发送到套接字,不过它多么小。在默认情况下,此选项处于关闭状态,并且与之相反,数据被聚集而以更大的数据块进行发送。{packet_size, Size}:设置数据包允许的最大长度。如果数据包比 Size 还大,那么将认为这个数据包无效。{packet, 0}:表示 Erlang 系统会把 TCP 数据原封不动地直接传送给应用程序{reuseaddr, true}:允许本地重复使用端口号{delay_send, true}:数据不是立即发送,而是存到发送队列里,等 socket 可写的时候再发送{backlog, 1024}: 缓冲待处理连接队列的最大长度,默认为5{exit_on_close, false}:设置为 false,那么 socket 被关闭之后还能将缓冲区中的数据发送出去{send_timeout, 15000}:设置一个时间去等待操作系统发送数据,如果底层在这个时间段后还没发出数据,那么就会返回{error,timeout}
这里就是网络服务接口常用到的配置, 之后就是监听回调的服务, 这里就设计出 echo 服务来做 Erlang 网络入门:
% 网络请求入口
net_main() ->
ListenPort = 6000, % 访问端口
ListenOpts = [
binary, % 二进制流
% 目前没有高阶进程调度池, 而且 echo 程序目前保持配置即可
{active, true},
% 系统级别帮你保证客户端和服务端的连接活跃性, 默认是关闭的
% 游戏服务一般都是自己做心跳维护的, 所以不需要系统做连接保活
{keepalive, false},
% 数据包直接推送给客户端而不会等待凑足待发送队列一次性推送
% 一般非高频网络服务会写入待发送队列之后一次性推送来提高效率, 游戏服务之类不需要
{nodelay, true},
% Erlang 会保证将TCP数据原封不动推送回客户端
{packet, 0},
% 允许对本地端口进行复用
{reuseaddr, true},
% 设置是否做待发送队列一次性推送, 同上, 游戏服务之类不需要
{delay_send, false},
% 缓冲待处理队列长度, 也就是数据连接之后消息队列长度
{backlog, 5120},
% Socket 被关闭之后数据是否还能将缓冲区数据推送, 游戏服务最好关闭连接之后一起推送
{exit_on_close, true},
% 系统发送数据的超时时间, 超过指定时间会返回 {error, timeout}
{send_timeout, 12800}
],
%% 开启监听
io:format("GenTcp Service Started By ~w~n", [ListenPort]),
case gen_tcp:listen(ListenPort, ListenOpts) of
{?ok, Listener} ->
% 接受客户端请求
case gen_tcp:accept(Listener) of
{ok, Socket} ->
io:format("[accept] connect ~p~n", [Socket]),
net_loop(Socket); % 开始接收数据并回显
Reason -> io:format("[accept]Error! Reason:~ts~n", [Reason])
end;
% 异常返回
Reason -> io:format("[listen]Error! Reason:~ts~n", [Reason])
end,
?ok.
% 客户端请求循环
net_loop(Socket) ->
receive
% 消息通知获取数据
{tcp, Socket, Bin} ->
io:format("[received] binary ~p~n", [Bin]), % 打印传入数据
gen_tcp:send(Socket, Bin), % 推送回客户Socket
net_loop(Socket); % 继续等待进程消息推送
{tcp_closed, Socket} -> io:format("[close] socket Closed")
end.
因为这里没有定制自己的访问事件池, 所以配置项应该采用
{active, true}来默认监听进程信号.
这里因为我都是 Linux 环境, 所以直接用系统命令测试:
# 进入 netcat 连接, 随便输入数据查看是否有反应
nc 127.0.0.1 6000
这里就是 Erlang 的网络数据交互请求初探, 实现了简单的 echo 服务, 后续就是高阶定制进程调度处理方式,
最后实现类似 nginx 的 worker 多进程监听和调度, 后续就是准备涉及到更加核心的 Erlang 关键点.
注意: 这里如果玩家会话关闭的时候会弹出 terminate, 这时候进程拦截到错误信号以为报错而退出进程.
在 C/C++ 当中也有相同的问题, Socket 方面需要屏蔽这种错误信号,
而 Erlang 也具有这种屏蔽功能: process_flag(trap_exit,boolean).