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
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.
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
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:
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.
The bind() function and its related types can be declared as follows:
Type IN_ADDR
S_addr As Long
End TypeType SOCK_ADDR
sin_family As Integer
sin_port As Integer
sin_addr As IN_ADDR
sin_zero(0 To 7) As Byte
End TypeDeclare Function inet_addr Lib “wsock32″_
(ByVal cp As String) As LongDeclare 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.
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 LongOpenConnection = 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 Longl = Len(strData)
ReDim Buff(l + 1) As ByteFor i = 1 To l
Buff(i – 1) = Asc(Mid(strData, i, 1))
Next
Buff(l) = 0WSAResult = 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
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.
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.
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 Longnfds = 0
timeout.tv_sec = 1
timeout.tv_usec = 0readfds.fd_count = 1
readfds.fd_array(0) = mlngSocket
writefds.fd_count = 0
exceptfds.fd_count = 0lngResult = 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 = “[email protected]”
objNewMail.To = “[email protected]”
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.