MeteorCat / Erlang进阶(四)

Created Tue, 16 Jul 2024 22:36:23 +0800 Modified Wed, 29 Oct 2025 23:24:53 +0800
1869 Words

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/2gen_tcp:recv/3 来读取这些消息。
  • {active, once}:将设置套接字为主动模式,但是一旦收到第一条消息,就将其设置为被动模式,并使用 gen_tcp:recv/2gen_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 服务, 后续就是高阶定制进程调度处理方式, 最后实现类似 nginxworker 多进程监听和调度, 后续就是准备涉及到更加核心的 Erlang 关键点.

注意: 这里如果玩家会话关闭的时候会弹出 terminate, 这时候进程拦截到错误信号以为报错而退出进程.

C/C++ 当中也有相同的问题, Socket 方面需要屏蔽这种错误信号, 而 Erlang 也具有这种屏蔽功能: process_flag(trap_exit,boolean).