Sockets


There may be times when you need to transfer data across a network (either a private network or the Internet) but the existing techniques and protocols don’t exactly suit your needs. For example, you can’t download resources using the techniques discussed at the start of this chapter, and you can’t use Web services (as described in Chapter 26) or remoting (as described in Chapter 27). When this happens, the best course of action is to roll your own protocol using sockets.

TCP/IP, and therefore, the Internet itself, is based on sockets. The principle is simple: Establish a port at one end and allow clients to “plug in” to that port from the other end. Once the connection is made, applications can send and receive data through a stream. For example, HTTP nearly always operates on port 80, so a Web server opens a socket on port 80 and waits for incoming connections (Web browsers, unless told otherwise, attempt to connect to port 80 in order to make a request of that Web server).

In .NET, sockets are implemented in the System.Net.Sockets namespace and use classes from System.Net and System.IO to get the stream classes. Although working with sockets can be a little tricky outside of .NET, the framework includes some superb classes that enable you to open a socket for inbound connections (System.Net.TcpListener) and for communication between two open sockets (System.Net.TcpClient). These two classes, in combination with some threading shenanigans, enable you build your own protocol through which you can send any data you like. With your own protocol, you have ultimate control over the communication.

To demonstrate these techniques, you’re going to build Wrox Messenger, a very basic instant messenger application similar to MSN Messenger.

Building the Application

You’ll wrap all the functionality of your application into a single Windows application, which will act as both a server that waits for inbound connections and a client that has established outbound connections.

Create a new project called WroxMessenger. Change the title of Form1 to Wrox Messenger and add a TextBox control called textConnectTo and a Button control called buttonConnect. The form should appear as shown in Figure 32-5.

image from book
Figure 32-5

You’ll learn more about this in greater detail later, but for now it’s very important that all of your UI code runs in the same thread, and that the thread is actually the main application that creates and runs Form1.

To keep track of what’s happening, you’ll add a field to Form1 that enables you to store the ID of the startup thread and report that ID on the caption. This helps provide a context for the thread/UI issues discussed later. You’ll also need some namespace imports and a constant specifying the ID of the default port. Add this code to Form1:

  Imports System.Net Imports System.Net.Sockets Imports System.Threading Public Class Form1    Private Shared _mainThreadId As Integer    Public Const ServicePort As Integer = 10101 

Next, create a New() method for Form1 (Form1.vb) and add this code to the constructor that populates the field and changes the caption:

 Public Sub New()    ' This call is required by the Windows Form Designer.    InitializeComponent()    ' Add any initialization after the InitializeComponent() call.    _mainThreadId = System.Threading.Thread.CurrentThread.GetHashCode()      Text &= "-" & _mainThreadId.ToString() End Sub

Note that you can get to the Form1.vb file’s New() method by using Visual Studio and selecting Form1 and New in the uppermost drop-downs in the document window. This causes the Form1.vb file’s New() method to be created on your behalf.

To listen for incoming connections, you’ll create a separate class called Listener. This class uses an instance of System.Net.Sockets.TcpListener to wait for incoming connections. Specifically, it opens a TCP port that any client can connect to - sockets are not platform-specific. Although connections are always made on a specific, known port, the actual communication takes place on a port of the TCP/IP subsystem’s choosing, which means you can support many inbound connections at once, despite the fact that each of them connects to the same port. Sockets are an open standard available on pretty much any platform you can think of. For example, if you publish the specification for your protocol, then developers working on Linux can connect to your Wrox Messenger service.

When you detect an inbound connection, you’ll be given a System.Net.Sockets.TcpClient object. This is your gateway to the remote client. To send and receive data, you need to get hold of a System.Net.NetworkStream object (returned through a call to GetStream on TcpClient), which returns a stream that you can use.

Create a new class called Listener. This thread needs members to hold an instance of a System.Threading.Thread object, and a reference back to the Form1 class that is the main form in the application. Not covered here is how to spin up and down threads, or synchronization. (Refer back to Chapter 24 if you need more information about that.)

Here’s the basic code for the Listener class:

  Imports System.Net.Sockets Imports System.Threading Public Class Listener  Private _main As Form1  Private _listener As TcpListener  Private _thread As Thread  Public Sub New(ByVal main As Form1)  _main = main  End Sub  Public Sub SpinUp()   ' create and start the new thread...   _thread = New Thread(AddressOf ThreadEntryPoint)   _thread.Start()  End Sub End Class 

