Creating a TCP Component in Visual Basic

By Rolando Lopez

Before many Web sites built with IIS can present a Web page, a TCP/IP connection with another host to exchange data must first be established. Only then is the result presented to the browser. For example, if you need to develop a Web site that allows its users to check their email, you may create a component that implements the POP protocol to connect with a POP server and retrieve the user's email. The following figure shows how this could be accomplished.

In fact, there are quite a few commercial components offering a complete kaleidoscope of protocols: POP, SMTP, HTTP, FTP, etc.

This article describes how to build a TCP/IP component in Visual Basic that you can use to connect to another machine and exchange data. In the next article, we will use this component to write a POP component to retrieve the user's email.

Our component will use the Windows Sockets API or Winsock API directly. This is an alternative to using a third-party component that needs special licensing agreements, registration fees, or that has been designed for GUI applications and not for a component to be used inside an ASP page.

The Winsock Control that ships with Visual Basic already provides easy access to TCP and UDP services. By setting properties and invoking methods of the control, you can easily connect to a remote machine and exchange data. This control works fine in a GUI application because only when an event occurs is a special procedure called. This allows the application to continue responding to the user while also performing network operations. For example, if the program allows it, the user may establish a simultaneous connection with another host, print a document, minimize the application, etc.

However, if you base your component in the Winsock Control you may easily end up having 'Busy Waiting' conditions in which a variable is continuously tested until some value appears. This should be avoided because it wastes CPU time. For instance, your control may have an OpenConnection method that does the following:


Public Sub OpenConnection()
oWinSock.RemotePort = mlngRemotePort
oWinSock.RemoteHost = mstrRemoteHost
oWinSock.Connect
Do Until oWinSock.State = sckOpen
DoEvents
Loop
End Sub 

The DoEvents() function yields execution so that the operating system (OS) can process other threads. Right now, you are thinking that no CPU resources are being wasted by this function. However, as soon as the OS finishes those tasks or their assigned time slice expires, it will return to the OpenConnection method and again test the state of the oWinsock object. If you use the Task Manager, you will notice CPU Usage is near 100%. Although this component may actually work in the Internet Information Server (IIS), you will probably notice other ASP pages are slower than before. This is because IIS is a multithreaded server with a number of threads servicing only HTTP requests. While one or more threads are inside the Busy Waiting section, other threads may be competing for CPU cycles to complete their own request.

In a component designed for use in an ASP page, in the ideal situation, you may want to block the thread until you have a response from the remote host:



Public Sub OpenConnection()
oASPWinSock.RemotePort = mlngRemotePort
oASPWinSock.RemoteHost = mstrRemoteHost
oASPWinSock.SleepUntilConnected
End Sub 

The Winsock API has several functions that are, by default, blocking functions. This means that when you use this redesigned OpenConnect() function to connect to another computer, the thread comes to a halt while waiting for the connection to be made, or until it times out, whichever comes first. When a blocking Winsock function is called, the operating system suspends the current thread and task-switches to another. The thread consumes very little processor time while waiting for the hypothetical SleepUntilConnected function to return. In IIS, this would be a much better solution because you want as many CPU cycles available to other 'working' threads. Sure you may try to set the Winsock Control to blocking mode, but then you may face other problems with the event-driven behavior of the Winsock Control.

The component in this article will use blocking functions to perform its job, and in the process we will describe the Winsock functions required.

System and Software Requirements

The full source code with examples can be downloaded at: http://15seconds.com/files/990408.zip

This control was developed under Visual Basic 5 with Service Pack 3. The OS software was Windows NT Server 4.0 with Service Pack 4, IIS 4.0, and Windows Scripting Host. However, you could use Windows 95 or 98 running ASP with Personal Web Service.

If you have never written an ActiveX component using VB, just jump to the Component Building section here on 15 Seconds and you will find a number of articles describing this process.

A Look Ahead

Here is a quick summary of each of the properties and methods of our component. We will go into the details after this summary.

Public Properties

Name

Type

Description

RemotePort

Long

