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
orLinkedList
. The server can then simply push theJSONObject
onto theLinkedList
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 toOP_WRITE
and handle the writing somewhere else. But a previous attempt to implement that failed.
Once I understand how this should work properly I'm looking at rewriting the server using Netty, or perhaps vert.x, because I'm really not experienced enough to build this right now, not on the time scale that's needed anyway. If I could comfortably handle a few thousand connections I'd be quite happy. But given the blocking nature of what I've built so far that's not the case.
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.serverapp.deviceserver.data.DataDAO;
import com.serverapp.deviceserver.data.JSONKeys;
public class ServerRunnable implements Runnable {
private static final Logger EVENT_LOGGER = Logger
.getLogger(DeviceServer.EVENT_LOGGER_NAME);
private static final String JSON_START_WRAPPER = "{\"DATA\" :[";
private static final String JSON_END_WRAPPER = "]}";
private static final String END_JSON_MSG = "End\" }";
private int serverPort;
@SuppressWarnings("unused")
private int queueLength;
private boolean testMode = false;
DataDAO database;
public ServerRunnable(int serverPort, int queueLength, boolean testMode) {
this.serverPort = serverPort;
this.queueLength = queueLength;
this.testMode = testMode;
database = (!testMode) ? new DataDAO() : null;
}
private boolean running;
private ServerSocketChannel serverSocketChannel;
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(2048);
@Override
public void run() {
try {
Selector selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(serverPort));
System.out.println(System.getProperty("java.version"));
EVENT_LOGGER.log(Level.INFO,
"DeviceServerV0.2 Runnable- Listening for connections on port: "
+ serverSocket.getLocalPort());
running = true;
@SuppressWarnings("unused")
SelectionKey serverAcceptKey = serverSocketChannel.register(
selector, SelectionKey.OP_ACCEPT);
while (running) {
@SuppressWarnings("unused")
int keyCount = selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = (SelectionKey) keyIterator.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel ssc = (ServerSocketChannel) key
.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// Here we add the new connection to the selector
SelectionKey newKey = sc.register(selector,
SelectionKey.OP_READ);
newKey.attach(new StringBuilder(JSON_START_WRAPPER));
keyIterator.remove();
} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel sc = (SocketChannel) key.channel();
readBuffer.clear();
int read = sc.read(readBuffer);
if (read < 0) {
EVENT_LOGGER.log(Level.INFO,
"Read < 0; building JSON");
StringBuilder stringBuilder = (StringBuilder) key
.attachment();
stringBuilder.append(JSON_END_WRAPPER);
String received = stringBuilder.toString();
try {
JSONObject json = new JSONObject(received);
processJsonObject(json);
} catch (JSONException e) {
EVENT_LOGGER.log(
Level.WARNING,
"A JSONException occurred: "
+ e.getMessage());
}finally{
key.cancel();
}
} else if (read == 0) {
EVENT_LOGGER.log(Level.INFO,
"Read == 0; building JSON");
StringBuilder stringBuilder = (StringBuilder) key
.attachment();
stringBuilder.append(JSON_END_WRAPPER);
String received = stringBuilder.toString();
try {
JSONObject json = new JSONObject(received);
processJsonObject(json);
} catch (JSONException e) {
EVENT_LOGGER.log(
Level.WARNING,
"A JSONException occurred: "
+ e.getMessage());
key.cancel();
}
writeToChannel(sc);
// TODO: when switching to OP_WRITE do we remove the
// key from keyIterator? I'm not sure
// key.interestOps(SelectionKey.OP_WRITE);
} else {
EVENT_LOGGER.log(Level.INFO, "Read == " + read);
readBuffer.flip();
StringBuilder stringBuilder = (StringBuilder) key
.attachment();
byte[] receivedBytes = new byte[readBuffer.limit()];
readBuffer.get(receivedBytes);
String result = new String(receivedBytes);
stringBuilder.append(result);
if (result.contains(END_JSON_MSG)) {
EVENT_LOGGER.log(Level.INFO,
"Writing end message to "
+ sc.getRemoteAddress()
.toString() + "...");
writeToChannel(sc);
stringBuilder.append(JSON_END_WRAPPER);
String received = stringBuilder.toString();
try {
JSONObject json = new JSONObject(received);
processJsonObject(json);
} catch (JSONException e) {
EVENT_LOGGER.log(
Level.WARNING,
"A JSONException occurred: "
+ e.getMessage());
// TODO: Removing key from keyIterator in
// JSONException, is this correct?
key.cancel();
}
}
key.cancel();
}
keyIterator.remove();
}
}
}
} catch (Exception e) {
EVENT_LOGGER.log(Level.SEVERE, e.getMessage());
}
}
/**
* @param sc
* @throws UnsupportedEncodingException
* @throws IOException
*/
private void writeToChannel(SocketChannel sc)
throws UnsupportedEncodingException, IOException {
String configTest = "{ \"Event\" : \"End\" }\\r\\n";
byte[] bytes = configTest.getBytes("US-ASCII");
writeBuffer.clear();
writeBuffer.put(bytes);
writeBuffer.flip();
while (writeBuffer.hasRemaining()) {
int written = sc.write(writeBuffer);
EVENT_LOGGER.log(Level.INFO, "Bytes Written: " + written);
}
EVENT_LOGGER.log(Level.INFO, "Finished writing to "
+ sc.getRemoteAddress().toString());
}
/**
* Analyse the JSONObject and process it accordingly
*
* @param json
* @throws JSONException
*/
private void processJsonObject(JSONObject json) throws JSONException {
JSONArray jsonA = json.getJSONArray("DATA");
EVENT_LOGGER.log(Level.INFO, jsonA.toString());
// System.out.println("JSON Received: " + jsonA.toString());
if (!testMode) {
String deviceId = "error";
for (int i = 0; i < jsonA.length(); i++) {
JSONObject msg = jsonA.getJSONObject(i);
if (msg.has(JSONKeys.MesageType.ID)) {
deviceId = msg.getString(JSONKeys.ParamsId.ID);
} else if (msg.has(JSONKeys.MesageType.EVENT)) {
@SuppressWarnings("unused")
int result;
switch (msg.getString(JSONKeys.MesageType.EVENT)) {
case JSONKeys.Events.BOOT:
result = database.insertBootEvent(msg, deviceId);
break;
case JSONKeys.Events.CONNECT:
result = database.insertConnectEvent(msg, deviceId);
break;
case JSONKeys.Events.END:
break;
case JSONKeys.Events.FAILED_TRANSMISSION:
result = database.insertFailedTransmissionEvent(msg,
deviceId);
break;
case JSONKeys.Events.IDLE:
result = database.insertIdleEvent(msg, deviceId);
break;
}
}
}
}
}
}