Multi-Client/Server GUI Application
Overview As part of the TCP Client-Server Laboratory Project you are to build a GUI (Form) client- server in which the IP address of the server can be specified by the user interactively through the Form. In this lecture we will review the development of Windows Form client- server applications based on tutorials by Andrew Pociu and presented in geekpedia. The principal features of these apps are: Clients can specify the IP address of the server. Multiple clients can connect to the same server. Each client can specify a name to be placed at the front of sent messages. All connected clients can see messages sent from every client. Multiple clients can send messages to the server concurrently.
Building the Client txtIP txtUser btnConnect btnSend txtLog txtSend
using System.Net; using System.Net.Sockets; using System.IO; using System.Threading; Since we will be making use of networking, streaming and threading objects, start by adding the following using statements: Referencing the Standard Classes
// Will hold the user name private string UserName = "Unknown"; private StreamWriter swSender; private StreamReader srReceiver; private TcpClient tcpServer; // Needed to update the form with messages from another thread private delegate void UpdateLogCallback(string strMessage); // Needed to set the form to a "disconnected" state from another thread private delegate void CloseConnectionCallback(string strReason); private Thread thrMessaging; private IPAddress ipAddr; private bool Connected; Declaring Objects Most of our objects can be private since we will not be using them in another class. Our object list includes (1) a stream reader and writer to send and receive messages, (2) a TcpClient object to connect to the server, (3) a delegate to be able to update the form with messages from another thread, (4) another delegate to be able to disconnect the form from another thread, and (5) objects to hold the UserName and ipAddr.
The btnConnect Click Event private void btnConnect_Click(object sender, EventArgs e) { // If we are not currently connected but awaiting to connect if (Connected == false) { // Initialize the connection InitializeConnection(); } else // We are connected, thus disconnect { CloseConnection("Disconnected at user's request."); } What happen's when you click the btnConnect button? What happen's when you click it again?
private void InitializeConnection() { ipAddr = IPAddress.Parse(txtIp.Text); //convert IP text to object tcpServer = new TcpClient(); // Start TCP connections to server tcpServer.Connect(ipAddr, 1986); Connected = true; // track whether we are connected or not UserName = txtUser.Text; // Prepare the form. txtIp.Enabled = false; // Disable & enable appropriate fields txtUser.Enabled = false; txtMessage.Enabled = true; btnSend.Enabled = true; btnConnect.Text = "Disconnect"; // Send the desired username to the server swSender = new StreamWriter(tcpServer.GetStream()); swSender.WriteLine(txtUser.Text); swSender.Flush(); // Start the thread for receiving messages and further communication thrMessaging = new Thread(new ThreadStart(ReceiveMessages)); thrMessaging.Start(); } The InitializeConnection( ) Method
private void ReceiveMessages() { srReceiver = new StreamReader(tcpServer.GetStream()); // from server string ConResponse = srReceiver.ReadLine(); if (ConResponse[0] == '1') // if 1st char = 1 connect was successful { // Update the form to tell it we are now connected this.Invoke(new UpdateLogCallback(this.UpdateLog), new object[] { "Connected Successfully!" }); } else // if 1st char != 1, the connection was unsuccessful { string Reason = "Not Connected: "; // reason starts at 3rd char Reason += ConResponse.Substring(2, ConResponse.Length - 2); this.Invoke(new CloseConnectionCallback(this.CloseConnection), new object[] { Reason }); return; // exit the method } while (Connected) // while connected read incoming lines from server { // Show the messages in the log TextBox this.Invoke(new UpdateLogCallback(this.UpdateLog), new object[] { srReceiver.ReadLine() }); } ReceiveMessages( ) Method
more on ReceiveMessages( ) The this.Invoke( ) calls tell the form to update itself. We can't directly update the form elements ourselves from this method because it's in a separate thread (remember we called it using ThreadStart( )) and cross-thread operations are illegal. Finally, the while (Connected) loop keeps calling the srReceiver.ReadLine( ) method which checks for incoming messages from the server. Next comes the method that we kept calling using this.Invoke() - all it does is to update the txtLog TextBox with the latest message: // called from a different thread in order to update the log TextBox private void UpdateLog(string strMessage) { // Append text also scrolls the TextBox to the bottom each time txtLog.AppendText(strMessage + "\r\n"); }
So far we've seen how to receive messages from the server, but nothing about how to send them. When do we want to send a message? When the Send button is clicked or when the Enter key is pressed while txtMessage has the focus. This should be hooked up to the Click event of the btnSend button: // We want to send the message when the Send button is clicked private void btnSend_Click(object sender, EventArgs e) { SendMessage(); } And this needs to be hooked up to the KeyPress event of txtMessage: // But we also want to send the message once Enter is pressed private void txtMessage_KeyPress(object sender, KeyPressEventArgs e) { // If the key is Enter if (e.KeyChar == (char)13) { SendMessage(); } Two Ways to Send a Message
Both btnSend and a Carriage Return invokes the SendMessage( ) method. // Sends the message typed in to the server private void SendMessage() { if (txtMessage.Lines.Length >= 1) { swSender.WriteLine(txtMessage.Text); swSender.Flush(); txtMessage.Lines = null; } txtMessage.Text = ""; } SendMessage( ) Method
// Closes a current connection private void CloseConnection(string Reason) { // Show the reason why the connection is ending txtLog.AppendText(Reason + "\r\n"); // Enable and disable the appropriate controls on the form txtIp.Enabled = true; txtUser.Enabled = true; txtMessage.Enabled = false; btnSend.Enabled = false; btnConnect.Text = "Connect"; // Close the objects Connected = false; swSender.Close(); srReceiver.Close(); tcpServer.Close(); } CloseConnection( ) Method
public Form1() { // On application exit, don't forget to disconnect first Application.ApplicationExit += new EventHandler(OnApplicationExit); InitializeComponent(); } // The event handler for application exit public void OnApplicationExit(object sender, EventArgs e) { if (Connected == true) { // Closes the connections, streams, etc. Connected = false; swSender.Close(); srReceiver.Close(); tcpServer.Close(); } An Event Handler is Needed for the Form This completes the Chat Client...
Building the Chat Server The Chat Server application will hold information on all the connected clients, await for messages from each and send incoming messages to all. Since the server does not initiate messages there is no need for a txtMessage textbox or Send Button. txtIP txtLog btnListen
using System.Net; using System.Net.Sockets; using System.IO; using System.Threading; As with the Chat Client, the Server will be making use of networking, streaming and threading objects, start by adding the following using statements: Referencing the Standard Classes
private delegate void UpdateStatusCallback(string strMessage); For the Chat Server we need to create only one object, a delegate to handle the messages to and from the Clients. This will be used to update the txtLog TextBox from another thread : Declaring Object(s)
private void btnListen_Click(object sender, EventArgs e) { // Parse the server's IP address out of the TextBox IPAddress ipAddr = IPAddress.Parse(txtIp.Text); // Create a new instance of the ChatServer object ChatServer mainServer = new ChatServer(ipAddr); // Hook the StatusChanged event handler to mainServer_StatusChanged ChatServer.StatusChanged += new StatusChangedEventHandler(mainServer_StatusChanged); // Start listening for connections mainServer.StartListening(); // Show that we started to listen for connections txtLog.AppendText("Monitoring for connections...\r\n"); } The btnListen_Click( ) Method
public void mainServer_StatusChanged(object sender, StatusChangedEventArgs e) { // Call the method that updates the form this.Invoke(new UpdateStatusCallback(this.UpdateStatus), new object[] { e.EventMessage }); } private void UpdateStatus(string strMessage) { // Updates the log with the message txtLog.AppendText(strMessage + "\r\n"); } mainServer_StatusChange( ) & UpdateStatus( ) In the btnListen_Click( ) method we instantiated a new StatusChanged event handler. This method, named mainServer_StatusChanged( ) calls the method that updates the Chat Server form. The method UpdateStatus( ), which is called by mainServer_ StatusChanged( ) actually changes the contents of the txtLog textbox. This completes the Chat Server Form1 code...
The ChatServer.cs Class Now we have to create a separate class. This is done within the.NET IDE in the followig manner: (1) Inside the Solution Explorer panel, right-click the Solution 'ChatServer' 1 Project (or whatever name you have given your project). (2) Select Add..New Item in the drop-down menu and sub-menu that appears (3) Choose Visual C# Class from the list of types of objects and name it ChatServer.cs (same name as project is OK). (4) Click the Add Button to create the shell for this new class.
using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.IO; using System.Threading; using System.Collections; This class needs the following references to System defined classes included in its project header. Referencing the Standard Classes
// Holds the arguments for the StatusChanged event public class StatusChangedEventArgs : EventArgs { // The argument we're interested in is a message describing the event private string EventMsg; // Property for retrieving and setting the event message public string EventMessage { get { return EventMsg; } set { EventMsg = value; } // Constructor for setting the event message public StatusChangedEventArgs(string strEventMsg) { EventMsg = strEventMsg; } Get( ) & Set( ) for the Event Arguments
// delegate needed to specify parameters we're passing with event public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e); Delegate for the Event Handler
class ChatServer { // This hash table stores users and connections (browsable by user) public static Hashtable htUsers = new Hashtable(30); // 30 users max // hash table stores connections and users (browsable by connection) public static Hashtable htConnections = new Hashtable(30); // 30 users max private IPAddress ipAddress; //stores IP address passed to it private TcpClient tcpClient; // this event notifies the form when a user connects, disconnects, etc.. public static event StatusChangedEventHandler StatusChanged; private static StatusChangedEventArgs e; // constructor sets IP address to one retrieved by instantiating object public ChatServer(IPAddress address) { ipAddress = address; } private Thread thrListener; //thread to hold the connection listener private TcpListener tlsClient; //TCP listening object bool ServRunning = false; //tells while loop to continue monitoring The ChatServer Class There are two hash tables defined for the client list because we will need to search by both connections and by users at different times in the operation of the application.
// Add the user to the hash tables public static void AddUser(TcpClient tcpUser, string strUsername) { // first add username and associated connection to hash tables ChatServer.htUsers.Add(strUsername, tcpUser); ChatServer.htConnections.Add(tcpUser, strUsername); // report new connection to all other users and to server form SendAdminMessage(htConnections[tcpUser] + " has joined us"); } // remove this user from the hash tables public static void RemoveUser(TcpClient tcpUser) { if (htConnections[tcpUser] != null) //if user is there { // show the info and tell the other users about disconnection SendAdminMessage(htConnections[tcpUser] + " has left us"); // Remove the user from the hash table ChatServer.htUsers.Remove(ChatServer.htConnections[tcpUser]); ChatServer.htConnections.Remove(tcpUser); } AddUser( ) & RemoveUser( ) Methods The AddUser() method adds a new user to the hash tables, and therefore to our list of connected chat clients. The RemoveUser() method does the opposite.
// this is called to raise the StatusChanged event public static void OnStatusChanged(StatusChangedEventArgs e) { StatusChangedEventHandler statusHandler = StatusChanged; if (statusHandler != null) { // Invoke the delegate statusHandler(null, e); } OnStatusChanged( ) The OnStatusChanged will fire the StatusChanged event, which is handled inside Form1.cs. This is a way of updating the form with the latest message from inside the ChatServer object.
for (int i = 0; i < tcpClients.Length; i++) //loop through client list { try //try sending a message to each { if (Message.Trim() == "" || tcpClients[i] == null) { continue; //if message blank or connection null, break out } swSenderSender = new StreamWriter(tcpClients[i].GetStream()); swSenderSender.WriteLine("Administrator: " + Message); swSenderSender.Flush(); swSenderSender = null; //send message to current user in loop } catch // if problem, the user is gone, remove him { RemoveUser(tcpClients[i]); } Send AdminMessage( ) // Send administrative messages public static void SendAdminMessage(string Message) { StreamWriter swSenderSender; // first, show in our application who says what e = new StatusChangedEventArgs("Administrator: " + Message); OnStatusChanged(e); // create an array of TCP clients, size = num of users we have TcpClient[] tcpClients = new TcpClient[ChatServer.htUsers.Count]; // copy the TcpClient objects into the array ChatServer.htUsers.Values.CopyTo(tcpClients, 0);. The SendAdminMessage sends an administrative message to all connected clients, by looping through the hash table, attempting to send each the message. If the message doesn't get through, they are assumed to be disconnected and they are removed.
// send messages from one user to all the others public static void SendMessage(string From, string Message) { StreamWriter swSenderSender; e = new StatusChangedEventArgs(From + " says: " + Message); OnStatusChanged(e); //show who says what // create array of TCP clients, size = num of users we have TcpClient[] tcpClients = new TcpClient[ChatServer.htUsers.Count]; ChatServer.htUsers.Values.CopyTo(tcpClients, 0); //copy object to array for (int i = 0; i < tcpClients.Length; i++) //loop through client list { try //try sending a message to each { if (Message.Trim() == "" || tcpClients[i] == null) { continue; //if blank or if connection is null, break out } // send message to the current user in the loop swSenderSender = new StreamWriter(tcpClients[i].GetStream()); swSenderSender.WriteLine(From + " says: " + Message); swSenderSender.Flush(); swSenderSender = null; } catch // if problem, user is gone, remove him { RemoveUser(tcpClients[i]); } SendMessage( ) Method SendMessage( ) method sends a message from a specific chat client to all the others.
public void StartListening() { IPAddress ipaLocal = ipAddress; //get IP of 1st device tlsClient = new TcpListener(1986); //create listener object tlsClient.Start(); //start listener and listen for connections ServRunning = true; //check for true before checking for connections thrListener = new Thread(KeepListening); //start new host thread thrListener.Start(); } private void KeepListening() { while (ServRunning == true) //while server is running... { tcpClient = tlsClient.AcceptTcpClient(); //accept pending conn Connection newConnection = new Connection(tcpClient); //new conn } StartListening( ) & KeepListening( ) Methods StartListening( ) method is the one we called inside Form1, and it initiates all other events. It defines the first needed objects and starts a new thread that keeps listening for connections, which is the KeepListening( ) method.
The Connection Class Looking inside KeepListening() method you will see that a new instance of an object of type Connection is created for each connected client (user). The Connection Class implements these objects. class Connection //this class handles connections, an instance for each user { TcpClient tcpClient; private Thread thrSender; //thread to send info to the client private StreamReader srReceiver; private StreamWriter swSender; private string currUser; private string strResponse; public Connection(TcpClient tcpCon) //constructor of the class { tcpClient = tcpCon; thrSender = new Thread(AcceptClient); //thread that accepts client thrSender.Start(); //thread to call the AcceptClient() method } private void CloseConnection() { tcpClient.Close(); //closes currently open objects srReceiver.Close(); swSender.Close(); }
private void AcceptClient() //occurs when new client is accepted { srReceiver = new System.IO.StreamReader(tcpClient.GetStream()); swSender = new System.IO.StreamWriter(tcpClient.GetStream()); currUser = srReceiver.ReadLine(); //read account info from client if (currUser != "") //response from client!!! { if (ChatServer.htUsers.Contains(currUser) == true) { // store user name in hash table swSender.WriteLine("0|This username already exists."); swSender.Flush(); // 0 means 'not connected' CloseConnection(); return; } else if (currUser == "Administrator") { swSender.WriteLine("0|This username is reserved."); swSender.Flush(); // 0 means 'not connected' CloseConnection(); return; } else { swSender.WriteLine("1"); swSender.Flush(); // 1 means connected successfully ChatServer.AddUser(tcpClient, currUser); } //add user to has tables and start listening } else { CloseConnection(); return; } AcceptClient( ) Method
try { // Keep waiting for a message from the user while ((strResponse = srReceiver.ReadLine()) != "") { // If it's invalid, remove the user if (strResponse == null) { ChatServer.RemoveUser(tcpClient); } else { // Otherwise send the message to all the other users ChatServer.SendMessage(currUser, strResponse); } catch { // If anything went wrong with this user, disconnect him ChatServer.RemoveUser(tcpClient); } The the constructor initializes the TcpClient object. The CloseConnection( ) method is called when we want to get rid of a currently connected client The AcceptClient( ) method checks to make sure the username is valid and if all is fine, it adds the user to the hash tables. Otherwise it removes the user. AcceptClient( ) Method concluded
References: