Socket handling for Scala
Coaxial provides a generic framework for client/server communications over a network, abstracting over protocol differences, including statefulness. Implementations are provided for sending and receiving UDP packets, and connecting and serving TCP connections.
- provides an abstract framework for data transmission through sockets
- supports both stateful (e.g. TCP) and stateless (e.g. UDP) connections
- server and client implementations for UDP, TCP and UNIX domain sockets
- allows servers and clients to process connections as state machines
- uses a safe, functional API
All terms and types are defined in the coaxial
package, which can be imported
with:
import coaxial.*
In general, a client connects to a listing server by calling either transmit
(for stateless transmission) or connect
(to start a stateful connection) on
an endpoint. The endpoint type determines the transmission protocol.
Endpoint types include,
- local UDP ports, e.g.
udp"3480"
- local TCP ports, e.g.
tcp"2205"
- remote ports, e.g.
host"example.com" on tcp"8844"
orip"192.168.1.24" on udp"1280"
- UNIX domain sockets
- any type for which an
Addressable
orConnectable
typeclass instance exists
For example, a simple message could be sent to a local UDP port with,
udp"7327".transmit(t"Hello world!")
This will send a single UDP packet containing the bytes of the string Hello world!
, and nothing further. This is useful for "fire-and-forget" messaging.
The payload of the UDP packet is a Text
string, but can be any type for which
a Transmissible
typeclass instance exists, which includes Text
, Bytes
and
anything that has a Spectacular
Encoder
.
All Addressable
endpoints can have data transmitted to them, but some, such
as TCP ports, can initiate a bidirectional connection. These are those
endpoints for which a Connectable
typeclass instance exists. Connectable
is
a subtype of Addressable
.
Starting a stateful connection, with the connect
method, requires a bit more
work. Its state is user-defined, and can be represented by any type. So a
connection requires the following:
- the initial state
- the type of the state, if it can't be inferred from the initial state
- the initial message, in a type wich is
Transmissible
- a lambda to interact with the server, interpreting responses, modifying state, and sending further messages
Of these, the lambda is where most of the work is done. It will be executed
sequentially for every chunk of data, as Bytes
, received from the server, and
must choose how to process that data.
The return type of the lambda must be one of the following Control
values:
Reply(message)
, to respond to the server with a new message on the current connectionContinue(state)
, to update the state to a new value, and wait for more data from the serverTerminate
, to abort the connectionConclude(message, state)
, to respond to the server with a final message, and update the state to a new value
Within the body of the lambda, the named contextual value, state
, provides
the current state.
The return type of connect
will be the final state value.
Here is an trivial example:
(host"remote.com" on tcp"1920").connect(t"")(t"DATA\n"): bytes =>
val text = bytes.as[Text]
val state2 = state+text
if state2.length < 100 then Control.Continue(state2)
else Control.Conclude(t"END\n", state2)
This will connect to TCP port 1920 on host remote.com
, sending the initial
request message, DATA
. When the server (hopefully) responds with some textual
data, we convert the bytes to text and append them to the existing state,
accessed with the named contextual value, state
. If we have less than 100
characters of text, then we continue waiting, updating the state to the new
value. But if we have already received more than 100 characters, we conclude
the connection by sending a final message, END
, to the server, updating the
state to its final value. The return value will be at least 100 characters of
text.
It should be clear from this example that more complex connection handlers are possible, and can be written in a style that's similar to folding over a stream.
The functional interface of connect
, whose lambda requires a Control
value to be specified means that it's impossible to forget how to handle the
input; the programmer must decide to reply, continue, conclude or terminate the
connection, otherwise the code will not compile.
Writing a server can be simpler than sending a client request. The listen
method is available on any type with a Bindable
typeclass instance, but this
notably includes:
- UNIX domain sockets
- TCP ports
- UDP ports
We can write a server by calling the listen
method on a Bindable
type, and
specifying, by means of a lambda, how a request should be handled.
Calling listen
will start a new server in a separate thread. Any incoming
request will dispatch it to the handler lambda. The type of the input will
depend on the type of the local endpoint. So a stateful connection will provide
an input type which can support an interactive session, whereas a stateless
connection will provide just the payload.
The return value of the lambda will also be determined from the endpoint type, as appropriate for stateful and stateless connections.
As an example, a simple UDP server takes input as a UdpPacket
and returns a
UdpResponse
. UdpPacket
is a case class with fields: data
, the payload
bytes; sender
, an Ipv4
or Ipv6
source address; and port
, the remote UDP
port.
UdpResponse
is an enumeration, which is either Ignore
(if no response is
expected to the UDP packet), or Reply(payload)
with the bytes to respond
with.
So a UDP server which responds to PING
messages with PONG
, but ignores
other messages could be implemented with just:
val server = udp"1722".listen: packet =>
if packet.as[Text] == t"PING" then Reply(t"PONG") else Ignore
The return value, server
, is a SocketService
representing the server
running in the background, and will be returned as soon as the port has been
bound. It may be stopped at any time by calling its stop
method, like so:
server.stop()
Coaxial is classified as embryotic. For reference, Soundness projects are categorized into one of the following five stability levels:
- embryonic: for experimental or demonstrative purposes only, without any guarantees of longevity
- fledgling: of proven utility, seeking contributions, but liable to significant redesigns
- maturescent: major design decisions broady settled, seeking probatory adoption and refinement
- dependable: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version
1.0.0
or later - adamantine: proven, reliable and production-ready, with no further breaking changes ever anticipated
Projects at any stability level, even embryonic projects, can still be used, as long as caution is taken to avoid a mismatch between the project's stability level and the required stability and maintainability of your own project.
Coaxial is designed to be small. Its entire source code currently consists of 350 lines of code.
Coaxial will ultimately be built by Fury, when it is published. In the meantime, two possibilities are offered, however they are acknowledged to be fragile, inadequately tested, and unsuitable for anything more than experimentation. They are provided only for the necessity of providing some answer to the question, "how can I try Coaxial?".
-
Copy the sources into your own project
Read the
fury
file in the repository root to understand Coaxial's build structure, dependencies and source location; the file format should be short and quite intuitive. Copy the sources into a source directory in your own project, then repeat (recursively) for each of the dependencies.The sources are compiled against the latest nightly release of Scala 3. There should be no problem to compile the project together with all of its dependencies in a single compilation.
-
Build with Wrath
Wrath is a bootstrapping script for building Coaxial and other projects in the absence of a fully-featured build tool. It is designed to read the
fury
file in the project directory, and produce a collection of JAR files which can be added to a classpath, by compiling the project and all of its dependencies, including the Scala compiler itself.Download the latest version of
wrath
, make it executable, and add it to your path, for example by copying it to/usr/local/bin/
.Clone this repository inside an empty directory, so that the build can safely make clones of repositories it depends on as peers of
coaxial
. Runwrath -F
in the repository root. This will download and compile the latest version of Scala, as well as all of Coaxial's dependencies.If the build was successful, the compiled JAR files can be found in the
.wrath/dist
directory.
Contributors to Coaxial are welcome and encouraged. New contributors may like to look for issues marked beginner.
We suggest that all contributors read the Contributing Guide to make the process of contributing to Coaxial easier.
Please do not contact project maintainers privately with questions unless there is a good reason to keep them private. While it can be tempting to repsond to such questions, private answers cannot be shared with a wider audience, and it can result in duplication of effort.
Coaxial was designed and developed by Jon Pretty, and commercial support and training on all aspects of Scala 3 is available from Propensive OÜ.
Coaxial cable is designed for the transmission of high-frequency data with minimal loss, alluding to the function of transmission of data that this library provides.
In general, Soundness project names are always chosen with some rationale, however it is usually frivolous. Each name is chosen for more for its uniqueness and intrigue than its concision or catchiness, and there is no bias towards names with positive or "nice" meanings—since many of the libraries perform some quite unpleasant tasks.
Names should be English words, though many are obscure or archaic, and it should be noted how willingly English adopts foreign words. Names are generally of Greek or Latin origin, and have often arrived in English via a romance language.
The logo shows a cross-section of a coaxial connector.
Coaxial is copyright © 2025 Jon Pretty & Propensive OÜ, and is made available under the Apache 2.0 License.