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.
Sí, 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.
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)spawn fn -> loop_server socket end
loop_accept_socket lsocket
end end
Ponemos el socket a la escucha y esperamos indefinidamente a que alguien se conecte. | |
Una vez se produce la conexión, lanzamos el proceso server | |
esperamos 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)loop_server socket
after :gen_tcp.close socket
end end
Leemos una línea, la escribimos en el socket | |
y repetimos indefinidamente. | |
En 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)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
Leemos 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 . | |
Vamos añadiendo el byte al buffer | |
Cuando el byte es un retorno de carro, es el momento de devolver lo recibido | |
En 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])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)
line end defp write_line line, socket do :gen_tcp.send socket, line end end
A erlang le podemos decir que queremos recibir los paquetes cortados por líneas de texto. | |
Haciendo 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
application
, actors
, supervisión y otros amigos otp
Comentarios