The obvious missing method here is ThreadEntryPoint(). This is where you need to create the socket and wait for inbound connections. When you get them, you’ll be given a TcpClient object, which you pass back to Form1, where the conversation window can be created. You would create this method in the Listener.vb class file.

To create the socket, create an instance of TcpListener and give it a port. In your application, the port you’re going to use is 10101. This port should be free on your computer, but if the debugger breaks on an exception when you instantiate TcpListener or call Start(), try another port. Once you have done that and called Start() to configure the object to listen for connections, you drop into an infinite loop and call AcceptTcpClient(). This method blocks until the socket is closed or a connection becomes available. If you get Nothing back, either the socket is closed or there’s a problem, so you drop out of the thread. If you get something back, then you pass the TcpClient over to Form1 through a call to the (not yet built) ReceiveInboundConnection() method:

  ' ThreadEntryPoint... Protected Sub ThreadEntryPoint()  ' Create a socket...  _listener = New TcpListener(Form1.ServicePort)  _listener.Start()  ' Loop infinitely, waiting for connections.  Do While True   ' Get a connection...   Dim client As TcpClient = _listener.AcceptTcpClient()   If client Is Nothing Then    Exit Do   End If   ' Process it...   _main.ReceiveInboundConnection(client)   Loop End Sub 

It’s in the ReceiveInboundConnection() method that you’ll create the Conversation form that the user can use to send messages.

Creating Conversation Windows

When building Windows Forms applications that support threading, there’s always the possibility of running into a problem with the Windows messaging subsystem. This is a very old part of Windows that powers the Windows user interface (the idea has been around since version 1.0 of the platform, although the implementation on modern Windows versions is far removed from the original).

Even those who are not familiar with old-school Windows programming, such as MFC, Win32, or even Win16 development, should be familiar with events. When you move a mouse over a form, you get MouseMove() events. When you close a form, you get a Closed() event. There’s a mapping between these events and the messages that Windows passes around to support the actual display of the windows. For example, whenever you receive a MouseMove() event, a message called WM_MOUSEMOVE is sent to the window, by Windows, in response to the mouse driver. In .NET, and other Rapid Application Development (RAD) environments such as VB and Delphi, this message is converted into an event that you can write code against.

Although this is getting way off the topic - you know how to build Windows Forms applications by now and don’t need the details of messages such as WM_NCHITTEST or WM_PAINT - it has an important implication. In effect, Windows creates a message queue for each thread into which it posts the messages that the thread’s windows have to work with. This queue is looped on a virtually constant basis, and the messages are distributed to the appropriate window (remember that small controls like buttons and text boxes are also windows). In .NET, these messages are turned into events, but unless the message queue is looped the messages don’t get through.

Suppose Windows needs to paint a window. It posts a WM_PAINT message to the queue. A message loop implemented on the main thread of the process containing the window detects the message and dispatches it on to the appropriate window where it is processed. Now suppose that the queue isn’t looped. The message is never picked up and the window is never painted.

In a Windows application, a single thread is usually responsible for message dispatch. This thread is typically (but not necessarily) the main application thread - the one that’s created when the process is first created. If you create windows in a different thread, then that new thread has to support the message dispatch loop so that messages destined for the windows get through. However, with Listener, you have no code for processing the message loop and there’s little point in writing any because the next time you call AcceptTcpClient() you’re going to block and everything will stop working.

The trick then is to create the windows only in the main application thread, which is the thread that created Form1 and is processing the messages for all the windows created in this thread. You can pass calls from one thread to the other by calling the Invoke() method of Form1.

This is where things start to get complicated. There is an awful lot of code to write to get to a point where you can see that the socket connection has been established and get conversation windows to appear. Here’s what you need to do:

  • Create a new Conversation form. This form needs controls for displaying the total content of the conversation, plus a TextBox control for adding new messages.

  • The Conversation window needs to be able to send and receive messages through its own thread.

  • Form1 needs to be able to initiate new connections. This will be done in a separate thread that is managed by the thread pool. When the connection has been established, a new Conversation window needs to be created and configured.

  • Form1 also needs to receive inbound connections. When it gets one of these, a new Conversation must be created and configured.

