LUCA: Reusable Communications Code

Dr. Dobb's Journal October 1997

Bridging the media and protocol gap

By Bennett M. Griffin

Bennett is president of Griffin Technologies and can be contacted at bennett@griftech.com.

Communications programming has traditionally been anything but reusable. A program designed to communicate asynchronously through a modem could do only that -- reusing the same code for TCP/IP communications across the Internet would be impossible. From a conceptual point of view, however, these two operations are identical. Each requires a communications connection to begin, data streams to be sent and received, and a termination of the connection.

In spite of these apparent similarities, the traditional approach to communications programming has prevented such reusability. This is because the traditional approach is hardware oriented -- the communications code depends on the protocols and drivers that are used.

An alternative approach is to take an object-oriented view of communications, in which differences between protocols and drivers are left to the internal implementations of the classes. This approach makes code reusability possible. The protocol and media used become details of the implementation. You are therefore free to look at the idea of data communications from the conceptual level -- open, read, write, close. Ultimately, this means that an object-oriented communications program designed for a dial-up modem would work identically on an ISDN connection or across the Internet without modification.

To demonstrate the simplicity of this approach, I present in this article a multiprotocol terminal program that can handle both async communications via the PC COM port and Telnet through TCP/IP over the Internet (as well as many other types of communications) using the same reusable code. I'll create this code using the Langner Universal Communications API (LUCA) and Visual C++ 5.0. LUCA, developed by Langner GmbH, supports C, C++, Delphi, and Visual Basic on Windows 95/NT, providing a universal API for data communications for popular transmission media and protocols.

Universal Communications

As Table 1 illustrates, LUCA integrates support for all popular protocols and transmission media in use today. Even with this functionality, the LUCA API (see Table 2) is straightforward, with 13 functions and 6 event handlers. The number and type of functions in the API underscores the difference between the LUCA's object-oriented view and traditional communications programming.

The functions mirror the conceptual view of communications programming. Vwrite() and Vread() transmit and receive data, including file transfers, faxes, e-mail, or anything else that needs to be communicated. You can understand and use Vwrite() and Vread() without knowledge of the protocol or media being used.

Vgetfile() and Vputfile() further automate communication with Zmodem, FTP, or other file-transfer protocols. They are actually add-on functions and are provided as source code. They help automate the read/write polling loops as well as the operating-system-specific file I/O commonly associated with file transfers.

The Link Identifier

To achieve this abstraction from protocols and media, there has to be some way to specify which protocols and media are being used in each specific situation. LUCA utilizes an approach based on a string called a "Link Identifier," which is passed to the Vopen() function to establish a communications channel and is constructed just like a protocol stack. In fact, it actually does create a protocol stack within LUCA out of different software modules.

For example, the link identifier "TELNET/TCP:telnet/SOCKET:myserver" specifies that the Telnet protocol should be run on the TCP protocol using the telnet port, which in turn runs on the Socket driver and connects to the host myserver. Similarly, the link identifier "TCP:1024/SOCKET:myserver" establishes a connection to myserver using TCP port 1024, again running on top of the Socket driver.

To establish a dial-up modem connection, the link identifier would be something like "ATMODEM:5551234/ASYNC /COM:2, SPEED=115200, FLOW=RTSCTS." In this case, you are loading the AT-style modem protocol to dial 555-1234 on top of an asynchronous connection via the COM driver attaching to COM2 at 115200 bps with hardware handshaking enabled. Whatever the specifics, the communications driver is always listed rightmost, and protocols are stacked to the left.

All of the protocol and driver-specific information is contained within this link identifier string. This data is not analyzed or processed by the LUCA API functions, but instead by the appropriate LUCA protocol or driver module. Consequently, the API itself is not bogged down by any protocol or driver details. This fact is one of the keys to code reusability and expandability.

