Search code examples
javaasynchronousnio2

Async reading in server-client: separate client messages


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) {
    }
}

Solution

  • 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.