Let’s look at these challenges one at a time.

Creating the Conversation Form

The simplest place to start is to build the new Conversation form, which needs three TextBox controls (textUsername, textMessages, and textMessage) and a Button control (buttonSend), as shown in Figure 32-6.

image from book
Figure 32-6

This class requires a number of fields and an enumeration. It needs fields to hold the username of the user (which you’ll default to Evjen), the underlying TcpClient, and the NetworkStream returned by that client. The enumeration indicates the direction of the connection (which will help you when debugging):

  Imports System.Net Imports System.Net.Sockets Imports System.Text Imports System.Threading Imports System.Runtime.Serialization.Formatters.Binary Public Class Conversation    Private _username As String = "Evjen"    Private _client As TcpClient    Private _stream As NetworkStream    Private _direction As ConversationDirection    Public Enum ConversationDirection As Integer       Inbound = 0       Outbound = 1    End Enum 

We won’t look into the issues surrounding establishing a thread for exchanging messages at this stage, but we will look at implementing the ConfigureClient() method. This method eventually does more work than this, but for now it sets a couple of fields and calls UpdateCaption():

  Public Sub ConfigureClient(ByVal client As TcpClient, _               ByVal direction As ConversationDirection)    ' Set it up...    _client = client    _direction = direction    ' Update the window...    UpdateCaption() End Sub Protected Sub UpdateCaption()    ' Set the text.    Dim builder As New StringBuilder(_username)    builder.Append(" - ")    builder.Append(_direction.ToString())    builder.Append(" - ")    builder.Append(Thread.CurrentThread.GetHashCode())    builder.Append(" - ")    If Not _client Is Nothing Then       builder.Append("Connected")    Else       builder.Append("Not connected")    End If    Text = builder.ToString() End Sub 

One debugging issue to deal with is that if you’re connecting to a conversation on the same machine, then you need a way to change the name of the user sending each message; otherwise, things get confusing. That’s what the topmost TextBox control is for. In the constructor, set the text for the textUsername.Text property:

  Public Sub New()    ' This call is required by the Windows Form Designer.    InitializeComponent()    ' Add any initialization after the InitializeComponent() call.    textUsername.Text = _username End Sub 

On the TextChanged event for this control, update the caption and the internal _username field:

  Private Sub textUsername_TextChanged(ByVal sender As System.Object, _                    ByVal e As System.EventArgs) _                    Handles textUsername.TextChanged    _username = textUsername.Text    UpdateCaption() End Sub 

Initiating Connections

Form1 needs to be able to both initiate connections and receive inbound connections - the application is both a client and a server. You’ve already created some of the server portion by creating Listener; now you’ll look at the client side.

The general rule when working with sockets is that anytime you send anything over the wire, you must perform the actual communication in a separate thread. Virtually all calls to send and receive do so in a blocking manner; that is, they block until data is received, block until all data is sent, and so on.

If threads are used well, the UI will keep running as normal, irrespective of the problems that may occur during transmitting and receiving. This is why in the InitiateConnection() method on Form1, you defer processing to another method called InitiateConnectionThreadEntryPoint(), which is called from a new thread:

  Public Sub InitiateConnection()    InitiateConnection(textConnectTo.Text) End Sub Public Sub InitiateConnection(ByVal hostName As String)    ' Give it to the threadpool to do...    ThreadPool.QueueUserWorkItem(AddressOf _    Me.InitiateConnectionThreadEntryPoint, hostName) End Sub Private Sub buttonConnect_Click(ByVal sender As System.Object, _                 ByVal e As System.EventArgs) _                 Handles buttonConnect.Click    InitiateConnection() End Sub 

Inside the thread, you try to convert the host name that you’re given into an IP address (localhost is used as the host name in the demonstration, but it could be the name of a machine on the local network or a host name on the Internet). This is done through the shared GetHostEntry() method on System.Net.Dns and returns a System.Net.IPHostEntry object. As a host name can point to multiple IP addresses, you’ll just use the first one that you’re given. You take this address expressed as an IP (for example, 192.168.0.4) and combine it with the port number to get a new System.Net.IPEndPoint. Then, you create a new TcpClient from this IPEndPoint and try to connect.

