From 03cdb9428f931e7f3239f547db93cdf7c3fbcdcf Mon Sep 17 00:00:00 2001 From: Alexander Heldt Date: Tue, 11 Nov 2025 20:36:55 +0100 Subject: [PATCH] TCP echo server --- gleam.toml | 2 ++ manifest.toml | 4 +++ src/musicplayer.gleam | 32 +++++++++++++++------- src/tcp/echo_server.gleam | 39 +++++++++++++++++++++++++++ src/tcp/tcp.gleam | 57 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/tcp/echo_server.gleam diff --git a/gleam.toml b/gleam.toml index 51ec110..6057b42 100644 --- a/gleam.toml +++ b/gleam.toml @@ -14,6 +14,8 @@ version = "1.0.0" [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0" +gleam_otp = ">= 1.2.0 and < 2.0.0" +gleam_erlang = ">= 1.3.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 61ab519..14d58d6 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,10 +2,14 @@ # You typically do not need to edit this file packages = [ + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, ] [requirements] +gleam_otp = { version = ">= 1.2.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } diff --git a/src/musicplayer.gleam b/src/musicplayer.gleam index ca2796e..1c30c55 100644 --- a/src/musicplayer.gleam +++ b/src/musicplayer.gleam @@ -1,23 +1,37 @@ import gleam/io +import tcp/echo_server import tcp/reason import tcp/tcp pub fn main() -> Nil { let socket_path = "/tmp/musicplayer.sock" - case tcp.connect(socket_path) { - Error(r) -> - io.println("Failed to connect to socket: " <> reason.to_string(r)) - Ok(socket) -> { - io.println("connected") + case echo_server.new(socket_path) { + Error(err) -> io.println("Failed to create echo server: " <> err) + Ok(_) -> { + case tcp.connect(socket_path) { + Error(r) -> + io.println("Failed to connect to socket: " <> reason.to_string(r)) + Ok(socket) -> { + io.println("connected") - let messages = ["hello, \n", "world!\n"] + let messages = ["hello, \n", "world!\n"] - messages |> send_messages(socket, _) + messages |> send_messages(socket, _) - io.println("closing") - tcp.close(socket) + case tcp.receive(socket) { + Error(r) -> + io.println( + "Failed to receive from socket: " <> reason.to_string(r), + ) + Ok(data) -> io.println("Recieved: " <> data) + } + + io.println("closing") + tcp.close(socket) + } + } } } } diff --git a/src/tcp/echo_server.gleam b/src/tcp/echo_server.gleam new file mode 100644 index 0000000..ac7fc79 --- /dev/null +++ b/src/tcp/echo_server.gleam @@ -0,0 +1,39 @@ +import gleam/otp/actor +import tcp/reason + +import tcp/tcp + +pub type Message { + Shutdown + ReadyToAccept(listen_socket: tcp.Socket) +} + +pub fn new(socket_path: String) -> Result(Nil, String) { + let server = actor.new(Nil) |> actor.on_message(handle_message) |> actor.start + + case tcp.listen(socket_path), server { + Ok(socket), Ok(b) -> { + let server = b.data + actor.send(server, ReadyToAccept(socket)) + Ok(Nil) + } + Error(r), _ -> Error(reason.to_string(r)) + _, Error(start_error) -> + case start_error { + actor.InitExited(_) -> Error("InitExited") + actor.InitFailed(_) -> Error("InitFailed") + actor.InitTimeout -> Error("InitTimeout") + } + } +} + +fn handle_message(_: Nil, message: Message) -> actor.Next(Nil, Message) { + case message { + Shutdown -> actor.stop() + + ReadyToAccept(_) -> { + // TODO tcp.accept + actor.continue(Nil) + } + } +} diff --git a/src/tcp/tcp.gleam b/src/tcp/tcp.gleam index 6bc1976..297b7fa 100644 --- a/src/tcp/tcp.gleam +++ b/src/tcp/tcp.gleam @@ -1,4 +1,6 @@ +import gleam/bit_array import gleam/bytes_tree +import gleam/result import tcp/reason.{type Reason} @@ -17,15 +19,52 @@ type ModeValue { type TCPOption { Active(Bool) Mode(ModeValue) + Ifaddr(Local) +} + +pub fn listen(socket_path: String) -> Result(Socket, Reason) { + // TODO maybe reuseaddr? + let options = [Mode(Binary), Active(False), Ifaddr(Local(socket_path))] + // port zero with `local` address + let port = 0 + + gen_tcp_listen(port, options) +} + +pub fn accept(socket: Socket) -> Result(Socket, Reason) { + gen_tcp_accept(socket) +} + +pub fn receive(socket: Socket) -> Result(String, Reason) { + // Get all bytes + let length = 0 + + // timeout in ms + let timeout = 1000 + + result.try(gen_tcp_recv(socket, length, timeout), fn(bt) { + case + bt + |> bytes_tree.to_bit_array + |> bit_array.to_string + { + // TODO what error? + Error(_) -> Error(reason.Ebadmsg) + Ok(s) -> Ok(s) + } + }) } pub fn connect(socket_path: String) -> Result(Socket, Reason) { let options = [Mode(Binary), Active(False)] + // port zero with `local` address + let port = 0 + // timeout in ms let timeout = 1000 - gen_tcp_connect(Local(socket_path), 0, options, timeout) + gen_tcp_connect(Local(socket_path), port, options, timeout) } pub fn send(socket: Socket, message: String) -> Result(Nil, Reason) { @@ -36,6 +75,22 @@ pub fn close(socket: Socket) -> Nil { gen_tcp_close(socket) } +// https://www.erlang.org/doc/apps/kernel/gen_tcp.html#listen/2 +@external(erlang, "gen_tcp", "listen") +fn gen_tcp_listen(port: Int, option: List(TCPOption)) -> Result(Socket, Reason) + +// https://www.erlang.org/doc/apps/kernel/gen_tcp.html#accept/1 +@external(erlang, "gen_tcp", "accept") +fn gen_tcp_accept(socket: Socket) -> Result(Socket, Reason) + +// https://www.erlang.org/doc/apps/kernel/gen_tcp.html#recv/3 +@external(erlang, "gen_tcp", "recv") +fn gen_tcp_recv( + socket: Socket, + length: Int, + timeout: Int, +) -> Result(bytes_tree.BytesTree, Reason) + // https://www.erlang.org/doc/apps/kernel/gen_tcp.html#connect/4 @external(erlang, "gen_tcp", "connect") fn gen_tcp_connect(