Table of Contents
Introduction
Hello, and welcome to the 4th edition of the Winsock.NET component. Previous versions included Winsock2007, Winsock2005, and the original Winsock.NET.
I have taken some time to try and make this the best version of the component you could want, and in doing so, I decided to create a version for each version of the .NET framework (1.1, 2.0, and 3.5)!
What started out as something to satisfy my own needs of the lack of Winsock support in the .NET framework has taken on a new direction. It started out shaky, with questionable data separation techniques (using the EOT character) and not very thread-safe event routines, and has morphed into using a decent packet header and thread-safe events. The best part is, the packet header can be turned off using LegacySupport
to allow communication with other servers/clients such as the Apache Web Server and the Telnet client.
I have even tested running a server built with VS2003 and running a client built using VB 2005 without LegacySupport
, and it worked just fine! This means, you could even serialize an object between the frameworks, as long as the Type
(full Type
- this includes the namespace) exists on each side.
Let's dive into the features, construction, and the use of this set of tools.
Features
There are some new features, compared to the last version of this component. Here is a list of features that this control contains. The new features are marked with an asterisk (*).
- Thread-safe event calling
- Object serialization
- UDP support
- IPv6 support
- Generics support* (not in the 2003 version)
- Design-time UI support* (revamped action list - not in the 2003 version)
- Legacy support
- Enhanced legacy support conversion*
- Easy to use file sending
WinsockCollection
for easy multiple connection handling
Construction
If I detailed the entire construction of this component, this article would be extremely long, and bore most people to tears, so I will try to focus my concentration on some of the major details.
The key to the construction of this component is the Socket
object. This allows communication across the network using TCP/UDP and IPv4/IPv6. The only hiccup is that in order to listen on both IPv4 and IPv6 at the same timen you need two Socket
objects - so the AsyncSocket
class contains two.
The first thing you'll notice about this version is I'm using an interface to define parts of the Winsock
object. This is really due to the fact I was working on the AsyncSocket
object first, and needed a placeholder until I started work on the Winsock
object itself. I could have just as easily used an exact reference to the object instead of the interface.
In order for your application to run smoothly, the processing that the component needs to do should be done in a separate thread - hence, everything uses asynchronous calls. If not handled properly, asynchronous calls could lead to problems when you try to update controls on a form from one of your event handlers. To prevent these problems, I've implemented a thread-safe event calling function.
Public Event Connected( _
ByVal sender As Object, _
ByVal e As WinsockConnectedEventArgs) _
Implements IWinsock.Connected
Public Sub OnConnected( _
ByVal e As WinsockConnectedEventArgs) _
Implements IWinsock.OnConnected
RaiseEventSafe(ConnectedEvent, New Object() {Me, e})
End Sub
Private Sub RaiseEventSafe( _
ByVal ev As System.Delegate, _
ByRef args() As Object)
Dim bFired As Boolean
If ev IsNot Nothing Then
For Each singleCast As System.Delegate In _
ev.GetInvocationList()
bFired = False
Try
Dim syncInvoke As ISynchronizeInvoke = _
CType(singleCast.Target, ISynchronizeInvoke)
If syncInvoke IsNot Nothing _
AndAlso syncInvoke.InvokeRequired Then
bFired = True
syncInvoke.Invoke(singleCast, args)
Else
bFired = True
singleCast.DynamicInvoke(args)
End If
Catch ex As Exception
If Not bFired Then singleCast.DynamicInvoke(args)
End Try
Next
End If
End Sub
Let's take a look at this.
First, you'll notice I've declared an event named Connected
(implements an event defined in the interface).
Next, you see the method that raises the event. Technically, it doesn't raise the event, but it allows the AsyncSocket
to raise it by calling the method OnConnected
. This method just calls another method that is designed to generically take an event and the parameters, and raise it in a thread safe manner. The other thing you'll notice about this method is the reference to ConnectedEvent
. In Visual Basic, each event creates a delegate behind the scenes with the same name as the event you specified - just appending Event to it. So, say, you create an event called MyEvent
. VB would create a delegate called MyEventEvent
. These delegates don't even show up in the IntelliSense menus, but they exist - and help us to create this thread-safe calling method.
Now, for the RaiseEventSafe
method. The first thing we do is declare a Boolean
to make sure the event doesn't fire twice within the same call. This can happen if there is an error in the event handler code you wrote! The Try...Catch
here would catch that error and then try to call the event again - resulting in two errors. As events can have multiple handlers, we need to cycle through each handler and call it - hence the For...Each
loop. With each handler, the routine attempts to cast the target of the handler to the ISynchronizeInvoke
interface. This is necessary to get the event called on the proper thread. If the cast is successful, the event is invoked on the original thread - if not, the event is still raised, but just on the current thread.
Object serialization is handled using an in-memory BinaryFormatter
(see the ObjectPacker
class). This allows any object that can be serialized to be sent to a remote computer. This is much simpler than trying to send all the properties across and reconstructing the object yourself.
Public Function [Get](Of dataType)() As dataType
Dim byt() As Byte = _asSock.GetData()
Dim obj As Object
If LegacySupport AndAlso _
GetType(dataType) Is GetType(String) Then
obj = System.Text.Encoding.Default.GetString(byt)
Else
obj = ObjectPacker.GetObject(byt)
End If
Return DirectCast(obj, dataType)
End Function
While not overly complicated to implement, Generics support has got to be one of my favorite features. Generics allows you to call a method specifying a Type
, and the method will use the type you specified for whatever purpose. In this case, it allows you to call the Get
and Peek
methods and have them return the data as the Type
you wanted - no more messy CType
or DirectCast
conversion routines in your event handlers! Watch out though - you could cause an error if you specify a type other than the type of the object in the buffer.
Another feature I'm excited about is the design-time UI support. I was finally able to achieve what I envisioned for the 2005 version of the component - and that was event links. The event links in the Action list will create and take you to the event handler for the specified event. It will only create the event handler if an event handler isn't already specified. It all hinges on the IEventBindingService
interface and the ShowCode
method of it. Here is the code to do this (specified in the WinsockActionList
class):
Public Sub TriggerStateChangedEvent()
CreateAndShowEvent("StateChanged")
End Sub
Private Sub CreateAndShowEvent(ByVal eventName As String)
Dim evService As IEventBindingService = _
CType( _
Me.Component.Site.GetService( _
GetType( _
System.ComponentModel.Design.IEventBindingService)), _
IEventBindingService)
Dim ev As EventDescriptor = GetEvent(evService, eventName)
If ev IsNot Nothing Then
CreateEvent(evService, ev)
Me.designerActionUISvc.HideUI(Me.Component)
evService.ShowCode(Me.Component, ev)
End If
End Sub
Private Sub CreateEvent( _
ByRef evService As IEventBindingService, _
ByVal ev As EventDescriptor)
Dim epd As PropertyDescriptor = _
evService.GetEventProperty(ev)
Dim strEventName As String = Me.Component.Site.Name & _
"_" & ev.Name
Dim existing As Object = epd.GetValue(Me.Component)
If existing Is Nothing Then
epd.SetValue(Me.Component, strEventName)
End If
End Sub
Private Function GetEvent( _
ByRef evService As IEventBindingService, _
ByVal eventName As String) As EventDescriptor
If evService Is Nothing Then Return Nothing
Dim edc As EventDescriptorCollection = _
TypeDescriptor.GetEvents(Me.Component)
If edc Is Nothing Or edc.Count = 0 Then
Return Nothing
End If
Dim ed As EventDescriptor = Nothing
Dim edi As EventDescriptor
For Each edi In edc
If edi.Name = eventName Then
ed = edi
Exit For
End If
Next edi
If ed Is Nothing Then
Return Nothing
End If
Return ed
End Function
The last bit of construction that I will cover is the enhanced legacy support conversion feature.
In the last few iterations of the component (the ones that use the ObjectPacker
), sending data using legacy support required you to convert the String
to a Byte
array yourself and having the component send the Byte
array. This was necessary as the ObjectPacker
ignored the Byte
array during serialization.
Now, the component checks if legacy support is active and the data to send is a String
; the component does the conversion for you, making sending much simpler. This also goes for the Generic enhanced versions of the Get
and Peek
methods as well - so, it works on returning data as well.
Using the Component
First, make sure the component is added to your Toolbox (not 100% necessary, but the easiest way; advanced users should know how to use whatever instructions you need from below with referenced components and creating the objects purely from code).
Servers
Servers need to both listen and be able to send and receive data. As such, some of what they need to do is very similar to what the clients do. Under this section, I'll cover just the server specific routines you need to know.
I'm going to assume you'll want to handle multiple connections. If you don't wish to handle multiple connections, then you won't need to use the WinsockCollection
- but for the purposes of instruction, we will use just that.
First, you'll need a listener. Add a Winsock
component to your form, and call it wskListener
. Go ahead and set the LocalPort
property equal to the port you want your application to use - this is the port we will listen on. Adding the component to the form automatically adds a reference to the DLL, so we can use anything in the Winsock
toolset (like the WinsockCollection
).
Since we want to handle multiple connections, we will need a place to store all the connections. We do this using the WinsockCollection
. Let's add one now - add the following code to the code designer for your form: Private WithEvents _wsks As New Winsock_Orcas.WinsockCollection(True)
. This code will create a new WinsockCollection
, with the option for auto-removal of disconnected connections enabled. We also specified WithEvents
to make creating event handlers for it easier. The form code should look like this:
Public Class Form1
Private WithEvents _wsks As _
New Winsock_Orcas.WinsockCollection(True)
End Class
You can either have the listener start listening at form startup, or on a button press. Go to the event for whichever method you wish, and enter the following code: wskListen.Listen()
. This should result in some code that looks similar to this:
Private Sub cmdListen_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles cmdListen.Click
wskListener.Listen()
End Sub
You could also specify an Integer
as a parameter, telling the component what port to listen on.
The other thing you will need to do is handle the incoming connection requests. Do this by creating a handler for the ConnectionRequest
event (you can use the Action list and choose ConnectionRequest
from the events list). We need to Accept
the incoming request using the WinsockCollection
to make it active. Do this by adding the following code to the ConnectionRequest
handler: _wsks.Accept(e.Client)
. This should look something like the following:
Private Sub wskListener_ConnectionRequest( _
ByVal sender As System.Object, _
ByVal e As _
Winsock_Orcas.WinsockConnectionRequestEventArgs) _
Handles wskListener.ConnectionRequest
_wsks.Accept(e.Client)
End Sub
With the client accepted, all of its events are raised through the WinsockCollection
. As the last step of the server specific side, I'll show you how to create handlers for the WinsockCollection
events.
Here is the definition of any of the events you could possible want:
Public Event Connected( _
ByVal sender As Object, _
ByVal e As WinsockConnectedEventArgs)
Public Event ConnectionRequest( _
ByVal sender As Object, _
ByVal e As WinsockConnectionRequestEventArgs)
Public Event CountChanged( _
ByVal sender As Object, _
ByVal e As WinsockCollectionCountChangedEventArgs)
Public Event DataArrival( _
ByVal sender As Object, _
ByVal e As WinsockDataArrivalEventArgs)
Public Event Disconnected( _
ByVal sender As Object, _
ByVal e As System.EventArgs)
Public Event ErrorReceived( _
ByVal sender As Object, _
ByVal e As WinsockErrorReceivedEventArgs)
Public Event SendComplete( _
ByVal sender As Object, _
ByVal e As WinsockSendEventArgs)
Public Event SendProgress( _
ByVal sender As Object, _
ByVal e As WinsockSendEventArgs)
Public Event StateChanged( _
ByVal sender As Object, _
ByVal e As WinsockStateChangedEventArgs)
You need to construct a method with the same structure as the event you want to handle, and then tell it to handle it using the Handles
clause of the method declaration. Here's an example:
Private Sub _wsks_ErrorReceived( _
ByVal sender As System.Object, _
ByVal e As Winsock_Orcas.WinsockErrorReceivedEventArgs) _
Handles _wsks.ErrorReceived
End Sub
Typical events that are handled are DataArrival
, ErrorReceived
, Connected
, Disconnected
, and StateChanged
- this applies to both servers and clients.
Clients
A client needs to be able to Connect
to a server as well as send and receive data, but they only need to handle one connection. Add a single Winsock
to your form for the client connection, and call it wskClient
.
We now need a way to connect to the server; you can do this in the form startup or a button press - take your pick. I'll be demonstrating with a button press. You can either set the RemoteHost
and RemotePort
manually (via code or the property designer) and call the Connect
method with no parameters, or you can specify the server and port as parameters to the Connect
method - which is the method I'll be using.
Private Sub cmdClientConnect_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles cmdClientConnect.Click
wskClient.Connect(txtServer.Text, CInt(nudPort.Value))
End Sub
Here, you can see I'm getting my server and port from other controls on the form, namely a TextBox
and a NumericUpDown
control. Doing it this way allows greater flexibility for the user, though you can hard code the server and port as well, with the following code: wskClient.Connect("localhost", 8080)
. Notice, you don't have to use the IP address of the remote computer - you can also specify the name. The component automatically tries to resolve the name to the IP address for you.
Client and Server
On both the client and server, you will need to handle the DataArrival
event. This event is raised every time something is received by the component. On the server side, you'll want the handler for this event to be attached to the WinsockCollection
; on the client side, you'll want it attached to the Winsock
object.
Let's take a look at a very simple DataArrival
implementation from both the client and the server side. Let's assume an extremely simple chat application, where the server must send out to all clients but the client that sent it the message.
Server-side handler:
Private Sub SendToAllBut( _
ByVal sender As Object, _
ByVal msg As String)
For Each wsk As Winsock_Orcas.Winsock In _wsks.Values
If wsk IsNot sender Then wsk.Send(msg)
Next
End Sub
Private Sub _wsks_DataArrival( _
ByVal sender As Object, _
ByVal e As Winsock_Orcas.WinsockDataArrivalEventArgs) _
Handles _wsks.DataArrival
Dim strIn As String = CType(sender, Winsock_Orcas.Winsock).Get(Of String)()
SendToAllBut(sender, strIn)
End Sub
Notice here, there is an extra method. The WinsockCollection
doesn't currently have sending capabilities, so we need another method to send the data to all the connected clients. Even if the WinsockCollection
did have sending abilities, you may want your own method - especially, if you will require users to login first, because, then you can check to make sure they are logged in, before sending them data.
The second thing you will notice is the Of String
in the Get
method. The Visual Studio 2005 and 2008 versions support Generics, which allows the ability to cast the method as a String
, thus doing the conversion of the Object
to a String
for you, making your handling code cleaner. Unfortunately, the 2003 version of VS doesn't support Generics, so you will just get an Object
back from the Get
method, which you can then convert to a String
in any manner you see fit.
There is one other thing you might put in the server, and that is a logging ability (to a TextBox
, or a file, etc...) for feedback. We'll keep this simple, with the server only relaying messages - not sending anything itself.
Now, let's look at the client-side handler.
Private Sub wskClient_DataArrival( _
ByVal sender As Object, _
ByVal e As Winsock_Orcas.WinsockDataArrivalEventArgs) _
Handles wskClient.DataArrival
Dim msg As String = _
CType(sender, Winsock_Orcas.Winsock).Get(Of String)()
txtLog.AppendText(msg & vbCrLf)
End Sub
Again, this is rather simple. The handler retrieves the data from the Winsock
into a String
, and then adds the String
to a TextBox
on the form.
There is one other thing you need to know - though you did see it in the code above: sending data. Sending data should be very simple as well. Simply call the Send
method on the Winsock
. You must pass it the data you wish to send. This can be any object, a String
, an Integer
, even custom classes you designed - as long as you marked it as Serializable
. If you are doing a custom class, you must make sure both the client and the server have the same class in their project.
Here is an example of sending text entered in the UI:
Private Sub cmdSend_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles cmdSend.Click
Dim dt As String = txtSend.Text.Trim()
If dt <> "" Then wskClient.Send(dt)
txtSend.Text = ""
txtSend.Focus()
End Sub
First, the data is checked to make sure there is data to send, and then sent - finally clearing the TextBox
and setting the focus to it again.
Finishing Up
This is the most advanced version of the component yet, and I've really enjoyed making it. I'm sure there are plenty of bugs/gotchas in it just waiting to be discovered. If you find any, let me know, and I'll try to make improvements to it as rapidly as I can. Also, feature requests, I'd love to hear them - though they will be evaluated on difficulty/usefulness and may not make it into future versions, I'd still love to hear them.
Watch out for where you may need to use LegacySupport
. Anytime you are interacting with clients/servers that do not use this control, you must enable LegacySupport
as they won't understand the packet header this component pre-pends to the outgoing data.
One of the best things you can do if you are trying to debug something that is going wrong is to handle the ErrorReceived
event. This can help you identify any errors you seem to be having in your event handler code, because the component actually traps them during the raising of the event.
Enjoy the component!
History
- 12.13.2007 - Fixed problems with
.GetUpperBound(0)
in PacketHeader.AddHeader
, and in AsyncSocket.ProcessIncoming
. - 12.26.2007 - Added a new event,
ReceiveProgress
. - 12.28.2007 -
Winsock.Get
and Winsock.Peek
updated to check for Nothing
. SyncLock
added to all qBuffer
instances in AsyncSocket
and _buff
(ProcessIncoming
). - 02.14.2008 - Fixed a bug in UDP receiving that caused it to always receive at the full byte buffer instead of the size of the incoming data.
- 03.24.2008 - Fixed
Listen
methods to properly raise state changed events for UDP as well as TCP. Modified IWinsock
, Winsock
, and AsyncSocket
to allow AsyncSocket
to modify the LocalPort
property of the component. - 03.25.2008 - Added a
NetworkStream
property to expose a NetworkStream
object that uses the connection made by this component. - 04.21.2008 - Fixed
RaiseEventSafe
in Winsock.vb and WinsockCollection.vb to use BeginInvoke
instead of Invoked
. Changed the order of operations in ReceiveCallbackUDP
to allow a remote IP address to be detected properly.