If at any time an exception is thrown (which can happen because the name couldn’t be resolved or the connection could not be established), you pass the exception to HandleInitiateConnectionException. If it succeeds, you pass it to ProcessOutboundConnection. Both of these methods will be implemented shortly:

  Private Sub InitiateConnectionThreadEntryPoint(ByVal state As Object)    Try       ' Get the host name...       Dim hostName As String = CStr(state)       ' Resolve...       Dim hostEntry As IPHostEntry = Dns.GetHostEntry(hostName)       If Not hostEntry Is Nothing Then          ' Create an end point for the first address.          Dim endPoint As New IPEndPoint(hostEntry.AddressList(0), ServicePort)          ' Create a TCP client...          Dim client As New TcpClient()          client.Connect(endPoint)          ' Create the connection window...          ProcessOutboundConnection(client)       Else          Throw New ApplicationException("Host '" & hostName & _             "' could not be resolved.")       End If    Catch ex As Exception       HandleInitiateConnectionException(ex)    End Try End Sub 

When it comes to HandleInitiateConnectionException, you start to see the inter-thread UI problems that were mentioned earlier. When there is a problem with the exception, you need to tell the user, which means you need to move the exception from the thread-pool-managed thread into the main application thread. The principle for this is the same; you need to create a delegate and call that delegate through the Invoke() method of the form. This method does all the hard work in marshaling the call across to the other thread.

Here’s what the delegates look like. They have the same parameters of the calls themselves. As a naming convention, it’s a good idea to use the same name as the method and tack the word Delegate on the end:

 Public Class Form1    Private Shared _mainThreadId As Integer    ' delegates...     Protected Delegate Sub HandleInitiateConnectionExceptionDelegate( _                                              ByVal ex As Exception) 

In the constructor for Form1, you capture the thread caller’s thread ID and store it in _mainThreadId. Here’s a method that compares the captured ID with the ID of the current thread:

  Public Shared Function IsMainThread() As Boolean    If Thread.CurrentThread.GetHashCode() = _mainThreadId Then       Return True    Else       Return False    End If End Function 

The first thing you do at the top of HandleInitiateConnectionException is check the thread ID. If it doesn’t match, then you create the delegate and call it. Notice how you set the delegate to call back into the same method because the second time it’s called you would have moved to the main thread; therefore, IsMainThread() returns True, and you can process the exception properly:

  Protected Sub HandleInitiateConnectionException(ByVal ex As Exception)    ' main thread?    If IsMainThread() = False Then       ' Create and call...       Dim args(0) As Object       args(0) = ex       Invoke(New HandleInitiateConnectionExceptionDelegate(AddressOf _          HandleInitiateConnectionException), args)       ' return       Return    End If    ' Show it.    MessageBox.Show(ex.GetType().ToString() & ":" & ex.Message) End Sub 

The result is that when the call comes in from the thread-pool-managed thread, IsMainThread() returns False, and the delegate is created and called. When the method is entered again as a result of the delegate call, IsMainThread() returns True and you see the message box.

When it comes to ProcessOutboundConnection(), you have to again jump into the main UI thread. However, the magic behind this method is implemented in a separate method called Process Connection(), which can handle either inbound or outbound connections. Here’s the delegate:

 Public Class Form1    Private Shared _mainThreadId As Integer    Private _listener As Listener    Protected Delegate Sub ProcessConnectionDelegate(ByVal client As _       TcpClient, ByVal direction As Conversation.ConversationDirection)    Protected Delegate Sub HandleInitiateConnectionExceptionDelegate(ByVal _       ex As Exception)

Here’s the method itself, which creates the new Conversation form and calls the ConfigureClient() method:

  Protected Sub ProcessConnection(ByVal client As TcpClient, _    ByVal direction As Conversation.ConversationDirection)    ' Do you have to move to another thread?    If IsMainThread() = False Then       ' Create and call...       Dim args(1) As Object       args(0) = client       args(1) = direction       Invoke(New ProcessConnectionDelegate(AddressOf ProcessConnection), args)       Return    End If    ' Create the conversation window...    Dim conversation As New Conversation()    conversation.Show()    conversation.ConfigureClient(client, direction) End Sub 

