Skip to main content
GameDev.net gamedev.net
🔒 Locked

[java] Multiple UPD & TCP clients server for multiplayer game

Started by EvilWeebl May 15, 2010 at 8:45 AM 5 replies 8.2k views
Original Post
EvilWeebl
EvilWeebl
Hi all, so im trying to make a multiplayer game in java and I have never really used java before. I need to make a game that transmits UDP data from each player to the server specifying positions etc relative to the game and TCP data for chatting. I have currently set up the TCP for chatting between multiple clients and am working on the UDP. My main problem is figuring out the architecture of it all?! They always block when listening so new threads are needed for everything which doesnt seem right. Is this the correct way to go about doing this? Excuse the crudeness of the drawing. description of your image Ive seen something about using selectable channels and selectors for non blocking. Is this of use to me? If so where can I find a good example of TCP and UDP being used in a multiple client server structure? Also, when this is implemented where do I implement a layer for my game? Any help is much appreciated.
rip-off
rip-off
I wouldn't go with having two channels for communication. Just use a high level datagram protocol, one with per-message reliability and ordering. I don't know if there are libraries for this in Java.

Generally, you don't want a thread per client. A UDP socket can be served by a single thread, and doesn't even need to be served asynchronously unless your game is quite complex. TCP sockets can be handled through a combination of non-blocking sends and select() style polling. If the expected number of clients is low (most games have ~32 as a limit) then this doesn't have a lot of drawbacks and is quite simple to implement.
EvilWeebl
EvilWeebl
Thanks for the reply but i'm still finding it hard to get my head around this design structure as to where threads should be created and what needs to be listening out for what etc.

Quote:
Generally, you don't want a thread per client

Why is this? What is a better way to go about this?

Quote:
A UDP socket can be served by a single thread, and doesn't even need to be served asynchronously unless your game is quite complex


So the server starts a single thread listening for UDP and each user gets given the InetAddress and port to access it? I imagine that each datagram is then sent with a field stating which player it came from correct? If so how do I go about parsing datagram packets?
rip-off
rip-off
I would design it with 0 additional threads to start with, and only add threads if it becomes necessary. You can set the socket into non-blocking mode so that your game loop doesn't pause when there are no packets.

Threads are relatively expensive, ideally you would spawn as few as possible. The number of active threads should be about the same as the number of logical processors the computer has. For blocking threads like a network thread, this doesn't strictly hold.

Quote:

So the server starts a single thread listening for UDP and each user gets given the InetAddress and port to access it? I imagine that each datagram is then sent with a field stating which player it came from correct? If so how do I go about parsing datagram packets?

Well, you can simply use a mapping data structure to look up clients by the IP Address of the received packets. The packet doesn't necessarily need to contain additional information about which player it came from. In fact, you would still need to do an IP check anyway, otherwise malicious players could masquerade as others by sending different player ID values in the packets. If your game has a small player limit (again, around ~32), even a simple linear search on an array would be fast enough to find the client object by IP.

Parsing packets is a different matter. A simple way to start might be to try wrapping a DataInputStream around a ByteArrayInputStream that you build from the packet's getData() and getLength() methods. You can mirror the parsing code in the packet building code, which might use a DataOutputStream().

You generally pack lots of logical messages into a single UDP packet, so your code will need to be able to extract them all. A "length" header is a typical way of doing this. Each logical packet type might have a unique identifier. This can be used to route the message into a method call on some game object. Again, for simplicity first you could go with an "enum" of all possible packet types, and a giant switch statement to route them.

Once you have something working, you can take a look at what your code requires and see if you are happy with the current structure, or whether to improve the design. But I would recommend against over-engineering from the start, I've done that with networked systems once or twice and I've always regretted it. It is simply too easy to imagine all sorts of scenarios that you never end up using.
EvilWeebl
EvilWeebl
Thanks for all your help so far! I think i'm getting the idea of it. So im looking into the whole non blocking TCP thing and have found what I think is a good example.

