Client/Server Interaction with Stream-Socket Connections

Client Server Interaction with Stream Socket Connections

The applications in Fig. 23.1 and Fig. 23.2 use the classes and techniques discussed in the previous two sections to construct a simple client/server chat application. The server waits for a client's request to make a connection. When a client application connects to the server, the server application sends an array of bytes to the client, indicating that the connection was successful. The client then displays a message notifying the user that a connection has been established.

Figure 23.1. Server portion of a client/server stream-socket connection.

(This item is displayed on pages 1237 - 1240 in the print version)

1 // Fig. 23.1: ChatServer.cs 2 // Set up a server that will receive a connection from a client, send a 3 // string to the client, chat with the client and close the connection. 4 using System; 5 using System.Windows.Forms; 6 using System.Threading; 7 using System.Net; 8 using System.Net.Sockets; 9 using System.IO; 10 11 public partial class ChatServerForm : Form 12 { 13 public ChatServerForm() 14 { 15 InitializeComponent(); 16 } // end constructor 17 18 private Socket connection; // Socket for accepting a connection 19 private Thread readThread; // Thread for processing incoming messages 20 private NetworkStream socketStream; // network data stream 21 private BinaryWriter writer; // facilitates writing to the stream 22 private BinaryReader reader; // facilitates reading from the stream 23 24 // initialize thread for reading 25 private void ChatServerForm_Load( object sender, EventArgs e ) 26 { 27 readThread = new Thread( new ThreadStart( RunServer ) ); 28 readThread.Start(); 29 } // end method CharServerForm_Load 30 31 // close all threads associated with this application 32 private void ChatServerForm_FormClosing( object sender, 33 FormClosingEventArgs e ) 34 { 35 System.Environment.Exit( System.Environment.ExitCode ); 36 } // end method CharServerForm_FormClosing 37 38 // delegate that allows method DisplayMessage to be called 39 // in the thread that creates and maintains the GUI 40 private delegate void DisplayDelegate( string message ); 41 42 // method DisplayMessage sets displayTextBox's Text property 43 // in a thread-safe manner 44 private void DisplayMessage( string message ) 45 { 46 // if modifying displayTextBox is not thread safe 47 if ( displayTextBox.InvokeRequired ) 48 { 49 // use inherited method Invoke to execute DisplayMessage 50 // via a delegate 51 Invoke( new DisplayDelegate( DisplayMessage ), 52 new object[] { message } ); 53 } // end if 54 else // OK to modify displayTextBox in current thread 55 displayTextBox.Text += message; 56 } // end method DisplayMessage 57 58 // delegate that allows method DisableInput to be called 59 // in the thread that creates and maintains the GUI 60 private delegate void DisableInputDelegate( bool value ); 61 62 // method DisableInput sets inputTextBox's ReadOnly property 63 // in a thread-safe manner 64 private void DisableInput( bool value ) 65 { 66 // if modifying inputTextBox is not thread safe 67 if ( inputTextBox.InvokeRequired ) 68 { 69 // use inherited method Invoke to execute DisableInput 70 // via a delegate 71 Invoke( new DisableInputDelegate( DisableInput ), 72 new object[] { value } ); 73 } // end if 74 else // OK to modify inputTextBox in current thread 75 inputTextBox.ReadOnly = value; 76 } // end method DisableInput 77 78 // send the text typed at the server to the client 79 private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) 80 { 81 // send the text to the client 82 try 83 { 84 if ( e.KeyCode == Keys.Enter && inputTextBox.ReadOnly == false ) 85 { 86 writer.Write( "SERVER>>> " + inputTextBox.Text ); 87 displayTextBox.Text += " SERVER>>> " + inputTextBox.Text; 88 89 // if the user at the server signaled termination 90 // sever the connection to the client 91 if ( inputTextBox.Text == "TERMINATE" ) 92 connection.Close(); 93 94 inputTextBox.Clear(); // clear the user's input 95 } // end if 96 } // end try 97 catch ( SocketException ) 98 { 99 displayTextBox.Text += " Error writing object"; 100 } // end catch 101 } // end method inputTextBox_KeyDown 102 103 // allows a client to connect; displays text the client sends 104 public void RunServer() 105 { 106 TcpListener listener; 107 int counter = 1; 108 109 // wait for a client connection and display the text 110 // that the client sends 111 try 112 { 113 // Step 1: create TcpListener 114 IPAddress local = IPAddress.Parse( "127.0.0.1" ); 115 listener = new TcpListener( local, 50000 ); 116 117 // Step 2: TcpListener waits for connection request 118 listener.Start(); 119 120 // Step 3: establish connection upon client request 121 while ( true ) 122 { 123 DisplayMessage( "Waiting for connection " ); 124 125 // accept an incoming connection 126 connection = listener.AcceptSocket(); 127 128 // create NetworkStream object associated with socket 129 socketStream = new NetworkStream( connection ); 130 131 // create objects for transferring data across stream 132 writer = new BinaryWriter( socketStream ); 133 reader = new BinaryReader( socketStream ); 134 135 DisplayMessage( "Connection " + counter + " received. " ); 136 137 // inform client that connection was successfull 138 writer.Write( "SERVER>>> Connection successful" ); 139 140 DisableInput( false ); // enable inputTextBox 141 142 string theReply = ""; 143 144 // Step 4: read string data sent from client 145 do 146 { 147 try 148 { 149 // read the string sent to the server 150 theReply = reader.ReadString(); 151 152 // display the message 153 DisplayMessage( " " + theReply ); 154 } // end try 155 catch ( Exception ) 156 { 157 // handle exception if error reading data 158 break; 159 } // end catch 160 } while ( theReply != "CLIENT>>> TERMINATE" && 161 connection.Connected ); 162 163 DisplayMessage( " User terminated connection " ); 164 165 // Step 5: close connection 166 writer.Close(); 167 reader.Close(); 168 socketStream.Close(); 169 connection.Close(); 170 171 DisableInput( true ); // disable InputTextBox 172 counter++; 173 } // end while 174 } // end try 175 catch ( Exception error ) 176 { 177 MessageBox.Show( error.ToString() ); 178 } // end catch 179 } // end method RunServer 180 } // end class ChatServerForm