Of course, ProcessOutboundConnection() needs to defer to ProcessConnection():

  Public Sub ProcessOutboundConnection(ByVal client As TcpClient)    ProcessConnection(client, Conversation.ConversationDirection.Outbound) End Sub 

Now that you can connect to something on the client side, let’s look at how to receive connections (on the server side).

Receiving Inbound Connections

You’ve already built Listener, but you haven’t created an instance of it or spun up its thread to wait for incoming connections. To do this, you need a field in Form1 to hold an instance of the object. You also need to tweak the constructor. Here’s the field:

 Public Class Form1 Private _mainThreadId As Integer Private _listener As Listener 

Here is the new code that needs to be added to the constructor:

 Public Sub New()    ' This call is required by the Windows Form Designer.    InitializeComponent()    ' Add any initialization after the InitializeComponent() call.    _mainThreadId = System.Threading.Thread.CurrentThread.GetHashCode()    Text &= "-" & _mainThreadId.ToString()    ' listener...    _listener = New Listener(Me)    _listener.SpinUp() End Sub

When inbound connections are received, you get a new TcpClient object. This is passed back to Form1 through the ReceiveInboundConnection() method. This method, like ProcessOutboundConnection(), defers to ProcessConnection(). Because ProcessConnection() already handles the issue of moving the call to the main application thread, ReceiveInboundConnection() looks like this:

  Public Sub ReceiveInboundConnection(ByVal client As TcpClient)    ProcessConnection(client, Conversation.ConversationDirection.Inbound) End Sub 

If you run the project now, you should be able to click the Connect button and see two windows - Inbound and Outbound (see Figure 32-7).

image from book
Figure 32-7

If you close all three windows, the application keeps running because you haven’t written code to close down the listener thread, and having an open thread like this keeps the application open. Select Debug image from book Stop Debugging in Visual Studio to close the application down by killing all running threads.

By clicking the Connect button you’re calling InitiateConnection(). This spins up a new thread in the pool that resolves the given host name (localhost) into an IP address. This IP address, in combination with a port number, is then used in the creation of a TcpClient object. If the connection can be made, then ProcessOutboundConnection() is called, which results in the first of the conversation windows being created and marked as “outbound.”

This example is somewhat artificial, as the two instances of Wrox Messenger should be running on separate computers. On the remote computer (if you’re connecting to localhost, this will be the same computer), a connection is received through the AcceptTcpClient() method of TcpListener. This results in a call to ReceiveInboundConnection(), which in turn results in the creation of the second conversation window, this time marked as “inbound.”

Sending Messages

The next step is to work out how to exchange messages between the two conversation windows. You already have a TcpClient in each case so all you have to do is squirt binary data down the wire on one side and pick it up at the other end. The two conversation windows act as both client and server, so both need to be able to send and receive.

There are three problems to solve:

  • You need to establish one thread to send data and another thread to receive data.

  • Data sent and received needs to be reported back to the user so that he or she can follow the conversation.

  • The data that you want to send has to be converted into a wire-ready format, which in .NET terms usually means serialization.

The power of sockets enables you to define whatever protocol you like for data transmission. If you wanted to build your own SMTP server, you could implement the (publicly available) specifications, set up a listener to wait for connections on port 25 (the standard port for SMTP), wait for data to come in, process it, and return responses as appropriate.

It’s best to work in this way when building protocols. Unless there are very strong reasons for not doing so, make your server as open as possible: Do not tie it to a specific platform. This is the way that things are done on the Internet. To an extent, things like Web Services should negate the need to build your own protocols; as you go forward, you will rely instead on the “remote object available to local client” paradigm.

Now it’s time to consider the idea of using the serialization features of .NET to transmit data across the network. After all, you’ve already seen this in action with Web Services and remoting. You can take an object in .NET, use serialization to convert it to a string of bytes, and expose that string down to a Web Service consumer, to a remoting client, or even to a file.

Chapter 27 discussed the BinaryFormatter and SoapFormatter classes. You could use either of those classes, or create your own custom formatter, to convert data for transmission and reception. In this case, you’re going to create a new class called Message and use BinaryFormatter to crunch it down into a wire-ready format and convert it back again for processing.

This approach isn’t ideal from the perspective of interoperability, because the actual protocol used is lost in the implementation of the .NET Framework, rather than being under your absolute control.

