Page 1 of 3 123 Last
  • Jump to page:
    #1
  1. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084

    Creating a multi-threaded multi-user chat room


    This will be a tutorial of sorts. I have promised a friend to teach him simple server-client programming in java.

    The reason I post here is because many people here know how to do things better than me, so if anyone reading this (that means YOU) sees something wrong, don't hesitate to point it out.

    This project will for simplicity's sake be a chat program and no more, but it can very easily be adapted to much much more.

    To start, I would like to credit http://www.ashishmyles.com/tcpchat/index.html as a good place to start.

    Step 1: Create a server that idles until someone connects to it, then prints out a message and closes. Create a client that connects to a server and gets a message and closes.

    ---------------

    SERVER

    I have arbitrarily picked port 9413 for this exercise.

    Driver class. Simple.
    java Code:
    public class Driver {
    	public static void main(String[] args) {
    		new ServerController();
    	}
    }


    This class will be called ServerController. It will be the central processing class (at least for now). I have imported more than necessary for future additions.
    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class ServerController {
    	private ServerSocket socketAcceptor;
     
    	public ServerController() {
    		String data = "Hello thar. This is trial 1.";
    		try {
    			socketAcceptor = new ServerSocket(9413);
    			Socket sock = socketAcceptor.accept();
    			System.out.print("Server has connected!\n");
    			PrintWriter out = new PrintWriter(sock.getOutputStream(), true);
    			System.out.println("Sending '" + data + "'");
    			out.print(data);
    			out.close();
    			sock.close();
    			socketAcceptor.close();
    		} catch (Exception e) {
    			System.out.println("Did not work somewhere");
    			e.printStackTrace();
    		}
    	}
    }



    -------------

    CLIENT

    Once again, a driver class. This will be changed in the future to point to a different class, one that controls the graphical user interface (GUI).
    java Code:
    public class Driver {
    	public static void main(String[] args) {
    		new Client();
    	}
    }


    Client. For the first trial, we just connect, receive, die.
    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class Client {
     
    	public Client() {
    		try {
    			Socket sock = new Socket("localhost", 9413);
    			BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
    			System.out.print("Receiving: '");
     
    			while (!in.ready()) {}
     
    			System.out.print(in.readLine()); // Read one line and output it
    			System.out.println("'");
     
    			in.close();
    		} catch (ConnectException e) {
    			System.out.println("ConnectException. Chances are the server is turned off or the port is blocked.");
    		} catch (Exception e) {
    			System.out.println("Whoops! It didn't work!");
    			e.printStackTrace();
    		}
    	}
    }




    ----------

    This first step serves two purposes:
    1) We see the basic idea.
    2) We make sure port 9413 is open and accessible. I recommend changing "localhost" to "127.0.0.1" then to your intranet IP address then to your IP address if you wish to open this up to outside access.


    Once we have played with this a little, we can move on to step two.

    Now, in the logical development, there are different possibilities for step two. We can:
    A) Implement a packet system instead of passing a String.
    B) Implement a two-way communications system instead of one-way. Have the client send a message that will get bounced back.
    C) Implement multithreading so that multiple users can connect to the server.

    I choose to make option C step 2. B and then A will follow.

    Comments on this post

    • swattkidd agrees
    Last edited by gimp; August 7th, 2008 at 09:29 PM.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  2. #2
  3. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    To start, let's modify the client slightly to fit out uses better. Here we have something that will catch all sorts of exceptions and tell you what's what. This is very useful to learn when an exception can occur; as you progress you can ignore most of these.

    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class Client {
     
    	public Client() {
    		try {
    			/** Create a new socket */
    			Socket sock = new Socket("localhost", 9413);
     
    			/** Create an input stream */
    			BufferedReader input;
    			try {
    				// initialize input stream and read from it, then close when finished
    				input = new BufferedReader(new InputStreamReader(sock.getInputStream()));
     
    				String line = "";
    				while ( (line = input.readLine()) != null) {
    					System.out.print("Receiving: '");
    					System.out.print(line); // Read one line and output it
    					System.out.println("'");
    				}
    				input.close();
    				sock.close();
    			} catch (SocketException e) {
    				System.out.println("SocketException. Means bad socket close.");
    			} catch (Exception e) { e.printStackTrace(); }
    		} catch (ConnectException e) {
    			System.out.println("ConnectException. Chances are the server is turned off or the port is blocked or wrong.");
    		} catch (IOException e) {
    			System.out.println("IOException. Bad stuff.");
    		} catch (SecurityException e) {
    			System.out.println("SecurityException. Bad stuff.");
    		} catch (IllegalArgumentException e) {
    			System.out.println("IllegalArgumentException. Bad port. must be 0 - 65535.");
    		} catch (Exception e) {
    			System.out.println("Something bad.");
    			e.printStackTrace();
    		}
    	}	
    }



    ------------

    Next, the server.

    First, we create a class to handle accounts. In its most basic form, it will have the username and the socket. We can immediately add more: An output stream to write to the socket, and a buffer of messages to send.

    Keep in mind this extends the Thread class, therefore we must have a run() method to use this in a multi-threaded fashion.

    addData() adds to the buffer.
    sendData() writes it to the stream.
    close() closes the stream and socket.
    java Code:
    import java.util.*;
    import java.net.*;
    import java.io.*;
     
    public class AccountHandler extends Thread {
    	/** Username */
    	private String name;
    	/** Password - not used yet */
    	private String pass;
    	/** Socket to the client*/
    	private Socket sock;
    	/** Output stream */
    	private DataOutputStream output;
    	/** Buffer of messages to send */
    	private ArrayList<String> messages;
     
    	/** No empty account handlers, otherwise big errors */
    	private AccountHandler() {}
    	public AccountHandler(String name, String pass, Socket s) {
    		messages = new ArrayList<String>();
    		this.name = name; this.pass = pass; this.sock = s;
     
    		System.out.println("Server has connected to " + sock.getInetAddress() + "!");
     
    		try { output = new DataOutputStream(sock.getOutputStream()); } catch (Exception e) {}
    	}
     
    	public void run() {
    		while (messages.size() > 0) { sendData(messages.remove(0)); }
    		close();
    	}
     
     
    	public void addData(String data) { messages.add(data); }
     
    	private void sendData(String data) {
    		try {
    			System.out.println("Sending " + name + " '" + data + "'");
    			output.writeBytes(data + "\n");
    		} catch (Exception e) { e.printStackTrace(); }
     
    		try { Thread.sleep(5000); } catch (InterruptedException e) {}
    	}
     
    	private void close() {
    		try {
    			System.out.println("Closing sock");
    			output.close();
    			sock.close();
     
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    }


    Next, let's have the server class that can handle multiple connections now.

    What we do here is this:
    First, we map users to their account classes and we list the users.
    Next, we accept connections. If something goes wrong we end the program.

    In acceptConnections(), we have temporarily put a message to be sent twice to the client. As you can see above, we have a 5 second delay between messages so that you have time to open multiple clients to see that they run simultaneously.

    We give accounts names temporarily based on their IP and account count. This will change once a login protocol is established. This is not a priority.
    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class ServerController {
    	/** Map of usernames to accounts */
    	private HashMap<String, AccountHandler> acctMap;
    	/** List of usernames */
    	private ArrayList<String> users;
    	/** Socket acceptor */
    	private ServerSocket socketAcceptor;
     
     
    	public ServerController() {
    		acctMap = new HashMap<String, AccountHandler>();
    		users = new ArrayList<String>();
     
    		String data = "Hello thar. This is trial 2.";
    		System.out.println("Initializing server.");
    		try {
    			socketAcceptor = new ServerSocket(9413);
    		} catch (Exception e) {
    			System.out.println("socketAcceptor wrong");
    			e.printStackTrace();
    		}
     
    		acceptConnections(data);
     
    		if (socketAcceptor != null && !socketAcceptor.isClosed()) {
    			try { socketAcceptor.close(); } catch (Exception e) {}
    		}
    	}
     
    	private void acceptConnections (String data) {
    		while (socketAcceptor != null && !socketAcceptor.isClosed()) {
    			try {
    				Socket sock = socketAcceptor.accept();
     
    				String username = sock.getInetAddress().toString() + " [" + users.size() + "]";
    				users.add(username);
    				AccountHandler userHandler = new AccountHandler(username, "", sock);
    				acctMap.put(username, userHandler);
     
    				userHandler.addData(data);
    				userHandler.addData(data);
    				userHandler.start();
     
    			} catch (Exception e) {
    				System.out.println("Did not work somewhere");
    				e.printStackTrace();
    			}
    		}
    	}
    }
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  4. #3
  5. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    Well, la-di-dah. We have some working pieces. What we want to do now is tidy work.

    For example, once a thread is created it just sits there. Even when it's useless.

    What we want to do is realize when an account is closed and delete it from the list. This will require only slight modifications.

    For now, let us only delete the account from the lists when its close method is called. Later on we can add to this by creating a ping system that will tell you when a socket is 'dead'.

    java Code:
    import java.util.*;
    import java.net.*;
    import java.io.*;
     
    public class AccountHandler extends Thread {
    	/** Username */
    	private String name;
    	/** Password - not used yet */
    	private String pass;
    	/** Socket to the client*/
    	private Socket sock;
    	/** Output stream */
    	private DataOutputStream output;
    	/** Buffer of messages to send */
    	private ArrayList<String> messages;
    	/** Handle to the server controller */
    	private ServerController server;
    	/** Running or closed? Running by default, closed when finished/disconnected */
    	private boolean running;
     
    	/** No empty account handlers, otherwise big errors */
    	private AccountHandler() {}
    	public AccountHandler(String name, String pass, Socket sock, ServerController server) {
    		messages = new ArrayList<String>();
    		running = true;
    		this.name = name; this.pass = pass; this.sock = sock; this.server = server;
     
    		System.out.println("Server has connected to " + name + "!");
     
    		try { output = new DataOutputStream(this.sock.getOutputStream()); } catch (Exception e) {}
    	}
     
    	/**
    	 *	Called by Thread.start()
    	 *	does whatever is needed (current: sends messages)
    	 *	closes when done
    	 */
    	public void run() {
    		while (messages.size() > 0 && running) { sendData(messages.remove(0)); }
    		close();
    	}
     
    	/**
    	 *	@param data: Add data to the buffer
    	 */
    	public void addData(String data) { messages.add(data); }
     
    	/**
    	 *	@param data: Send data into the stream
    	 */
    	private void sendData(String data) {
    		try {
    			System.out.println("Sending " + name + " '" + data + "'");
    			output.writeBytes(data + "\n");
    		catch (SocketException e) {
    			close();
    		} catch (Exception e) { e.printStackTrace(); }
     
    		try { Thread.sleep(5000); } catch (InterruptedException e) {}
    	}
     
    	/**
    	 *	Close the account thread. Close all streams and the socket and request to be removed.
    	 */
    	private void close() {
    		end();
    		try {
    			System.out.println("Closing socket to " + name);
    			output.close();
    			sock.close();
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    		server.remove(this);
    	}
     
    	/** Set running to a value - self explanatory */
    	public void setRunning(boolean running) { this.running = running; }
     
     
    	/** Accessor methods - get name, password, socket */
    	public String getAccName() { return name; } // getName() is declared in Thread
    	public String getPass() { return pass; }
    	public Socket getSock() { return sock; }
     
    	/** String representation */
    	public String toString() { return name + ", " + pass; }
    }


    We made little changes. Added a handler to the server controller, added a boolean about whether or not this thread is running (will be used later to pause execution using synchronized). Added a method to set running to false, edited the close() method to ask the server controller to remove this from the list.

    We also added accessor methods which aren't used quite yet apart from getName(), as follows:

    java Code:
    public class ServerController {
    	...
    	/**
    	 *	Remove an account from the account list and map
    	 *	@param acctName:	Account name, as String
    	 *	@param acct:		Account, as AccountHandler
    	 */
    	public void remove(String acctName) {
    		users.remove(acctName);
    		acctMap.remove(acctName);
    	}
    	public void remove(AccountHandler acct) { this.remove(acct.getAccName()); }
    }


    In addition, in method acceptConnections(String), we change our call to the constructor to
    Code:
    AccountHandler userHandler = new AccountHandler(username, "", sock, this);
    Last edited by gimp; August 7th, 2008 at 11:41 PM.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  6. #4
  7. Daniel Schildsky
    Devshed Intermediate (1500 - 1999 posts)

    Join Date
    Mar 2004
    Location
    KL, Malaysia.
    Posts
    1,540
    Rep Power
    1621

    Impressive


    Impressive indeed!

    How about doing the same thing with Java NIO?
    When the programming world turns decent, the real world will turn upside down.
  8. #5
  9. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    How about later? If I get motivated enough maybe. Until then it's vanilla, it's simple. Or you can do it :P
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  10. #6
  11. No Profile Picture
    Contributing User
    Devshed Novice (500 - 999 posts)

    Join Date
    Jul 2005
    Location
    Bay Area, California
    Posts
    841
    Rep Power
    1682
    In post #2, you make AccountHandler extend java.lang.Thread, whereas it would be ideal to implement java.lang.Runnable. You are also starting the thread via ServerController, whereas in practice you might use a ThreadPoolExecutor with TTL threads, thereby removing creation/destruction costs with heavy connect/disconnect operations.

    In post #3, you allow AccountHandler threads to mutate the shared ServerController maps without any form of synchronization. Your exception management would make me uncomfortable in a production system as you eat them instead of using try/finally blocks, which might risk not closing the socket.

    If I was to design this I would probably think in terms of topics (chat rooms) and listeners (users). An approach could be something on the lines of a thread per chat room, which blocks on a work queue and pushes a message to each output queue for a user. This keeps messages ordered. A user thread would drain its output queue and offer input to the chatroom topic's work queue. Or I might look deeper into NIO, which I have not yet used directly, to avoid a thread per connection. However it turned out, I would try to model users as actors and the chat room as a topic. I would try to decide whether message ordering was required or if best-effort was good enough.

    Comments on this post

    • tfecw agrees : Wanted to toss in my concerns with the exception management and threading concerns.
  12. #7
  13. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    Thank you novaX, I will look into what you've said and redo pieces so I can have a good framework to continue with.

    Comments on this post

    • NovaX agrees : You're welcome. Its a great example so far.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  14. #8
  15. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    I have received some good advice above so I will utilize it now.

    Before we continue with B), which is two-way communication, and A), which is a packet system, let's instead rethink our logic slightly.

    One approach is to have a ChatRoom class that runs as its own thread and handles input/output to and from all users (actors) connected to said chat room by handling their socket connections. There are major flaws: First, all chat rooms a user is in will receive packets meant for one chat room. Second, before a user enters a chat room, what is there to receive his packets?

    Instead, I will keep a single socket handle per AccountHandler instance; one instance per connected user. Each AccountHandler will be a Runnable as advised above. The chat room class will not; it will basically have buffers to send to everyone. I will have to devise a blocking system.

    I will rewrite using ThreadPoolExecutor which I have never done before.



    Also,
    you allow AccountHandler threads to mutate the shared ServerController maps without any form of synchronization
    How would you advise I do this? A thread is allowed to remove itself from the list and maps. At this point, what is there to synchronize with? How?

    This post contains no code.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  16. #9
  17. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    First, let's edit the client slightly to make sure sockets and streams close.

    The edit is really small. Added a finally block and moved close commands into it.
    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class Client {
     
    	public Client() {
    		try {
    			/** Create a new socket */
    			Socket sock = new Socket("localhost", 9413);
     
    			/** Create an input stream */
    			BufferedReader input = null;
    			try {
    				// initialize input stream and read from it, then close when finished
    				input = new BufferedReader(new InputStreamReader(sock.getInputStream()));
     
    				String line = "";
    				while ( (line = input.readLine()) != null) {
    					System.out.print("Receiving: '");
    					System.out.print(line); // Read one line and output it
    					System.out.println("'");
    				}
     
    			} catch (SocketException e) {
    				System.out.println("SocketException. Means bad socket close.");
    			} catch (Exception e) { e.printStackTrace(); }
    			finally {
    				if (input != null)	{ input.close(); }
    				if (sock != null)	{ sock.close(); }
    			}
    		} catch (ConnectException e) {
    			System.out.println("ConnectException. Chances are the server is turned off or the port is blocked or wrong.");
    		} catch (IOException e) {
    			System.out.println("IOException. Can't read file.");
    		} catch (SecurityException e) {
    			System.out.println("SecurityException. Security settings messing you up.");
    		} catch (IllegalArgumentException e) {
    			System.out.println("IllegalArgumentException. Bad port. must be 0 - 65535.");
    		} catch (Exception e) {
    			System.out.println("Something bad.");
    			e.printStackTrace();
    		}
    	}
    }


    ---------------


    SERVER

    Now, we will follow the advice given above.

    First, let's change the account handler slightly. Make it a runnable. Add more finally blocks to make sure things close.

    java Code:
    import java.util.*;
    import java.net.*;
    import java.io.*;
     
    public class AccountHandler implements Runnable {
    	/** Username */
    	private String name;
    	/** Password - not used yet */
    	private String pass;
    	/** Socket to the client*/
    	private Socket sock;
    	/** Output stream */
    	private DataOutputStream output;
    	/** Buffer of messages to send */
    	private ArrayList<String> messages;
    	/** Handle to the server controller */
    	private ServerController server;
    	/** Running or closed? Running by default, closed when finished/disconnected */
    	private boolean running;
     
    	/** No empty account handlers, otherwise big errors */
    	private AccountHandler() {}
    	public AccountHandler(String name, String pass, Socket sock, ServerController server) {
    		messages = new ArrayList<String>();
    		running = true;
    		this.name = name; this.pass = pass; this.sock = sock; this.server = server;
     
    		System.out.println("Server has connected to " + name + "!");
     
    		try { output = new DataOutputStream(this.sock.getOutputStream()); }
    		catch (IOException e) { close(); System.out.println("Can't get outpustream"); e.printStackTrace(); }
    	}
     
    	/**
    	 *	does whatever is needed (current: sends messages)
    	 *	closes when done
    	 */
    	public void run() {
    		while (messages.size() > 0 && running) { sendData(messages.remove(0)); }
    		close();
    	}
     
    	/**
    	 *	@param data: Add data to the buffer
    	 */
    	public void addData(String data) { messages.add(data); }
     
    	/**
    	 *	@param data: Send data into the stream
    	 */
    	private void sendData(String data) {
    		try {
    			System.out.println("Sending " + name + " '" + data + "'");
    			output.writeBytes(data + "\n");
    		} catch (IOException e) {
    			System.out.println("Can't write bytes"); e.printStackTrace();
    			close();
    		}
     
    		try { Thread.sleep(5000); } catch (InterruptedException e) {}
    	}
     
    	/**
    	 *	Close the account thread. Close all streams and the socket and request to be removed.
    	 */
    	private void close() {
    		setRunning(false);
    		try {
    			System.out.println("Closing socket to " + name);
    			output.close();
    			sock.close();
    		} catch (IOException e) {
    			System.out.println("Error closing datastream");
    			e.printStackTrace();
    		} finally {
    			server.remove(this);
    		}
     
    	}
     
    	/** Set running to a value - self explanatory */
    	public void setRunning(boolean running) { this.running = running; }
     
     
    	/** Accessor methods - get name, password, socket */
    	public String getAccName() { return name; }
    	public String getPass() { return pass; }
    	public Socket getSock() { return sock; }
     
    	/** String representation */
    	public String toString() { return name + ", " + pass; }
    }



    And now the main server class as well. Here we add a pool and a queue system. For now, let's make MAX_THREADS 2. We will expand this in the future. Also, notice we import java.util.concurrent.* which is new.

    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
    import java.util.concurrent.*;
     
    public class ServerController {
    	/** Map of usernames to accounts */
    	private HashMap<String, AccountHandler> acctMap;
    	/** List of usernames */
    	private ArrayList<String> users;
    	/** Socket acceptor */
    	private ServerSocket socketAcceptor;
    	/** Maximum number of threads allowed to connect */
    	private final static int MAX_THREADS = 2;
    	/** ThreadPoolExecutor: Runs threads, obviously */
    	private final ThreadPoolExecutor pool;
    	/** Thread container */
    	private final ArrayBlockingQueue<Runnable> workQueue;
     
    	public ServerController() {
    		acctMap = new HashMap<String, AccountHandler>();
    		users = new ArrayList<String>();
     
    		String data = "Hello thar. This is trial 3.";
    		System.out.println("Initializing server.");
    		try {
    			socketAcceptor = new ServerSocket(9413);
    		} catch (Exception e) {
    			System.out.println("socketAcceptor wrong");
    			e.printStackTrace();
    		}
     
    		workQueue = new ArrayBlockingQueue<Runnable>(MAX_THREADS);
    		pool = new ThreadPoolExecutor(MAX_THREADS, MAX_THREADS, 60, TimeUnit.SECONDS, workQueue);
     
    		acceptConnections(data);
     
    		try { socketAcceptor.close(); } catch (Exception e) {} finally { pool.shutdown(); }
    	}
     
    	private void acceptConnections(String data) {
    		while (socketAcceptor != null && !socketAcceptor.isClosed()) {
    			try {
    				Socket sock = socketAcceptor.accept();
     
    				String username = sock.getInetAddress().toString() + " [" + users.size() + "]";
    				users.add(username);
    				AccountHandler userHandler = new AccountHandler(username, "", sock, this);
    				acctMap.put(username, userHandler);
     
    				userHandler.addData(data);
    				userHandler.addData(data);
     
    				pool.execute(userHandler);
     
    			} catch (Exception e) {
    				System.out.println("Did not work somewhere");
    				e.printStackTrace();
    			}
    		}
    	}
     
    	/**
    	 *	Remove an account from the account list and map
    	 *	@param acctName:	Account name, as String
    	 *	@param acct:		Account, as AccountHandler
    	 */
    	public void remove(String acctName) {
    		users.remove(acctName);
    		acctMap.remove(acctName);
    	}
    	public void remove(AccountHandler acct) { this.remove(acct.getAccName()); }
    }



    So, once again, this works. as before, we are just sending a message and the same message again in 5 seconds, then closing in 5 seconds.

    If anything here is not understood, I recommend viewing the APIs for ThreadPoolExecutor. As always, I attempt to make everything as simple as possible.

    The next revision will be a client revision; having everything in the constructor isn't very pretty.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  18. #10
  19. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class Client {
     
    	/** Socket handle */
    	private Socket sock;
    	/** Input stream reader as BufferedReader */
    	private BufferedReader input;
     
    	/**
    	 *	Constructor
    	 *	Create socket, read from it, close it
    	 */
    	public Client() {
    		try {
    			/** Create a new socket. Server localhost (127.0.0.1), post 9413 */
    			sock = new Socket("localhost", 9413);
    			read();
    			close();
    		} catch (ConnectException e) {
    			System.out.println("ConnectException. Chances are the server is turned off or the port is blocked or wrong.");
    		} catch (SecurityException e) {
    			System.out.println("SecurityException. Security settings messing you up.");
    		} catch (Exception e) {
    			System.out.println("Something bad.");
    			e.printStackTrace();
    		}
    	}
     
    	/**
    	 *	Read from the socket while the socket is open
    	 *	Echo out all information
    	 */
    	private void read() {
    		try {
    			input = new BufferedReader(new InputStreamReader(sock.getInputStream()));
    			String line = "";
    			while ( (line = input.readLine()) != null) {
    				System.out.print("Receiving: '");
    				System.out.print(line); // Read one line and output it
    				System.out.println("'");
    			}
    		} catch (SocketException e) {
    			System.out.println("SocketException. Means bad socket close.");
    		} catch (Exception e) { e.printStackTrace(); }
    		finally {
     
    		}
    	}
     
    	/**
    	 *	Close the input stream and the socket.
    	 */
    	private void close() {
    		try {
    			if (input != null)	{ input.close(); }
    			if (sock != null)	{ sock.close(); }
    		} catch (IOException e) {
    			System.out.println("Bitch!");
    			e.printStackTrace();
    		}
    	}
    }


    Very simple, very small changes to the client. This makes reading the bloody thing easier, of course, and allows for easier future development.


    Next, we will enable two-way communications. The client will say a random string (number is easier) to the server and the server will bounce back a reply.


    Also please excuse the unprofessional error messages that may pop up from time to time. They're my way of keeping my sanity.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  20. #11
  21. No Profile Picture
    Contributing User
    Devshed Novice (500 - 999 posts)

    Join Date
    Jul 2005
    Location
    Bay Area, California
    Posts
    841
    Rep Power
    1682
    How would you advise I do this? A thread is allowed to remove itself from the list and maps. At this point, what is there to synchronize with? How?
    The data structure is being mutated concurrently, so it must be synchronized against other mutations. A mutation may also be done while others are reading, such as a remove from a linked list while a reader is walking down it looking for its element. You can use the concurrent data structures or, if you need a bundle of operations performed atomically, a mutex.

    pool = new ThreadPoolExecutor(MAX_THREADS, MAX_THREADS, 60, TimeUnit.SECONDS, workQueue);
    In this usage, please use the Executors factory to create a cached executor. Your direct settings could lead to some confusion, as:
    • A fixed pool size means a bounded number of users. If the user count exceeded the size, they would not be handled until a user disconnected. This would be a confusing bug!
    • The setting could lead someone into thinking they could set a core size != max size, with a TTL between them. This won't work, and while stated in the JavaDoc, is a flaw in the constructor by making it seem possible.
  22. #12
  23. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    See, I did think something was wrong when setting core != max and it wasn't working. Thanks for that, I will add that to the next piece of the puzzle.



    I have also decided to skip directly to the chatroom setting with two users for simplicity. This will also be in the next update or the one directly after.
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  24. #13
  25. <?PHP user_title("gimp"); ?>
    Devshed Supreme Being (6500+ posts)

    Join Date
    Jan 2005
    Location
    Internet
    Posts
    7,652
    Rep Power
    6084
    Took a break Back to work.


    So we left off with a client that could not send information, but could only receive. It is time to fix this problem.

    Let's start by splitting up the problem into two pieces: Sending and receiving.

    Put sending into Client. Add ClientReader implements Runnable as a reader (obviously).

    Note I do not spawn this with a ThreadPoolExecutor like it should be. This will be fixed in an upcoming version; for now I focus on simplicity because a client will have only one reader thread spawned by the main thread.

    Also note that we do not call close() in Client unless something 'bad' happens.

    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class Client {
     
    	/** Socket handle */
    	private Socket sock;
    	/** Output stream, writes to the socket */
    	private DataOutputStream output;
    	/** Reads from socket, runs as its own Thread */
    	private ClientReader reader;
     
    	/**
    	 *	Constructor
    	 *	Create socket, read from it, close it
    	 */
    	public Client() {
    		try {
    			/** Create a new socket. Server localhost (127.0.0.1), post 9413 */
    			sock = new Socket("localhost", 9413);
    			output = new DataOutputStream(this.sock.getOutputStream());
    			sendData("Hi!");
     
     
    			reader = new ClientReader(sock);
    			Thread readerThread = new Thread(reader); readerThread.start();
     
    			sendData("Hi!");
    		} catch (ConnectException e) {
    			System.out.println("ConnectException. Chances are the server is turned off or the port is blocked or wrong.");
    			close();
    		} catch (SecurityException e) {
    			System.out.println("SecurityException. Security settings messing you up.");
    			close();
    		} catch (IOException e) {
    			close(); System.out.println("Can't get outputsream"); e.printStackTrace();
    		} catch (Exception e) {
    			System.out.println("Something bad.");
    			e.printStackTrace();
    			close();
    		}
    	}
     
     
     
    	/**
    	 *	Close the input stream and the socket.
    	 */
    	private void close() {
    		try {
    			reader.close();
    			sock.close();
    			output.close();
    		} catch (IOException e) {
    			System.out.println("Bitch!");
    			e.printStackTrace();
    		}
    	}
     
    	private void sendData(String data) {
    		try {
    			System.out.println("Sending '" + data + "'");
    			output.writeBytes(data + "\n");
    		} catch (IOException e) {
    			System.out.println("Can't write bytes"); e.printStackTrace();
    			close();
    		}
    	}
    }


    And here is the ClientReader. Simple.

    java Code:
    import java.util.*;
    import java.io.*;
    import java.net.*;
     
    public class ClientReader implements Runnable {
    	/** Input stream reader as BufferedReader */
    	private BufferedReader input;
    	/** Handle to the socket */
    	private Socket sock;
    	/** Whether or not we're running */
    	private boolean running;
     
    	public ClientReader(Socket sock) {
    		this.sock = sock;
    		running = true;
    	}
     
    	/**
    	 *	Read from the socket while the socket is open
    	 *	Echo out all information
    	 */
    	private void read() {
    		try {
    			input = new BufferedReader(new InputStreamReader(sock.getInputStream()));
    			String line = "";
     
    			while ( (line = input.readLine()) != null) {
    				System.out.print("Receiving: '");
    				System.out.print(line); // Read one line and output it
    				System.out.println("'");
    			}
    		} catch (SocketException e) {
    			System.out.println("SocketException. Means bad socket close.");
    			close();
    		} catch (Exception e) { e.printStackTrace(); }
    	}
     
    	public void run() {
    		while (running) { read(); }
    	}
     
    	public void close() {
    		setRunning(false);
    		try { input.close(); } catch (IOException e) { System.out.println("Error closing input"); e.printStackTrace(); }
    	}
     
    	private void setRunning(boolean r) { running = r; }
    }



    Let's assume we have created a server that takes a message and bounces it back. Note we send the server the String "Hi!".

    Here is the output:

    Code:
    Sending 'Hi!'
    Sending 'Hi!'
    Receiving: 'Hi!'
    Receiving: 'Hi!'
    Notice it doesn't close because we didn't tell it to. Then when we end the server, we get

    Code:
    Sending 'Hi!'
    Sending 'Hi!'
    Receiving: 'Hi!'
    Receiving: 'Hi!'
    SocketException. Means bad socket close.
    Press any key to continue...
    Because we don't have an elegant close system yet. However, this does work fine as a socket close detection system for now.

    Note that we send hi, create a listen thread, send hi, and then get two "hi"s back from the server. Why? Well, we send a second hi faster than the listening thread spawns itself.

    If we added a pause after the listen thread spawn and before the second send, the output would be

    Code:
    Sending 'Hi!'
    Receiving: 'Hi!'
    Sending 'Hi!'
    Receiving: 'Hi!'
    Chat Server Project & Tutorial | WiFi-remote-control sailboat (building) | Joke Thread
    “Rational thinkers deplore the excesses of democracy; it abuses the individual and elevates the mob. The death of Socrates was its finest fruit.”
    Use XXX in a comment to flag something that is bogus but works. Use FIXME to flag something that is bogus and broken. Use TODO to leave yourself reminders. Calling a program finished before all these points are checked off is lazy.
    -Partial Credit: Sun

    If I ask you to redescribe your problem, it's because when you describe issues in detail, you often get a *click* and you suddenly know the solutions.
    Ches Koblents
  26. #14
  27. Contributing User
    Devshed Novice (500 - 999 posts)

    Join Date
    Feb 2006
    Location
    True
    Posts
    873
    Rep Power
    106
    Wow!! I haven't been here in a while but this seems brilliant, I haven't been able to look through it fully yet, but this is great, good job man!
  28. #15
  29. No Profile Picture
    Contributing User
    Devshed Novice (500 - 999 posts)

    Join Date
    Jul 2005
    Location
    Bay Area, California
    Posts
    841
    Rep Power
    1682
    See, I did think something was wrong when setting core != max and it wasn't working. Thanks for that, I will add that to the next piece of the puzzle.
    Yep, its definately not obvious and I had to experiment with it to understand what was going on. If I remember correctly, the flaw is due to how the executor determines when to start a new thread. A thread is only added when it cannot offer more elements to the work-queue which means it must be full before a new thread is created. The cached executor uses a SynchronousQueue to force a handoff between two threads. An offer will always fail, hence the creation.

    Ideally, you'd want an unbounded work-queue (LinkedBlockingQueue) and for threads to be created as needed. Instead, it adds work forever and thus never grows past the core size (worst case, at zero). An bounded work-queue is okay (SynchronousQueue or ArrayBlockingQueue), but you need to decide how to handle the rejection. Generally caller-runs is acceptable, but if not you need what feels like a hack.

    I guess its not so much of a flaw but breaks the principle of least surprise, which requires you to dig into the source to fully understand why it won't work.
Page 1 of 3 123 Last
  • Jump to page:

IMN logo majestic logo threadwatch logo seochat tools logo