Click here to Skip to main content
15,891,657 members
Articles / Programming Languages / Visual Basic
Article

The Ultimate Socket Library

Rate me:
Please Sign up or sign in to vote.
4.73/5 (46 votes)
13 Jun 20044 min read 270.6K   2.9K   64   77
All what you need to know about sockets.

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:

VB
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):

VB
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.

VB
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:

VB
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.

Conclusion

Enough abstract and give me the code, this is how you are thinking right now. This is not yet a full proof library; I think there are still more for this small monster to grow. Hope I got your attention and I will appreciate your feedback.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United Arab Emirates United Arab Emirates
You can check my blog for more information.

Comments and Discussions

 
QuestionUnhandled exception - syncObject.Invoke ?? Pin
JenniferH4-Oct-04 10:29
JenniferH4-Oct-04 10:29 
AnswerRe: Unhandled exception - syncObject.Invoke ?? Pin
JenniferH5-Oct-04 0:07
JenniferH5-Oct-04 0:07 
GeneralRe: Unhandled exception - syncObject.Invoke ?? Pin
The Silence7-Oct-04 4:14
The Silence7-Oct-04 4:14 
GeneralRe: Unhandled exception - syncObject.Invoke ?? Pin
sysgod9-Jan-05 17:47
sysgod9-Jan-05 17:47 
GeneralUsing this lib with Bytes array Pin
sampoo21-Sep-04 8:53
sampoo21-Sep-04 8:53 
GeneralRe: Using this lib with Bytes array Pin
The Silence21-Sep-04 23:01
The Silence21-Sep-04 23:01 
GeneralRe: Using this lib with Bytes array Pin
sampoo24-Sep-04 4:50
sampoo24-Sep-04 4:50 
GeneralASYNC TCP Non-Blocking Multi - Sockets Class Pin
M@dHatter3-Jul-04 15:57
M@dHatter3-Jul-04 15:57 
This is my class that works like winsock. It shows how to make sockets that are async and non blocking and that can handle multiple connections.

'#Coded By: Adam Smith
'#Date Created: Dec 12, 2003
'#Description: ASYNC TCP Non-Blocking Multi - Sockets Class
'#Contact Email: ibulwark@hotmail.com

#Region "Imports"
Imports System
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
#End Region

Namespace ASYNC_TCP

Public Class StateObject

'Connection StateObject-------------------
Public WorkSocket As Socket = Nothing
Public BufferSize As Integer = 32767
Public Buffer(32767) As Byte
Public StrBuilder As New StringBuilder
'-----------------------------------------

End Class

Public Class TCP_Server

#Region "Events"

'Class Public Events-----------------------------
Public Event onDataReceived(ByVal IPAddress As String, ByVal DataOf As String, ByVal CGUID As String)
Public Event onConnection(ByVal CGUID As String, ByVal IPAddress As String, ByVal ConnectionDate As Date)
Public Event onDisConnection(ByVal CGUID As String, ByVal DisConnection_Date As Date)
Public Event onError(ByVal Message As String, ByVal LineNumber As Integer, ByVal FunctionName As String)
Public Event onSendComplete(ByVal CGUID As String)
'------------------------------------------------

#End Region

#Region "Structures"

Private Structure ClientConnections

'Client Computer Connections------------
Public SSocket As ASYNC_TCP.TCP_Socket
Public IPAddress As String
Public CGUID As String
Public SocketState As String
Public ConnectionDate As Date
'---------------------------------------

End Structure

#End Region

#Region "Variables"

'Make Class Variables---------------
Private P_IPAddress As String
Private P_Port As Integer
Private P_NumberOfConnections As Integer
Private P_TotalConnections As Integer
'-----------------------------------

'Socket Variables-------------------
Private SSocket As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
Private Clients() As ClientConnections
'-----------------------------------

#End Region

#Region "Properties"

'Connection Properties----------------------------
Public ReadOnly Property IPAddress()
Get
Return P_IPAddress
End Get
End Property
Public ReadOnly Property Port()
Get
Return P_Port
End Get
End Property
Public ReadOnly Property NumberOfConnections()
Get
Return P_NumberOfConnections
End Get
End Property
Public ReadOnly Property TotalConnections()
Get
Return P_TotalConnections
End Get
End Property
'-------------------------------------------------