If you want to build an open protocol, this is not the best way to do it. Unfortunately, the best way is beyond the scope of this book, but a good place to start is to look at existing protocols and standards and model any protocol on their approach. BinaryFormatter is quick and dirty, which is why you’re going to use it.

The Message Class

The Message class contains two fields, _username and _message, which form the entirety of the data that you want to transmit. The code for this class follows; note how the Serializable attribute is applied to it so that BinaryFormatter can change it into a wire-ready form. You’re also providing a new implementation of ToString:

  Imports System.Text <Serializable()> Public Class Message    Private _username As String    Private _message As String    Public Sub New(ByVal name As String)       _username = name    End Sub    Public Sub New(ByVal name As String, ByVal message As String)       _username = name       _message = message    End Sub    Public Overrides Function ToString() As String       Dim builder As New StringBuilder(_username)       builder.Append(" says:")       builder.Append(ControlChars.CrLf)       builder.Append(_message)       builder.Append(ControlChars.CrLf)       Return builder.ToString()    End Function End Class 

Now all you have to do is spin up two threads, one for transmission and one for reception, updating the display. You need two threads per conversation, so if you have 10 conversations open, you need 20 threads plus the main UI thread, plus the thread running TcpListener.

Receiving messages is easy. When calling Deserialize() on BinaryFormatter, you give it the stream returned to you from TcpClient. If there’s no data, then this blocks. If there is data, then it’s decoded into a Message object that you can display. If you have multiple messages coming down the pipe, then BinaryFormatter keeps processing them until the pipe is empty. Here’s the method for this, which should be added to Conversation. Remember that you haven’t implemented ShowMessage() yet:

  Protected Sub ReceiveThreadEntryPoint()    ' Create a formatter...    Dim formatter As New BinaryFormatter()    ' Loop    Do While True       ' Receive...       Dim message As Message = formatter.Deserialize(_stream)       If message Is Nothing Then          Exit Do       End If       ' Show it...       ShowMessage(message)    Loop End Sub 

Transmitting messages is a bit more complex. You want a queue (managed by a System.Collections .Queue) of outgoing messages. Every second, you’ll examine the state of the queue. If you find any messages, you use BinaryFormatter to transmit them. Because you’ll be accessing this queue from multiple threads, you use a System.Threading.ReaderWriterLock to control access. To minimize the amount of time you spend inside locked code, you quickly transfer the contents of the shared queue into a private queue that you can process at your leisure. This enables the client to continue to add messages to the queue through the UI, even though existing messages are being sent by the transmit thread.

First, add these members to Conversation:

 Public Class Conversation    Private _username As String = "Evjen"    Private _client As TcpClient    Private _stream As NetworkStream    Private _direction As ConversationDirection    Private _receiveThread As Thread    Private _transmitThread As Thread    Private _transmitQueue As New Queue()    Private _transmitLock As New ReaderWriterLock() 

Now, add this method again to Conversation:

  Protected Sub TransmitThreadEntryPoint()    ' Create a formatter...    Dim formatter As New BinaryFormatter()    Dim workQueue As New Queue()    ' Loop    Do While True       ' Wait for the signal...       Thread.Sleep(1000)       ' Go through the queue...       _transmitLock.AcquireWriterLock(-1)       Dim message As Message       workQueue.Clear()       For Each message In _transmitQueue          workQueue.Enqueue(message)       Next       _transmitQueue.Clear()       _transmitLock.ReleaseWriterLock()       ' Loop the outbound messages...       For Each message In workQueue          ' Send it...          formatter.Serialize(_stream, message)       Next    Loop End Sub 

When you want to send a message, you call one version of the SendMessage() method. Here are all of the implementations, and the Click handler for buttonSend:

  Private Sub buttonSend_Click(ByVal sender As System.Object, _    ByVal e As System.EventArgs) Handles buttonSend.Click    SendMessage(textMessage.Text) End Sub Public Sub SendMessage(ByVal message As String)    SendMessage(_username, message) End Sub Public Sub SendMessage(ByVal username As String, ByVal message As String)    SendMessage(New Message(username, message)) End Sub Public Sub SendMessage(ByVal message As Message)    ' Queue it    _transmitLock.AcquireWriterLock(-1)    _transmitQueue.Enqueue(message)    _transmitLock.ReleaseWriterLock()    ' Show it...    ShowMessage(message) End Sub 