// Create the server socket channelServerSocketChannel server = ServerSocketChannel.open();// nonblocking I/Oserver.configureBlocking(false);// host-port 8000server.socket().bind(new java.net.InetSocketAddress(host,8000));// Create the selectorSelector selector = Selector.open();// Recording server to selector (type OP_ACCEPT)server.register(selector,SelectionKey.OP_ACCEPT);// Infinite server loopfor(;;) {  // Waiting for events  selector.select();  // Get keys  Set keys = selector.selectedKeys();  Iterator i = keys.iterator();  // For each keys...  while(i.hasNext()) {    SelectionKey key = (SelectionKey) i.next();    // Remove the current key    i.remove();    // if isAccetable = true    // then a client required a connection    if (key.isAcceptable()) {      // get client socket channel      SocketChannel client = server.accept();      // Non Blocking I/O      client.configureBlocking(false);      // recording to the selector (reading)      client.register(selector, SelectionKey.OP_READ);      continue;    }    // if isReadable = true    // then the server is ready to read     if (key.isReadable()) {      SocketChannel client = (SocketChannel) key.channel();      // Read byte coming from the client      int BUFFER_SIZE = 32;      ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);      try {        client.read(buffer);      }      catch (Exception e) {        // client is no longer active        e.printStackTrace();        continue;      }      // Show bytes on the console      buffer.flip();      Charset charset=Charset.forName("ISO-8859-1");      CharsetDecoder decoder = charset.newDecoder();      CharBuffer charBuffer = decoder.decode(buffer);      System.out.print(charBuffer.toString());      continue;    }  }}


so as I read this (and tell me if im incorrect) the serversocketchannel is registered to look out for connections and that is reflected in the key.isAcceptable but if that is so why does it also detect the isReadable?

Anyway moving onto my next question, if I implemented this and checked to see if a client had sent a message how do I then send this message to all clients connected? Do I (A): store a list of all the socket channels at the point of 'SocketChannel client = server.accept();' and then some how send a message on all of these(if so how)? Or (B): do something with OP_WRITE...I saw this somewhere and thought it might have relevance.

Of course if both these suggestions are wrong then are there better ways?

Great thanks for any help you can provide.
rip-off
rip-off
I'm afraid I can't give you direct help with that code snippet, I'd have to spend some time digging through the documentation to confirm.

However, I got the impression you wanted to use UDP for the game data. Switching to TCP is a big change, it can effectively prevent you from hosting certain types of high-twitch games over real internetwork conditions. Or are you still thinking of using a dual TCP/UDP connection? This will also cause issues in real deployment, thought typically it will just mean that NAT hosts (i.e. most average people hosting a game) will have more configuration to do.

What kind of game are you writing?
EvilWeebl
EvilWeebl
That's correct I am trying to use both TCP and UDP. It is to be a bomber game with a chat feature at the bottom. TCP for the chat and UDP for the game data.

this is currently the code I have for my server and client:

SERVER:
import java.awt.List;import java.io.*;import java.net.*;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.*;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;import java.util.Iterator;import java.util.Vector;public class Server {	private ServerSocketChannel ss = null;	private int Port;	private Vector<SocketChannel> clients = new Vector<SocketChannel>();	private static int BUFFER_MAX = 100;// Bytes		public Server(int p)throws IOException{		this.Port = p;		this.ss = ServerSocketChannel.open();		ss.configureBlocking(false);		ss.socket().bind(new InetSocketAddress(this.Port));	}		public static void main(String[] args) throws IOException{		Server server = new Server(Integer.parseInt(args[0]));		//Server server = new Server(7000);		server.run();	}		public void run(){		try{			Selector selector = Selector.open();			ss.register(selector, ss.validOps());			while(true){				System.out.println("SELECTING");				//wait for events				selector.select();				System.out.println("SELECTED");				//get keys				Iterator i = selector.selectedKeys().iterator();				//loop over keys				while(i.hasNext()){					SelectionKey key = (SelectionKey)i.next();									//remove current key					i.remove();									if(!key.isValid())						continue;										//client requires connection					if(key.isAcceptable()){						try{							//get client socket channel							SocketChannel client = ss.accept();							client.configureBlocking(false);							client.register(selector, SelectionKey.OP_READ);							clients.add(client);						}						catch(Exception e){							e.printStackTrace();							continue;						}					}										//key is readable					if(key.isReadable()){						try{							SocketChannel client = (SocketChannel)key.channel();							ByteBuffer buffer = ByteBuffer.allocate(BUFFER_MAX);							long bytesRead = client.read(buffer);							if (bytesRead == -1)							{								key.cancel();								client.close();							}							//show bytes on console							buffer.flip();							Charset charset=Charset.forName("ISO-8859-1");							CharsetDecoder decoder = charset.newDecoder();							CharBuffer charBuffer = decoder.decode(buffer);							System.out.print(charBuffer.toString());							SendToAll(charBuffer.toString());						}						catch(Exception e){							//client disconnected							e.printStackTrace();							key.cancel();							continue;						}					}				}				}		}		catch(Exception e){			e.printStackTrace();		}	}	public void SendToAll(String message) throws IOException{		ByteBuffer buffer = ByteBuffer.allocate(BUFFER_MAX);		buffer = ByteBuffer.wrap(message.getBytes());		for (SocketChannel client : clients){				client.write(buffer);			}		}}


CLIENT:
import java.awt.BorderLayout;import java.awt.Color;import java.awt.Dimension;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import java.io.IOException;import java.net.ConnectException;import java.net.InetAddress;import java.net.InetSocketAddress;import java.net.Socket;import java.net.UnknownHostException;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.SocketChannel;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;import java.util.Iterator;import javax.swing.BorderFactory;import javax.swing.JFrame;import javax.swing.JPanel;import javax.swing.JScrollPane;import javax.swing.JTextArea;import javax.swing.JTextField;public class Client implements ActionListener{		private InetAddress serverIP;	private int serverPORT;	private SocketChannel sc;	private Selector selector;	private Avatar clientAvatar;	private ByteBuffer writeBuffer = ByteBuffer.allocate(BUFFER_MAX);;	private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_MAX);;	private String outString = "";	protected JTextArea jTextArea;	protected JTextField textField;	private static int BUFFER_MAX = 100;// Bytes		public Client(String serverIP, int serverPort){		clientAvatar = new Avatar(0,0,Color.green,"Player1");		createGUI();		try {			this.serverIP= InetAddress.getByName(serverIP);			this.serverPORT = serverPort;		} catch (UnknownHostException e) {			System.err.print("Server IP unresolved..exit program..");			e.printStackTrace();			System.exit(1);		}	}		public static void main(String[] args) {		Client client = new Client(args[0] ,Integer.parseInt(args[1]));		//Client client = new Client("127.0.0.1", 7000);		client.run();	}		public void run() {		//-0- Initialise variables		//-1- Create TCP socket (connecting on local Host IP / Port: 7000)		//-2- Log socket details		//-3- Read one message from server		//-4- Close connection (housekeeping)		if(initClientSockets()){			logSocket(sc.socket());			process();			closeConnection();		}	}		public boolean initClientSockets(){		try{			//create socket channel			sc = SocketChannel.open();						//set to non blocking			sc.configureBlocking(false);						//connect to server			sc.connect(new InetSocketAddress(serverIP,serverPORT));					} catch (ConnectException e) {			jTextArea.append(" \n CONNECTION TO SERVER IMPOSSIBLE..is server running ?" + e + "\n");			System.err.println(" \n CONNECTION TO SERVER IMPOSSIBLE..is server running ?" + e);			e.printStackTrace();			return false;		} catch (UnknownHostException e) {			jTextArea.append(" \n CONNECTION TO SERVER 7000 \n");			System.err.println(" \n CONNECTION TO SERVER 7000");			e.printStackTrace();			return false;		} catch (IOException e) {			e.printStackTrace();			return false;		}finally{			if(sc==null){				jTextArea.append("SOCKET CREATION IMPOSSIBLE...Quitting now.. \n");				System.err.println("SOCKET CREATION IMPOSSIBLE...Quitting now..");				textField.setEnabled(false);				return false;			}		}		return true;	}		public void logSocket(Socket s){		System.out.println("--- TCP Socket----");		System.out.println("Local IP : "+ s.getLocalAddress());		System.out.println("Local PORT : "+ s.getLocalPort());		System.out.println("Remote IP : "+ s.getInetAddress());		System.out.println("Remote PORT : "+ s.getPort());	}		public void process(){		try {					//create selector			selector = Selector.open();						//register socket channel			sc.register(selector, SelectionKey.OP_CONNECT);						while(true){				//wait for connection				selector.select();								// Get keys				Iterator i = selector.selectedKeys().iterator();				System.out.println(selector.selectedKeys().size());					// For each key...				while (i.hasNext()) {					SelectionKey key = (SelectionKey)i.next();						  					// Remove the current key					i.remove();										if(!key.isValid())						continue;										// Attempt a connection					  if (key.isConnectable()) {						  						  // Connection OK						  System.out.println("Server Found");						  						  // Get the socket channel held by the key						  SocketChannel channel = (SocketChannel)key.channel();						  						  // Close pending connections						  if (channel.isConnectionPending())							  channel.finishConnect();					  }					  					  if (key.isReadable()){						  System.out.println("in readable");						  SocketChannel socketChannel = (SocketChannel) key.channel();						  						  // Clear out our read buffer so it's ready for new data						  this.readBuffer.clear();						  						  // Attempt to read off the channel						  int numRead;						  try {							  numRead = socketChannel.read(this.readBuffer);						  }						  catch (IOException e) {							  // The remote forcibly closed the connection, cancel							  // the selection key and close the channel.							  key.cancel();							  socketChannel.close();							  return;						  }						  if (numRead == -1) {							  // Remote entity shut the socket down cleanly. Do the							  // same from our end and cancel the channel.							  key.channel().close();							  key.cancel();							  return;						  }						  						  readBuffer.flip();						  Charset charset=Charset.forName("ISO-8859-1");						  CharsetDecoder decoder = charset.newDecoder();						  CharBuffer charBuffer = decoder.decode(readBuffer);						  System.out.print(charBuffer.toString());						  readBuffer.clear();					  }					  					  if (key.isWritable()) {						  System.out.println("in writable");						  if(outString!=null){							  SocketChannel socketChannel = (SocketChannel) key.channel();							  System.out.println(outString);							  writeBuffer = ByteBuffer.wrap(outString.getBytes());							  socketChannel.write(writeBuffer);							  writeBuffer.clear();							  outString = "";						  }					  }				}			}		}		catch (IOException e) {			e.printStackTrace();		}	}		public void createGUI(){		JFrame frame = new JFrame("Boom Men Client");		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);		frame.getContentPane().setLayout(new BorderLayout());		GraphicZone graphicZone = new GraphicZone();		graphicZone.setPreferredSize(new Dimension(600,400));		graphicZone.addAvatar(clientAvatar);		JPanel textZone = new JPanel();		textZone.setLayout(new BorderLayout());		textZone.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));				//Create a text area		jTextArea = new JTextArea();		jTextArea.setLineWrap(true);		jTextArea.setWrapStyleWord(true);		jTextArea.setEditable(false);		JScrollPane scrollPane = new JScrollPane(jTextArea);		scrollPane.setPreferredSize(new Dimension(100,100));        scrollPane.setBorder(BorderFactory.createCompoundBorder(        										BorderFactory.createEmptyBorder(0,0,5,0),        										BorderFactory.createCompoundBorder(        												scrollPane.getBorder(),        												BorderFactory.createEmptyBorder(5,5,5,5))));        		//Create text field for the user		textField = new JTextField();		textField.addActionListener(this);				//Bring it all together		frame.add(graphicZone, BorderLayout.CENTER);		frame.add(textZone, BorderLayout.PAGE_END);		textZone.add(scrollPane, BorderLayout.CENTER);		textZone.add(textField, BorderLayout.PAGE_END);		frame.pack();		frame.setResizable(true);		frame.setVisible(true);	}		public void closeConnection(){		try {			sc.close();			sc.keyFor(selector).cancel();			System.out.println("Closing");		} catch (IOException e) {			e.printStackTrace();		}	}	public void actionPerformed(ActionEvent e) {		//Copy message from the text field to the text area		outString = textField.getText();		if(outString!= null){			jTextArea.append(outString + "\n");		}		textField.setText("");		//Makes the new text visible even if an earlier message was being viewed		jTextArea.setCaretPosition(jTextArea.getDocument().getLength());			}}


So what im trying to achieve is for the client to implement actionlistener and when the user writes something in the textField then it will be sent to the server and then from the server be broadcast to all clients connected.

At the moment it connects but then it never seems to go into the writable key part(or the readable for that matter but i presume thats only when a message is sent)? How do I make it so that the message is sent to the server when the user writes a message? do I need to create a key for it or set something with OP_WRITE?

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.