Click here to Skip to main content
15,896,726 members
Articles / Programming Languages / C

A Generic C-Language TCP Client Application

Rate me:
Please Sign up or sign in to vote.
3.67/5 (2 votes)
9 May 2010CPOL4 min read 29.1K   793   20  
A library for writing simple TCP client applications
/// @source      Client.c
/// @description Implementation of class Client.
//  See licensing information in the file README.TXT.

// -----------------------------------------------------------------------------

//  Application Description

/** @mainpage A C-Language Library for Writing Generic TCP %Client Applications

    A library for writing simple TCP client applications, written in the
    C-language with an object oriented approach, using the Berkeley sockets
    interface, and intended to be cross-platform.  @par

    A <b> generic client </b> written with this library is a simple TCP client
    application created following a specific pattern that is built around three
    concepts: <b> state</b>, <b> event</b> and <b> command</b>. @par

    <b> State </b> is one of these     <br>
       (1) <b> CONNECTED_IDLE </b>     <br>
       (2) <b> NOT_CONNECTED </b>.     @par

    <b> Command </b> is one of these:  <br>
       (1) <b>CONNECT</b>              <br>
       (2) <b>SEND</b>                 <br>
       (3) <b>RECV</b>                 <br>
       (4) <b>CLOSE</b>.               @par

    <b>Event</b> is one of these:         <br>
       (1) <b>CONNECTION_CREATED</b>      <br>
       (2) <b>CONNECTION_DESTROYED</b>    <br>
       (3) <b>CONNECT_ERROR</b>           <br>
       (4) <b>RECV_COMPLETE</b>           <br>
       (5) <b>RECV_TIMEOUT</b>            <br>
       (6) <b>SEND_COMPLETE</b>.          @par

    There are also two other entities involved: an instance of the GenericClient
    class, provided by the library, and the <b>application code</b> 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. @par

    The general flow of control in an application written with this library is
    like this: <br>
       (1) the <b> application code </b> passes up a <b> command</b> to
           the GenericClient instance, <br>
       (2) which processes the received <b> command</b> and passes down an
           <b>event</b> to the <b> application code</b>; <br>
       (3) the <b> application code </b> then processes the <b> event </b>
           received from the GenericClient instance, and the cycle 
           repeats again. @par

    The GenericClient instance is driven by a state machine, which is described 
    below in pseudo-code:

    @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
    @endcode

    The <b> application code</b> 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. @par

    The pseudo-code for the <b>application code</b> is open, but generally will
    be like the following:

    @verbatim

    1. if event is CONNECTION_CREATED
          prepare message to send to the server
          pass the command SEND to the library

    2. if event is SEND_COMPLETE
          update whatever controls needed by the application
          pass the command RECV to the library

    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 library

    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 library
          else
             pass the command CLOSE to the library

    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 library

    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 library
          else
             end application

    @endverbatim

    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. @par

    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 no justify spending too much time in their development. @par


    The C code that implements the pseudo-code above is something like this:
    @code

    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();
          client_sleep(5);
          genCli_connect();
          break;

       case CLI_EVT_CONNECTION_DESTROYED:
          printOperationError();
          genCli_connect();
          break;

       default:
          printf("*invalid event %d\n", genCli_event());
          abort();
          break;
    } // switch

    @endcode

    Please refer to the <b>gen_client_1</b> example project for more details.
    @par

    The library also provides additional funcionality, presented by the Client
    class but in fact implemented by other classes (the Client class here acting
    as a façade). @par

    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 the <b>gen_client_1</b> 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):
       @link Client::client_messageBuffer client_messageBuffer @endlink,
       @link Client::client_messageSize client_messageSize @endlink, and
       @link Client::client_setMessageSize client_setMessageSize @endlink.
       @par

    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):
       @link Client::client_logInfo  client_logInfo @endlink,
       @link Client::client_logWarn  client_logWarn @endlink,
       @link Client::client_logDebug client_logDebug @endlink,
       @link Client::client_logTrace client_logTrace @endlink,
       @link Client::client_logError client_logError @endlink and
       @link Client::client_logFatal client_logFatal @endlink.
       @par

    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
    <a href="http://www.codeproject.com/KB/IP/TCPServerFramework.aspx">
    article</a>. Specifically, the <b>gen_client_1</b> example shipped
    with this project works with the <b> server_2 </b> and <b> server_3 </b>
    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.
*/

// -----------------------------------------------------------------------------

// includes

// common configuration options & declarations
#include "config.h" // always include first

// C language includes
#include <assert.h>
#include <stdlib.h> // abort
#include <string.h>

