TCP echo server

This commit is contained in:
Alexander Heldt
2025-11-11 20:36:55 +01:00
parent c76d1aaa53
commit 03cdb9428f
5 changed files with 124 additions and 10 deletions

View File

@@ -14,6 +14,8 @@ version = "1.0.0"
[dependencies] [dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0" 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] [dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0" gleeunit = ">= 1.0.0 and < 2.0.0"

View File

@@ -2,10 +2,14 @@
# You typically do not need to edit this file # You typically do not need to edit this file
packages = [ 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 = "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" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
] ]
[requirements] [requirements]
gleam_otp = { version = ">= 1.2.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }

View File

@@ -1,11 +1,15 @@
import gleam/io import gleam/io
import tcp/echo_server
import tcp/reason import tcp/reason
import tcp/tcp import tcp/tcp
pub fn main() -> Nil { pub fn main() -> Nil {
let socket_path = "/tmp/musicplayer.sock" let socket_path = "/tmp/musicplayer.sock"
case echo_server.new(socket_path) {
Error(err) -> io.println("Failed to create echo server: " <> err)
Ok(_) -> {
case tcp.connect(socket_path) { case tcp.connect(socket_path) {
Error(r) -> Error(r) ->
io.println("Failed to connect to socket: " <> reason.to_string(r)) io.println("Failed to connect to socket: " <> reason.to_string(r))
@@ -16,11 +20,21 @@ pub fn main() -> Nil {
messages |> send_messages(socket, _) messages |> send_messages(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") io.println("closing")
tcp.close(socket) tcp.close(socket)
} }
} }
} }
}
}
fn send_messages(socket: tcp.Socket, messages: List(String)) -> Nil { fn send_messages(socket: tcp.Socket, messages: List(String)) -> Nil {
case messages { case messages {

39
src/tcp/echo_server.gleam Normal file
View File

@@ -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)
}
}
}

View File

@@ -1,4 +1,6 @@
import gleam/bit_array
import gleam/bytes_tree import gleam/bytes_tree
import gleam/result
import tcp/reason.{type Reason} import tcp/reason.{type Reason}
@@ -17,15 +19,52 @@ type ModeValue {
type TCPOption { type TCPOption {
Active(Bool) Active(Bool)
Mode(ModeValue) 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) { pub fn connect(socket_path: String) -> Result(Socket, Reason) {
let options = [Mode(Binary), Active(False)] let options = [Mode(Binary), Active(False)]
// port zero with `local` address
let port = 0
// timeout in ms // timeout in ms
let timeout = 1000 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) { pub fn send(socket: Socket, message: String) -> Result(Nil, Reason) {
@@ -36,6 +75,22 @@ pub fn close(socket: Socket) -> Nil {
gen_tcp_close(socket) 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 // https://www.erlang.org/doc/apps/kernel/gen_tcp.html#connect/4
@external(erlang, "gen_tcp", "connect") @external(erlang, "gen_tcp", "connect")
fn gen_tcp_connect( fn gen_tcp_connect(