Introduction
This article describes a library for writing simple TCP client applications, written is in the C language with an object oriented approach, using the standard Berkeley sockets interface, and intended to be cross-platform. The development was done in a system with the Ubuntu Linux distribution installed, with later porting and a little testing in Windows 2003 with Visual Studio Express 2008.
Background
When I was writing the example programs for my CodeProject article, "A Small C-Language TCP Server Framework" (see here), I noticed something strange: it was easier to write the server examples than the client examples! So I decided to look into the matter, and the result was a slimmed down version of the original server project, now with only the functionality for writing simple client applications, in an easy and straightforward manner.
Description
First, I'll introduce some terminology.
A generic client is a simple TCP client application written with the library described in this article.
This generic client follows a specific pattern that is built around three concepts: state, event and command.
State is one of these:
CONNECTED_IDLE
NOT_CONNECTED
Command is one of these:
CONNECT
SEND
RECV
CLOSE
Event is one of these:
CONNECTION_CREATED
CONNECTION_DESTROYED
CONNECT_ERROR
RECV_COMPLETE
RECV_TIMEOUT
SEND_COMPLETE
There are also two other entities involved: an instance of the GenericClient
class, provided by the library, and the application code written by the user of the library. The GenericClient
class is informally derived from the Client
class, which provides the basic methods for writing client applications.
The general flow of control in an application written with this library is like this:
- The application code passes up a command to the
GenericClient
instance. - The
GenericClient
instance processes the received command and passes down an event to the application code. - The application code then processes the event received from the
GenericClient
instance, and the cycle repeats again.
The GenericClient
instance is driven by a state machine, which is described below in pseudo-code:
1. if state is NOT_CONNECTED
// only accepts the CONNECT command
do connect
if result is OK
set state to CONNECTED_IDLE
return event CONNECTION_CREATED
else
// remains in the state NOT_CONNECTED
return event CONNECT_ERROR
2. if state is CONNECTED_IDLE
2.1 if command is SEND
do send
if result is OK
// remains in the state CONNECTED_IDLE
return event SEND_COMPLETE
else
set state to NOT_CONNECTED
return event CONNECTION_DESTROYED
2.2 if command is RECV
do recv
if result is OK
// remains in the state CONNECTED_IDLE
return event RECV_COMPLETE
else if occurred TIMEOUT
// remains in the state CONNECTED_IDLE
return event RECV_TIMEOUT
else // error
set state to NOT_CONNECTED
return event CONNECTION_DESTROYED
2.3 if command is CLOSE
do close
set state to NOT_CONNECTED
return event CONNECTION_DESTROYED
The application code doesn't have to be concerned with how the GenericClient
instance works; its only concern is how to handle the events it receives from the GenericClient
instance.
The pseudo-code for the application code is open, but generally will be like the following:
1. if event is CONNECTION_CREATED
prepare message to send to the server
pass the command SEND to the generic client instance
2. if event is SEND_COMPLETE
update whatever controls needed by the application
pass the command RECV to the generic client instance
3. if event is RECV_COMPLETE
process reply received from the server
prepare another message to send to the server
pass the command SEND to the generic client instance
4. if event is RECV_TIMEOUT (*** optional ***)
do whatever the application needs in case of timeout
if business rules says try again
prepare another message to send to the server
pass the command SEND to the generic client instance
else
pass the command CLOSE to the generic client instance
5. if event is CONNECTION_DESTROYED
// can be the result of SEND, RECV or CLOSE commands
// (most errors will automatically destroy the connection)
do whatever cleanup the application needs
pass the command CONNECT to the generic client instance
6. if event is CONNECT_ERROR
do whatever the application needs in this case
if business rules says try again
pass the command CONNECT to the generic client instance
else
end application
Using the pattern described above, writing simple TCP client applications is a snap, because nowhere the TCP/IP networking code is seen. In practice, only the application code needs to be written.
The goal here is not writing production-grade applications, but small utilities for testing scenarios, for prototyping new functionalities, for finding application bugs in servers, etc. In these cases, speed of development is of paramount importance, because often the programs created are discardable, throwaway utilities, with limited scope and functionality, which do not justify spending too much time in their development.
The C code that implements the pseudo-code above is something like this:
switch (genCli_waitEvent())
{
case CLI_EVT_CONNECTION_CREATED:
prepareFirstMessage();
genCli_send();
break;
case CLI_EVT_RECV_COMPLETE:
processServerReply();
prepareAnotherMessage();
genCli_send();
break;
case CLI_EVT_SEND_COMPLETE:
genCli_recv();
break;
case CLI_EVT_RECV_TIMEOUT:
prepareAnotherMessage();
genCli_send();
break;
case CLI_EVT_CONNECT_ERROR:
printConnectionError();
genCli_connect();
break;
case CLI_EVT_CONNECTION_DESTROYED:
printOperationError();
genCli_connect();
break;
default:
printf("*invalid event %d\n", genCli_event());
abort();
break;
}
The methods genCli_xxx
above are provided by the class GenericClient
. The application programmer does not need to worry about all the boilerplate code usually associated with TCP/IP programming. Please refer to the gen_client_1
example project for more details.
The library also provides additional functionality, presented by the Client
class but is in fact implemented by other classes (the Client
class here acting as a façade).
The most important of the additional functionality is related to the Message
class. It provides an encapsulation for the buffers used when exchanging messages between client and server, and also provides the framing that delimits messages on the wire. Please see the documentation for the Message
class, for the Client
class, and for the gen_client_1
example that is shipped with this project. For the record, the more important methods provided by the Message
class for the use of applications are (encapsulated by the Client
class): client_messageBuffer
, client_messageSize
, and client_setMessageSize
.
There is also a Log
class, that is used internally by the library but can also be used by applications. Its main methods are (again, encapsulated by the Client
class):
client_logInfo
client_logWarn
client_logDebug
client_logTrace
client_logError
client_logFatal
There are some other classes used internally by the library that may or may not be useful when writing the client applications: Mutex
, Thread
, Time
and Timeout
.
This library is written to inter-operate with servers written using the library described in this CodeProject article. Specifically, the gen_client_1
example shipped with this project works with the server_2
and server_3
examples shipped with the article above.
To adapt the library to work with other servers, it's very likely that the Message
structure declared in the MessageImpl.h
file will have to be changed. Specifically, refer to the on-the-wire format of the messages exchanged between the client and the server.
Conclusion
TCP client applications usually follow a common pattern: open the connection with the server, send requests, then receive and process replies. In case of communications error, almost always the safest course is to close the connection and start anew. In this library, I tried to consolidate this common behavior, so that the boring and repetitive work is done only once, and the application programmer needs to be concerned only with the business rules pertinent to the application. Of course, this is the use of a very well known principle of software engineering: the DRY (Don't Repeat Yourself) principle.
History
This is the first version of the article, and covers the version 1.00 of the library.