Lets take a look at an example of using Java for communication across a network. The program being demonstrated uses a server and a client program to network a game of “Wheel of Fortune” between 3 players.
Each “player” is represented by a class Player and is a thread on the server. This class also does the bulk of the logic. The reason for designing it this way is that we can easily scale the server to any number of players and also reuse several pieces of our code.
Let’s take a look at our Server.main():
public static void main(String[] args)
{
////////////////////////////////
// ARGUMENT ACCEPTANCE //
////////////////////////////////
// Argument Protection: avoid any non-whole number strings.
try
{
//- Avoid blank arguments.
if(args[0].equals("")){
System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
System.exit(0);
}
//- Avoid negative and zero arguments.
if(Integer.parseInt(args[0]) < 0){
System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
System.exit(0);
}
}
catch(NumberFormatException e)
{
//- Avoid arguments that are not numeric.
System.out.println("Usage Error: invalid argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
System.exit(0);
}
catch(ArrayIndexOutOfBoundsException e)
{
//- Avoid arguments that are not existent.
System.out.println("Usage Error: missing argument\nFormat: java Server [port number]\n\nExample: java Server 4390");
System.exit(0);
}
// Save the passed port number
int port = Integer.parseInt(args[0]);
// Create our Server object
Server server = new Server(port);
// Initiate game
server.listenSocket();
// Close game and server
System.exit(0);
}
Outside of the normal argument validation the main only initiates one line of code to run the game: server.listenSocket();. This method performs all of the game creation code such as player joining etc. Lets look at Server.listenSocket() now:
public void listenSocket()
{
///////////////////////////////
// CREATE SERVER
///////////////////////////////
try
{
server = new ServerSocket(this.port);
}
catch (IOException e) {
System.out.println("Could not listen on port " + this.port);
System.exit(-1);
}
///////////////////////////////
// CREATE PHRASE
///////////////////////////////
// Read the phrases file into an array
this.phrases = readFile("phrases.txt");
// Pick a random phrase line from the file
Server.phrase = this.phrases.get(generator.nextInt(this.phrases.size()));
// Split the phrase line by tab
this.phrase_split = Server.phrase.split("\t");
// Save our phrase
Server.phrase = this.phrase_split[0];
// Save the phrase's category
Server.category = this.phrase_split[1];
// Create our hidden phrase to display made of hyphens
Server.displayPhrase = "";
for(int i=0; i < Server.phrase.length(); i++)
{
// IF there is a space, make it shown
if(Server.phrase.charAt(i) == ' '){
Server.displayPhrase = Server.displayPhrase.concat(" ");
}
// For each letter represent it with a -
else{
Server.displayPhrase = Server.displayPhrase.concat("-");
}
}
///////////////////////////////
// ACCEPT PLAYERS
///////////////////////////////
System.out.println("Server initiated and game is set. Awaiting players...");
// Keep waiting until all players have joined
while(true)
{
// Temporary player object
Player p;
try
{
// Create our player object for the client joining
p = new Player(server.accept(), playerCount, openPlay);
// Turn that player object into a thread
Thread t = new Thread(p);
// Add the player to the player list
players_o.add(playerCount, p);
// Add the thread to the player thread list
players.add(playerCount, t);
// Notify server of new player
System.out.println("Player " + playerCount + " Accepted!");
playerCount++;
}
catch (IOException e){
System.out.println("Accept failed: " + this.port);
System.exit(-1);
}
// IF we have all three players needed, stop accepting more players
if(players.size() >= 3){
System.out.println("Maximum amount of players have joined.");
break;
}
}
// Start the threads running, therefore starting the game
System.out.println("Game has begun.");
for(int j=0; j < playerCount; j++)
{
players.get(j).start();
}
// Once the threads are done the game is done
// The server simply needs to wait until the player threads have completed their game
for(int j=0; j < playerCount; j++)
{
// Close down the thread when it is complete
try{
players.get(j).join();
} catch (InterruptedException e){}
}
// Notify the server that the game is over
System.out.println("Game is over.");
}
Server.listenSocket() operates in the following steps:
- First it opens the port set in the argument for listening.
- The server then uses other logic to parse a random phrase for the game from a file
- Once that is complete, the server awaits the players to connect.
- Each time a player connects, it creates an instance of the Player class but does not begin their thread immediately.
- After all players have joined, the threads for each player are started in the order by which they have joined
- At that point the logic of the Server class is done, and it simply idles until each player’s thread has closed.
So the server is quite simple in that is sets up the game and dictates when it should end and begin. Like I said previously, the real logic of the game lies within the Player class threads. Lets take a look at some pieces of that code:
///////////////////////////////
// ESTABLISH CONNECTION TO CLIENT
///////////////////////////////
try
{
// Create our Communication Tools
in = new BufferedReader(new InputStreamReader(player.getInputStream()));
out = new PrintWriter(player.getOutputStream(), true);
}
catch (IOException e) {
System.out.println("in or out failed");
System.exit(-1);
}
The Player.run() method for the thread starts with establishing a connection to the client over the network. The reference to “player” is actually an instance of the Java class Socket that was passed into the constructor of the player instance by the server. This code merely demonstrates that each player instance on the server is a one-to-one relationship with a client across a network.
// Continue running the game until the game has been ended
while(Server.gameOn){
// THE FOLLOWING IS A CRITICAL SECTION AND SHOULD ONLY BE RUN IF IT IS THE PLAYERS TURN
// IF it is the player's turn to play
if(Server.playerTurn == playerId){
// Utilize the Semaphore to ensure no one else can play
// WAIT (openPlay);
try{
openPlay.acquire();
}catch (InterruptedException e){ System.out.println("wait(openPlay) failure"); }
// Announce to the player that it is their turn
Server.announce("Player " + this.playerId + "'s turn...", this.playerId);
// Loop until turn has ended
while(true){
The next section of code is more or less the game loop. Each player thread will loop continually while the game is on, but will not continue logically until until it is their turn as designated by the serve. Once it becomes the player’s turn an internal game play loop begins. Here is an overview of the internal player loop:
// IF the player has not made a spin
if(this.spin == 0)
{
// Ask the player to spin the wheel
out.println("respond@It is your turn. Type \"spin\" to spin the wheel.");
try {
line = in.readLine().trim();
}
catch (IOException e) {
System.out.println("Read failed");
System.exit(-1);
}
// IF the player has spun the wheel
if(line.equals("spin")){
// Choose a random value for the spin wheel
this.spin_val = generator.nextInt(24);
// CODE removed for tutorial but it contains logic for three possible spins:
// Bankrupt and Lose Turn
if(this.spin_val == 22){
...
}
// Else If Lose Turn
else if(this.spin_val == 23){
...
}
// Else Save the dollar amount spun
else{
...
}
}
else{
this.spin = 0;
}
}
// The player has already spun the wheel and is ready to play
else{
// CODE removed for tutorial but it contains logic for three possible spins:
// IF the player has not made a choice for their turn
if(this.decision == 0 || this.decision > 3){
// Provide the player with the menu to choose from
// 1. Guess the Phrase
// 2. Buy a vowel
// 3. Guess a consonant
...
}
// IF the player has decided to "Guess phrase"
else if(this.decision == 1){
// Ask the player for their guess at the phrase
...
// Check to see if the player's guess was right
// IF the player is correct
if(line.toLowerCase().equals(Server.phrase.toLowerCase())){
// Award the player their money and announce their victory to waiting players
out.println("null@Phrase \"" + line + "\" is correct! You win $" + this.budget + "!");
Server.announce("Player " + this.playerId + " has won the game and $" + this.budget + " by guessing \"" + line + "\".", this.playerId);
// Turn off the game
Server.gameOn = false;
break;
}
// IF the player was wrong with their guess
else{
// Notify them and the other players they were wrong
out.println("null@Phrase \"" + line + "\" is incorrect. Your turn is over.");
Server.announce("Player " + this.playerId + " guessed \"" + line + "\", but was wrong.", this.playerId);
// Cause the player's turn to end
// Unset their spin and decision
// Move to the next player
// Release the critcal section
this.spin = 0;
this.decision = 0;
this.nextPlayerTurn();
openPlay.release();
break;
}
}
// IF the player decided to "Buy a vowel"
else if(this.decision == 2){
// Ensure the player has enough money to buy a vowel
if(this.budget >= 250){
// Ask the player which vowel they want
...
// IF there is a single letter that is a vowel
if(line.length() == 1){
// While checking if the letter has been guessed already, add it to the guessed list
if(!this.addCheckLetter(line)){
// Lookup the number of instances the letter appears in the phrase
letterLocations = findLetterOccurances(line, Server.phrase);
// IF there are letter occurances
// The player is CORRECT
if(letterLocations.size() > 0){
// Remove the cost of the vowel from their budget
...
// Notify the player and others of their correct guess
...
// Update the hidden phrase, showing the vowel
// Notify everyone of the new, partially exposed, phrase
...
// The player retains their turn
// Reset their decision and spin and keep control of critical section
...
}
// IF there are no letter instances
// The player is INCORRECT
else{
// Notify the player and others of the mistake
...
// Cause the player to lose their turn
// unset their spin and decision
// move to the next player
// and release the critical section
...
}
}
// IF the vowel has already been guessed before
else{
// ask again
...
}
}
// IF the player did not respond with a single letter vowel
else{
...
}
}
// IF the player did not have enough money to purchase a vowel
else{
..
}
}
// IF the player chose to "Guess a consonant"
else if(this.decision == 3){
// Ask the player for the consonant guess
...
// IF the consonant response is valid
if(line.length() == 1){
// While checking if the letter has been guessed already, add it to the guessed list
if(!this.addCheckLetter(line)){
// Lookup the number of instances the letter appears in the phrase
letterLocations = findLetterOccurances(line, Server.phrase);
// IF there are letter occurances
// The player is CORRECT
if(letterLocations.size() > 0){
// Reward the player with their spin money times the number of letter occurances
...
// Notify the player and others of the correct guess
.
// Update the hidden phrase, showing the vowel
// Notify everyone of the new, partially exposed, phrase
...
// The player retains their turn
// Reset their decision and spin and keep control of critical section
...
}
// IF the player's guess was wrong
else{
// Notify the player and others of the mistake
...
// Cause the player to lose their turn
// unset their spin and decision
// move to the next player
// and release the critical section
...
}
}
// IF the consonant has already been guessed before
else{
...
}
}
// IF the player did not respond with a single letter consonant
else{
....
}
} // END DECISION IFS
} // END SPIN IF
} // END WHILE TRUE
} // END IF TURN
} // END WHILE gameOn
I removed sections of code related to the presentation of game state to the players as well as general logic for gameplay like if the player had guessed correctly or incorrectly. The focus here is more to present you with the notion that the internal player loop comprises most of the game logic. The reason for this design was so we could have any number of players, and using a semaphores and threads they could interact without having to organize their interactions.
The last piece of important code is our client program. Lets take a look at its main() first:
public static void main(String[] args)
{
////////////////////////////////
// ARGUMENT ACCEPTANCE //
////////////////////////////////
// Argument Protection: avoid any non-whole number strings.
try
{
//- Avoid blank arguments.
if(args[0].equals("") && args[1].equals("")){
System.out.println("Usage Error: missing arguments\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
System.exit(0);
}
//- Avoid negative and zero arguments.
if(Integer.parseInt(args[1]) < 0){
System.out.println("Usage Error: invalid arguments\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
System.exit(0);
}
}
catch(NumberFormatException e)
{
//- Avoid arguments that are not numeric.
System.out.println("Usage Error: invalid argument\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
System.exit(0);
}
catch(ArrayIndexOutOfBoundsException e)
{
//- Avoid arguments that are not existent.
System.out.println("Usage Error: missing argument\nFormat: java Client [host name] [port number]\n\nExample: java Server net02 4390");
System.exit(0);
}
// Save our host and port arguments
int port = Integer.parseInt(args[1]);
String host = args[0];
// Create our client object
Client client = new Client(host, port);
// Game on
client.listenSocket();
client.communicate();
}
Very similarly structured to the server program the only real portions of importance are the lines “client.listenSocket();” and “client.communicate();”. Here are the two methods highlighted:
///////////////////////////////
// OPEN SOCKET CONNECTION
///////////////////////////////
public void listenSocket(){
try{
// Create our socket
socket = new Socket(this.host, this.port);
// Create our communication tools
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
catch (UnknownHostException e) {
System.out.println("Unknown host: " + this.host);
System.exit(1);
}
catch (IOException e) {
System.out.println("No I/O");
System.exit(1);
}
System.out.println("Welcome, please wait to begin.");
}
///////////////////////////////
// READ or WRITE to SOCKET
///////////////////////////////
public void communicate()
{
// Create our communication buffers and tools
Scanner sc = new Scanner(System.in);
String[] command = new String[2];
String line = "";
char inline[] = new char[1000];
// Keep looking for messages from the server
while(true)
{
// Reset the command recieved from the server to make sure the next one is clear
command = new String[2];
command[0] = "";
command[1] = "";
// READ FROM THE SERVER
try{
// Take a line from the server
line = in.readLine();
// Split the line so we know the reponse indetifier and the message
command = line.split("@");
// Special terminate command to end our client object
if(command[0].equals("terminate"))
System.exit(0);
// If the command identifier and the message are existant
if(!command[0].equals("") && !command[1].equals("")){
// Print the message
System.out.println(command[1]);
}
}
catch (IOException e){
System.out.println("Read failed");
System.exit(1);
}
catch (ArrayIndexOutOfBoundsException e){}
catch (NullPointerException e){}
// IF response identifier given, ask for a respons to send to the server
if(command[0].equals("respond")){
// Ask the user to respond
String name = sc.nextLine();
// Send the server the response
out.println(name);
}
}
}
The method listenSocket() only exists to initialize our input and output streams for the network communication. Simple stuff really thanks to Java.
The method communicate() on the other hand contains all the important logic for the client. As you can see the method itself is extremely generic and has no concept of game rules or game messages. In fact its so generic that this method could be used in conjunction with any game server.
That means that our client is coded in a fashion that makes it highly reusable which is a very vital aspect to good software design. In fact the server program itself is very generic and reusable since the entire game logic is within the Player class’ run() method.
This is both very important in that we simply have to swap out Player classes to have entirely different games, making the communication and thread logic reusable and therefore more valuable.
Feel free to download the source and run it for yourself.
Download: SocketWheelOfFortune.zip
To run:
- Download source zip.
- Decompress and compile Server.Java and Client.java
- Run Server with arguments [port], like “java Server 4444″
- Run 3 instances of Client with [host] [port] like “java Client localhost 4444″
- The server will wait until all three players have joined.