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
ExecutionContextinterfaces, 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,
ExecutionContextandExecutionContextFactoryinterfaces, 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#executemethod and in contextual callbacks passed to aConnection, modification of anExecutionContextis thread safe; synchronization, locking, etc... are not required.
However, if theExecutionContextis 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
Messageobject- The method of deserialization depends on the
MessageReaderowned by the Connection; Jenjin-IO provides an implementation using Gson for convenience.
- The method of deserialization depends on the
-
The deserialized
Messageis placed into the "incoming" queue of theConnection. -
During the next message execution cycle, the
Connectioninvokes theexecutemethod of all messages in the "inbound" queue in the order in which they were received, passing in itsExecutionContextas a parameter and storing any non-null return values in the "outgoing" message queue. -
During the next message broadcast cycle, any
Messagesin 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
ExecutionContextinto theConnectionwhen it is built. -
withSocket- This method is used to set the
MessageReaderandMessageWriterfrom the input and output streams belonging to the given socket - This method will throw an
IllegalStateExceptionif theMessageIOFactoryhas not been set - This method will throw an
IllegalStateExceptionif theMessageReaderorMessageWriterhave already been set
- This method is used to set the
-
withInputStreamandwithOutputStream- These methods is used to set the
MessageReaderorMessageWriter(respectively) from the givenInputStreamorOutputStream - These methods will throw an
IllegalStateExceptionif theMessageIOFactoryhas not been set - These methods will throw an
IllegalStateExceptionif theMessageReaderorMessageWriter(respectively) has already been set
- These methods is used to set the
-
withMessageIOFactory- This method accepts a
MessageIOFactory, which is used to create aMessageReaderand/orMessageWriterfrom a rawInputStreamand/orOutputStream. withSocket,withInputStreamandwithOutputStreamwill all throw anIllegalStateExceptionif this has not first been invoked.GsonMessageIOFactoryis 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
-
withMessageReaderandwithMessageWriter- These methods directly set the
MessageReaderandMessageWriterto be used by the builtConnection. (forwithMessageReader) andwithOutputStream(forMessageWriter). - If the
MessageReaderorMessageWriterhas 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
withSocketorwithInputStreamandwithOutputStream) before thebuildmethod is called, or anIllegalStateExceptionwill be thrown.
Note: These methods will be called internally by
withSocket,withInputStream
-
withErrorCallback- This method will cause the built
Connectionto invoke the specifiedBiConsumerif it encounters an error; it is recommended that thestopmethod be called on theConnectionat the end of this callback so that theConnectioncloses as cleanly as possible.
- This method will cause the built
-
withContextualTasks- This method takes in one or more
Consumersthat accept anExecutionContextparameter, which will be invoked by the builtConnectionafter each incoming message is executed. - This callback is useful when there are parts of your application that need to access the
ExecutionContextof aConnectionbut 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
Consumerthat accepts aConnection, which is invoked after the builtConnectionhas 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, orExecutionContextare not set, thebuildmethod will throw an IllegalStateException. TheMessageReaderis set automatically if thewithInputStreamorgetSocketmethods are used; similarly, theMessageWriteris set automatically if thewithOutputStreamorwithSocketmethods are used.
Connection connection = builder.build();Note: The
SingleConnectionBuilderclass 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
buildmethod accepts aSocketinstead of having no parameters
- Each time
buildis called, a newConnectionwill be created from the given Socket.
- There is a new
withExecutionContextFactorymethod, that accepts anExecutionContextFactorythat will be used to generate a newExecutionContextfor eachConnectionbuilt with this builder. - The
withMessageReader,withMessageWriter,withInputStream,withOutputStreamandwithExecutionContextmethods are not present
Important: The callbacks (
Consumers,BiConsumers) passed into aMultiConnectionBuildershould 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
SSLServerSocketif possible.
- A
MultiConnectionBuilder- Necessary for building a new
Connectionfor 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 builtServerto accept inbound connections.
- This method takes in a Java
Important: If the
buildmethod is called without theServerSocketbeing set, anIllegalStateExceptionwill be thrown.
withMultiConnectionBuilder- This method takes in a
MultiConnectionBuilderthat is used by the builtServerto generate newConnectionsfrom inbound sockets.
- This method takes in a
Important: If the
buildmethod is called without theMultiConectionBuilderbeing set, anIllegalStateExceptionwill be thrown.
-
withContextualTasks- This method allows for contextual callbacks in a similar fashion to the
withContextualTasksmethod ofSingleConnectionBuilder, but allows for access to theServerobject. - Use of this method is helpful for things like broadcasting a message to all connections, without giving the
Connectionobjects 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 newConnectionis added to the server. - These callbacks are useful for when you want to perform an action (such as logging) whenever a new
Connectionis made.
- This method accepts one or more
-
withConnectionRemovedCallbacks- This method accepts one or more
Consumer<Connection>parameters, which are invoked any time aConnectionis removed from theServer - Once again, these are useful when you need to be notified of a
Connectionbeing removed from theServer.
- This method accepts one or more
-
withStartupCallbacks- This method accepts one or more
Consumer<Server>parameters, which are invoked after theServerhas been started.
- This method accepts one or more
-
withShutdownCallbacks- This method accepts one or more
Consumer<Server>parameters, which are invoked after theServerhas attempted to shut down and gracefully close all existingConnections.
- This method accepts one or more
Note: The
ServerBuilderclass 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_HOMEenvironment 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 buildJenjin-IO is licensed under the MIT license