Individual threads
- You can use the simple InputStream/OutputStream (or Reader/Writer) API, and wrapping Streams around each other. In my current project, I'm using a stack of
- MessageFormatter, a custom formatting class
- PrintWriter
- OutputStreamWriter
- DebugOutputStream (custom class, which makes a copy for debugging purposes)
- DeflatorOutputStream (a custom subclass, in fact, which supports flushing)
- the OutputStream of a SSLSocket
and the other way around on the receiving side. This is quite comfortable, since you only have to deal with the top layer in your program logic (the rest is mostly one constructor call each).
- you need a new Thread (or even pair of threads, depending on architecture) for each connection.
(In my project I have a MessageParser-Thread for each connection, which then gives individual jobs to a ThreadPool, and these jobs then may write to one or several of the open connections, not only the one which spawned them. The writing is synchronized, of course.)
- each thread needs quite a bit of stack space, which can be problematic if you are on a resource limited machine.
In the case of short-lived connections, you actually don't want a new thread for each connection, but only a new Runnable executed on a ThreadPool, since construction of new Threads takes a bit of time. (Note: With JDK 19, we can get cheap virtual threads. Use these instead, the thread pools are hidden in the background by the platform.)
nonblocking IO
- if you have such a multi-layer architecture with multiple conversions, you have to arrange it all by yourself. It is possible:
- my MessageFormatter can write to a CharBuffer as well as to a Writer.
- for the Char-to-Byte-formatting, use CharsetEncoder/CharsetDecoder (transfers data between CharBuffer and ByteBuffer).
- for the compressing, I used wrapper classes around Deflater/Inflater, which transfers data between two ByteBuffers.
- for the encryption, I used a SSLEngine (one for each connection), which also uses ByteBuffers for input and output.
- Then, write to a SocketChannel (or in the other direction, read from it).
Still, the management overhead is quite a hassle, as you must track where the data is. (I actually had a two pipelines for each connection, and one or two threads managing the data in all the pipelines, between this waiting on a Selector for new data on the sockets (or for new space there, in the outgoing case). (The actual processing after the parsing of the Messages still took place in spawned Runnables in a ThreadPool.)
- You need only a small number of threads. This was actually the reason I tried to do this asynchronously. It worked (with the same synchronous client), but was way slower than my multi-thread solution, so I put this back until we run out of memory for too much threads. (Until now, there are not so much connections at the same time.)