elixir GenServer로 tcp echoing 서버를 만들어보자.


lib/tcptest/echo/server.ex

defmodule Tcptest.Server do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, :no_args, name: __MODULE__)
  end

  def init(:no_args) do
    {:ok, socket} =
      :gen_tcp.listen(2566, [:binary, packet: :line, active: false, reuseaddr: true])

    IO.puts("Server is listening on port 2566...")
    Process.send_after(self(), {:loop, socket}, 0)
    {:ok, nil}
  end

  def handle_info({:loop, socket}, _) do
    accept(socket)
  end

  defp accept(socket) do
    {:ok, client_socket} = :gen_tcp.accept(socket)
    spawn_link(Tcptest.Handler, :start_link, [client_socket])
    send(self(), {:loop, socket})
    {:noreply, nil}
  end
end
  • 2566번 포트를 열고 라인으로 구분되는 패킷을 받는다.
  • handle_info 콜백이 :loop 메시지를 받으면, Tcptest.Handler 프로세스를 생성하고, 또 다음 연결을 수신할 수 있도록, 자기 자신에게 :loop 메시지를 다시 보낸다 (메시지 이름이 loop인 이유!)

lib/tcptest/echo/handler.ex

defmodule Tcptest.Handler do
  use GenServer

  def start_link(socket) do
    GenServer.start_link(__MODULE__, socket)
  end

  def init(client_socket) do
    :gen_tcp.send(client_socket, "WELCOME\r\n")
    Process.send_after(self(), {:tcp, client_socket}, 0)
    {:ok, nil}
  end

  def handle_info({:tcp, socket}, _) do
    case :gen_tcp.recv(socket, 0, 1000 * 30) do
      {:ok, data} ->
        data = String.trim(data)
        handle_msg(socket, data)

      {:error, reason} ->
        IO.puts("err on recv from socket > #{inspect(reason)}")
        handle_msg(socket, "CLOSE")
    end

    {:noreply, nil}
  end

  def handle_msg(socket, "CLOSE") do
    :gen_tcp.close(socket)
    {:stop, :normal, nil}
  end

  def handle_msg(socket, data) do
    :gen_tcp.send(socket, "Echo: #{data}\n")
    send(self(), {:tcp, socket})
    {:noreply, nil}
  end
end
  • Tcptest.Server로 부터 Tcptest.Handler 프로세스가 생성된다.
  • Tcptest.Handler 는 tcp 연결 요청을 보낸 클라이언트에게 WELCOME 메시지를 보내고, 응답을 수신한다. 이때 타임아웃은 30초로 설정해두엇다.
  • 정상적인 메시지가 수신되면 handle_msg 함수를 호출해서 Echoing 한다. 만약 메시지가 CLOSE 라면 Echoing 하지 않고 종료한다.
  • 비정상적인 메시지가 수신되면 로그를 남기고 종료한다.