Introduction
Though Microsoft has done a great job wrapping Winsock API, developers are now facing a new challenge; thread-safety and asynchronous calls. Still the developer can get out without handling these issues, soon to realize that the application hangs or behaves unexpectedly due to the server asynchronously replying back, or that the UI will hang till the server responds. The goal of this project is to have a reliable socket library that will serve as a base for all TCP connections; covering most of the common mistakes where others left.
Ways of receiving data
You can receive data in three ways. First, you can block the application thread until a packet has been received, by calling the Receive method of a Socket object. Obviously, this is not a quite good solution for real-world scenarios. The second option is you can go into a loop (busy loop), polling the Socket object to determine if a packet has been received. This is an OK solution except that it will slow down your main application thread every time you tell the socket to poll and wait for a period of time. The third and last option is to receive data asynchronously by telling the socket to BeginReceive data into byte array, and once data has been received, call a special procedure that is set up to receive the data. Of course, by now you can tell what option I�ve chosen.
Working example
In my project, I�ve developed a component where you can just drag and drop into your application and whoo-ya, it's working. Let�s look into how and why it is done in this way.
Connection
The first thing you need to do in socket application is to connect to a specific port. There are three overloads of this function for ease of use:
Collapse
Public Sub Connect(ByVal hostNameOrAddress As String, ByVal port As Int32)
Public Sub Connect(ByVal serverAddress as IPAddress, ByVal port as Int32)
Public Sub Connect(ByVal endPoint As IPEndPoint)
The main function is as follows (error handling in the code snippet is omitted):
Collapse
Public Sub Connect(ByVal endPoint As IPEndPoint)
_mySocket = New Socket(AddressFamily.InterNetwork, _
SocketType.Stream, ProtocolType.Tcp)
_mySocket.Connect(endPoint)
If IsConnected() = True Then
RaiseEvent Connected(True)
End If
Dim bytes(_packetSize - 1) As Byte
try
_mySocket.BeginReceive(bytes, 0, bytes.Length, _
SocketFlags.None, AddressOf ReceiveCallBack, bytes)
Catch ex As Exception
Throw New Exception("Error receiving data", ex)
If IsConnected() = False Then
RaiseEvent Connected(False)
End If
End Try
End Sub
After establishing a connection, we raise a connection event which I will illustrate soon; once raised, we declare an array of bytes to receive the data. Now, the first trick is to use the BeginInReceive. This tells the socket to wait for a received data on another thread -a new one- from the thread pool, ha? Rings a bill? Yes. It is asynchronous, now I have my application running and not hanging to receive a packet. The function receives the bytes array which will be filled, its length, socket flag, an address of the callback function, and a state object. The callback function will be called when either an entire packet is received or the buffer is full. Again, if there is any error, a connection event is raised stating that the connection is lost and there were an error in receiving the data.
What about ReceiveCallback
ReceiveCallback function is the handler which will handle the packet when received. This is the core of the asynchronous operation. Take a look at the code and then I will explain.
Collapse
Private Sub ReceiveCallBack(ByVal ar As IAsyncResult)
Dim bytes() As Byte = ar.AsyncState
Dim numBytes As Int32 = _mySocket.EndReceive(ar)
If numBytes > 0 Then
ReDim Preserve bytes(numBytes - 1)
Dim received As String = _ascii.GetString(bytes)
'--Now we need to raise the received event.
' args() is used to pass an argument from this thread
' to the synchronized container's ui thread.
Dim args(0) As Object
Dim d As New RaiseReceiveEvent(AddressOf OnReceive)
args(0) = received
'--Invoke the private delegate from the thread.
_syncObject.Invoke(d, args)
End If
If IsConnected() = False Then
Dim args() As Object = {False}
Dim d As New RaiseConnectedEvent(AddressOf OnConnected)
_syncObject.Invoke(d, args)
Else
'--Yes, then resize bytes to packet size
ReDim bytes(PacketSize - 1)
'--Call BeginReceive again, catching any error
Try
_mySocket.BeginReceive(bytes, 0, bytes.Length, _
SocketFlags.None, AddressOf ReceiveCallBack, bytes)
Catch ex As Exception
'--Raise the exception event
Dim args() As Object = {ex}
Dim d As New RaiseExceptionEvent(AddressOf OnExcpetion)
_syncObject.Invoke(d, args)
'--If not connected, raise the connected event
If IsConnected() = False Then
args(0) = False
Dim dl As New RaiseConnectedEvent(AddressOf OnConnected)
_syncObject.Invoke(dl, args)
End If
End Try
End If
End Sub
Now that you�ve gone through the code, what do you think? Don�t worry, I will explain. First, we have a local variable to hold what I received from the server. Then I immediately issue EndReceive which will free any resources used by the BeginRecieve to create a new thread, and so forth. I also resize the array of bytes with the actual size. We, then, declare a local delegate object, and instead of modifying the main application (the user interface, for example) inside this component, it raises an event (Received) via OnReceive method. And since the event is raised on the main thread, it�s completely thread-safe. This is done by calling Invoke method from the private object _syncObject. But what is _syncObject? It is of type ISynchronizeInvoke. Ha? Well _syncObject is used to switch the context from the socket thread to the main thread (in this case, the UI component). This is achieved by having the SynchronizingObject property available to the main control. Here is the code for this property:
Collapse
Public Property SynchronizingObject() As _
System.ComponentModel.ISynchronizeInvoke
Get
If _syncObject Is Nothing And Me.DesignMode Then
Dim designer As IDesignerHost = Me.GetService(GetType(IDesignerHost))
If Not (designer Is Nothing) Then
_syncObject = designer.RootComponent
End If
End If
Return _syncObject
End Get
Set(ByVal Value As System.ComponentModel.ISynchronizeInvoke)
If Not Me.DesignMode Then
If Not (_syncObject Is Nothing) And Not (_syncObject Is Value) Then
Throw New Exception("Property ca not be set at run-time")
Else
_syncObject = Value
End If
End If
End Set
End Property
Events
There are three events and so three delegates:
* Connections: fires when a connection is opened or the connection is closed.
* Exception: fires whenever an exception happens.
* Received: fires whenever a packet is received.
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment