Search code examples
javasocketsniosfml

Delays while sending data with Java NIO


I need your advice on a Java NIO package. I have an issue with delays while sending packets over network. The original code is actually my port of the SFML book source code to Java, but here I'll show you only a minimal working example, where the problem is reproduced. Though this code does contain some pieces from SFML library (actually creating a window and an event loop), I believe this has no impact on the issue.

Here I'll show only parts of the code, full version is available here.

So, the program has two entities: Server and Client. If you start an application in a server mode, then a Server is created, starts to listen for new connections, and a new Client is automatically created and tries to connect to the Server. In client mode only a Client is created and connects to the Server.

The application also creates a new basic GUI window and starts an event loop, where everything happens.

The Client sends packets to the Server. It handles them by just logging the fact of accepting. There are two types of packets the Client can send: periodical packet (with an incremental ID) and an event packet (application reacts to pressing SPACE or M buttons).

Client sends packets:

public void update(Time dt) throws IOException {
    if (!isConnected) return;

    if (tickClock.getElapsedTime().compareTo(Time.getSeconds(1.f / 20.f)) > 0) {
        Packet intervalUpdatePacket = new Packet();
        intervalUpdatePacket.append(PacketType.INTERVAL_UPDATE);
        intervalUpdatePacket.append(intervalCounter++);

        PacketReaderWriter.send(socketChannel, intervalUpdatePacket);

        tickClock.restart();
    }
}

public void handleEvent(Event event) throws IOException {
    if (isConnected && (event.type == Event.Type.KEY_PRESSED)) {
        KeyEvent keyEvent = event.asKeyEvent();

        if (keyEvent.key == Keyboard.Key.SPACE) {
            LOGGER.info("press SPACE");
            Packet spacePacket = new Packet();
            spacePacket.append(PacketType.SPACE_BUTTON);
            PacketReaderWriter.send(socketChannel, spacePacket);
        }

        if (keyEvent.key == Keyboard.Key.M) {
            LOGGER.info("press M");
            Packet mPacket = new Packet();
            mPacket.append(PacketType.M_BUTTON);
            PacketReaderWriter.send(socketChannel, mPacket);
        }
    }
}

Server accepts packets:

private void handleIncomingPackets() throws IOException {
    readSelector.selectNow();

    Set<SelectionKey> readKeys = readSelector.selectedKeys();
    Iterator<SelectionKey> it = readKeys.iterator();

    while (it.hasNext()) {
        SelectionKey key = it.next();
        it.remove();

        SocketChannel channel = (SocketChannel) key.channel();

        Packet packet = null;
        try {
            packet = PacketReaderWriter.receive(channel);
        } catch (NothingToReadException e) {
            e.printStackTrace();
        }

        if (packet != null) {
            // Interpret packet and react to it
            handleIncomingPacket(packet, channel);
        }
    }
}

private void handleIncomingPacket(Packet packet, SocketChannel channel) {
    PacketType packetType = (PacketType) packet.get();

    switch (packetType) {
        case INTERVAL_UPDATE:
            int intervalId = (int) packet.get();
            break;
        case SPACE_BUTTON:
            LOGGER.info("handling SPACE button");
            break;
        case M_BUTTON:
            LOGGER.info("handling M button");
            break;
    }
}

Here is a PacketReaderWriter object:

package server;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class PacketReaderWriter {
    private static final int PACKET_SIZE_LENGTH = 4;
    private static final ByteBuffer packetSizeReadBuffer = ByteBuffer.allocate(PACKET_SIZE_LENGTH);
    private static ByteBuffer clientReadBuffer;

    private static byte[] encode(Packet packet) throws IOException {
        try (
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos)
        ) {
            oos.writeObject(packet);
            return baos.toByteArray();
        }
    }

    private static Packet decode(byte[] encodedPacket) throws IOException, ClassNotFoundException {
        try (ObjectInputStream oi = new ObjectInputStream(new ByteArrayInputStream(encodedPacket))) {
            return (Packet) oi.readObject();
        }
    }

    public static void send(SocketChannel channel, Packet packet) throws IOException {
        byte[] encodedPacket = encode(packet);

        ByteBuffer packetSizeBuffer = ByteBuffer.allocate(PACKET_SIZE_LENGTH).putInt(encodedPacket.length);
        packetSizeBuffer.flip();

        // Send packet size
        channel.write(packetSizeBuffer);

        // Send packet content
        ByteBuffer packetBuffer = ByteBuffer.wrap(encodedPacket);
        channel.write(packetBuffer);
    }

    public static Packet receive(SocketChannel channel) throws IOException, NothingToReadException {
        int bytesRead;

        // Read packet size
        packetSizeReadBuffer.clear();
        bytesRead = channel.read(packetSizeReadBuffer);

        if (bytesRead == -1) {
            channel.close();
            throw new NothingToReadException();
        }

        if (bytesRead == 0) return null;

        packetSizeReadBuffer.flip();
        int packetSize = packetSizeReadBuffer.getInt();

        // Read packet
        clientReadBuffer = ByteBuffer.allocate(packetSize);
        bytesRead = channel.read(clientReadBuffer);

        if (bytesRead == -1) {
            channel.close();
            throw new NothingToReadException();
        }

        if (bytesRead == 0) return null; 

        clientReadBuffer.flip();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(clientReadBuffer.array(), 0, bytesRead);
        clientReadBuffer.clear();

        try {
            return decode(baos.toByteArray());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

And here is the problem: I have quite big delays between pressing a button (and sending a corresponding packet from the Client) and accepting this packet on the Server. If I start a new instance of the application in a client mode (just add a new Client in short), the delays become even bigger.

I don’t see any reason why these periodical packets create so much network load that other packets just cannot get through, but maybe I'm just missing something. Here I have to say that I’m not a Java expert, so don’t blame me too much for not seeing something obvious :)

Does anyone have any ideas?

Thanks!


Solution

  • I decided to take a look at the Github repo.

    Your Server.run() looks like this.

        public void run() {
        while (isRunning) {
            try {
                handleIncomingConnections();
                handleIncomingPackets();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            try {
                // Sleep to prevent server from consuming 100% CPU
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    The sleep(100) will result in approximately 10 calls to handleIncomingPackets() per second. handleIncomingPackets() in turn will select a Client channel and call handleIncomingPacket() on a single received Packet. In total the server will be able to handle 10 Packets/second per Client if I understand it correctly.

    The Client on the other hand tries to send 20 packets per second of the type PacketType.INTERVAL_UPDATE. Either the Client must send fewer packets per second or the Server needs to be able to handle more packets per second.

    The current sleep(100) means that there will always be a latency of up to around 100ms before the server can respond to a single packet, even in a non-overloaded situation. This might be fine though if you make sure you really read all packets available on the channel instead of just a single one each time.

    In summary: the smallest change you'd have to do to improve response times is to decrease the sleep() time. 10 ms would be fine. But I'd also suggest trying to check if there's more than one packet available in each iteration.

    Update: In the c++ file you linked my hunch is that it's reading more than one packet per iteration.

    <snip>
    while (peer->socket.receive(packet) == sf::Socket::Done)
            {
                // Interpret packet and react to it
                handleIncomingPacket(packet, *peer, detectedTimeout);
    </snip>
    

    The while loop will read all available packets. Compared to your Java version where you read a single packet per client per server iteration.

    if (packet != null) {
        // Interpret packet and react to it
        handleIncomingPacket(packet, channel);
    }
    

    You need to make sure that you read all available packets the Java version also.

    If you just want to convince yourself that the client code sends more packets than the server code can handle it's quickly done by setting the sleep() to 10 ms temporarily.