This method of modular protocol stacking also allows for some unconventional, but handy, abilities. For example, the protocol module for HDLC framing can be easily stacked on top of a character-based protocol such as an async COM port, instantly producing an error-detecting, block-oriented transmission.

This modular design also enables expandability. As additional LUCA modules are written to support new drivers and protocols, all you need to update old code to enable use of the newest driver is the latest LUCA module and an updated link identifier. Protocols higher in the stack such as FTP and Telnet will continue to work with the new driver. Just as ODBC has lead to generic database processing code, LUCA enables completely reusable communications code.

Distributed Communications

Sometimes it is desirable to use multiple types of communications to accomplish one task. Network, or segmented, links are communications links that are made up of more than one communications type. Network modem servers are a good example of segmented links. The communication is first carried through the LAN to the modem server, and then via the modem server's COM port to a modem. Networked links allow applications to utilize the communications resources of multiple computers and systems at different locations.

Usually each segment in a networked link has different characteristics. By including the ">" redirection symbol in a link identifier, LUCA creates a networked link of two or more segments. For example, a two-segment link that utilizes TCP on the first half and the 3964R async protocol on the second would have a link identifier such as "HDLC/TCP: 3030/SOCKET:remote_pc>3964R /ASYNC/COM:1." In this example, COM:1 (listed in the second portion of the link) refers to the COM1 port on the computer specified by the remote_pc IP address.

This again demonstrates code reusability, because the LUCA API functions work with this networked link just like any standard end-to-end connection. No specific code is required to enable networked links, and the details are again contained within the link identifier string.

A Generic Terminal

To see this idea in action, I'll present a C++ communications terminal application that works across multiple protocols and media. The most interesting aspect of this program is that it doesn't contain a single line of protocol or media-specific code. It will open Telnet sessions, dial-up modem connections, even connect to a PLC using the Siemens 3964R protocol. This same application could easily be written in C, Visual Basic, or Delphi.

Listing One contains the main function loop. The only major data structure used is a class called TermPort, which inherits from the LUCA class Vport. This class only has two public methods, a constructor and a method named active(), which is the heart of the communications write loop. Private methods overriding most of the default event handling are also defined.

After a syntax message is displayed, the application waits for a link identifier to be entered by the user. At that point, a new TermPort object is created using the link identifier supplied by the user. The application then loops on the active() method, pausing briefly between each call.

The TermPort constructor (see Listing Two) is where the link identifier is actually used and the connection is established. Vopen() is called with the given link identifier. If the link identifier is invalid, or if the specified link isn't possible to open, an error is produced. Otherwise, the communications link is established and is ready to send and receive data.

Character transmission is handled by the active() method (Listing Three). Each time it is called, it checks to see if any characters have been entered from the keyboard. If escape was pressed, the connection is closed via the Vclose() function. Otherwise, the character is sent out across the communications link with the Vwrite() function.

Reading character data is the responsibility of the OnCANREAD() event handler (see Listing Four). This event is fired when there is data waiting to be read from the communications link. It calls the Vread() function with a buffer in which to store the incoming data. This data is then sent to the standard output device for display.

Listing Five contains the OnDISCONNECT() event-handler code. This example app outputs a summary of the link parameters utilized in the just-completed connection within this handler. This information is obtained from the Vgetpar() function.

The only other code required to implement this "generic" terminal application is some simple event-handling for connection confirmation and error reporting; see Listing Six. There are two different event handlers for error reporting. OnERROR() handles link-connection errors, and calls Vclose() to ensure the failed connection has been properly terminated. OnNONSTD() provides a way for protocol and driver modules to relay protocol- and driver-specific errors. It uses the Vgetpar() function to retrieve the actual error message set by the protocol or driver module.

As you can see from the program code, all the specifics regarding the type of communication are managed by the Vopen() and Vclose() functions. With this strategy, the same code for reading and writing data can be used regardless of the particular type of communications. This provides the code reusability that has escaped communications software in the past.

Conclusions