#End Region

#Region "Public Subs and Functions"

Public Sub Close()

Try

'Kill All Connections----------------
Shutdown_All_Connections()
'------------------------------------

'Close Connection-------------
SSocket.Close()
SSocket = Nothing
ReDim Clients(0)
'-----------------------------

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "Close")
Err.Clear()
'--------------------------------

End Try

End Sub
Public Sub doListen(ByVal IPAddress As String, ByVal Port As Integer, ByVal NumberOfConnections As String)

'Make IpEndPoint------------------
Dim ListenAddress As IPEndPoint = New IPEndPoint(Dns.GetHostByAddress(IPAddress).AddressList(0), Port)
'---------------------------------

'Reset Clients-----------
ReDim Clients(0)
P_TotalConnections = 0
'------------------------

Try

SSocket.Bind(ListenAddress)
SSocket.Listen(NumberOfConnections)
SSocket.BeginAccept(New AsyncCallback(AddressOf doIncomingConnection), SSocket)

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "doListen")
Err.Clear()
'--------------------------------

End Try

End Sub
Public Sub Send(ByVal tmp_Data As String, ByVal CGUID As String)

Try

Dim i As Integer
For i = 1 To UBound(Clients)

If Clients(i).CGUID = CGUID Then
Clients(i).SSocket.Send(tmp_Data)
Exit Sub
End If

System.Windows.Forms.Application.DoEvents()

Next


Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "Send")
Err.Clear()
'--------------------------------

End Try

End Sub
Public Sub SendAll(ByVal tmp_Data As String)

Try

Dim i As Integer
For i = 1 To UBound(Clients)

Clients(i).SSocket.Send(tmp_Data)

System.Windows.Forms.Application.DoEvents()

Next


Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "SendAll")
Err.Clear()
'--------------------------------

End Try

End Sub
Public Function GET_GUIDS() As String

Try
Dim i As Integer
Dim CGUIDS As String
If UBound(Clients) > 0 Then
For i = 1 To UBound(Clients)

If Clients(i).SocketState = True Then
CGUIDS += Clients(i).CGUID & ","
End If

System.Windows.Forms.Application.DoEvents()

Next

Return CGUIDS

End If

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "GET_GUIDS")
Err.Clear()
'--------------------------------

End Try

End Function
Public Sub Disconnect_User(ByVal CGUID As String)

Try
Dim i As Integer

If UBound(Clients) > 0 Then
For i = 1 To UBound(Clients)

If Clients(i).CGUID = CGUID Then
Clients(i).SSocket.Close()
Exit Sub
End If

System.Windows.Forms.Application.DoEvents()
Next
End If

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "Disconnect_User")
Err.Clear()
'--------------------------------

End Try

End Sub

#End Region

#Region "Helper Subs and Functions"

Private Sub DisConnect_Client(ByVal CGUID As String)

Dim i As Integer

For i = 1 To UBound(Clients)

If Clients(i).CGUID = CGUID Then
Clients(i).SocketState = False
Clients(i).SSocket.Close()
Exit Sub
End If

System.Windows.Forms.Application.DoEvents()

Next
End Sub
Private Function OpenSocket() As Integer

'Variables-----------------
Dim p As Integer
'--------------------------

For p = 1 To UBound(Clients) + 1

Try

'Check to see if socket is open----------------
If Clients(p).SocketState = False Then
Return p
Exit For
End If
'----------------------------------------------

Catch ex As Exception

'Return new socket if error--------------------
ReDim Preserve Clients(UBound(Clients) + 1)
Return UBound(Clients)
Exit For
'----------------------------------------------

End Try

Next

End Function
Private Sub Shutdown_All_Connections()

Try

Dim i As Integer
For i = 1 To UBound(Clients)

Clients(i).SSocket.Close()
Clients(i).SocketState = False
System.Windows.Forms.Application.DoEvents()

Next

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "Shutdown_All_Connections")
Err.Clear()
'--------------------------------

End Try

End Sub
Private Sub doIncomingConnection(ByVal ar As IAsyncResult)