Sets the remote port number to connect to. For example, if you need to use this component to retrieve Web pages, you should set this property to 80, which is the default port number for HTTP protocol. Write-only.

RemoteHostIP

String

Sets the remote machine to which a control sends or receives data. You must provide an IP address string in dotted format, such as "100.0.1.1". Write-only.

LocalHostIP

String

Sets the IP address of the local machine as a string in dotted format. This property is very useful if your server is multihomed. Write-only.

ErrorDescription,ErrorCode

String, Long

Returns the description and code of the last Winsock error. Read-only.

To be able to connect to the remote host you must assign the right values to RemotePort, RemotHostIP and LocalHostIP.

Public Methods

Name

Parameters

Returns

Description

OpenConnection

None

Boolean

Establishes a connection with remote host.

SendData

strData

Boolean

Sends data to remote host.

ReceiveData

strData, lngBytesReceived

Boolean

Retrieves any data received from remote host.

IsDataAvailable

None

Boolean

Determines if there is any data available for ReceiveData().

ShutdownConnection

None

None

Closes connection with remote host.

Private Methods

Name

Data Type

Description

mwsaData

WSA_DATA

Used to keep Winsock startup information.

mlngSocket

Long

Descriptor identifying the socket we are using.

mlngRemotePort

Long

Remote port number to connect to

mstrLocalHostIP

String

IP address of the local machine as a string in dotted format.

msaLocalAddr

SOCK_ADDR

IP address of the local machine in the internal formatt

mstrRemoteHostIP

String

IP address of remote computer as a string in dotted format.

msaRemoteAddr

SOCK_ADDR

IP address of remote computer in the internal format

mlngLastErrorCode

Long

Error code for the last error found.

mstrLastErrorDesc

String

Error description for the last error found.

Implementation Details

The Winsock functions used are declared in the WinsockDefs.bas module. We are not going to describe all the Winsock functions available but only the ones we are actually going to use.

OpenConnection
The OpenConnection method establishes a connection to a peer. It returns true if the connection was successful. If one of the required Winsock calls fails, this method sets the appropriate values to mlnLastErrorCode and mstrLastErrorDesc, so you can use the properties ErrorDescription and ErrorCode to obtain additional info.

This is the longest method because it issues many calls, but don't worry because all others methods usually only make one main call.