ShowMessage() is responsible for updating textMessages so that the conversation remains up to date (notice how you add the message both when you send it and when you receive it so that both parties have an up-to-date thread). This is a UI feature, so it is good practice to pass it over to the main application thread for processing. Although the call in response to the button click comes off the main application thread, the one from inside ReceiveThreadEntryPoint() does not. Here’s what the delegate looks like:

 Public Class Conversation    ' members...    Private _username As String = "Evjen"    Private _client As TcpClient    Private _stream As NetworkStream    Private _direction As ConversationDirection    Private _receiveThread As Thread    Private _transmitThread As Thread    Private _transmitQueue As New Queue()    Private _transmitLock As New ReaderWriterLock()    Public Delegate Sub ShowMessageDelegate(ByVal message As Message) 

Here’s the method implementation:

  Public Sub ShowMessage(ByVal message As Message)    ' Thread?    If Form1.IsMainThread() = False Then       ' Run...       Dim args(0) As Object       args(0) = message       Invoke(New ShowMessageDelegate(AddressOf ShowMessage), args)       ' Return...       Return    End If    ' Show it...    textMessages.Text &= message.ToString() End Sub 

All that remains now is to spin up the threads. This should be done from within ConfigureClient(). Before the threads are spun up, you need to get hold of the stream and store it in the private _stream field. After that, you create new Thread objects as normal:

 Public Sub ConfigureClient(ByVal client As TcpClient, _       ByVal direction As ConversationDirection)    ' Set it up...    _client = client    _direction = direction    ' Update the window...    UpdateCaption()    ' Get the stream...    _stream = _client.GetStream()    ' Spin up the threads...    _transmitThread = New Thread(AddressOf TransmitThreadEntryPoint)    _transmitThread.Start()    _receiveThread = New Thread(AddressOf ReceiveThreadEntryPoint)    _receiveThread.Start() End Sub

At this point, you should be able to connect and exchange messages, as shown in Figure 32-8.

image from book
Figure 32-8

Note that the screen shots show the username of the inbound connection as Tuija. This was done with the textUsername text box so that you can follow which half of the conversation comes from where.

Shutting Down the Application

You’ve yet to solve the problem of neatly closing the application, or, in fact, dealing with one person in the conversation closing down his or her window, indicating a wish to end the conversation. When the process ends (whether neatly or forcefully), Windows automatically mops up any open connections and frees up the port for other processes.

Suppose you have two computers, one window per computer, as you would in a production environment. When you close your window, you’re indicating that you want to end the conversation. You need to close the socket and spin down the transmission and reception threads. At the other end, you should be able to detect that the socket has been closed, spin down the threads, and tell the user that the other user has terminated the conversation.

This all hinges on being able to detect when the socket has been closed. Unfortunately, Microsoft has made this very hard, thanks to the design of the TcpClient class. TcpClient effectively encapsulates a System.Net.Sockets.Socket class, providing methods for helping to manage the connection lifetime and communication streams. However, TcpClient does not have a method or property that answers the question, “Am I still connected?” Therefore, you need get hold of the Socket object that TcpClient is wrapping and then you can use its Connected property to find out if the connection has been closed.

TcpClient does support a property called Client that returns a Socket, but this property is protected, meaning you can only access it by inheriting a new class from TcpClient. There is another way, though: You can use reflection to get at the property and call it without having to inherit a new class.

Microsoft claims that this is a legitimate technique, even though it appears to violate every rule in the book about encapsulation. Reflection is designed not only for finding out which types are available, and learning which methods and properties each type supports, but also for invoking those methods and properties whether they’re protected or public. Therefore, in Conversation, you need to store the socket:

 Public Class Conversation    Private _username As String = "Evjen"    Private _client As TcpClient    Private _socket As Socket 

In ConfigureClient(), you need to use Reflection to peek into the Type object for TcpClient and dig out the Client property. Once you have a System.Reflection.PropertyInfo for this property, you can retrieve its value by using the GetValue() method. Don’t forget to import the

