A VBX for UDP
A custom control for network development
Frank E. Redmond III
Frank is the director of software development for a distributed-information-systems company in Michigan. He can be contacted via CompuServe at 76352,343.
A variety of communication protocols are available for building network applications: IPX/SPX, NetBIOS, and Named Pipes, to name a few. On a network-related project I was recently involved in, however, we decided to implement TCP/IP, primarily because it is independent of both network topology and platform.
Our job was to facilitate communication over a LAN between several applications; therefore, nearly 70 percent of the code I wrote was communications related. Initially, our applications were to be deployed on Windows-based machines, but since we eventually intend to support platforms such as Macintosh and UNIX, our network communications protocol had to be as platform independent as possible; hence, our decision to support TCP/IP.
To create the user interface on the front end, I turned to Visual Basic (VB), which sped up the development process and allowed me to encapsulate the network-related code into a VB Custom Control (VBX). In this article, I'll present that VBX, along with an overview of TCP/IP protocols. Additionally, I'll briefly describe how to port VBXs to OLE Custom Controls (OCXs) to ensure compatibility with future versions of Visual Basic and other development environments.
TCP/IP Overview
Transmission Control Protocol (TCP) and Internet Protocol (IP) are the two most important protocols of the Internet Protocol Suite. TCP/IP is topology independent: It works with bus, ring, and star network topologies, spanning LANs and WANs. Since TCP/IP is freely available for independent implementation, it is also network-operating-system and platform independent. In addition, TCP/IP is independent of the physical network hardware: It can be used with Ethernet, token ring, and others.
Of the two, IP is more important because it's used by all other TCP/IP protocols. IP is responsible for moving data from computer to computer using an "IP address," a unique 32-bit number assigned to each computer. Other higher-level protocols are used to move data between programs on different computers by way of a "port," a unique 16-bit number assigned to each program. Combined, an IP address and a program port number constitute a TCP/IP socket, which uniquely identifies every program on every computer. The two most popular protocols that rely on TCP/IP sockets are TCP and User Datagram Protocol (UDP).
TCP is considered "connection oriented," because the two communicating machines exchange a handshaking dialogue before data transmission begins. TCP uses a checksum to validate received data. If the checksums don't match, the sender automatically resends the data, without programmer intervention; thus TCP guarantees that data is delivered, and delivered in order. TCP is best used for stream-oriented data.
UDP is considered "connectionless." It requires less overhead since there's no handshaking. However, UDP provides unreliable data delivery: Data may be duplicated, arrive out of order, or not arrive at all. Though unreliable, UDP is very efficient and allows you to utilize your own ACK/NAK. UDP is best suited for record-oriented data where all of the information can be sent in one packet.
TCP/IP supports Windows-based machines via the Windows Sockets (WinSock) API, which lets you write applications to the WinSock specification. Applications will then run on any Winsock-compatible TCP/IP protocol stack. The WinSock API is in the form of a DLL--WINSOCK.DLL for 16-bit applications and WSOCK32.DLL for 32-bit apps. You simply write to the WinSock API and link your applications with the appropriate library.
WinSock API functions fall into one of three categories: Windows Sockets functions, Windows Sockets database functions, and Windows-specific functions. Windows Sockets functions are a subset of the Berkeley sockets routines; see Table 1. Windows Sockets database functions convert the human-readable host and client names into a computer-usable format; see Table 2. Windows-specific functions are extensions to support the event-driven architecture of Windows; see Table 3. (For more information, see Network Interrupts, by Ralf Brown and Jim Kyle, Addison-Wesley, 1994.)
Control Details
As previously mentioned, our LAN-based control project called for extensive network communication. To help with code reuse, I implemented the network routines as a VBX. To use the TCP protocol, one socket must be established for every client that the host will communicate with. If a host is connected to 100 clients using TCP, then the host will need to establish 100 sockets, as opposed to establishing one UDP socket on the host and communicating with all 100 clients. In light of this (and because a limited number of TCP sockets can be opened simultaneously for communication), I wrote a UDP-based VBX that is invisible at run time. This VBX establishes and terminates the socket, and asynchronously sends and receives data.
In relation to the WinSock API, you first initialize the underlying WinSock DLL and confirm that the version of the DLL is compatible with the VBX's requirements; see Example 1. This is done in the VBINITCC routine, which is called each time an application loads the VBX.
Next, the control's Connected property is set to True, and a socket is established in response. The Connected property can only be set at run time, and before it is set, no read/write activity can take place.
You then call WSAAsyncSelect to set up an asynchronous event notifier that generates a user-defined message (WM_USER_ASYNC_SELECT) whenever data arrives or a request to send data is made; see Example 2.
For the VBX, a request to send data is made by setting the control's Send property to True; this is possible only at run time. The control responds by placing a WM_USER_ASYNC_SELECT message in its own queue via PostMessage. Finally, WSACleanup is called to release all resources allocated by WSAStartup. There must be one call to WSACleanup for every call to WSAStartup. In the case of the VBX, WSACleanup is called during the VBTERMCC routine, which in turn is called each time an application unloads the VBX; see Example 3. The rest of the code is pretty straightforward C; see the file datagram.c in Listing One. The complete VBX (source and binary) is available electronically; see "Availability," page 3. For more information on WinSock programming, see Programming WinSock, by Arthur Dumas (Sams, 1995).
VBX-to-OCX Porting
To be compatible with future versions of Visual Basic and other development environments, I ported the UDP VBX to an OLE Custom Control (OCX). The OLE Control Development Kit (CDK) made porting the UDP control a two-step process:
- Use the OLE CDK to create a working skeleton.
- Appropriately place the code that is specific to the UDP VBX in the newly formed OCX skeleton.
Conclusion
To illustrate the use of the UDP control, I've written a simple chat program that's available electronically. As Figure 1 illustrates, the program allows two users to communicate over a TCP/IP connection. To send a message, the user types a message in the Messages Out box and presses the Send button. Messages received will automatically appear in the Messages In box. The program is simple, thanks to the UDP control. Table 4 is a list of the properties supported by the UDP control, while Example 5 is a typical UDP event. These, as well as the sample code for the chat program, can be used as a reference for programming with the UDP control.
Figure 1: Sample chat program.
Table 1: Windows Sockets functions.
Copyright © 1995, Dr. Dobb's Journal
Function Description
<I>accept</I> Accept an incoming connection.
<I>bind</I> Bind a name to a socket.
<I>closesocket</I> Close a socket.
<I>connect</I> Initiate a connection.
<I>getsockname</I> Get the name bound to a socket.
<I>getsockopt</I> Get the settings for a socket.
<I>htonl</I> Convert <I>u_long</I> to network byte-order.
<I>htons</I> Convert <I>u_short</I> to network byte-order.
<I>inet_addr</I> Convert dotted-decimal IP address into 32-bit number.
<I>inet_ntoa</I> Convert 32-bit number into dotted decimal IP address.
<I>ioctlsocket</I> I/O control of a socket.
<I>listen</I> Listen for connections to this socket.
<I>ntohl</I> Convert <I>u_long</I> to host byte-order.
<I>ntohs</I> Convert <I>u_short</I> to host byte-order.
<I>recv</I> Receive data on a connected socket.
<I>recvfrom</I> Receive data on a socket, along with IP address
and port number.
<I>select</I> Synchronous I/O multiplexing.
<I>send</I> Send data over a connected socket.
<I>sendto</I> Send data to a specific socket.
<I>setsockopt</I> Set socket options.
<I>shutdown</I> Shut down.
<I>socket</I> Create an endpoint for communication.
Table 2: Windows Sockets database functions.
Function Description
<I>gethostbyaddr</I> Return host name, given IP address.
<I>gethostbyname</I> Return IP address, given host name.
<I>gethostname</I> Return host name.
<I>getprotobyname</I> Return protocol name and number, given a protocol name.
<I>getprotobynumber</I> Return protocol name and number, given a protocol number.
<I>getservbyname</I> Return service name and port, given name and protocol.
<I>getservbyport</I> Return service name and port, given port and protocol.
Table 3: Windows Sockets Windows-specific functions.
Function Description
<I>WSAAsyncGetHostByAddr</I> Asynchronous version of <I>gethostbyaddr</I>.
<I>WSAAsyncGetHostByName</I> Asynchronous version of <I>gethostbyname</I>.
<I>WSAAsyncGetProtoByName</I> Asynchronous version of <I>getprotobyname</I>.
<I>WSAAsyncGetProtoByNumber</I>Asynchronous version of <I>getprotobynumber</I>.
<I>WSAAsyncGetServByName</I> Asynchronous version of <I>getservbyname</I>.
<I>WSAAsyncGetServByPort</I> Asynchronous version of <I>getservbyport</I>.
<I>WSAAsyncSelect</I> Asynchronous version of <I>select</I>.
<I>WSACancelAsyncRequest</I> Cancel outstanding <I>WSAAsyncGet</I> call.
<I>WSACancelBlockingCall</I> Cancel outstanding <I>blocking</I> call.
<I>WSACleanup</I> Release resources allocated by <I>WSAStartup</I>.
<I>WSAGetLastError</I> Return details of last API error.
<I>WSAIsBlocking</I> Determine if there is an outstanding blocking call.
<I>WSASetBlockingHook</I> "Hook" underlying blocking mechanism.
<I>WSASetLastError</I> Set error code to be returned by <I>WSAGetLastError</I>.
<I>WSAStartup</I> Initialize underlying DLL.
<I>WSAUnhookBlockingHook</I> Restore original blocking mechanism.
Table 4: UDP control properties and usage chart.
Property Description
<I>Connected</I> Establishes control's send/receive services;
undefinable at design time.
<I>Disconnected</I> Terminates control's send/receive services;
undefinable at design time.
<I>ErrorCode</I> Displays code representing error status of last UDP operation.
<I>MaxBufferSize</I> Size in bytes of the largest packet allowed; read-only.
<I>MyAddress</I> TCP/IP address (in dotted decimal notation) of the
hosting computer; read-only.
<I>MyPort</I> Port number that the control will respond to.
<I>Send</I> Toggled to True to send data; undefinable at design time.
<I>ToAddress</I> Destination TCP/IP address (in dotted decimal notation)
identifying the computer that data will be sent to.
<I>ToData</I> Data to be sent.
<I>ToPort</I> Destination port identifying the application that data
will be sent to.
Example 1: Confirming DLL version.
//initialize underlying WinSock DLL
if (WSAStartup(VersionRequested,&wsaData)!=0) return FALSE;
//make sure that the VBX supports this version of WinSock
if ((LOBYTE(wsaData.wVersion)!=LOBYTE(VersionRequested))||
(HIBYTE(wsaData.wVersion)!=HIBYTE(VersionRequested)))
{
WSACleanup();
return FALSE;
}
Example 2: Setting up an asynchronous event notifier.
if (WSAAsyncSelect(DATAGRAMDEREF(hctl)->mappsocket, hwnd,
WM_USER_ASYNC_SELECT, FD_READ|FD_WRITE)==SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
Example 3: Releasing resources.
void FAR PASCAL _export VBTERMCC(void)
{
//terminate the usage of the WinSock DLL
WSACleanup();
}//VBTERMCC
Example 4: Hiding control at run time.
void CUdpCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, const
Rect& rcInvalid)
{
CBitmap bitmap;
BITMAP bmp;
CPictureHolder picHolder;
CRect rcSrcBounds;
if (AmbientUserMode()==MODE_DESIGN)
{
//load bitmap
bitmap.LoadBitmap(IDB_UDP); bitmap.GetObject(sizeof(BITMAP),&bmp);
rcSrcBounds.right=bmp.bmWidth;
rcSrcBounds.bottom=bmp.bmHeight;
//create picture and render picHolder.CreateFromBitmap((HBITMAP)
// bitmap.m_hObject,NULL,FALSE); picHolder.Render(pdc,rcBounds,rcSrcBounds);
}
else ShowWindow(SW_HIDE);
}
Example 5: Definition of UDP control DataIn event.
Sub UDP1_DataIn(FromAddress As String,
FromPort As Integer, FromData As String)
Listing One
#include "datagram.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#define ERR_None 0L
#define ERR_MethodNotSupported 421
#define ERR_ReadOnlyProperty 20000 //should be 383 but I created
//my own text for the message
#define ERR_NOTCONNECTED 20001
#define HOST_NAME_LEN 50
#define WM_USER_ASYNC_SELECT (WM_USER+201)
#define MAKEWORD(a, b) ((WORD)(((BYTE)(a)) | ((WORD)((BYTE)(b))) << 8))
HANDLE hmodDLL;
WSADATA wsaData;
BOOL initialized=FALSE;
WORD VersionRequested=MAKEWORD(1,1);
//compatible with WinSock 1.1
unsigned int MaxUDPBufferSize;
char hostAddress[16];
int FAR PASCAL WEP(int);
int FAR PASCAL LibMain(hModule, wDataSeg, cbHeapSize, lpszCmdLine)
HANDLE hModule;
WORD wDataSeg;
WORD cbHeapSize;
LPSTR lpszCmdLine;
{
hmodDLL = hModule;
return 1;
}//LibMain
BOOL FAR PASCAL _export VBINITCC(USHORT usVersion,BOOL fRuntime)
{
// this function is automatically called whenever an instance of the
// control loaded into memory
char hostName[HOST_NAME_LEN];
PHOSTENT phostent;
IN_ADDR in;
//Run-Time-Only
//uncomment the following lines to make a run-time only control
/*
if (!fRuntime)
return FALSE;
*/
if (WSAStartup(VersionRequested,&wsaData)!=0) return FALSE;
//make sure that an appropriate version of WinSock is supported
if ((LOBYTE(wsaData.wVersion)!=LOBYTE(VersionRequested))||
(HIBYTE(wsaData.wVersion)!=HIBYTE(VersionRequested)))
{
WSACleanup();
return FALSE;
}
if (!initialized)
{
//only need to get the host name one time
gethostname(hostName,HOST_NAME_LEN);
phostent=gethostbyname(hostName);
memcpy(&in,phostent->h_addr,4);
memcpy(hostAddress,inet_ntoa(in),16);
//only need to get the max buffer size one time
MaxUDPBufferSize=(unsigned int)wsaData.iMaxUdpDg;
//the follwing is done because the calls to recv and sendto
//take an integer as their second parameter, not an unsigned int
if (MaxUDPBufferSize>INT_MAX)
MaxUDPBufferSize=INT_MAX;
initialized=TRUE;
}
// Register control(s)
return VBRegisterModel(hmodDLL, &modelDATAGRAM);
}//VBINITCC
int FAR PASCAL WEP(int nShutdownFlag)
{
return 1;
}//End WEP
void FAR PASCAL _export VBTERMCC(void)
{
// this function is automatically called whenever an instance of the
// control is unloaded from memory
WSACleanup();
return;
}//VBTERMCC
LONG FAR PASCAL _export DataGramCtlProc(HCTL hctl,HWND hwnd,USHORT msg,
USHORT wp,LONG lp)
{
HSZ tempVBString;
LPSTR lpstr=NULL;
LPSTR destAddr=NULL;
char fromAddr[16];
char *tempIn;
SOCKADDR_IN addr;
int addrLen=sizeof(addr);
int fromPort;
unsigned int nBytesSent,nBytesRecv;
unsigned int stringLen;
EVENT_PARAMS params;
IN_ADDR inFrom;
switch (msg)
{
case WM_NCCREATE:
//default the properties
DATAGRAMDEREF(hctl)->mToPort=0;
tempVBString=VBCreateHsz((_segment)hctl,(LPSTR)hostAddress);
DATAGRAMDEREF(hctl)->mMyAddress=tempVBString;
DATAGRAMDEREF(hctl)->mMyPort=0;
DATAGRAMDEREF(hctl)->mMaxBufferSize=(long int)MaxUDPBufferSize;
DATAGRAMDEREF(hctl)->mErrorCode=0;
DATAGRAMDEREF(hctl)->mSend=FALSE;
DATAGRAMDEREF(hctl)->mConnected=FALSE;
DATAGRAMDEREF(hctl)->mDisconnected=TRUE;
DATAGRAMDEREF(hctl)->mappsocket=INVALID_SOCKET;
break;
case WM_NCDESTROY:
//free string memory allocated with VBCreateHsz
if (DATAGRAMDEREF(hctl)->mMyAddress)
{
VBDestroyHsz(DATAGRAMDEREF(hctl)->mMyAddress);
DATAGRAMDEREF(hctl)->mMyAddress=NULL;
}
//close any opened socket
if (DATAGRAMDEREF(hctl)->mappsocket!=INVALID_SOCKET)
{
if (closesocket(DATAGRAMDEREF(hctl)->mappsocket)==SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
DATAGRAMDEREF(hctl)->mappsocket=INVALID_SOCKET;
}
break;
case VBM_METHOD:
//there are no methods supported
return VBSetErrorMessage(ERR_MethodNotSupported,
"Method not applicable for this object.");
case WM_USER_ASYNC_SELECT:
if (WSAGETSELECTERROR(lp)!=0)
return ERR_None;
switch(WSAGETSELECTEVENT(lp))
{
case FD_READ:
if (!DATAGRAMDEREF(hctl)->mSend)
{
tempIn=(char *)calloc(MaxUDPBufferSize,sizeof(char));
//read the data
nBytesRecv=recvfrom(DATAGRAMDEREF(hctl)->mappsocket,
tempIn,MaxUDPBufferSize,0,(LPSOCKADDR)&addr,&addrLen);
if (nBytesRecv==SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
else
{
//get the from address
memcpy(&inFrom,&addr.sin_addr.s_addr,4);
memcpy(fromAddr,inet_ntoa(inFrom),16);
//get the from port
fromPort=ntohs(addr.sin_port);
//set the event parameters
params.FromPort=&fromPort;
params.FromAddr = VBCreateHlstr(fromAddr,
lstrlen(fromAddr));
params.FromData = VBCreateHlstr(tempIn,
lstrlen(tempIn));
//fire the event
VBFireEvent(hctl,IEVENT_DATAGRAM_DATAIN,¶ms);
//free string memory allocated with VBCreateHlstr
VBDestroyHlstr(params.FromData);
VBDestroyHlstr(params.FromAddr);
}
if (tempIn)
free(tempIn);
}
break;
case FD_WRITE:
//has there been a notification to send data
if (DATAGRAMDEREF(hctl)->mSend)
{
//get the destination port
addr.sin_family=AF_INET;
addr.sin_port=htons(DATAGRAMDEREF(hctl)->mToPort);
//get the destination address
destAddr=VBLockHsz(DATAGRAMDEREF(hctl)->mToAddress);
addr.sin_addr.s_addr=inet_addr(destAddr);
VBUnlockHsz(DATAGRAMDEREF(hctl)->mToAddress);
if (DATAGRAMDEREF(hctl)->mToData)
{
lpstr=VBLockHsz(DATAGRAMDEREF(hctl)->mToData);
stringLen=lstrlen(lpstr);
//send the data
nBytesSent=sendto(DATAGRAMDEREF(hctl)->mappsocket,
lpstr,stringLen,0,(LPSOCKADDR)&addr,sizeof(addr));
if (nBytesSent==SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
//unlock it
VBUnlockHsz(DATAGRAMDEREF(hctl)->mToData);
}
//reset the send flag
DATAGRAMDEREF(hctl)->mSend=FALSE;
}
break;
default:
break;
}
return ERR_None;
case VBM_SETPROPERTY:
//called whenever a property is set
switch(wp)
{
case IPROP_DATAGRAM_TOADDRESS:
return ERR_None;
case IPROP_DATAGRAM_TOPORT:
return ERR_None;
case IPROP_DATAGRAM_TODATA:
return ERR_None;
case IPROP_DATAGRAM_MYADDRESS:
//read-only
//clear it out
if (DATAGRAMDEREF(hctl)->mMyAddress)
VBDestroyHsz(DATAGRAMDEREF(hctl)->mMyAddress);
//then set it to what the default is
tempVBString=VBCreateHsz((_segment)hctl,(LPSTR)hostAddress);
DATAGRAMDEREF(hctl)->mMyAddress=tempVBString;
return VBSetErrorMessage(ERR_ReadOnlyProperty,
"Property is read-only.");
case IPROP_DATAGRAM_MYPORT:
return ERR_None;
case IPROP_DATAGRAM_MAXBUFFERSIZE:
//read-only
DATAGRAMDEREF(hctl)->mMaxBufferSize=(long int)MaxUDPBufferSize;
return VBSetErrorMessage(ERR_ReadOnlyProperty,
"Property is read-only.");
case IPROP_DATAGRAM_ERRORCODE:
return ERR_None;
case IPROP_DATAGRAM_SEND:
if (VBGetMode()==MODE_DESIGN)
//cannot set this property at design-time
DATAGRAMDEREF(hctl)->mSend=FALSE;
else
//place a FD_WRITE message in the application message que
PostMessage(hwnd,WM_USER_ASYNC_SELECT,
DATAGRAMDEREF(hctl)->mappsocket,
WSAMAKESELECTREPLY(FD_WRITE,0));
return ERR_None;
case IPROP_DATAGRAM_CONNECTED:
if (VBGetMode()==MODE_DESIGN)
//cannot set this property as design-time
DATAGRAMDEREF(hctl)->mConnected=FALSE;
else
{
if (DATAGRAMDEREF(hctl)->mConnected)
{
addr.sin_family=AF_INET;
addr.sin_port=htons(DATAGRAMDEREF(hctl)->mMyPort);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
DATAGRAMDEREF(hctl)->mappsocket=socket(AF_INET,SOCK_DGRAM,0);
if (DATAGRAMDEREF(hctl)->mappsocket==INVALID_SOCKET)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
else
{
if (bind(DATAGRAMDEREF(hctl)->mappsocket,
(LPSOCKADDR)&addr,sizeof(addr))==SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
else
{
//find out the port number that may have been
//automatically assigned if myPort was 0
getsockname(DATAGRAMDEREF(hctl)->mappsocket,
(LPSOCKADDR)&addr,&addrLen);
DATAGRAMDEREF(hctl)->mMyPort=ntohs(addr.sin_port);
//setup the asynchronous read/write handler
if (WSAAsyncSelect(DATAGRAMDEREF(hctl)->mappsocket,
hwnd,WM_USER_ASYNC_SELECT,
FD_READ|FD_WRITE)==SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
}
//set disconnected to false
DATAGRAMDEREF(hctl)->mDisconnected=FALSE;
}
}
else
//cannot toggle to false
DATAGRAMDEREF(hctl)->mConnected=TRUE;
}
return ERR_None;
case IPROP_DATAGRAM_DISCONNECTED:
if (VBGetMode()==MODE_DESIGN)
//cannot set this property as design-time
DATAGRAMDEREF(hctl)->mDisconnected=TRUE;
else
{
if (DATAGRAMDEREF(hctl)->mDisconnected)
{
//close any opened socket
if (DATAGRAMDEREF(hctl)->mappsocket!=INVALID_SOCKET)
{
if (closesocket(DATAGRAMDEREF(hctl)->mappsocket)==
SOCKET_ERROR)
DATAGRAMDEREF(hctl)->mErrorCode=WSAGetLastError();
DATAGRAMDEREF(hctl)->mappsocket=INVALID_SOCKET;
}
//set connected to false
DATAGRAMDEREF(hctl)->mConnected=FALSE;
}
else
//cannot toggle to false
DATAGRAMDEREF(hctl)->mDisconnected=TRUE;
}
return ERR_None;
default:
break;
}
break;
}
return VBDefControlProc(hctl, hwnd, msg, wp, lp);
}//DataGramCtlProc