This method does the following:

  1. Initialize Winsock API calling the WSAStartup(), which initializes the Winsock services and returns some information on the state of these services. You have to pass two parameters to this function. The first parameter is the Winsock version number that your application needs. For most applications, this is simply a matter of passing the value &H0101. The second parameter is a pointer to a type called WSA_DATA. This type holds information about the Winsock implementation and operating system. WSAStartup() returns zero if successful, otherwise it returns the appropriate error code itself.
  2. Once a process has made a successful WSAStartup() call, it may proceed to make other Windows Sockets calls as needed. In our case we need to create a new socket so we use the socket() function, which creates and returns a socket descriptor of type SOCKET. A SOCKET is defined as an unsigned 32-bit integer, so in Visual Basic we will use the Long type. The socket() function is declared as follows:
    
    Declare Function socket Lib "wsock32" _
    	(ByVal af As Long, ByVal type_specification As Long,_
    	ByVal protocol As Long) As Long
    
    
    The returned socket is stored in the private member mlngSocket and is used in much the same way that you would use a file descriptor. You must pass this socket to access most of the Winsock functions including those performing send or receive operations.

    The socket() function requires three parameters that tell what type of socket to create. The first parameter specifies which network protocol to use, but as long as we are using Winsock 1.1, this network protocol is the constant PF_INET (Internet Protocol).

    The second parameter specifies which type of socket to create. All the applications that use sockets will use stream sockets, which are connection-based sockets, so SOCK_STREAM is the type we need. To be able to send and receive data through the socket, each application socket must be connected to a corresponding socket in another computer. You cannot use a stream socket to send a broadcast message to all computers on a network. Thanks to the overhead associated with establishing and maintaining a connection to another computer, stream sockets do have guaranteed packet delivery in the original packet order.

    The last parameter is the protocol that you are using. In most cases, you use the Internet Protocol, which is defined as 0 in the Winsock definition.

    If a new socket cannot be created, the constant INVALID_SOCKET is returned. If the function returns this error value, the OpenConnection method will call the private method SetLastErrorCode. This method sets the error code and description using the WSAGetLastError() function, which gets the error status for the last operation that failed.

  3. Now that we have a new socket in mlngSocket, we need to associate our local address with it. By default, a socket is automatically assigned an address when you try to connect to another system, but if you want to use a specific address you use the bind() function. In many cases, IIS Web sites are multihomed, and you may want to specify which IP address to use. This is the only reason for the public property LocalHostIP.

    The bind() function and its related types can be declared as follows:

    
    Type IN_ADDR
        S_addr As Long
    End Type
    
    Type SOCK_ADDR
        sin_family As Integer
        sin_port As Integer
        sin_addr As IN_ADDR
        sin_zero(0 To 7) As Byte
    End Type
    
    Declare Function inet_addr Lib "wsock32"_
    	(ByVal cp As String) As Long
    
    Declare Function bind Lib "wsock32"_
    	(ByVal s As Long, addr As SOCK_ADDR,_
    	ByVal namelen As Long) As Long
    
    
    The first parameter of the bind() function is the socket descriptor that is to be bound to the address specified. The socket() function returns this socket. The third parameter is the length of the socket address record that is being passed as the second parameter. This is a simple matter of calling the Len function to retrieve the record size of the SOCK_ADDR type.

    The declaration for the bind() function shows that the second parameter is a pointer to a record structure called SOCK_ADDR. In this record, the sa_family integer is the address type, which usually is PF_INET for the Internet address family. The second integer, sin_port, is the TCP port to which this socket is to be bound. According to the documentation, if the port is specified as 0, the Windows Sockets implementation will assign a unique port to the application with a value between 1024 and 5000. We use this zero value since usually it does not matter which source port you are using.

    The actual address goes into sin_addr, which is declared as another record structure. In the original Winsock.h header, this structure is defined as a union that provides three different ways to access and manipulate the Internet address to be used. Since Visual Basic does not provide union structures, we will use only the sin_addr as a 32-bit integer. This does not represent a problem because instead of giving the integer representation for an IP address we will use the inet_addr() function to convert an IP address as a string in dotted format to its 32-bit representation.

    If you want the component to use the default address configured for the computer on which the component is called, you can specify this address by setting the S_addr variable to the defined constant INADDR_ANY. This constant tells the bind() function to assign the passed socket to the Internet address already in use by the computer on which it is running.

    The sin_zero array is not used in the SOCK_ADDR type.

  4. Finally we need to connect with the remote host using the connect() function. Its declaration is much like the definition for the bind() function. It uses the same three parameters:
    
    Declare Function connect Lib "wsock32"_
    	(ByVal s As Long, name As SOCK_ADDR,_
    	ByVal namelen As Integer) As Long
    
    
    The main difference is that in the connect() function, you are concerned with the address of the socket on the other end of the connection. Therefore, you must populate the SOCK_ADDR record with the other computer's address and port.

    Remember we are using blocking functions, so the thread will block until the connection is established or until it times out.

    Finally, here is the complete OpenConnection method:

    
    Public Function OpenConnection() As Boolean
    Dim WSAResult As Long
        
        OpenConnection = False
         
        'Initialize Winsock API
        WSAResult = WSAStartup(&H101, mwsaData)
        If WSAResult <> WSANOERROR Then
           SetLastErrorCode "Error en OpenConnection::WSAStartup"
           Exit Function
        End If
        
         'Create new socket
         mlngSocket = socket(PF_INET, SOCK_STREAM, 0)
         If (mlngSocket = INVALID_SOCKET) Then
            SetLastErrorCode "Error in OpenConnection::socket()"
            Exit Function
         End If
        
         'Bind socket to Local IP
         msaLocalAddr.sin_family = PF_INET
         msaLocalAddr.sin_port = 0
         msaLocalAddr.sin_addr.S_addr = inet_addr(mstrLocalHostIP)
         If (msaLocalAddr.sin_addr.S_addr = INADDR_NONE) Then
            SetLastErrorCode "Error in OpenConnection::inet_addr()"
            Exit Function
         End If
         WSAResult = bind(mlngSocket, msaLocalAddr, Len(msaLocalAddr))
         If (WSAResult = SOCKET_ERROR) Then
            SetLastErrorCode "Error in OpenConnection::bind()"
            Exit Function
         End If
        
         'Connect with remote host
         msaRemoteAddr.sin_family = PF_INET
         msaRemoteAddr.sin_port = htons(mlngRemotePort)
         msaRemoteAddr.sin_addr.S_addr = inet_addr(mstrRemoteHostIP)
         If (msaLocalAddr.sin_addr.S_addr = INADDR_NONE) Then
            SetLastErrorCode "Error in OpenConnection::inet_addr()"
            Exit Function
         End If
         msaRemoteAddr.sin_zero(0) = 0
         WSAResult = connect(mlngSocket, msaRemoteAddr, Len(msaRemoteAddr))
         If (WSAResult = SOCKET_ERROR) Then
            SetLastErrorCode "Error in OpenConnection::connect()"
         Else
            OpenConnection = True
         End If
    End Function
    
    