Data communications has been one of the last areas of computer programming to embrace the object-oriented approach and the design efficiencies that come with it. With an object-oriented approach such as LUCA's, you can enjoy the same level of code reusability employed by database access, file I/O, and innumerable other areas of software design within the framework of data communications.


Listing One

// We define our own Terminal Port here as a subclass of LUCA's Vport// class.  active() is called from the main loop as often as possible
// to send characters out the port.

class TermPort : public Vport { public: TermPort(char *link_identifier, char esc_char); // create link int active(); // while it's active, do something else private: int is_active; char escape; // overloaded event handlers void OnCONNECT(); void OnDISCONNECT(); void OnCANREAD(); void OnNONSTD(); void OnERROR(); }; int main(int argc, char **argv) { char link_identifier[255]; TermPort *port; cout << "To use this simple terminal example, you have to enter " << endl; cout << "a LUCA Link Identifier." << endl << endl;

// Get a new link identifier from the user or command line. if (argc == 1) { cout << "Link Identifier (enter 'quit' to exit):" cin >> link_identifier; } else strcpy(link_identifier, argv[1]);

// Main loop for communication. 0x1b is the ESC key. port = new TermPort(link_identifier, 0x1b); while (port->active()) Sleep(50); delete port;

cout << "Press any key to return to the operating system." << endl << flush; char c = getch(); return 0; }

Back to Article

Listing Two

TermPort::TermPort(char *link_identifier, char esc_char){
  char errorstr[255];

// Tell the user how to disconnect

escape = esc_char; cout << "Terminal mode. Hit" if (escape < 32) { if (escape == 0x1b) cout << "ESC"; else cout << "Ctrl-" << (char)(escape+"@"); } else cout << escape; cout << " to disconnect." << endl; cout << "Wait for connect..." << endl; if ((Vopen(link_identifier)) < 0) { // oops, it failed. Verror(errorstr); cout << "No connection: " << errorstr << endl; is_active = 0; } else { is_active = 1; } }

Back to Article

Listing Three

int TermPort::active(){
  // Try and fetch a character from the keyboard and sent it to port.

if (is_active) { if (kbhit()) { char c = getch(); if (c == escape) { is_active = 0; cout << "Closing connection." << endl; Vclose(); } else if Vwrite(&c, 1) < 0) { cout << "Write Error" << endl; is_active = 0; } } } return is_active; }

Back to Article

Listing Four

void TermPort::OnCANREAD(){
  // If we can read, we'll send all data to the console screen.
  unsigned char buffer[8192];

int n = Vread(buffer, sizeof(buffer)); if (n > 0) { buffer[n] = '\0'; cout << buffer << flush; } else if (n == 0) { cout << "Read EOF" << endl << flush; } else { cout << "Read Error" << endl << flush; } }

Back to Article

Listing Five

void TermPort::OnDISCONNECT(){
  // The connection was lost.  Inform the user what happened and print
  // all communications parameters that were used on this connection.
  char param[256];
  char value[256];
  int n;

cout << "The following parameters were used:" << endl;

n = Vgetpar("", param); while (n > 0) { Vgetpar(param, value); cout << " " << param << " = " << value << endl; n = getpar("+", param); } cout << "Disconnected." << endl << flush;

Back to Article

Listing Six

void TermPort::OnCONNECT(){
  // everything went well
  cout << "Connected." << endl << flush;
}
void TermPort::OnNONSTD()
{
  // something extra came up -- print it.
  char eventname[255];
  if (Vgetpar("NONSTD", eventname) >= 0) {
    cout << "Non Standard Event: " << eventname << endl << flush;
  }
}
void TermPort::OnERROR()
{
  // Something else went wrong.  Close down for good measure.
  char err[255];

Verror(err); cout << "Error! " << err << endl << flush; is_active = 0; Vclose(); }

Back to Article

DDJ


Copyright © 1997, Dr. Dobb's Journal