System.Reflection namespace:

 Public Sub ConfigureClient(ByVal client As TcpClient, _               ByVal direction As ConversationDirection)    ' Set it up...    _client = client    _direction = direction    ' Update the window...    UpdateCaption()    ' Get the stream...    _stream = _client.GetStream()    ' Get the socket through reflection...    Dim propertyInfo As PropertyInfo = _       _client.GetType().GetProperty("Client", _       BindingFlags.Instance Or BindingFlags.Public)    If Not propertyInfo Is Nothing Then       _socket = propertyInfo.GetValue(_client, Nothing)    Else       Throw New Exception("Couldn't retrieve Client property from TcpClient")    End If    ' Spin up the threads...    _transmitThread = New Thread(AddressOf TransmitThreadEntryPoint)    _transmitThread.Start()    _receiveThread = New Thread(AddressOf ReceiveThreadEntryPoint)    _receiveThread.Start() End Sub

Applications are able to check the state of the socket either by detecting when an error occurs because you’ve tried to send data over a closed socket or by actually checking whether the socket is connected. If you either don’t have a Socket available in socket (that is, it is Nothing), or if you have one and it tells you you’re disconnected, you give the user some feedback and exit the loop. By exiting the loop, you effectively exit the thread, which is a neat way of quitting the thread. Notice as well that you might not have a window at this point (you might be the one who closed the conversation by closing the window), so you wrap the UI call in a Try Catch (the other side will see a <disconnect> message):

 Protected Sub TransmitThreadEntryPoint()    ' Create a formatter...    Dim formatter As New BinaryFormatter()    Dim workQueue As New Queue()    ' name...    Thread.CurrentThread.Name = "Tx" & _direction.ToString()    ' Loop...    Do While True       ' Wait for the signal...       Thread.Sleep(1000)       ' Disconnected?       If _socket Is Nothing OrElse _socket.Connected = False Then          Try             ShowMessage(New Message("Debug", "<disconnect>"))          Catch          End Try          Exit Do       End If       ' Go through the queue...

ReceiveThreadEntryPoint() also needs some massaging. When the socket is closed, the stream is no longer valid and so BinaryFormatter.Deserialize() throws an exception. Likewise, you quit the loop and therefore neatly quit the thread:

 Protected Sub ReceiveThreadEntryPoint()    ' Create a formatter...    Dim formatter As New BinaryFormatter()    ' Loop...    Do While True       ' Receive...       Dim message As Message = Nothing       Try          message = formatter.Deserialize(_stream)       Catch       End Try       If message Is Nothing Then          Exit Do       End If       ' Show it...       ShowMessage(message)    Loop End Sub

How do you deal with actually closing the socket? You tweak the Dispose() method of the form itself (you can find this method in the Windows-generated code section of the file), and if you have a _socket object you close it:

  Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)    If disposing Then       If Not (components Is Nothing) Then          components.Dispose()       End If    End If    ' Close the socket...    If Not _socket Is Nothing Then       _socket.Close()       _socket = Nothing    End If    MyBase.Dispose(disposing) End Sub 

Now you’ll be able to start a conversation; and if one of the windows is closed, <disconnect> will appear in the other, as shown in Figure 32-9. In the background, the four threads (one transmit, one receive per window) will spin down properly.

image from book
Figure 32-9

The application itself still won’t close properly, even if you close all the windows, because you need to stop the Listener when Form1 closes. To do so, make Listener implement IDisposable:

 Public Class Listener   Implements IDisposable   Public Sub Dispose() Implements System.IDisposable.Dispose     ' Stop it...     Finalize()     GC.SuppressFinalize(Me)   End Sub   Protected Overrides Sub Finalize()      ' Stop the listener...      If Not _listener Is Nothing Then         _listener.Stop()         _listener = Nothing      End If      ' Stop the thread...      If Not _thread Is Nothing Then         _thread.Join()         _thread = Nothing      End If      ' Call up...      MyBase.Finalize() End Sub 

Now all that remains is to call Dispose() from within Form1. A good place to do this is in the Closed event handler:

  Protected Overrides Sub OnClosed(ByVal e As System.EventArgs)    If Not _listener Is Nothing Then       _listener.Dispose()       _listener = Nothing    End If End Sub 

After the code is compiled again, the application can be closed.




Professional VB 2005 with. NET 3. 0
Professional VB 2005 with .NET 3.0 (Programmer to Programmer)
ISBN: 0470124709
EAN: 2147483647
Year: 2004
Pages: 267

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net