Try
Dim obj_Socket As Socket = CType(ar.AsyncState, Socket)
Dim obj_Connected As Socket = obj_Socket.EndAccept(ar)
Dim tmp_GUID As String = System.Guid.NewGuid.ToString
Dim RemoteIP As IPEndPoint = CType(obj_Connected.RemoteEndPoint, IPEndPoint)

'Make New Connection-------------------------
Dim x As Integer = OpenSocket()
Clients(x).SSocket = New ASYNC_TCP.TCP_Socket(obj_Connected, tmp_GUID)
Clients(x).IPAddress = RemoteIP.Address.ToString
Clients(x).CGUID = tmp_GUID
Clients(x).SocketState = True
Clients(x).ConnectionDate = Now()
'--------------------------------------------

'Add Event Handlers--------------------------
AddHandler Clients(x).SSocket.onDataReceived, AddressOf DataReceived
AddHandler Clients(x).SSocket.onDisConnection, AddressOf DisConnection
AddHandler Clients(x).SSocket.onError, AddressOf ErrorMsg
AddHandler Clients(x).SSocket.onSendComplete, AddressOf SendComplete
'--------------------------------------------

'Update Total Connections--------------------
P_TotalConnections += 1
'--------------------------------------------

RaiseEvent onConnection(tmp_GUID, RemoteIP.Address.ToString, Clients(x).ConnectionDate)

obj_Socket.BeginAccept(New AsyncCallback(AddressOf doIncomingConnection), obj_Socket)

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "onIncomingConnection")
Err.Clear()
'--------------------------------

End Try

End Sub

#End Region

#Region "Event Subs and Functions"

Private Sub DataReceived(ByVal IPAddress As String, ByVal DataOf As String, ByVal CGUID As String)
RaiseEvent onDataReceived(IPAddress, DataOf, CGUID)
End Sub
Private Sub ErrorMsg(ByVal Message As String, ByVal LineNumber As Integer, ByVal FunctionName As String)
RaiseEvent onError(Message, LineNumber, FunctionName)
End Sub
Private Sub DisConnection(ByVal CGUID As String, ByVal DisConnection_Date As Date)

'Subtract one from total connections----
P_TotalConnections -= 1
'---------------------------------------

'Disconnect Client---------
DisConnect_Client(CGUID)
'--------------------------

RaiseEvent onDisConnection(CGUID, DisConnection_Date)

End Sub
Private Sub SendComplete(ByVal CGUID As String)
RaiseEvent onSendComplete(CGUID)
End Sub

#End Region

End Class

Public Class TCP_Socket

#Region "Events"

'Public Class Events-----------------------------
Public Event onDataReceived(ByVal IPAddress As String, ByVal DataOf As String, ByVal CGUID As String)
Public Event onDataSent(ByVal CGUID As String)
Public Event onError(ByVal Message As String, ByVal LineNumber As Integer, ByVal FunctionName As String)
Public Event onDisConnection(ByVal CGUID As String, ByVal DisConnection_Date As Date)
Public Event onSendComplete(ByVal CGUID As String)
'------------------------------------------------

#End Region

#Region "Properties"

'Class Properties------------------------
Public ReadOnly Property CGUID() As String
Get
Return P_CGUID
End Get
End Property
'----------------------------------------

#End Region

#Region "Class Variables"

'Class Variables--------------------
Private P_CGUID As String
Private P_tmpSocket As Socket
'-----------------------------------

#End Region

#Region "Public Subs and Functions"

Public Sub New(ByVal tmp_Socket As Socket, ByVal tmp_SocketID As String)

Try
P_CGUID = tmp_SocketID
P_tmpSocket = tmp_Socket
Dim obj_Socket As Socket = tmp_Socket
Dim obj_SocketState As New StateObject
obj_SocketState.WorkSocket = obj_Socket

obj_Socket.BeginReceive(obj_SocketState.Buffer, 0, obj_SocketState.BufferSize, 0, New AsyncCallback(AddressOf onDataArrival), obj_SocketState)

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "New")
Err.Clear()
'--------------------------------

End Try

End Sub
Public Sub Send(ByVal tmp_Data As String)

Try

Dim obj_StateObject As New StateObject
obj_StateObject.WorkSocket = P_tmpSocket
Dim Buffer As Byte() = Encoding.ASCII.GetBytes(tmp_Data)

