IO Utilities for Client/Server Connections
Jenjin-IO provides a library for creating simple Socket-based connections, utilizing a customizable serialization scheme for messages.
-
For a client-side or peer-to-peer connection:
- Implementing (at minimum) the Message and
ExecutionContext
interfaces, then building and starting aConnection
- Implementing (at minimum) the Message and
-
For a server that accepts Jenjin-IO client-side connections:
- Implementing (at minimum) the Message,
ExecutionContext
andExecutionContextFactory
interfaces, creating a
MultiConnectionBuider
, building and starting aServer
- Implementing (at minimum) the Message,
This interface should be implemented by any application using the Jenjin-IO; it can contain Connection
-specific data
that persists across incoming and outgoing Messages
. Whenever a Connection
executes a Message
, the
ExecutionContext
belonging to that Connection
is passed as a parameter to the Message#execute
method.
Important: In the
Message#execute
method and in contextual callbacks passed to aConnection
, modification of anExecutionContext
is thread safe; synchronization, locking, etc... are not required.
However, if theExecutionContext
is accessed from threads other than these, care should be taken to make sure that any access to it is made safe.
This interface should be implemented by any Server-side application using the Jenjin-IO API; it has one method
(createInstance
) which should return a new, distinct, and mutable ExecutionContext
. The
Server
class must be passed an implementation of this interface (typically through a ServerBuilder
) so that
when new connections are created, they can be passed their own ExecutionContext
instance without affecting those
of existing or further new connections.
The Message
interface is core to the Jenjin-IO API; it determines what data is received by a
Connection
and what is done when data is received. It contains a single (default) method called execute
which takes a single parameter (the ExecutionContext
belonging to the Connection
that received the message)
and returns an optional Message
that is queued up to be sent by the Connection
.
The Connection
class is the "glue" that ties the other components of the Jenjin-IO API together. Its main
responsibility is to spawn and maintain the threads responsible for reading, executing, and writing Messages
.
When a Connection
is started (by calling the start
method), it automatically begins retrieving any messages
sent to it. When it receives a message, the following process takes place:
-
The raw data of the message is deserialized into a
Message
object- The method of deserialization depends on the
MessageReader
owned by the Connection; Jenjin-IO provides an implementation using Gson for convenience.
- The method of deserialization depends on the
-
The deserialized
Message
is placed into the "incoming" queue of theConnection
. -
During the next message execution cycle, the
Connection
invokes theexecute
method of all messages in the "inbound" queue in the order in which they were received, passing in itsExecutionContext
as a parameter and storing any non-null return values in the "outgoing" message queue. -
During the next message broadcast cycle, any
Messages
in the "outgoing" queue are serialized into raw data and sent.
Connections
can either be built manually (for client-side or peer-to-peer connections) using the
SingleConnectionBuilder
class, or they can be built automatically by a Server
using a MultiConnectionBuilder
.
This class is used to build a single Connection
; there are a few different configurations that can be done when
building a Connection
that are of interest:
-
withExecutionContext
-
This method is used to pass an
ExecutionContext
into theConnection
when it is built. -
withSocket
- This method is used to set the
MessageReader
andMessageWriter
from the input and output streams belonging to the given socket - This method will throw an
IllegalStateException
if theMessageIOFactory
has not been set - This method will throw an
IllegalStateException
if theMessageReader
orMessageWriter
have already been set
- This method is used to set the
-
withInputStream
andwithOutputStream
- These methods is used to set the
MessageReader
orMessageWriter
(respectively) from the givenInputStream
orOutputStream
- These methods will throw an
IllegalStateException
if theMessageIOFactory
has not been set - These methods will throw an
IllegalStateException
if theMessageReader
orMessageWriter
(respectively) has already been set
- These methods is used to set the
-
withMessageIOFactory
- This method accepts a
MessageIOFactory
, which is used to create aMessageReader
and/orMessageWriter
from a rawInputStream
and/orOutputStream
. withSocket
,withInputStream
andwithOutputStream
will all throw anIllegalStateException
if this has not first been invoked.GsonMessageIOFactory
is provided as a convenience; implementing your own is not necessary (though it is encouraged to better suit the needs of your application)
- This method accepts a
-
withMessageReader
andwithMessageWriter
- These methods directly set the
MessageReader
andMessageWriter
to be used by the builtConnection
. (forwithMessageReader
) andwithOutputStream
(forMessageWriter
). - If the
MessageReader
orMessageWriter
has already been set, these methods will throw anIllegalStateException
.
- These methods directly set the
Important: These methods must both be invoked with non-null values (either explicitly, or by calling
withSocket
orwithInputStream
andwithOutputStream
) before thebuild
method is called, or anIllegalStateException
will be thrown.
Note: These methods will be called internally by
withSocket
,withInputStream
-
withErrorCallback
- This method will cause the built
Connection
to invoke the specifiedBiConsumer
if it encounters an error; it is recommended that thestop
method be called on theConnection
at the end of this callback so that theConnection
closes as cleanly as possible.
- This method will cause the built
-
withContextualTasks
- This method takes in one or more
Consumers
that accept anExecutionContext
parameter, which will be invoked by the builtConnection
after each incoming message is executed. - This callback is useful when there are parts of your application that need to access the
ExecutionContext
of aConnection
but should not be accessible from aMessage
. (UI components updating based on the current state of the context may be an example)
- This method takes in one or more
-
withShutdownCallback
- This method takes in a
Consumer
that accepts aConnection
, which is invoked after the builtConnection
has halted its threads and attempted to close its backing streams. - This method can also take an
Iterable<Connection>
or be invoked multiple times if multiple callbacks are desired.
- This method takes in a
Once you've configured your connection, you can build it with the build
method:
Important: If the
MessageReader
,MessageWriter
, orExecutionContext
are not set, thebuild
method will throw an IllegalStateException. TheMessageReader
is set automatically if thewithInputStream
orgetSocket
methods are used; similarly, theMessageWriter
is set automatically if thewithOutputStream
orwithSocket
methods are used.
Connection connection = builder.build();
Note: The
SingleConnectionBuilder
class is fluent; it can be used like so:
private Connection getConn(Socket sock, ExecutionContext context) {
return new SingleConnectionBuilder()
.withMessageIOFactory(new GsonMessageIOFactory())
.withSocket(sock)
.withExecutionContext(context)
.withErrorCallback((connection, throwable) -> connection.stop())
.build();
}
The MultiConnectionBuilder
class is very simliar to the SingleConnectionBuilder
class, with a few key
differences:
- The
build
method accepts aSocket
instead of having no parameters
- Each time
build
is called, a newConnection
will be created from the given Socket.
- There is a new
withExecutionContextFactory
method, that accepts anExecutionContextFactory
that will be used to generate a newExecutionContext
for eachConnection
built with this builder. - The
withMessageReader
,withMessageWriter
,withInputStream
,withOutputStream
andwithExecutionContext
methods are not present
Important: The callbacks (
Consumers
,BiConsumers
) passed into aMultiConnectionBuilder
should be immutable, as each callback will be passed into every connection rather than being copied.
The Server
class is a convenience class provided by Jenjin-IO that is capable of accepting multiple client Connections
. It is not terribly robust, so it may be prudent to examine the source and create your own
implementation that better suits your needs.
A Server
requires the following objects (which are supplied from a ServerBuilder
:
- A
ServerSocket
- This is necessary to listen for inbound socket connections
- Use an
SSLServerSocket
if possible.
- A
MultiConnectionBuilder
- Necessary for building a new
Connection
for each inbound socket.
- Necessary for building a new
This class has two broadcast
methods, one which takes a single Message
parameter, which is broadcast to
all existing Connections
, and a second which takes both a Message
parameter and a Predicate<Connection>
parameter, which broadcasts to all Connections
which fulfill the Predicate
.
Much like the SingleConnectionBuilder
and MultiConnectionBuilder
classes, the ServerBuilder
class is
responsible for configuring and building a Server
.
The ServerBuilder
class has several methods that help with configuring a Server
:
withServerSocket
- This method takes in a Java
ServerSocket
, which will be used by the builtServer
to accept inbound connections.
- This method takes in a Java
Important: If the
build
method is called without theServerSocket
being set, anIllegalStateException
will be thrown.
withMultiConnectionBuilder
- This method takes in a
MultiConnectionBuilder
that is used by the builtServer
to generate newConnections
from inbound sockets.
- This method takes in a
Important: If the
build
method is called without theMultiConectionBuilder
being set, anIllegalStateException
will be thrown.
-
withContextualTasks
- This method allows for contextual callbacks in a similar fashion to the
withContextualTasks
method ofSingleConnectionBuilder
, but allows for access to theServer
object. - Use of this method is helpful for things like broadcasting a message to all connections, without giving the
Connection
objects direct access to theServer
.
- This method allows for contextual callbacks in a similar fashion to the
-
withConnectionAddedCallbacks
- This method accepts one or more
Consumer<Connection>
parameters, which are invoked any time a newConnection
is added to the server. - These callbacks are useful for when you want to perform an action (such as logging) whenever a new
Connection
is made.
- This method accepts one or more
-
withConnectionRemovedCallbacks
- This method accepts one or more
Consumer<Connection>
parameters, which are invoked any time aConnection
is removed from theServer
- Once again, these are useful when you need to be notified of a
Connection
being removed from theServer
.
- This method accepts one or more
-
withStartupCallbacks
- This method accepts one or more
Consumer<Server>
parameters, which are invoked after theServer
has been started.
- This method accepts one or more
-
withShutdownCallbacks
- This method accepts one or more
Consumer<Server>
parameters, which are invoked after theServer
has attempted to shut down and gracefully close all existingConnections
.
- This method accepts one or more
Note: The
ServerBuilder
class is fluent; it can be used like so:
private Server getServer(ServerSocket sock, MultiConnectionBuilder mcb) {
return new ServerBuilder()
.withServerSocket(sock)
.withMultiConnectionBuilder(mcb)
.withConnectionAddedCallback(this::doSomething)
.build();
}
These interfaces need not be explicitly implemented by your application; Jenjin-IO provides a few convenience classes (relying on the Gson library) that implement working versions of them in the com.jenjinstudios.io.serialization package
However, these classes are not optimized for performance or bandwidth, and it may be prudent to implement your own versions that better cater to the particular needs of your application.
This interface exposes methods to create a MessageReader
and MessageWriter
from an InputStream
and
OutputStream
respectively.
This interface exposes a read
method which returns a Message
and a close
method that should close
backing streams and perform any necessary cleanup.
This interface exposes a wrute
method which accepts a Message
and a close
method that should close
backing streams and perform any necessary cleanup.
Note: To build Jenjin-IO from source, you must have Java 8 installed and your
JAVA_HOME
environment variable pointing at the Java 8 installation.
Jenjin-IO is built using gradle; to build the library and run all tests, simply run this command in the Jenjin-IO directory:
./gradlew build
Jenjin-IO is licensed under the MIT license