// Windows-specific includes
#if PLATFORM(Windows)
#pragma warning(disable: 4115)
#include <windows.h>
#endif

// Linux-specific includes
#if PLATFORM(Linux)
#include <arpa/inet.h>  // inet_ntoa
 #endif

// framework includes
#include "Client.h"        /* client_xxx functions  */
#include "Socket.h"        /* socket_xxx functions  */
#include "util/Log.h"      /* log_xxx functions     */
#include "util/Timeout.h"  /* timeout_xxx functions */

// -----------------------------------------------------------------------------

// global declarations

/// Client class.
/// This class provides convenience members and methods to write simple client
/// applications.
/// @class Client

/// Identification for the Log file
/// @private @memberof Client
static cchar sourceID[] = "ZCL";

/// Global counter used when sending messages
/// @private @memberof Client
static uint orgSeqNo;

/// Default server IP address in binary (127.1)
/// @private @memberof Client
static uint serverAddress = 0x7F000001;

/// Default server IP address as string
/// @private @memberof Client
static char serverAddressStr[50] = "127.1";

/// Default server TCP port
/// @private @memberof Client
static ushort servicePort = 4000;

/// The client socket
/// @private @memberof Client
static int clientSocket;

/// Default server reply timeout in seconds.
/// (0 means immediate, -1 means infinite).
/// @private @memberof Client
static uint replyTimeout = TIMEOUT_WAIT_FOREVER;

/// Default timeout in seconds when sending a message to the server.
/// In principle the client doesn't need to mess with this, but it's here anyway
/// @private @memberof Client
static ushort sendTimeout = 5;

/// State of the connection
/// @private @memberof Client
static bool isConnected;

// ---------------

// State of the client, valid only immediately after a socket operation

/// Tells if the connection has been closed after an operation end in error.
/// @private @memberof Client
static bool connectionClosed;  

/// Tells if there has been a format error on the wire right after a receive
/// operation.
/// @private @memberof Client
static bool formatError;

/// Contains the number of the error that occurred immediately after a socket
/// operation.                                                                     
/// @private @memberof Client
static int lastSocketError;

/// Contains the type of the error that occurred immediately after a socket
/// operation. 
/// @private @memberof Client
static int lastErrorType;

// private interface with GenericClient (sync if changed in either place)
// see also client_errorType
enum // for lastErrorType
{
   ERRTYPE_CLOSED = 1  ,  // connection closed by server
   ERRTYPE_SOCKET      ,  // socket error
   ERRTYPE_FORMAT      ,  // invalid format on the wire
   ERRTYPE_SENDTIMEOUT ,  // timeout in send
};

/// Tells id there has been a timeout after a receive operations ends in
/// error.
/// @private @memberof Client
static bool timeoutOccurred;

// ---------------

/// Tells if tracing is enabled.
/// @private @memberof Client
static bool isTracing;

// ---------------

// internal helper functions
/// @cond hides_from_doxygen
static int recvBuffer(int, char*, int, uint, bool);
/// @endcond

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Closes the connection.

    @memberof Client
*/