Figure 23.2. Client portion of a client/server stream-socket connection.

(This item is displayed on pages 1242 - 1246 in the print version)

1 // Fig. 23.2: ChatClient.cs 2 // Set up a client that will send information to and 3 // read information from a server. 4 using System; 5 using System.Windows.Forms; 6 using System.Threading; 7 using System.Net.Sockets; 8 using System.IO; 9 10 public partial class ChatClientForm : Form 11 { 12 public ChatClientForm() 13 { 14 InitializeComponent(); 15 } // end constructor 16 17 private NetworkStream output; // stream for receiving data 18 private BinaryWriter writer; // facilitates writing to the stream 19 private BinaryReader reader; // facilitates reading from the stream 20 private Thread readThread; // Thread for processing incoming messages 21 private string message = ""; 22 23 // initialize thread for reading 24 private void ChatClientForm_Load( object sender, EventArgs e ) 25 { 26 readThread = new Thread( new ThreadStart( RunClient ) ); 27 readThread.Start(); 28 } // end method ChatClientForm_Load 29 30 // close all threads associated with this application 31 private void ChatClientForm_FormClosing( object sender, 32 FormClosingEventArgs e ) 33 { 34 System.Environment.Exit( System.Environment.ExitCode ); 35 } // end method ChatClientForm_FormClosing 36 37 // delegate that allows method DisplayMessage to be called 38 // in the thread that creates and maintains the GUI 39 private delegate void DisplayDelegate( string message ); 40 41 // method DisplayMessage sets displayTextBox's Text property 42 // in a thread-safe manner 43 private void DisplayMessage( string message ) 44 { 45 // if modifying displayTextBox is not thread safe 46 if ( displayTextBox.InvokeRequired ) 47 { 48 // use inherited method Invoke to execute DisplayMessage 49 // via a delegate 50 Invoke( new DisplayDelegate( DisplayMessage ), 51 new object[] { message } ); 52 } // end if 53 else // OK to modify displayTextBox in current thread 54 displayTextBox.Text += message; 55 } // end method DisplayMessage 56 57 // delegate that allows method DisableInput to be called 58 // in the thread that creates and maintains the GUI 59 private delegate void DisableInputDelegate( bool value ); 60 61 // method DisableInput sets inputTextBox's ReadOnly property 62 // in a thread-safe manner 63 private void DisableInput( bool value ) 64 { 65 // if modifying inputTextBox is not thread safe 66 if ( inputTextBox.InvokeRequired ) 67 { 68 // use inherited method Invoke to execute DisableInput 69 // via a delegate 70 Invoke( new DisableInputDelegate( DisableInput ), 71 new object[] { value } ); 72 } // end if 73 else // OK to modify inputTextBox in current thread 74 inputTextBox.ReadOnly = value; 75 } // end method DisableInput 76 77 // sends text the user typed to server 78 private void inputTextBox_KeyDown( object sender, KeyEventArgs e ) 79 { 80 try 81 { 82 if ( e.KeyCode == Keys.Enter && inputTextBox.ReadOnly == false ) 83 { 84 writer.Write( "CLIENT>>> " + inputTextBox.Text ); 85 displayTextBox.Text += " CLIENT>>> " + inputTextBox.Text; 86 inputTextBox.Clear(); 87 } // end if 88 } // end try 89 catch ( SocketException ) 90 { 91 displayTextBox.Text += " Error writing object"; 92 } // end catch 93 } // end method inputTextBox_KeyDown 94 95 // connect to server and display server-generated text 96 public void RunClient() 97 { 98 TcpClient client; 99 100 // instantiate TcpClient for sending data to server 101 try 102 { 103 DisplayMessage( "Attempting connection " ); 104 105 // Step 1: create TcpClient and connect to server 106 client = new TcpClient(); 107 client.Connect( "127.0.0.1", 50000 ); 108 109 // Step 2: get NetworkStream associated with TcpClient 110 output = client.GetStream(); 111 112 // create objects for writing and reading across stream 113 writer = new BinaryWriter( output ); 114 reader = new BinaryReader( output ); 115 116 DisplayMessage( " Got I/O streams " ); 117 DisableInput( false ); // enable inputTextBox 118 119 // loop until server signals termination 120 do 121 { 122 // Step 3: processing phase 123 try 124 { 125 // read message from server 126 message = reader.ReadString(); 127 DisplayMessage( " " + message ); 128 } // end try 129 catch ( Exception ) 130 { 131 // handle exception if error in reading server data 132 System.Environment.Exit( System.Environment.ExitCode ); 133 } // end catch 134 } while ( message != "SERVER>>> TERMINATE" ); 135 136 // Step 4: close connection 137 writer.Close(); 138 reader.Close(); 139 output.Close(); 140 client.Close(); 141 142 Application.Exit(); 143 } // end try 144 catch ( Exception error ) 145 { 146 // handle exception if error in establishing connection 147 MessageBox.Show( error.ToString(), "Connection Error", 148 MessageBoxButtons.OK, MessageBoxIcon.Error ); 149 System.Environment.Exit( System.Environment.ExitCode ); 150 } // end catch 151 } // end method RunClient 152 } // end class ChatClientForm

(a)

(b)

(c)

(d)

(e)

(f)

(g)

Both the client and the server applications contain TextBoxes that enable users to type messages and send them to the other application. When either the client or the server sends the message "TERMINATE," the connection between the client and the server terminates. The server then waits for another client to request a connection. Figure 23.1 and Fig. 23.2 provide the code for classes Server and Client, respectively. Figure 23.2 also contains screen captures displaying the execution between the client and the server.

ChatServerForm Class

We begin by discussing class ChatServerForm (Fig. 23.1). In the constructor, line 27 creates a THRead that will accept connections from clients. The ThreadStart delegate object that is passed as the constructor's argument specifies which method the Thread should execute. Line 28 starts the Thread, which uses the ThreadStart delegate to invoke method RunServer (lines 104179). This method initializes the server to receive connection requests and process connections. Line 115 instantiates a TcpListener object to listen for a connection request from a client at port 50000 (Step 1). Line 118 then calls TcpListener method Start, which causes the TcpListener to begin waiting for requests (Step 2).

Accepting the Connection and Establishing the Streams

Lines 121173 declare an infinite loop that begins by establishing the connection requested by the client (Step 3). Line 126 calls method AcceptSocket of the TcpListener object, which returns a Socket upon successful connection. The thread in which method AcceptSocket is called blocks (i.e., stops executing) until a connection is established. The returned Socket object manages the connection. Line 129 passes this Socket object as an argument to the constructor of a NetworkStream object, which provides access to streams across a network. In this example, the NetworkStream object uses the streams of the specified Socket. Lines 132133 create instances of the BinaryWriter and BinaryReader classes for writing and reading data. We pass the NetworkStream object as an argument to each constructorBinaryWriter can write bytes to the NetworkStream, and BinryReader can read bytes from NetworkStream. Line 135 calls DisplayMessage, indicating that a connection was received. Next, we send a message to the client indicating that the connection was received. BinaryWriter method Write has many overloaded versions that write data of various types to a stream. Line 138 uses method Write to send to the client a string notifying the user of a successful connection. This completes Step 3.

Receiving Messages from the Client

Next, we begin the processing phase (Step 4). Lines 145161 declare a do...while statement that executes until the server receives a message indicating connection termination (i.e., CLIENT>>> TERMINATE). Line 150 uses BinaryReader method ReadString to read a string from the stream. Method ReadString blocks until a string is read. This is the reason that we execute method RunServer in a separate Thread (created at lines 2728, when the Form loads). This THRead ensures that our Server application's user can continue to interact with the GUI to send messages to the client, even when this thread is blocked while awaiting a message from the client.

Modifying GUI Controls from Separate Threads

Windows Form controls are not thread safea control that is modified from multiple threads is not guaranteed to be modified correctly. The Visual Studio 2005 Documentation[1] recommends that only the thread which created the GUI should modify the controls. Class Control provides method Invoke to help ensure this. Invoke takes two argumentsa delegate representing a method that will modify the GUI and an array of objects representing the parameters of the method. At some point after Invoke is called, the thread that originally created the GUI will (when it's not executing any other code) execute the method represented by the delegate, passing the contents of the object array as the method's arguments.

[1] The MSDN article "How to: Make Cross-Thread Calls to Windows Forms Controls" can be found at msdn2.microsoft.com/library/ms171728(en-us,vs.80).aspx.

Line 40 declares a delegate type named DisplayDelegate, which represents methods that take a string argument and do not return a value. Method DisplayMessage (lines 4456) meets those requirementsit receives a string parameter named message and does not return a value. The if statement in line 47 tests displayTextBox's InvokeRequired property (inherited from class Control), which returns TRue if the current thread is not allowed to modify this control directly and returns false otherwise. If the current thread executing method DisplayMessage is not the thread that created the GUI, then the if condition evaluates to true and lines 5152 call method Invoke, passing to it a new DisplayDelegate representing the method DisplayMessage itself and a new object array consisting of the string argument message. This causes the thread that created the GUI to call method DisplayMessage again at a later time with the same string argument as the original call. When that call occurs from the thread that created the GUI, the method is allowed to modify displayTextBox directly, so the else body (line 55) executes and appends message to displayTextBox's Text property.

Lines 6076 provide a delegate definition, DisableInputDelegate, and a method, DisableInput, to allow any thread to modify the ReadOnly property of inputTextBox using the same techniques. A thread calls DisableInput with a bool argument (TRue to disable; false to enable). If DisableInput is not allowed to modify the control from the current thread, DisableInput calls method Invoke. This causes the thread that created the GUI to call DisableInput at a later time and set inputTextBox.ReadOnly to the value of the bool argument.

Terminating the Connection with the Client

When the chat is complete, lines 166169 close the BinaryWriter, BinaryReader, NetworkStream and Socket (Step 5) by invoking their respective Close methods. The server then waits for another client connection request by returning to the beginning of the while loop (line 121).

Sending Messages to the Client

When the server application's user enters a string in the TextBox and presses the Enter key, event handler inputTextBox_KeyDown (lines 79101) reads the string and sends it via method Write of class BinaryWriter. If a user terminates the server application, line 92 calls method Close of the Socket object to close the connection.

Terminating the Server Application

Lines 3236 define event handler ChatServerForm_FormClosing for the FormClosing event. The event closes the application and calls method Exit of class Environment with parameter ExitCode to terminate all threads. Method Exit of class Environment closes all threads associated with the application.

ChatClientForm Class

Figure 23.2 lists the code for the ChatClientForm class. Like the ChatServerForm object, the ChatClientForm object creates a Thread (lines 2627) in its constructor to handle all incoming messages. ChatClientForm method RunClient (lines 96151) connects to the ChatServerForm, receives data from the ChatServerForm and sends data to the ChatServerForm. Lines 106107 instantiate a TcpClient object, then call its Connect method to establish a connection (Step 1). The first argument to method Connect is the name of the serverin our case, the server's name is "localhost", meaning that the server is located on the same machine as the client. The localhost is also known as the loopback IP address and is equivalent to the IP address 127.0.0.1. This value sends the data transmission back to the sender's IP address. [Note:We chose to demonstrate the client/server relationship by connecting between programs that are executing on the same computer (localhost). Normally, this argument would contain the Internet address of another computer.] The second argument to method Connect is the server port number. This number must match the port number at which the server waits for connections.

The ChatClientForm uses a NetworkStream to send data to and receive data from the server. The client obtains the NetworkStream in line 110 through a call to TcpClient method GetStream (Step 2). The do...while statement in lines 120134 loops until the client receives the connection-termination message (SERVER>>> TERMINATE). Line 126 uses BinaryReader method ReadString to obtain the next message from the server (Step 3). Line 127 displays the message, and lines 137140 close the BinaryWriter, BinaryReader, NetworkStream and TcpClient objects (Step 4).

Lines 3975 declare DisplayDelegate, DisplayMessage, DisableInputDelegate and DisableInput just as in lines 4076 of Fig. 23.1. These once again are used to ensure that the GUI is modified by only the thread that created the GUI controls.

When the user of the client application enters a string in the TextBox and presses the Enter key, event handler inputTextBox_KeyDown (lines 7893) reads the string from the TextBox and sends it via BinaryWriter method Write. Notice here that the ChatServerForm receives a connection, processes it, closes it and waits for the next one. In a real-world application, a server would likely receive a connection, set up the connection to be processed as a separate thread of execution and wait for new connections. The separate threads that process existing connections could then continue to execute while the server concentrates on new connection requests.

Connectionless Client Server Interaction with Datagrams

Категории