Code Monkey home page Code Monkey logo

jenjin-io's Introduction

Jenjin-IO

IO Utilities for Client/Server Connections

GitHub license Build Status Coverage Status

Usage

Jenjin-IO provides a library for creating simple Socket-based connections, utilizing a customizable serialization scheme for messages.

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 a Connection, modification of an ExecutionContext is thread safe; synchronization, locking, etc... are not required.
However, if the ExecutionContext 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:

  1. 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.
  2. The deserialized Message is placed into the "incoming" queue of the Connection.

  3. During the next message execution cycle, the Connection invokes the execute method of all messages in the "inbound" queue in the order in which they were received, passing in its ExecutionContext as a parameter and storing any non-null return values in the "outgoing" message queue.

  4. 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 the Connection when it is built.

  • withSocket

    • This method is used to set the MessageReader and MessageWriter from the input and output streams belonging to the given socket
    • This method will throw an IllegalStateException if the MessageIOFactory has not been set
    • This method will throw an IllegalStateException if the MessageReader or MessageWriter have already been set
  • withInputStream and withOutputStream

    • These methods is used to set the MessageReader or MessageWriter (respectively) from the given InputStream or OutputStream
    • These methods will throw an IllegalStateException if the MessageIOFactory has not been set
    • These methods will throw an IllegalStateException if the MessageReader or MessageWriter (respectively) has already been set
  • withMessageIOFactory

    • This method accepts a MessageIOFactory, which is used to create a MessageReader and/or MessageWriter from a raw InputStream and/or OutputStream.
    • withSocket, withInputStream and withOutputStream will all throw an IllegalStateException 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)
  • withMessageReader and withMessageWriter

    • These methods directly set the MessageReader and MessageWriter to be used by the built Connection. (for withMessageReader) and withOutputStream (for MessageWriter).
    • If the MessageReader or MessageWriter has already been set, these methods will throw an IllegalStateException.

Important: These methods must both be invoked with non-null values (either explicitly, or by calling withSocket or withInputStream and withOutputStream) before the build method is called, or an IllegalStateException will be thrown.

Note: These methods will be called internally by withSocket, withInputStream

  • withErrorCallback

    • This method will cause the built Connection to invoke the specified BiConsumer if it encounters an error; it is recommended that the stop method be called on the Connection at the end of this callback so that the Connection closes as cleanly as possible.
  • withContextualTasks

    • This method takes in one or more Consumers that accept an ExecutionContext parameter, which will be invoked by the built Connection after each incoming message is executed.
    • This callback is useful when there are parts of your application that need to access the ExecutionContext of a Connection but should not be accessible from a Message. (UI components updating based on the current state of the context may be an example)
  • withShutdownCallback

    • This method takes in a Consumer that accepts a Connection, which is invoked after the built Connection 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.

Once you've configured your connection, you can build it with the build method:

Important: If the MessageReader, MessageWriter, or ExecutionContext are not set, the build method will throw an IllegalStateException. The MessageReader is set automatically if the withInputStream or getSocket methods are used; similarly, the MessageWriter is set automatically if the withOutputStream or withSocket 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:

  1. The build method accepts a Socket instead of having no parameters
  • Each time build is called, a new Connection will be created from the given Socket.
  1. There is a new withExecutionContextFactory method, that accepts an ExecutionContextFactory that will be used to generate a new ExecutionContext for each Connection built with this builder.
  2. The withMessageReader, withMessageWriter, withInputStream, withOutputStream and withExecutionContext methods are not present

Important: The callbacks (Consumers, BiConsumers) passed into a MultiConnectionBuilder 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.

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 built Server to accept inbound connections.

Important: If the build method is called without the ServerSocket being set, an IllegalStateException will be thrown.

  • withMultiConnectionBuilder
    • This method takes in a MultiConnectionBuilder that is used by the built Server to generate new Connections from inbound sockets.

Important: If the build method is called without the MultiConectionBuilder being set, an IllegalStateException will be thrown.

  • withContextualTasks

    • This method allows for contextual callbacks in a similar fashion to the withContextualTasks method of SingleConnectionBuilder, but allows for access to the Server object.
    • Use of this method is helpful for things like broadcasting a message to all connections, without giving the Connection objects direct access to the Server.
  • withConnectionAddedCallbacks

    • This method accepts one or more Consumer<Connection> parameters, which are invoked any time a new Connection 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.
  • withConnectionRemovedCallbacks

    • This method accepts one or more Consumer<Connection> parameters, which are invoked any time a Connection is removed from the Server
    • Once again, these are useful when you need to be notified of a Connection being removed from the Server.
  • withStartupCallbacks

    • This method accepts one or more Consumer<Server> parameters, which are invoked after the Server has been started.
  • withShutdownCallbacks

    • This method accepts one or more Consumer<Server> parameters, which are invoked after the Server has attempted to shut down and gracefully close all existing Connections.

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();
}

Other important classes

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.


Building

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

License

Jenjin-IO is licensed under the MIT license

jenjin-io's People

Contributors

floralvikings avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

jenjin-io's Issues

Better use of generics

Currently the Message and Connection classes make decent use of generics; both of the (.*)ConnectionBuilder classes, the ServerBuilder class, and the Server class could use a boost in this department, mainly to avoid ugly casting of ExecutionContext arguments in Lambdas.

Conditional Contextual Tasks

It would be excellent to have an (optional) way of making a contextual task conditional; I'm thinking something like a Predicate<Message> that can be passed along with each contextual task, and the task will only execute if the predicate evaluates to true.

I'm thinking this could really help #6 because it would allow for completely ignoring deauthentication requests when not already authenticated and whatnot, and also because it would give a pretty convenient way to inject the Authenticator into the execution thread:

connectionBuilder.withContextualTask( (context) -> {
    if(!context.isAuthenticated) {
        authenticator.authenticate(context);
    }
}, (message) -> message instanceof AuthenticationRequest);

Integration Test fails intermittently

Only seen in CI builds, likely due to the fact that the test contains a large number of threads and blocking. Would be better to use timeouts instead.

Create adaptTo argument for MessageAdapter

We can adapt message from a different type of message; why not to a different type of message?

Obviously this can be default empty, and the existing adaptFrom argument could be made optional as well.

Authentication Interface

Given the following:

  1. User information is stored in the ExecutionContext
  2. A variable number of strings are required to authenticate
    • Usually, but not necessarily exactly, two (think two-factor auth)
  3. Authentication can be handled by multiple backends; SQL, Mongo, Flat Files, APIs, etc...

It seems reasonable to have an interface for authentication, something like the following

public interface Authenticator<T extends ExecutionContext>
{
    /**
     * Authenticate the user with the given credentials, populating the given ExecutionContext
     * IF AND ONLY IF the authentication is successful.
     *
     * @param context The context that will be populated with user data IF the authentication
     * is successful.  If the authentication is not successful, the context should remain 
     * unmodified.
     *
     * @param credentials One or more Strings identifying the user.
     *
     * @return true if the authentication was a success and the context was populated with user
     * data, false if the authentication was a failure and the context was not modified.
     *
     * @throws AuthenticationException If there is an unexpected exception when handling the 
     * authentication.
     */
    bool authenticate(T context, String... credentials) throws AuthenticationException;

    /**
     * Deauthenticate the user with the given credentials, depopulating the given 
     * ExecutionContext IF AND ONLY IF the authentication is successful.
     *
     * @param context The context that will be depopulated with user data IF the 
     * deauthentication is successful.  If the deauthentication is not successful,
     * the context should remain unmodified.  
     * (Maybe not, just a rough draft)
     *
     * @param credentials One or more Strings identifying the user.
     *
     * @throws AuthenticationException If there is an unexpected exception when handling the 
     * authentication.
     */
    void deauthenticate(T context, String... credentials) throws AuthenticationException;

    /**
     * Update the user with the given credentials from the given 
     * ExecutionContext.
     *
     * @param context The context that will be used to update user data.  
     * The context should remain unmodified.
     *
     * @param credentials One or more Strings identifying the user.
     *
     * @throws AuthenticationException If there is an unexpected exception when handling the 
     * update.
     */
    void update(T context, String... credentials) throws AuthenticationException;
}

Since the generic type of Server is also <T extends ExecutionContext> it should be an enormous pain drop right in without causing problems.

There'd have to be a way to access it (preferably from a Message containing the credentials); maybe a new interface like AuthenticationRequest or something that has a method returning credentials? That way a Message implementation could implement both interfaces.

That would result in some gross casting, though, so maybe it should be implemented as a part of the ExecutionContext?

Anyway, definitely an idea worth exploring.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.