HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

TCP Server using NIO to save data from IoT clients

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
nioiotclientstcpsaveusingserverfromdata

Problem

I've built a small single threaded TCP server using NIO.
This server is used by small client devices to report things like temperature, when the device has been switched on, when it switches off, and various other parameters. The server then saves those parameters to a database.

We've implemented a very simple protocol to handle the process. The clients report a string, which the server can parse as JSON. Once the string has been converted to a JSONObject it is processed by a database handler class, which saves the various parameters to various tables.

This server will eventually need to handle a lot of connections.
After looking at the code with fresh eyes this morning I realised there are quite a few problems with it. What I've spotted so far is the following (tell me if I'm wrong here):

  • I handle saving the data to the database in the server main thread. This will slow down the server because it's blocking while it saves data to the database. In order to rectify this I should perhaps make a thread pool for saving to the database, and share some sort of list with the threads, possibly a Queue or LinkedList. The server can then simply push the JSONObject onto the LinkedList and notify the thread pool that there is data ready to be processed. Would this be a reasonable thing to do?



  • Right now I'm not using OP_WRITE. I'm reading the data from the channel and appending it to a string, then once I see the 'end' message in the string I simply write to the channel. This is also blocking the server. How should I implement the OP_WRITE functionality. Most examples of NIO servers that I've come across write to the channel after they have finished reading. This seems wrong.



  • Some parts of my if/else statement seem redundant. But I'm unsure how to fix it. It strikes me that once I read all the bytes and locate the 'end' message I should switch the channel to OP_WRITE and handle the writing somewhere else. But a previous attempt to implement that fail

Solution

This will need to be a little bit of a history lesson, and tutorial.....
History

In the good old days, Java server programmers would have one Server Socket that listened for connections, and for each incoming connection, would create a thread to handle it. The threads would block, waiting for IO from the socket, and that's OK, but not great:

  • Threads were expensive to create in terms of time



  • scheduling many threads was hard



  • concurrency is hard



Then, two things happened at about the same time:

  • Java introduced NIO



  • Thread handling in Java became much more efficient



The NIO concepts were fantastic because:

  • You have a single thread handling all network processing. A CPU is faster than a network connection, so, just so long as you had fast processing, a single thread is more than enough for an interface.



  • You can reduce the number of active threads from 1-per-client to be 1-per-CPU (or so).



Instead of having each socket handled by a thread, you have a small-ish pool of threads ready, and waiting, to handle the few sockets that actually do something.

At the same time (actually, shortly after) as NIO was introduced, the cost factor of creating, and maintaining many threads was significantly reduced, in Java, on the core platforms (Intel, AMD, etc.). As a result, it became effectively just as reasonable to have 1 thread per client again, even if there are thousands of clients.

The issue is now mostly confined to places where thread handling is still hard, slow, and expensive.

NIO is still relevant today where there are many, many client connections, or where the infrastructure still has expensive context-switches, or thread startup times.
Tutorial

For NIO Selectors to make sense in Java, you should follow particular pattern in your code:

  • set up your selector and the thread that manages it.



  • set up a limited thread pool (about as many threads as you have CPU's/Cores)



  • plan your protocol for communication



  • set up a state machine for each client



Now, with the above, the important part is the state machine for each client. The state machine must have a very small 'footprint' on the network side. All it does is read the channel when it is told that data is available. If the data is a complete message, it flips state to 'Processing'. When the processing is complete, it switches to 'Writing', and when writing is complete it switches back to 'Reading'.

If you set it up right, Client states will be:

  • created when you accept a socket.



  • switch to Reading if the client is a valid client (using your protocol).



  • be attached to a SelectionKey.OP_READ



  • when data is available, the handler is updated with the new data



  • if the data constitutes a full message, the state changes to Processing and:



  • the OP_READ is deregistered.



  • the client handler is scheduled to run on a different thread



  • the main thread goes back to monitoring the selector



  • you identify that a client handler has completed a task (using a blocking queue, and a separate thread?)



  • you register the socket for OP_WRITE with the client handler attached.



  • you get the Write-ready notification, and copy the data on to the socket output.



  • you return the state to Reading



Review

Your code is doing far too much work in the selector thread.

you are:

  • selecting channels (fine)



  • reading socket content (fine)



  • processing JSON content (not fine)



  • building output responses (not fine)



  • outputting socket content.



In addition I have the following observations:

  • you are doing too much in that single handler loop. You should at least have functions for the various routines (read, write, accept).



  • you do not have a protocol for the data on the stream - how do you identify when the client is a valid client? What if someone browses to your socket with a web browser, what then? What about ssh? You should expect a client to identify itself before handling it.



  • you expect all messages to be received 'in full' on each loop. What if only the first 20 bytes of the message are available when you read?



  • you don't specify the character encoding for strings read from the socket. Use a specific encoding (always UTF-8?) so that if your client is running on a different host than your server, and they have different default encodings, it will always work.

Context

StackExchange Code Review Q#80055, answer score: 4

Revisions (0)

No revisions yet.