Elixir. TCP server sencillo

Para ser telnet friendly recibiremos y enviaremos texto separado por retornos de carro. El programa se limitará a responder lo mismo que reciba.
Para trocear los mensajes en líneas separadas por retorno de carro, leeremos byte a byte y los meteremos en un buffer.
NoteSí, hay otra forma de hacerlo más sencillo, pero hacerlo a mano es más didáctico. Al final también indicaremos la forma erlangish
¿Es mejor que el buffer sea un binary o una lista?
iex> {microsecs, :ok} = :timer.tc fn -> (1..1000 |> Enum.each fn _ -> (1..1000 |> (Enum.reduce [], &( [rem(&1, 10)+48|&2])) |> Enum.reverse |> to_string) end) end; IO.puts microsecs/1000000.0
3.493395


iex> {microsecs, :ok} = :timer.tc fn->(1..1000 |> Enum.each fn _ -> (1..1000 |> Enum.reduce "", &(&2 <> (<<rem(&1, 10)+48>>))) end) end; IO.puts microsecs/1000000.0
7.098454
La respuesta es una lista.
Crearemos un socket tcp, que pondremos a la escucha. Luego le pondremos a esperar conexiones. Para cada conexión nueva, lanzaremos el programa de eco y pondremos otro proceso esperando otra conexión.
tcp_echo.png
Figure 1. Diagrama de procesos
Aunque por el momento no lo consideraremos, accept connection debería rearrancarse en caso de caída.
Los procesos run server n no es necesario ni conveniente que se arranquen. También conviene mantenerlos aislados (no enlazados con el proceso padre). Si uno de estos procesos falla (y el fallo es parte de la implementación en este caso), afectará a dicha conexión y ninguna otra.

Socket escuchando

defmodule TcpExample do

    def  new  port  do
        {:ok, lsocket} = (:gen_tcp.listen  port, [  active:         false,
                                                    reuseaddr:      true])
        spawn_link   fn ->  loop_accept_socket  lsocket   end
    end


    defp  loop_accept_socket   lsocket   do
        {:ok, socket} = :gen_tcp.accept(lsocket)    1
        spawn  fn ->  loop_server  socket   end     2
        loop_accept_socket   lsocket                3
    end

end
1Ponemos el socket a la escucha y esperamos indefinidamente a que alguien se conecte.
2Una vez se produce la conexión, lanzamos el proceso server
3esperamos otra conexión.

Run server

defmodule TcpExample2 do

    def  new  port  do
    ...

    defp  loop_accept_socket   lsocket   do
    ...

    defp  loop_server  socket  do

        try  do:

            read_line(socket)  |>  write_line(socket)   1
            loop_server  socket                         2

        after   :gen_tcp.close  socket                  3

    end



end
1Leemos una línea, la escribimos en el socket
2y repetimos indefinidamente.
3En caso de que haya un error, liberamos el socket "aceptado".
El resto son detalles, interesantes pero detalles.

Read and write line

defmodule TcpExample2 do

    def  new  port  do
    ...

    defp  loop_accept_socket   lsocket   do
    ...

    defp  loop_server  socket  do
    ...

    defp  read_line  socket,  buffer\\[]  do

        {:ok, byte} =  :gen_tcp.recv(socket, 1, 5000)       1
        buffer = [byte | buffer]                            2
        if  byte == '\n',   do:     (buffer |> Enum.reverse  |>  to_string),    3
        else:                       (read_line  socket, buffer)                 4

    end


    defp   write_line   line,  socket  do
        :gen_tcp.send   socket,  line
    end

end
1Leemos carácter a carácter esperando como mucho 5 segundos entre ellos. Si en 5 segundos no recibimos nada,recv terminará, pero no devolverá un {:ok, _} y se provocará un fallo. Este fallo matará el proceso (bien) pero previamente realizará un close.
2Vamos añadiendo el byte al buffer
3Cuando el byte es un retorno de carro, es el momento de devolver lo recibido
4En otro caso, seguimos leyendo

Código completo

defmodule TcpExample2 do

    def  new  port  do
        {:ok, lsocket} = (:gen_tcp.listen  port, [  active:         false,
                                                    reuseaddr:      true])
        spawn_link   fn ->  loop_accept_socket  lsocket   end
    end


    defp  loop_accept_socket   lsocket   do
        {:ok, socket} = :gen_tcp.accept(lsocket)
        spawn  fn ->  loop_server  socket   end
        loop_accept_socket   lsocket
    end


    defp  loop_server  socket  do

        try  do:

            read_line(socket)  |>  write_line(socket)
            loop_server  socket

        after   :gen_tcp.close  socket

    end


    defp  read_line  socket,  buffer\\[]  do

        {:ok, byte} =  :gen_tcp.recv(socket, 1, 5000)
        buffer = [byte | buffer]
        if  byte == '\n',   do:     (buffer |> Enum.reverse  |>  to_string),
        else:                       (read_line  socket, buffer)

    end


    defp   write_line   line,  socket  do
        :gen_tcp.send   socket,  line
    end

end

Pequeña mejora

defmodule TcpExample2 do

    def  new  port  do
        {:ok, lsocket} = (:gen_tcp.listen  port, [:binary,
                                                  packet: :line,
                                                  active: false])  1
        spawn_link   fn ->  loop_accept_socket  lsocket   end
    end


    defp  loop_accept_socket   lsocket   do
    ...

    defp  loop_server  socket  do
    ...

    defp  read_line  socket   do

        {:ok, line} = :gen_tcp.recv(socket, 0, 5000)                2
        line

    end


    defp   write_line   line,  socket  do
        :gen_tcp.send   socket,  line
    end

end
1A erlang le podemos decir que queremos recibir los paquetes cortados por líneas de texto.
2Haciendo trivial la recepción de las líneas
Ver keywords list
iex> [:binary, packet: :line, active: false] == [:binary, {:packet, :line}, {:active, false}]
true
Próximamente, habrá que completar el desarrollo con applicationactors, supervisión y otros amigos otp

Comentarios

Entradas populares de este blog

Software libre

Servicios, servicios, servicios... (y Amazon)

Tecnologías divertidas