P_tmpSocket.BeginSend(Buffer, 0, Buffer.Length, 0, New AsyncCallback(AddressOf SendComplete), obj_StateObject)

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "Send")
Err.Clear()
'--------------------------------

End Try

End Sub
Public Sub Close()

Try
P_tmpSocket.Shutdown(SocketShutdown.Both)
P_tmpSocket.Close()

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "Close")
Err.Clear()
'--------------------------------

End Try

End Sub

#End Region

#Region "Helper Subs and Functions"

Private Sub onDataArrival(ByVal ar As IAsyncResult)

Dim obj_SocketState As StateObject = CType(ar.AsyncState, StateObject)
Dim obj_Socket As Socket = obj_SocketState.WorkSocket
Dim RemoteIP As IPEndPoint = CType(obj_Socket.RemoteEndPoint, IPEndPoint)

Try

Dim BytesRead As Integer = obj_Socket.EndReceive(ar)

If BytesRead > 0 Then

RaiseEvent onDataReceived(RemoteIP.Address.ToString, Encoding.ASCII.GetString(obj_SocketState.Buffer, 0, BytesRead), P_CGUID)
obj_Socket.BeginReceive(obj_SocketState.Buffer, 0, obj_SocketState.BufferSize, 0, New AsyncCallback(AddressOf onDataArrival), obj_SocketState)

End If

Catch e As Exception

If Err.Description = "An existing connection was forcibly closed by the remote host" Then
RaiseEvent onDisConnection(P_CGUID, Now())
End If

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "onDataArrival")
Err.Clear()
'--------------------------------

End Try

End Sub
Private Sub SendComplete(ByVal ar As IAsyncResult)

Try
Dim obj_SocketState As StateObject = CType(ar.AsyncState, StateObject)
Dim obj_Socket As Socket = obj_SocketState.WorkSocket
Dim send As Integer = obj_Socket.EndSend(ar)

RaiseEvent onSendComplete(P_CGUID)

Catch ex As Exception

'Make Error Message--------------
RaiseEvent onError(Err.Description, Err.Erl, "SendComplete")
Err.Clear()
'--------------------------------

End Try

End Sub

#End Region

End Class

End Namespace

Latez,
Vector

GeneralRe: ASYNC TCP Non-Blocking Multi - Sockets Class Pin
The Silence3-Jul-04 21:06
The Silence3-Jul-04 21:06 
GeneralRe: Article Quality... Pin
Anonymous14-Jun-04 16:22
Anonymous14-Jun-04 16:22 
GeneralRe: Article Quality... Pin
The Silence14-Jun-04 19:16
The Silence14-Jun-04 19:16 
GeneralRe: Article Quality... Pin
Corinna John14-Jun-04 21:29
Corinna John14-Jun-04 21:29 
GeneralRe: Article Quality... Pin
Bobby Cannon15-Jun-04 1:19
Bobby Cannon15-Jun-04 1:19 
GeneralThread Pool maybe not such a good idea Pin
Lonnie McCullough14-Jun-04 12:22
Lonnie McCullough14-Jun-04 12:22 
GeneralRe: Thread Pool maybe not such a good idea Pin
The Silence14-Jun-04 19:57
The Silence14-Jun-04 19:57 
GeneralRe: Thread Pool maybe not such a good idea Pin
Lee Alexander14-Jun-04 21:08
Lee Alexander14-Jun-04 21:08 
GeneralRe: Thread Pool maybe not such a good idea Pin
The Silence14-Jun-04 21:58
The Silence14-Jun-04 21:58 
GeneralRe: Thread Pool maybe not such a good idea Pin
Lee Alexander14-Jun-04 22:05
Lee Alexander14-Jun-04 22:05 
GeneralRe: Thread Pool maybe not such a good idea Pin
The Silence14-Jun-04 22:14
The Silence14-Jun-04 22:14 
GeneralRe: Thread Pool maybe not such a good idea Pin
Lee Alexander14-Jun-04 23:33
Lee Alexander14-Jun-04 23:33 
GeneralRe: Thread Pool maybe not such a good idea Pin
The Silence15-Jun-04 0:14
The Silence15-Jun-04 0:14 
GeneralRe: Thread Pool maybe not such a good idea Pin
Hugo Hallman15-Jun-04 2:58
Hugo Hallman15-Jun-04 2:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.