Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

A Generic C-Language TCP Client Application

3.67/5 (2 votes)
9 May 2010CPOL4 min read 29.5K   799  
A library for writing simple TCP client applications

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:

  1. CONNECTED_IDLE
  2. NOT_CONNECTED

Command is one of these:

  1. CONNECT
  2. SEND
  3. RECV
  4. CLOSE

Event is one of these:

  1. CONNECTION_CREATED
  2. CONNECTION_DESTROYED
  3. CONNECT_ERROR
  4. RECV_COMPLETE
  5. RECV_TIMEOUT
  6. 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:

  1. The application code passes up a command to the GenericClient instance.
  2. The GenericClient instance processes the received command and passes down an event to the application code.
  3. 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:

C++
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;
} // switch

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)