SendData

Once you have established a socket connection with another computer, you want to be able to send data through that connection. The SendData() method receives a string that must be sent to the remote host. It returns true if the operation is successful. If some error occurs, then you can retrieve additional info using the ErrorCode and ErrorDescription properties. Sending data through a socket is a simple matter of using the send() function, which is declared as follows:


Declare Function send Lib "wsock32"_
	(ByVal s As Long, buffer As Any,_
	ByVal length As Long, ByVal flags As Long) As Long

The first parameter passed to this function is the socket connection through which to send the data. The second parameter is a pointer to a buffer that contains data to be sent. This buffer is a byte array passed directly to the function. To pass an entire numeric array you pass the first element of the array by reference. This works since numeric array data is always laid out sequentially in memory. The third parameter is the number of bytes to send. The last parameter being passed to the send() function is a flag that tells the socket how to send the data. We don't need any special sending instructions, so this flag is zero.

The send() function returns the number of bytes that were sent, unless an error occurred, in which case it returns the SOCKET_ERROR result code. Since we are using blocking functions, the send() function will not return until the data has completely been sent or an error occurs.

The complete SendData() method is as follows:


Public Function SendData(ByVal strData As String) As Boolean
    Dim WSAResult As Long, i As Long, l As Long
      
    l = Len(strData)
    ReDim Buff(l + 1) As Byte
    
    For i = 1 To l
        Buff(i - 1) = Asc(Mid(strData, i, 1))
    Next
    Buff(l) = 0

    WSAResult = send(mlngSocket, Buff(0), l, 0)
    If WSAResult = SOCKET_ERROR Then
        SetLastErrorCode "Error en SendData::send"
        SendData = False
    Else
        SendData = True
    End If
End Function

ReceiveData

This method retrieves any data the remote host has sent through the socket. It receives a variant about where to store the data and another variant about where to store the number of bytes retrieved. The method uses variant parameters since it is designed especially for use in scripting languages like VBScript where all variables are variant types. To receive data, you use the recv() function defined as:


Declare Function recv Lib "wsock32"_
	(ByVal s As Long, buffer As Any,_
	ByVal length As Long, ByVal flags As Long) As Long

This function works much like the send() function. The primary difference between send() and recv() is that the buffer pointer passed to the recv() function should point to an empty buffer, with the buffer length parameter specifying how much memory was allocated for the buffer.

Like the send() function, the recv() function returns the number of bytes that were received. If an error occurs, the function returns SOCKET_ERROR. The recv() function is also a blocking function, so it will not return until the remote host has sent something or the connection is lost. You can use the IsDataAvailable() method to determine whether there is some data to retrieve or not.

If recv() succeeds, we must convert the array of bytes to a variant string, so we use the StrConv() function on this array.

Here is the ReceiveData() method:


