Java - UDP Datagram sending on main thread crashes game

Started by
8 comments, last by Glass_Knife 10 years ago

I'm trying to make a simple network game using a client and server.

Currently I have two threads running for both my client and server: - The Main thread which calculates player positions, physics, draws things to the screen, etc. - The Client/Server thread which handles sending/receiving of datagrams from/to the server or from/to the client

The reason I have two threads for each is because the receive() method call is BLOCKING, so I don't want it to block any of my gameplay while its waiting for a new datagram.

This works absolutely fine until I get to the point where I want to be able to send a datagram in my main thread.

EXAMPLE
In the client, in my main thread, I have a keypress event where when my client presses the 'z' key, it sends a datagram to the server as a test. However, the program just completely stalls, no exceptions. It stalls to the point where I need to close it via task manager in Windows.

The socket I'm using to send to is declared and initialized in my ClientThread class (which extends Thread). It gets initialized when run() is called in my ClientThread:


      public void run() {
          try {
              // Construct the socket
              socket = new DatagramSocket();
              ...

I can easily send datagrams in my ClientThread/ServerThread, but when I try to send them in my MainThread at any given time (e.g. when a client presses Z and wants to send a test message), the program stalls/crashes.

Why is it crashing and what can I do to fix this?
Is it okay to use one thread for receiving/sending, and another thread for doing other calculations but also including sending datagrams at any given time? (e.g. when a player presses the 'z' key or something else).

I have a feeling it has something to do with the fact that the DatagramSocket is declared and initialized inside the ClientThread/ServerThread class

and the MainThread is trying to gain access to it at any given time it wants (again, when a player presses the 'z' key for example).

Advertisement

These are very complicated questions. Make sure you aren't trying something too complicated.

For the UDP server, you should use a NonBlocking selector so that a single thread can handle all the send and receive messages. Here is an example I just googled:

http://www.studytrails.com/java-io/non-blocking-io-multiplexing.jsp

What you're looking for is an "NIO (New IO) Java Server." This is really hard to get right. I wrote a small UDP server using this years ago, and it was two years of debugging before it was stable.

I would also suggest (without knowing what you're doing this may not be correct but is at least the right direction) that you use a BlockingQueue as a way to pass messages to waiting threads. http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html

Good Luck!

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532


Is it okay to use one thread for receiving/sending, and another thread for doing other calculations but also including sending datagrams at any given time?
Nope. Responsibilities man! If you already got one part of the program responsible for sending data, let it be THE place to send data.

You can use queues like Glass_Knife suggested for passing the tasks around. Game thread could send a task to the network thread, and that one will send the data when it can. That way you can organize how you send the data in one place (which is pretty important in networking code AFAIK).

But all of this goes beyond your current actual problem (ie, thread blocking).

"I AM ZE EMPRAH OPENGL 3.3 THE CORE, I DEMAND FROM THEE ZE SHADERZ AND MATRIXEZ"

My journals: dustArtemis ECS framework and Making a Terrain Generator


But all of this goes beyond your current actual problem (ie, thread blocking).

Yes, I may have jumped the gun with ideas. I've been down this road before. Having a single thread to send/recv UDP with the selectors, and then pass message to a blocking queue so the other end (thread, game loop, something) can pull the messages out when there's time. It can also use the queue to send messages.

But you're right, the problem is that the slow server code can't live in the game loop because it may block if there are network issues.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

All right so majority here says I definitely should not be using my MainThread to send datagrams out using the DatagramSocket specified in my NetworkThread (ClientThread/ServerThread).

Instead I should use my MainThread to send tasks/events to my NetworkThread which will then execute what I want via the network.

Example:

A player presses the 'z' key to send a test message to the server.

Instead of the main thread directly sending the datagram, the mainthread sends a task to the networkthread

and based on the task I specified, it will send the corresponding message accordingly.

So lets say I manage to get tasks working. I'm still not really sure how I'm going to combat not being able to send/receive datagrams in the same NetworkThread issue.

I read through the links you posted and I'm not sure how I would implement them in my current project which uses two threads at the moment:

The main thread

A Network thread

Currently using two threads. Should I continue to use threads?

Should I possibly have an additional thread:

The main thread

A Network thread for SENDING

A Network thread for RECEIVING

If so, would the two threads be allowed to share the same DatagramSocket?

Or would they each have their own DatagramSocket instance, with the same host IP address and host port number?

Or would it be better to use this ServerSocketChannel you linked here:
http://www.studytrails.com/java-io/non-blocking-io-multiplexing.jsp

I've never used that before and I'm not sure how to implement it in my example. Would I have to completely throw away the DatagramSocket?

Or would I still be using it?

Sorry I'm just a little confused on how I would implement these solutions in my project.

EDIT:

I also found this:
http://www.eecs.harvard.edu/~mdw/proj/java-nbio/javadoc/seda/nbio/NonblockingDatagramSocket.html
Would it be better just to have one thread for everything if I'm able to successfuly implement this?

That way because its nonblocking, I would be able to:

- Compute game physics/logic in main thread

- Send datagrams in main thread

- Receive datagrams in main thread

Thoughts?


Sorry I'm just a little confused on how I would implement these solutions in my project.

I think that's my fault. Linking you to the NIO server isn't good if you've never done anything like that before. How much time do you have to get this working? If you have time and are really trying to understand how to make a networked game, then you need to understand the two principles seperatly before trying to put them together.

One type of application used for learning/studying/understanding networked applications is a simple chat server. If you can get that working, you're well on your way. A lot of the same problems you'll encounter in a game will come up with a chat server.

1. Coding a server to listen for multiple clients.

2. What does the server do when clients disconnect?

3. How does a client connect to the server?

4. How does the client send chat messages?

5. How does the client receive message from other clients?

6. How does the server communicate who's in the chat room?

7. What do clients do if the server disconnects?

The great thing about a chat room is that the GUI isn't hard, and the messages are just text, and it seems really easy. But try one, and then setup a server, and send it to a few friends, and see what happens.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532


Sorry I'm just a little confused on how I would implement these solutions in my project.

I think that's my fault. Linking you to the NIO server isn't good if you've never done anything like that before. How much time do you have to get this working? If you have time and are really trying to understand how to make a networked game, then you need to understand the two principles seperatly before trying to put them together.

One type of application used for learning/studying/understanding networked applications is a simple chat server. If you can get that working, you're well on your way. A lot of the same problems you'll encounter in a game will come up with a chat server.

1. Coding a server to listen for multiple clients.

2. What does the server do when clients disconnect?

3. How does a client connect to the server?

4. How does the client send chat messages?

5. How does the client receive message from other clients?

6. How does the server communicate who's in the chat room?

7. What do clients do if the server disconnects?

The great thing about a chat room is that the GUI isn't hard, and the messages are just text, and it seems really easy. But try one, and then setup a server, and send it to a few friends, and see what happens.

Well what I've currently got right now is basically I'm trying to be able to have functions like so when it comes to sending/receiving messages:

clearbuffer();

writebyte(1); //Message ID

writestring("John");

writeint(5);

send(datagramSocket);

On the other end:

switch(readbyte()){
case 1:

String name = readstring();

int x = readint();

}

And so far this is working out great.

However I can only do this inside the NetworkThread.

Even for a simple chat system, I would want to be able to press a button "Send" that sends text from the GUI JTextField.

But the button press event for the "Send" button is triggered inside the Main Thread. In this case, as what's happening currently, the game will halt.

Should I just have three threads?

Main Thread

NetworkSendThread

NetworkReceiveThread

Or should I just use one thread:

Main Thread

and instead of using a DatagramSocket which is blocking, I would use this socket:

http://www.eecs.harvard.edu/~mdw/proj/java-nbio/javadoc/seda/nbio/NonblockingDatagramSocket.html

A huge advantage in terms of simplicity is that I can have my main game loop, and network loop all in one thread.

Right now I have two problems:

- How would I get my Main Thread to communicate with my NetworkThread,

e.g. When the player presses the 'z' key, somehow trigger an event in the NetworkThread telling that I've done so, and then the NetworkThread

would take care of sending the data through the network using its DatagramSocket.

- How on earth do I do sending/receiving on two separate threads? Would both of those threads need to somehow share the DatagramSocket?


Even for a simple chat system, I would want to be able to press a button "Send" that sends text from the GUI JTextField.
But the button press event for the "Send" button is triggered inside the Main Thread. In this case, as what's happening currently, the game will halt.

This is what the BlockingQueue can solve. The thread that generates the message, the thread that you don't want to block, creates a message and gives it to the blocking queue, then that thread continues on and never blocks. Inside a different thread, the queue.get() method blocks until there is something in the queue. In this case, the message. That thread sends the message with the Datagram and the calls queue.get() again, going to sleep until there is another message.

The NIO server I linked allows you to send and receive from the same thread. You could combine the NIO server with the queue, but now it is getting complicated. This is how the server I wrote behaved. The selector.select() method blocks until there is something to handle. This method wakes up when there is something to handle with the UDP socket, such as a connect, or data to read, or data to write.

If you want to be simple, have the UDP send a game state message and then receive a game state message. Honestly I'm not sure the best way to do this.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

Hmm well thanks for all the help. I will try to use the BlockingQueue you suggested.

In any case if anyone wants to take a stab at it, here's my implementation so far:

ClientThread.java:

package Network;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;
 
import java.net.*;
import networkresearch.NetworkResearch;
import utils.ByteHandle;
import Network.NetworkBuffer;
import networkresearch.Player;
 
public class ClientThread extends Thread {
 
    public DatagramSocket socket;
    public DatagramPacket packet;
    public NetworkBuffer buffer;
    private NetworkResearch ui;
    private final static int PACKETSIZE = 100;
    InetAddress host;
    int serverport;
 
    //byte[] dataBuffer;
    public ClientThread(NetworkResearch observer) throws UnknownHostException {
        this.ui = observer;
        socket = null;
        host = InetAddress.getByName("127.0.0.1");
        serverport = Integer.parseInt("1024");
        buffer = new NetworkBuffer();
    }
 
    /*
     * Sends buffer to server
     */
    @Override
    public void run() {
        try {
            // Construct the socket
            socket = new DatagramSocket();
            buffer.clearBuffer();
            Integer pid = 0;
            ui.console.println("BYTE: " + pid.byteValue());
            NetworkUtils.writeByte(pid.byteValue(), buffer);
            packet = new DatagramPacket(buffer.getData(), buffer.getData().length, host, serverport);
            socket.send(packet);
 
            // Set a receive timeout, 2000 milliseconds
            //socket.setSoTimeout(2000);
            // Prepare the packet for receive
            while (true) {
                packet.setData(new byte[PACKETSIZE]);
 
                // Wait for a response from the server
                socket.receive(packet);
                buffer.clearBuffer(); // Clear the buffer of any previous data it had
                buffer.setData(packet.getData());
                // Message ID
                Byte b = NetworkUtils.readByte(buffer);
                int mid = b.intValue();
                ui.console.println("CLIENT MESID: " + mid);
                switch (mid) {
                    case 0:
                        ui.console.println("Server says welcome!");
 
                        buffer.clearBuffer();
                        NetworkUtils.writeByte(new Integer(1).byteValue(), buffer);
                        DatagramPacket sendPacket = new DatagramPacket(buffer.getData(), buffer.getData().length, packet.getAddress(), packet.getPort());
                        socket.send(sendPacket);
                        break;
                }
            }
 
 
        } catch (Exception e) {
            ui.console.println(e.toString());
            ui.state = "menu";
            ui.setPane(ui.mainPanel);
        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }
 
    public void stopClient() {
        System.out.println("STOPPING CLIENT");
        socket.close();
        this.stop();
    }
}
 
ServerThread.java:

package Network;
 
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;
import networkresearch.NetworkResearch;
import networkresearch.Player;
import utils.ByteHandle;
 
public class ServerThread extends Thread {
 
    private final static int PACKETSIZE = 100;
    private NetworkResearch ui;
    //DatagramPacket packet;
    DatagramSocket socket;
    NetworkBuffer buffer;
 
    public ServerThread(NetworkResearch observer) {
        this.ui = observer;
    }
 
    @Override
    public void run() {
        try {
            buffer = new NetworkBuffer();
            int port = Integer.parseInt("1024");
            socket = new DatagramSocket(port);
 
            ui.console.println("SERVER STARTED ON UDP PORT: " + port);
 
 
            while (true) {
                // Create a packet
                DatagramPacket packet = new DatagramPacket(new byte[PACKETSIZE], PACKETSIZE);
                // Receive a packet (blocking)
                socket.receive(packet);
                buffer.clearBuffer(); // Clear the buffer of any previous data it had
                buffer.setData(packet.getData());
                // Message ID
                Byte b = NetworkUtils.readByte(buffer);
                int mid = b.intValue();
                ui.console.println("SERVER MESID: " + mid);
                switch (mid) {
                    //Initial connection packet
                    case 0:
                        Player player = new Player(ui.gamePanel, "goblin");
                        ui.gamePanel.players.add(player);
 
                        buffer.clearBuffer();
                        NetworkUtils.writeByte(new Integer(0).byteValue(), buffer);
                        DatagramPacket sendPacket = new DatagramPacket(buffer.getData(), buffer.getData().length, packet.getAddress(), packet.getPort());
                        socket.send(sendPacket);
                        break;
 
                    case 1:
                        ui.console.println("Client sends a response back!");
                        break;
                }
                /*
                 byte[] newData = "Hello Client".getBytes();
                 DatagramPacket newPacket = new DatagramPacket(newData, newData.length, packet.getAddress(), packet.getPort());
                 // Return the packet to the sender
                 socket.send(newPacket);
                 */
            }
        } catch (Exception e) {
            System.out.println(e);
        }
    }
 
    public void stopServer() {
        System.out.println("STOPPING SERVER");
        socket.close();
        this.stop();
    }
}
GamePanel.java:

package networkresearch;
 
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
 
import Network.ClientThread;
import Network.NetworkUtils;
import Network.ServerThread;
import java.awt.Dimension;
import java.net.UnknownHostException;
import javax.swing.JFrame;
 
public class GamePanel extends CorePanel {
 
    NetworkResearch ui;
    GameThread gameThread;
    ClientThread clientThread;
    ServerThread serverThread;
    BufferedImage[] images_players;
    public ArrayList<Player> players;
    BufferedImage gameBackground;
    long startTime = 0;
    long endTime = 0;
 
    GamePanel(NetworkResearch ui) throws IOException {
        images_players = new BufferedImage[5];
        images_players[0] = ui.loadAsset("player_knight.png");
        images_players[1] = ui.loadAsset("player_goblin.png");
        images_players[2] = ui.loadAsset("player_ogre.png");
        images_players[3] = ui.loadAsset("player_skeleton.png");
        images_players[4] = ui.loadAsset("player_wolf.png");
        this.ui = ui;
        JTextField nameField = new JTextField("Test Player");
        nameField.setColumns(6);
        //add(nameField);
 
        gameBackground = ui.loadAsset("gameBackground.png");
        players = new ArrayList();
    }
 
    @Override
    public void keyTyped(KeyEvent e) {
    }
 
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
            goBackToMenu();
        }
 
        if (e.getKeyChar() == 'z') {
            ui.console.println("SENDING FROM CLIENT TO SERVER!");
            /**
             * THIS IS WHERE THE GAME WILL CRASH! ATTEMPTING TO SEND A DATAGRAM
             * OUTSIDE OF THE ClientThread CLASS!
             */
            try {
                int x = 5, y = 10;
                clientThread.buffer.clearBuffer();
                NetworkUtils.writeInt(x, clientThread.buffer);
                // Send it
                clientThread.socket.send(clientThread.packet);
            } catch (IOException ex) {
                Logger.getLogger(GamePanel.class.getName()).log(Level.SEVERE, null, ex);
            }
 
        }
 
        for (Player player : players) {
            switch (e.getKeyChar()) {
                case 'a':
                    player.keyLeft = true;
                    break;
                case 's':
                    player.keyDown = true;
                    break;
                case 'd':
                    player.keyRight = true;
                    break;
                case 'w':
                    player.keyUp = true;
                    break;
                case ' ':
                    player.keySpace = true;
                    break;
            }
        }
    }
 
    @Override
    public void keyReleased(KeyEvent e) {
        for (Player player : players) {
            switch (e.getKeyChar()) {
                case 'a':
                    player.keyLeft = false;
                    break;
                case 's':
                    player.keyDown = false;
                    break;
                case 'd':
                    player.keyRight = false;
                    break;
                case 'w':
                    player.keyUp = false;
                    break;
                case ' ':
                    player.keySpace = false;
                    break;
            }
        }
    }
 
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(gameBackground, 0, 0, this);
        for (Iterator<Player> it = players.iterator(); it.hasNext();) {
            it.next().paint(g);
        }
    }
 
    @Override
    public void start() {
        gameThread = new GameThread(this);
        try {
            clientThread = new ClientThread(ui);
        } catch (UnknownHostException ex) {
            Logger.getLogger(GamePanel.class.getName()).log(Level.SEVERE, null, ex);
        }
        serverThread = new ServerThread(ui);
        this.setFocusable(true);
        gameThread.start();
        if (ui.state.equals("inserver")) {
            ui.setTitle("SERVER");
            serverThread.start();
            ui.console.setTitle("Console: Server");
        } else if (ui.state.equals("inclient")) {
 
            Player player = new Player(this, "goblin");
            players.add(player);
 
            ui.setTitle("CLIENT");
            clientThread.start();
            ui.console.setTitle("Console: Client");
        }
        startTime = new Date().getTime();
    }
 
    public void update() {
        for (Iterator<Player> it = players.iterator(); it.hasNext();) {
            it.next().update();
        }
    }
 
    public void goBackToMenu() {
        System.out.println("GOING BACK TO MENU");
        ui.state = "menu";
        ui.setPane(ui.mainPanel);
        ui.curPanel.repaint();
    }
 
    @Override
    public void stop() {
        if (gameThread.isAlive()) {
            gameThread.stop();
        }
        if (clientThread.isAlive()) {
            clientThread.stopClient();
        } else if (serverThread.isAlive()) {
            serverThread.stopServer();
        }
    }
}
The game stalls/crashes in GamePanel.java at the try/catch block (its commented).

I see two problems. One is with the way you're trying to use the client. The other is a misunderstanding of the UDP protocol.

First, the way you are using the client may seem correct, and at first, I wasn't sure what was wrong. The problem is this part:


        if (e.getKeyChar() == 'z') {
            ui.console.println("SENDING FROM CLIENT TO SERVER!");
            /**
             * THIS IS WHERE THE GAME WILL CRASH! ATTEMPTING TO SEND A DATAGRAM
             * OUTSIDE OF THE ClientThread CLASS!
             */
            try {
                int x = 5, y = 10;
                clientThread.buffer.clearBuffer();
                NetworkUtils.writeInt(x, clientThread.buffer);
                // Send it
                clientThread.socket.send(clientThread.packet);
            } catch (IOException ex) {
                Logger.getLogger(GamePanel.class.getName()).log(Level.SEVERE, null, ex);
            }
 
        }

The problem is subtle. If you don't use the NIO kind of socket, then the socket can only do one thing at a time. I made a quick test to verify this. At least on my machine, if you do a socket.receive(), and while you're waiting for a response, you call socket.send() from a different thread, then the sent message will not go until after the socket.receive() returns.

The server starts up, then the client connects, sends the first message, and the server responds. But then the client waits for a second message, before anything else has been sent to the server. So this is one problem. Now when you say that the game locks up or crashes, that is confusing, because the socket.send() method doesn't block until the message is sent, so I suspect something else is wrong too.

But this is just a symptom of the problem, not that actual problem. The real problem is that UDP is an unreliable connectionless network protocol. Think of it like writing a message on a piece of paper, wading it up, and throwing it over a fence. There is no guarantee that anyone got the message. Setting the client up to send a message and wait for a response doesn't work, because you can't assume that anything will ever come back. Imagine if you started the Client before the server, or the server was restarted in the middle of communication. The client.send() message would just go away, never delivered to anyone. Then no one would ever send a message back.

If you want that kind of send/receive message passing, you need to use TCP/IP where you get back responses and know when the connection is gone. If you're going to use UDP you either need a NIO socket that can send and receive at the same time, or you need two different sockets. One to send, and one to receive. And yes, this makes things way more complicated.

This is why I suggested a chat server. A lot of these things will come up.

I think, therefore I am. I think? - "George Carlin"
My Website: Indie Game Programming

My Twitter: https://twitter.com/indieprogram

My Book: http://amzn.com/1305076532

This topic is closed to new replies.

Advertisement