I'm implementing a simple server using AsynchronousServerSocketChannel
. For testing purposes, I created a tiny client prototype that sends two messages, "hi"
and "stackoverflow"
, then disconnects. On server side, I read the arrived messages and print them to standard output. When the client executed, I'm expecting to receive:
message [hi], bytecount 2
message [stackoverflow], bytecount 13
The problem is, that sometimes both messages already arrived when server invokes reading callback so I get
message [histackoverflow], bytecount 15
instead.
The question is, if it is possible to ensure on server side that the messages arrive separately and if yes, how to do it?
Here's my CompletionHandler
prototype that handles client connections:
class CommunicationHandler implements CompletionHandler<AsynchronousSocketChannel, Void> {
private final AsynchronousServerSocketChannel server;
public CommunicationHandler(final AsynchronousServerSocketChannel server) {
this.server = server;
}
@Override
public void failed(Throwable ex, Void attachment) {}
@Override
public void completed(final AsynchronousSocketChannel client, Void attachment) {
// handle client messages
final ByteBuffer buffer = ByteBuffer.allocateDirect(Server.BUFFER_SIZE);
final Session session = new Session();
try {
client.read(buffer, session, new CompletionHandler<Integer, Session>() {
@Override
public void completed(Integer byteCount, final Session currSession) {
if (byteCount == -1) {
return;
}
buffer.flip();
// TODO forward buffer to message handler (probably protocol?)
System.out.println("message [" + convertToString(buffer) + "], byteCount " + byteCount);
buffer.clear();
// read next message
client.read(buffer, currSession, this);
}
@Override
public void failed(Throwable ex, final Session currSession) {}
});
}
// accept the next connection
server.accept(null, this);
}
ByteBuffer
to String
conversion:
public static String convertToString(ByteBuffer bb) {
final byte[] bytes = new byte[bb.remaining()];
bb.duplicate().get(bytes);
return new String(bytes);
}
Here is a test client prototype:
public class Client {
public final void start() {
try (AsynchronousSocketChannel client = AsynchronousSocketChannel.open();) {
Future<Void> connCall = client.connect(InetAddress.getByName("127.0.0.1"), 8060));
connCall.get();
// client is now connected
// send greeting message
Future<Integer> writeCall = client.write(Charset.forName("utf-8").encode(CharBuffer.wrap("hi")));
writeCall.get();
// Thread.sleep(5000L);
writeCall = client.write(Charset.forName("utf-8").encode(CharBuffer.wrap("stackoverflow")));
writeCall.get();
client.close();
} catch (IOException e) {
} catch (InterruptedException ex) {
} catch (ExecutionException ex) {
}
}
In addition to the possibility of getting two (or even more) writes in one read, for larger messages (usually about 3k or more) you can get one write split over several reads. TCP is a stream protocol and does not preserve record boundaries, unless by chance: What is a message boundary? There are two solutions that work in general, although with async channel I think you'll need to do your own buffer management which may be confusing and hard to test:
add an explicit length field before each record
add a delimiter after each record when there is a byte not otherwise used, or an escape can be used to distinguish data from the delimiter
and several others that have been tried:
as your comment suggests, wait long enough that the first request has always been read before the second is sent. On the local networks and test systems used by developers this usually is a few milliseconds or even less; on the real Internet it is fairly often several seconds, sometimes minutes, and in theory can be hours or even days.
if records are never longer than a few fragments (maybe 10k or so) use UDP (available in Java as DatagramSocket
but not as a NIO channel AFAICS) and implement your own protocols to handle message loss, duplication and reordering (which is hard to do and often ends up failing in some obscure cases that were discovered and avoided or fixed in TCP 30 years ago)
use SCTP (not available in Java at all AFAICS, and not too many other systems either)
Aside: your test client sends data in UTF-8, but new String (byte[])
uses the default encoding which is platform-dependent and not necessarily UTF-8. I'm not sure it's guaranteed but in practice all usable encodings include ASCII as a subset, and your example data is ASCII. But if you want to support actual UTF-8 data code for it.