Public Function ReceiveData(strData, lngBytesReceived) As Boolean
    Const MAX_BUFF_SIZE = 10000
    Dim Buff(0 To MAX_BUFF_SIZE) As Byte
    Dim WSAResult As Long
    
    WSAResult = recv(mlngSocket, Buff(0), MAX_BUFF_SIZE, 0)
    If WSAResult = SOCKET_ERROR Then
        SetLastErrorCode "Error in RecvData::recv"
        strData = ""
        lngBytesReceived = 0
        ReceiveData = False
    Else
        lngBytesReceived = WSAResult
        Buff(lngBytesReceived) = 0
        strData = Left(StrConv(Buff(), vbUnicode), lngBytesReceived)
        ReceiveData = True
    End If
End Function

IsDataAvailable

Determines if there is any data available for ReceiveData(). It returns true if there is any data to be received. If this function returns False and you call ReceiveData(), your thread will block until the remote host has sent any data through the connection. Most protocols built on top of TCP/IP provide some means to determine if the data is complete or not. For example, the POP protocol used to retrieve email sends an end-of-message marker to indicate that the message has completely been sent. This minimizes the use of functions such as IsDataAvailable().

To determine the status of one or more sockets, you can use the select() function. Since select can be misunderstood by the Visual Basic compiler we will use the alias sselect() instead:



Public Const FD_SETSIZE = 64
Type FD_SET
    fd_count As Long
    fd_array(0 To FD_SETSIZE - 1) As Long
End Type
Type TIME_VAL
    tv_sec As Long
    tv_usec As Long
End Type

Declare Function sselect Lib "wsock32"_ 
	Alias "select" (ByVal nfds As Long, readfds As FD_SET,_
	writefds As FD_SET, exceptfds As FD_SET,_
	timeout As TIME_VAL) As Long

The first parameter of the sselect() function is nfds, which was used in the original Berkeley Sockets API. It is included in the Windows Sockets version of the sselect() function to be compatible with the Berkeley Sockets function of the same name, but it is ignored in Windows Sockets.

The readfds, writefds, and exceptfds parameters identify a set of sockets for which read, write, or error status is of interest. Upon return from the function, it updates the set of descriptors with the subset for which the condition is true. For instance, if a program wants to find out whether data is waiting in the queues of three sockets, it will specify those socket descriptors in the readfds set. If only one of the sockets specified is readable at the time of the call to sselect(), it removes the other socket descriptors from the readfds set, leaving only the readable socket and returning the number of sockets meeting the conditions. In the previous example, this value would be one.

The timeout parameter limits the amount of time that the sselect() function waits for a socket to become readable, to be writable to, or to have an exception occur. The timeout parameter is a pointer to a TIME_VAL type, which specifies the number of seconds and microseconds to wait.

In the FD_SET record, fd_count indicates the number of sockets available in the array fd_array. This array must be populated with the sockets you are interested in. In our case, this value will be mlngSocket, and the fd_count will be 1.

The readfds will be the only array populated by the IsDataAvailable() method. As you can see in the following definition for the IsDataAvailable() method, if the function sselect() returns a value greater than zero, it means that mlngSocket is readable because there is some data waiting to be retrieved from the Winsock buffers.


Public Function IsDataAvailable() As Boolean
    Dim readfds As FD_SET, writefds As FD_SET, exceptfds As FD_SET
    Dim timeout As TIME_VAL
    Dim lngResult As Long, nfds As Long
    
    nfds = 0
    timeout.tv_sec = 1
    timeout.tv_usec = 0
    
    readfds.fd_count = 1
    readfds.fd_array(0) = mlngSocket
    writefds.fd_count = 0
    exceptfds.fd_count = 0
    
    lngResult = sselect(nfds, readfds, writefds, exceptfds, timeout)
    If lngResult = SOCKET_ERROR Then
        SetLastErrorCode "Error in IsDataAvailable::select"
        IsDataAvailable = False
    Else
        If lngResult > 0 Then IsDataAvailable = True Else IsDataAvailable = False
    End If
End Function

ShutdownConnection

