23.6. Client/Server Interaction with Stream-Socket Connections Figures 23.1 and 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 a String 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. 1 ' Fig. 23.1: FrmChatServer.vb 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 Imports System.Threading 5 Imports System.Net 6 Imports System.Net.Sockets 7 Imports System.IO 8 9 Public Class FrmChatServer 10 Private connection As Socket ' Socket for accepting a connection 11 Private readThread As Thread ' Thread for processing incoming messages 12 Private socketStream As NetworkStream ' network data stream 13 Private writer As BinaryWriter ' facilitates writing to the stream 14 Private reader As BinaryReader ' facilitates reading from the stream 15 16 ' initialize thread for reading 17 Private Sub FrmChatServer_Load(ByVal sender As System.Object, _ 18 ByVal e As System.EventArgs) Handles MyBase.Load 19 20 readThread = New Thread(New ThreadStart(AddressOf RunServer)) 21 readThread.Start() 22 End Sub ' FrmChatServer_Load 23 24 ' close all threads associated with this application 25 Private Sub FrmChatServer_FormClosing(ByVal sender As System.Object, _ 26 ByVal e As System.Windows.Forms.FormClosingEventArgs) _ 27 Handles MyBase.FormClosing 28 29 System.Environment.Exit(System.Environment.ExitCode) 30 End Sub ' FrmChatServer_FormClosing 31 32 ' Delegate that allows method DisplayMessage to be called 33 ' in the thread that creates and maintains the GUI 34 Private Delegate Sub DisplayDelegate(ByVal message As String) 35 36 ' method DisplayMessage sets txtDisplay's Text property 37 ' in a thread-safe manner 38 Private Sub DisplayMessage(ByVal message As String) 39 ' if modifying txtDisplay is not thread safe 40 If txtDisplay.InvokeRequired Then 41 ' use inherited method Invoke to execute DisplayMessage 42 ' via a Delegate 43 Invoke(New DisplayDelegate(AddressOf DisplayMessage), _ 44 New Object() {message}) 45 ' OK to modify txtDisplay in current thread 46 Else 47 txtDisplay.Text &= message 48 End If 49 End Sub ' DisplayMessage 50 51 ' Delegate that allows method DisableInput to be called 52 ' in the thread that creates and maintains the GUI 53 Private Delegate Sub DisableInputDelegate(ByVal value As Boolean) 54 55 ' method DisableInput sets txtInput's ReadOnly property 56 ' in a thread-safe manner 57 Private Sub DisableInput(ByVal value As Boolean) 58 ' if modifying txtInput is not thread safe 59 If txtInput.InvokeRequired Then 60 ' use inherited method Invoke to execute DisableInput 61 ' via a Delegate 62 Invoke(New DisableInputDelegate(AddressOf DisableInput), _ 63 New Object() {value}) 64 ' OK to modify txtInput in current thread 65 Else 66 txtInput.ReadOnly = value 67 End If 68 End Sub ' DisableInput 69 70 ' send the text typed at the server to the client 71 Private Sub txtInput_KeyDown(ByVal sender As System.Object, _ 72 ByVal e As System.Windows.Forms.KeyEventArgs) _ 73 Handles txtInput.KeyDown 74 ' send the text to the client 75 Try 76 If e.KeyCode = Keys.Enter And txtInput.ReadOnly = False Then 77 writer.Write("SERVER>>>" & txtInput.Text) 78 txtDisplay.Text &= vbCrLf & "SERVER>>> " & txtInput.Text 79 80 ' if the user at the server signaled termination 81 ' sever the connection to the client 82 If txtInput.Text = "TERMINATE" Then 83 connection.Close() 84 End If 85 txtInput.Clear() ' clear the userí-s input 86 End If 87 Catch ex As SocketException 88 txtDisplay.Text &= vbCrLf & "Error writing object" 89 End Try 90 End Sub ' txtInput_KeyDown 91 92 ' allows a client to connect; displays text the client sends 93 Public Sub RunServer() 94 Dim listener As TcpListener 95 Dim counter As Integer = 1 96 97 ' wait for a client connection and display the text 98 ' that the client sends 99 Try 100 ' Step 1: create TcpListener 101 Dim local As IPAddress = IPAddress.Parse("localhost") 102 listener = New TcpListener(local, 50000) 103 104 ' Step 2: TcpListener waits for connection request 105 listener.Start() 106 107 ' Step 3: establish connection upon client request 108 While True 109 DisplayMessage("Waiting for connection" & vbCrLf) 110 111 ' accept an incoming connection 112 connection = listener.AcceptSocket() 113 114 ' create NetworkStream object associated with socket 115 socketStream = New NetworkStream(connection) 116 117 ' create objects for transferring data across stream 118 writer = New BinaryWriter(socketStream) 119 reader = New BinaryReader(socketStream) 120 121 DisplayMessage( _ 122 "Connection " & counter & " received." & vbCrLf) 123 124 ' inform client that connection was successfull 125 writer.Write("SERVER>>> Connection successful") 126 127 DisableInput(False)' enable txtInput 128 Dim theReply As String = "" 129 130 ' Step 4: read string data sent from client 131 Do 132 Try 133 ' read the string sent to the server 134 theReply = reader.ReadString() 135 136 ' display the message 137 DisplayMessage(vbCrLf & theReply) 138 Catch ex As Exception 139 ' handle exception if error reading data 140 Exit Do 141 End Try 142 Loop While theReply <> "CLIENT>>> TERMINATE" And _ 143 connection.Connected 144 145 DisplayMessage(vbCrLf & "User terminated connection" & vbCrLf) 146 147 ' Step 5: close connection 148 writer.Close() 149 reader.Close() 150 socketStream.Close() 151 connection.Close() 152 153 DisableInput(True) ' disable txtInput 154 counter += 1 155 End While 156 Catch ex As Exception 157 MessageBox.Show(ex.ToString()) 158 End Try 159 End Sub ' RunServer 160 End Class ' FrmChatServer_Load | Figure 23.2. Client portion of a client/server stream-socket connection. 1 ' Fig. 23.2: FrmChatClient.vb 2 ' Set up a client that will send information to and 3 ' read information from a server. 4 Imports System.Threading 5 Imports System.Net.Sockets 6 Imports System.IO 7 8 Public Class FrmChatClient 9 Private output As NetworkStream ' stream for receiving data 10 Private writer As BinaryWriter ' facilitates writing to the stream 11 Private reader As BinaryReader ' facilitates reading from the stream 12 Private readThread As Thread ' Thread for processing incoming messages 13 Private message As String = "" 14 15 ' initialize thread for reading 16 Private Sub FrmChatClient_Load(ByVal sender As System.Object, _ 17 ByVal e As System.EventArgs) Handles MyBase.Load 18 19 readThread = New Thread(New ThreadStart(AddressOf RunClient)) 20 readThread.Start() 21 End Sub ' FrmChatClient_Load 22 23 ' close all threads associated with this application 24 Private Sub FrmChatClient_FormClosing(ByVal sender As System.Object, _ 25 ByVal e As System.Windows.Forms.FormClosingEventArgs) _ 26 Handles MyBase.FormClosing 27 28 System.Environment.Exit(System.Environment.ExitCode) 29 End Sub ' FrmChatClient_FormClosing 30 31 ' Delegate that allows method DisplayMessage to be called 32 ' in the thread that creates and maintains the GUI 33 Private Delegate Sub DisplayDelegate(ByVal message As String) 34 35 ' method DisplayMessage sets txtDisplay's Text property 36 ' in a thread-safe manner 37 Private Sub DisplayMessage(ByVal message As String) 38 ' if modifying txtDisplay is not thread safe 39 If txtDisplay.InvokeRequired Then 40 ' use inherited method Invoke to execute DisplayMessage 41 ' via a Delegate 42 Invoke(New DisplayDelegate(AddressOf DisplayMessage), _ 43 New Object() {message}) 44 ' OK to modify txtDisplay in current thread 45 Else 46 txtDisplay.Text & = message 47 End If 48 End Sub ' DisplayMessage 49 50 ' Delegate that allows method DisableInput to be called 51 ' in the thread that creates and maintains the GUI 52 Private Delegate Sub DisableInputDelegate(ByVal value As Boolean ) 53 54 ' method DisableInput sets txtInput's ReadOnly property 55 ' in a thread-safe manner 56 Private Sub DisableInput(ByVal value As Boolean) 57 ' if modifying txtInput is not thread safe 58 If txtInput.InvokeRequired Then 59 ' use inherited method Invoke to execute DisableInput 60 ' via a Delegate 61 Invoke(New DisableInputDelegate(AddressOf DisableInput), _ 62 New Object() {value}) 63 ' OK to modify txtInput in current thread 64 Else 65 txtInput.ReadOnly = value 66 End If 67 End Sub ' DisableInput 68 69 ' sends text the user typed to server 70 Private Sub txtInput_KeyDown(ByVal sender As System.Object, _ 71 ByVal e As System.Windows.Forms.KeyEventArgs) _ 72 Handles txtInput.KeyDown 73 74 Try 75 If e.KeyCode = Keys.Enter And txtInput.ReadOnly = False Then 76 writer.Write("CLIENT>>> " & txtInput.Text) 77 txtDisplay.Text &= vbCrLf & "CLIENT>>> " & txtInput.Text 78 txtInput.Clear() 79 End If 80 Catch ex As SocketException 81 txtDisplay.Text &= vbCrLf & "Error writing object" 82 End Try 83 End Sub ' txtInput_KeyDown 84 85 ' connect to server and display server-generated text 86 Public Sub RunClient() 87 Dim client As TcpClient 88 89 ' instantiate TcpClient for sending data to server 90 Try 91 DisplayMessage("Attempting connection" & vbCrLf) 92 93 ' Step 1: create TcpClient and connect to server 94 client = New TcpClient() 95 client.Connect("localhost", 50000) 96 97 ' Step 2: get NetworkStream associated with TcpClient 98 output = client.GetStream() 99 100 ' create objects for writing and reading across stream 101 writer = New BinaryWriterx(output) 102 reader = New BinaryReader(output) 103 104 DisplayMessage(vbCrLf & "Got I/O streams" & vbCrLf) 105 DisableInput(False) ' enable txtInput 106 107 ' loop until server signals termination 108 Do 109 ' Step 3: processing phase 110 Try 111 ' read message from server 112 message = reader.ReadString() 113 DisplayMessage(vbCrLf & message) 114 Catch ex As Exception 115 ' handle exception if error in reading server data 116 System.Environment.Exit(System.Environment.ExitCode) 117 End Try 118 Loop While message <> "SERVER>>> TERMINATE" 119 120 ' Step 4: close connection 121 writer.Close() 122 reader.Close() 123 output.Close() 124 client.Close() 125 126 Application.Exit() 127 Catch ex As Exception 128 ' handle exception if error in establishing connection 129 MessageBox.Show(ex.ToString(), "Connection Error" , _ 130 MessageBoxButtons.OK, MessageBoxIcon.Error) 131 System.Environment.Exit(System.Environment.ExitCode) 132 End Try 133 End Sub ' RunClient 134 End Class ' FrmChatClient
(a)
(b)
(c)
(d)
(e)
(f)
(g) | The client and server applications both 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 FrmChatServer and FrmChatClient, respectively. Figure 23.2 also contains screen captures displaying the execution between the client and the server. FrmChatServer Class In class FrmChatServer (Fig. 23.1), FrmChatServer_Load (lines 1722) creates a Thread that will accept connections from clients (line 20). The THReadStart delegate object that is passed as the THRead constructor's argument specifies which method the Thread executes. Line 21 starts the Thread, which uses the ThreadStart delegate to invoke method RunServer (lines 93159). This method initializes the server to receive connection requests and process connections. Line 102 instantiates a TcpListener object to listen for a connection request from a client at port 50000 (Step 1). Line 105 then calls TcpListener method Start, which causes the TcpListener to begin waiting for requests (Step 2). Accepting the Connection and Establishing the Streams Lines 108155 are an infinite loop that begins by establishing the connection requested by the client (Step 3). Line 112 calls method AcceptSocket of the TcpListener object, which returns a Socket upon successful connection. The thread in which method Accept-Socket is called blocks (i.e., stops executing) until a connection is established. The returned Socket object manages the connection. Line 115 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 118119 create instances of the BinaryWriter andBinaryReader classes for writing and reading data. We pass the NetworkStream object as an argument to each constructorBinaryWriter can write bytes to the NetworkStream, and BinaryReader can read bytes from NetworkStream. Line 121 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 125 uses method Write to send the client a String notifying the user of a successful connection. This completes Step 3. Receiving Messages from the Client We now begin the processing phase (Step 4). Lines 131143 loop until the server receives the message CLIENT>>> TERMINATE, which indicates that the connection should be terminated. Line 134 uses BinaryReader method ReadString to read a String from the stream. Method ReadString blocks until a String is read. This is why we execute method RunServer in a separate Thread (created in lines 2021, when the Form loads). This THRead ensures that the 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 recommends that only the thread which created the GUI should modify the controls.[1] 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. (Delegates were introduced in Section 13.3.3.) At some point after Invoke is called, the thread that originally created the GUI will (when 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 Thread-Safe Calls to Windows Forms Controls" can be found at msdn2.microsoft.com/en-us/library/ms171728.aspx. Line 34 declares a Delegate type named DisplayDelegate, which represents methods that take a String argument and do not return a value. Method DisplayMessage (lines 3849) meets these requirementsit receives a String parameter named message and does not return a value. The If statement in line 40 tests txtdisplay'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, the If condition evaluates to true and lines 4344 call method Invoke, passing 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 txTDisplay directly, so the Else body (line 47) executes and appends message to txtdisplay's Text property. Lines 5368 define the Delegate, DisableInputDelegate, and a method, DisableInput, to allow any thread to modify the ReadOnly property of txtInput using the same techniques. A thread calls DisableInput with a Boolean 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 to set txtInput.ReadOnly's value to the Boolean argument. Terminating the Connection with the Client When the chat is complete, lines 148151 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 loop (line 108). Sending Messages to the Client When the server application's user enters a String in the TextBox and presses the Enterkey, event handler txtInput_KeyDown (lines 7190) reads the String and sends it via method Write of class BinaryWriter. If a user terminates the server application, line 83 calls method Close of the Socket object to close the connection. Terminating the Server Application Lines 2530 define event handler FrmChatServer_FormClosing for the FormClosing event. The event closes the application and calls method Exit of class Environment with parameter ExitCode. Method Exit terminates all threads associated with the application. FrmChatClient Class Figure 23.2 lists the code for class FrmChatClient. Like the FrmChatServer object, the FrmChatClient object creates a Thread (lines 1920) in its constructor to handle all incoming messages. FrmChatClient method RunClient (lines 86133) connects to the FrmChatServer, receives data from the FrmChatServer and sends data to the FrmChatServer. Lines 9495 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 local computer. 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. 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. [Note: The host name localhost is commonly used to test networking applications on one computer. This is particularly useful if you don't have separate computers on which to execute the client and server. Normally, localhost would be replaced with the hostname or IP address of another computer.] The FrmChatClient uses a NetworkStream to send data to and receive data from the server. The client obtains the NetworkStream in line 98 through a call to TcpClient method GetStream (Step 2). Lines 108118 loop until the client receives the connectiontermination message (SERVER>>> TERMINATE). Line 112 uses BinaryReader method ReadString to obtain the next message from the server (Step 3). Line 113 displays the message, and lines 121124 close the BinaryWriter, BinaryReader, NetworkStream and TcpClient objects (Step 4). Lines 3367 declare DisplayDelegate, DisplayMessage, DisableInputDelegate and DisableInput just as in lines 3468 of Fig. 23.1. These once again are used to ensure that the GUI is modified only by 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 txtInput_KeyDown (lines 7083) reads the String from the TextBox and sends it to the server via BinaryWriter method Write. In this client/server chat program, the FrmChatServer 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. |