TC2API void client_closeConnection(void)
{
   log_func(client_closeConnection);
   log_finfo("closing the connection with the server");

   socket_close(clientSocket);
   isConnected = false;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Tries to connect with the server.

    @memberof Client
*/

TC2API bool client_connect(void)
{
   log_func(client_connect);
   log_finfo("trying to connect with the server %s", serverAddressStr);

   isConnected = false;

   if (socket_connect(clientSocket, serverAddress, servicePort))
   {
      log_finfo("ok, connection successful");
      isConnected = true;
      return true;
   }

   lastSocketError = socket_error();

   log_ferror("error %d trying to connect with the server", lastSocketError);
   return false;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Tells if the connection has been closed right after an operation
    (send/receive) ends in error.

    @return
    true (connection has been closed) / false (connection has not been closed)

    @memberof Client
*/

TC2API bool client_connectionClosed(void)
{
   return connectionClosed;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Creates the client socket.

    @post
    either succeeds or aborts the application

    @memberof Client
*/

TC2API void client_createClientSocket(void)
{
   log_func(client_createClientSocket);
   log_finfo("creating the client socket");

   clientSocket = socket_createClientSocket();

// don't do it for now 
// socket_setNonBlocking(clientSocket);
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Retrieves the value of the error that occurred immediately after a socket
    operation.

    @return
    number of error that occurred immediately after a socket operation

    @memberof Client
*/

TC2API uint client_error(void)
{
   return lastSocketError;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Retrieves the type of the error that occurred immediately after a socket
    operation.

    @return
    type of the error (only for use of the class GenericClient)

    @memberof Client
*/

TC2API ushort client_errorType(void)
{
   return lastErrorType;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Reports if there has been a format error on the wire right after a receive
    operation.

    @return
    true (a format error occurred) / false (a format error not occurred)

    @memberof Client
*/

TC2API bool client_formatError(void)
{
   return formatError;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Initializes the Client façade class.

    @memberof Client
*/

TC2API void client_init(void)
{
   log_func(client_init);
   log_finfo("Client class inicialization");

   socket_init();
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Checks if the client is connected to the server.

    @return
    true (is connected) / false (is not connected)

    @memberof Client
*/

TC2API bool client_isConnected(void)
{
   return isConnected;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Receives a message from the server.

    @param
    msg : address of a Message structure which will be used to store data
    received from the server

    @return
    true (reception successful) / false (reception failure)

    @memberof Client
*/

TC2API bool client_recvMessage(Message* msg)
{
   int ret;
   Timeout timeout;
   log_func(client_recvMessage);
   static cchar msgclose[] = "now closing connection";

   /// @cond hides_from_doxygen
   #define REM_TIME timeout_remaining(&timeout)
   /// @endcond

   assert(msg);

   timeout_init(&timeout, replyTimeout);

   // tries to receive the message's first part
   log_fdebug("trying to receive %d bytes (first part of message)",
      message_size1());

   ret = recvBuffer(clientSocket, message_start(msg), message_size1(),
      REM_TIME, true /*first part of message*/);

   if (ret != (int)message_size1())
   {
      log_ferror("error when receiving first part of message from server");

      // if timeout and not one byte has arrived then don't close connection
      if (timeoutOccurred && (lastErrorType != ERRTYPE_FORMAT))
         return false;
      goto ERROR_WITH_CLOSE;
   }

   // consistency check
   if (!message_isValidPrefix(msg))
   {
      log_ferror("invalid message prefix [%c] received from server",
         message_prefix(msg));
      goto FORMAT_ERROR_WITH_CLOSE;
   }

   // consistency check
   if (message_size(msg) > message_maxBodySize())
   {
      log_ferror("invalid message size [%d] received from server",
          message_size(msg));
      goto FORMAT_ERROR_WITH_CLOSE;
   }

   // tries to receive the message's second part
   log_fdebug("trying to receive %d bytes (second part of message)",
      message_size2(msg));

   ret = recvBuffer(clientSocket, message_start(msg)+message_size1(),
      message_size2(msg), REM_TIME, false /*not first part of message*/);

   if (ret != (int)message_size2(msg))
   {
      log_ferror("error when receiving seconf part of message from server");
      goto ERROR_WITH_CLOSE;
   }

   // consistency check
   if (!message_isValidSuffix(msg))
   {
      log_ferror("invalid message suffix [%c] received from server",
         message_suffix(msg));
      goto FORMAT_ERROR_WITH_CLOSE;
   }

   log_fdebug("received %d bytes from server, body:%d orgId:%d orgSeq:%d",
      message_size1()+message_size2(msg), message_size(msg),
      message_orgId(msg), message_orgSeqNo(msg));

   return true;

FORMAT_ERROR_WITH_CLOSE:
   formatError = true;
   lastErrorType = ERRTYPE_FORMAT;
ERROR_WITH_CLOSE:
   log_fwarn(msgclose);
   client_closeConnection();
   return false;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Tries to send a message to the server.

    @param
    msg : address of a Message structure with the data that will be sent to the
    server

    @return
    true (operation successful) / false (operation failed)

    @memberof Client
*/

TC2API bool client_sendMessage(Message* msg)
{
   int ret, size;
   log_func(client_sendMessage);

   assert(msg);

   // to make it easier to find this message in the server's log
   message_setOrgId(msg, thread_pid());
   message_setOrgSeqNo(msg, orgSeqNo++);

   size = message_size1() + message_size2(msg);

   // clears indicators
   lastSocketError = 0;
   lastErrorType = 0;
   timeoutOccurred = false;

   // tries to send the message
   log_fdebug("sending %d bytes to server, body:%d orgId:%d orgSeq:%d",
      size, message_size(msg), message_orgId(msg), message_orgSeqNo(msg));

   ret = socket_sendBuffer(clientSocket, message_start(msg), size, sendTimeout,
      isTracing);

   if (ret == size)
   {
      log_fdebug("ok, message sent successfully");
      return true; // ok
   }

   if (ret == E_SOCKET_TIMEOUT)
   {
      log_fwarn("timeout occurred when writing socket");
      timeoutOccurred = true;
      lastErrorType = ERRTYPE_SENDTIMEOUT;
   }

   else // if (ret == SOCKET_ERROR)
   {
      lastSocketError = socket_error();
      log_ferror("error %d when writing socket", lastSocketError);
      lastErrorType = ERRTYPE_SOCKET;
   }

   log_fwarn("now closing connection");
   client_closeConnection();

   return false;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Configures how many seconds to wait for a reply from the server.

    @param [in]
    seconds : how many seconds to wait for a reply from the server.

    @memberof Client
*/

TC2API void client_setReplyTimeout(ushort seconds)
{
   log_func(client_setReplyTimeout);
   log_finfo("reply timeout: %d seconds", seconds);

   replyTimeout = seconds;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Configures the server IP address.

    @param
    ipAddress : IP address of the server

    @memberof Client
*/

TC2API void client_setServerAddress(cchar* ipAddress)
{
   log_func(client_setServerAddress);
   log_finfo("using server address: %s", ipAddress);

   strncpy(serverAddressStr, ipAddress, sizeof(serverAddressStr)-1);
   serverAddress = inet_addr(serverAddressStr);
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Configures the server TCP port.

    @param
    port : service port on which the server is waiting.

    @memberof Client
*/

TC2API void client_setServicePort(ushort port)
{
   log_func(client_setServicePort);
   log_finfo("using port: %d", port);

   servicePort = port;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Enables or disables tracing.

    @param
    flag : true/false

    @memberof Client
*/

TC2API void client_setTrace(bool flag)
{
   isTracing = flag;
}

// -----------------------------------------------------------------------------
// PUBLIC INTERFACE
// -----------------------------------------------------------------------------

/** Reports if there has been a timeout after a receive operations ends in
    error.

    @return
    true (timeout occurred) / false (timeout not occurred)

    @memberof Client
*/

TC2API bool client_timeout(void)
{
   return (timeoutOccurred && (lastErrorType == ERRTYPE_FORMAT));
}

// -----------------------------------------------------------------------------
// INTERNAL HELPER FUNCTIONS
// -----------------------------------------------------------------------------

/** Receives a specific-length buffer from peer application, and checks for
    error.

    @return
    number of bytes received, or error

    @retval
    the number of bytes received, or error

    @retval
     0 : (SOCKET_CLOSED) connection was closed by the server.

    @retval
    -1 : (SOCKET_ERROR) an error occurred.

    @retval
    -2 : (SOCKET_TIMEOUT) not all bytes arrived within the specified time
    interval.

    @private @memberof Client
*/

static int recvBuffer(int socket, char* buf, int len, unsigned nSeconds,
   bool firstPart)
{
   int ret;
   log_func(recvBuffer);

   // clears indicators
   connectionClosed = false;
   formatError = false;
   lastSocketError = 0;
   lastErrorType = 0;
   timeoutOccurred = false;

   ret = socket_recvBuffer(socket, buf, len, nSeconds, isTracing);

   if (ret == len) // most likely result
      goto END;

   if (ret == E_SOCKET_CLOSED) // second most likely result
   {
      log_fwarn("connection closed when reading socket");
      connectionClosed = true;
      lastErrorType = ERRTYPE_CLOSED;
      goto END;
   }

   if (ret == E_SOCKET_TIMEOUT) // third most likely result
   {
      log_fwarn("timeout occurred when reading socket");
      // timeout not considered error if not one byte received,
      // only considered error if occurs int the middle of a message
      if (!firstPart || socket_partialLenReceived())
         lastErrorType = ERRTYPE_FORMAT;
      timeoutOccurred = true;
      goto END;
   }

   if (ret == E_SOCKET_ERROR) // fourth most likely result
   {
      lastSocketError = socket_error();
      log_ferror("error %d when reading socket", lastSocketError);
      lastErrorType = ERRTYPE_SOCKET;
      goto END;
   }

   // should not happen (!)
   abort();

END:
   return ret;
}

// -----------------------------------------------------------------------------
// the end

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
zvx
Software Developer
Brazil Brazil
I'm a long-time software developer living in Brazil.

I've been developing software for retail and banking automation in C/C++ for many years now. In the old days I even did some COBOL programming, and some assembly for the 8080.

My experience ranges from low level software such as interface code for serial devices for DOS and Windows (bar code scanners, printers, cash dispensers, etc) and goes to writing end user applications for POS terminals and bank ATMs. In between I've done a great deal of TCP/IP programming using the basic Berkeley sockets interface, which is my main interest nowadays.

Comments and Discussions