After you finish with a socket, you must close it. Remember that a socket is much like a file. After you finish reading from or writing to a file, you must close it also. This can be achieved by calling the closesocket() function on any mlngSocket. However under most circumstances, you must shut down a socket before closing it. You can do this with the shutdown() function which, tells the socket to stop sending or receiving data, depending on the shutdown method specified. If you specify the shutdown method number two, then send and receive operations are no longer allowed. We also use the WSACancelBlockingCall() function to cancel any pending blocking calls, and finally the WSACleanup() function is called to perform the actual cleanup and shutdown operations.

Theoretically, you should capture and check the return value from all of these functions to see whether an error occurred, and although you may add those checks yourself, none are much of a concern when you are terminating the component.


Public Sub ShutdownConnection()
     shutdown(mlngSocket, 2)
     closesocket(mlngSocket)
     WSACancelBlockingCall
     WSACleanup
     mlngSocket = 0
End Sub


Samples

The following script uses our component to test a Web server. If it can't connect, it sends an email to the administrator. You need the SMTP service installed on the computer from which the script is to be executed. The SMTP service ships with IIS 4. You could schedule this script with the 'AT' command to monitor the Web service periodically.


Dim tcp, strData, l, bOk
Dim objNewMail

Set tcp = WScript.CreateObject("VBWinsock.TCPIP")
tcp.LocalHostIP = "100.0.1.1"
tcp.RemoteHostIP = "100.0.1.2"
tcp.RemotePort = 80

bOk = True
bOk = bOk AND tcp.OpenConnection
bOk = bOk AND tcp.SendData("GET http://100.0.1.2/somepage.htm" & Chr(13) & Chr(10))
bOk = bOk AND tcp.ReceiveData(strData, l)
tcp.ShutdownConnection
Set tcp = Nothing

If Not bOk Then 
	Set objNewMail = WScript.CreateObject("CDONTS.NewMail") 
	objNewMail.From = "monitor@domain.com"
	objNewMail.To = "admin@domain.com"
	objNewMail.Subject = "100.0.1.2 failed at " & Now
	objNewMail.Importance = 1
	objNewMail.Body = "The script couldn't retrieve the test page."
	objNewMail.Send
	Set objNewMail = Nothing
End If


More Samples

If you download the source code, you will find the script test_tcp.vbs, which retrieves a Web page. Additionally you will find an ASP page that can be used with IIS to retrieve any Web page you specify. The Visual Basic project also includes a test project for our in-process component, so you may want to use the VBWinsock.vbg included in the source code.

Remember that the component can be used with most of the protocols built on top of TCP/IP. These are only a few samples.

What's next?

In the next article, I will use this component to write a POP component that allows for reading email from a Web page. In the process, we will review the POP protocol, its commands, and responses.

About the Author

Rolando Lopez, a software engineer who has been working in ASP and Visual Basic for 18 months, has designed and implemented several components used in newspapers, eCommerce, and other sites.



Downloads

Comments

  • Excellent

    Posted by AYDIN EBRAHIMI HOMAY on 07/27/2013 09:26pm

    Really good article so thank you ...

    Reply
  • File 990408.zip was not found

    Posted by Skand Bhargava on 12/24/2012 06:22am

    Thanks for a great article. I have been reading about problem related to winsock version problems while distributing an application. an this problem be solved by including the winsock dll in distribution pack? Is there a better way of communicating using TCP/Ip between computer and another device now? I am using MS Access 2000 (developers version).

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • In this on-demand webcast, Oracle ACE and Toad Product Architect Bert Scalzo discusses 10 powerful and hidden features in Toad® that help increase your productivity and DB performance. Watch this webcast today.

  • As a development and deployment platform, RHEL offers an efficient, scalable, and robust operating environment with certified security and flexible deployment options in physical and virtualized environments. To assess and quantify the business benefits of RHEL, IDC recently conducted in-depth interviews with IT staff members of 21 companies using RHEL servers. The organizations represent a broad range of industries and have an average of 22,700 employees. RHEL servers accounted for 23% of the servers …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds