1
\$\begingroup\$

Question: What do you think about this design and the implementation? This is a simple networking framework for IPv4, IPv6 TCP client and TCP server for Linux and MS-Windows in C++14. It uses a single-thread solution around the system call select(), that is IO-multiplexing. Connect and network read are asynchronous, network write is synchronous. The iomux.hpp file implements the framework as a header only library and chatpp.cpp implements a chat application as example.

On http://www.andreadrian.de/non-blocking_connect/index.html is a lot of information about an older version of this source code.

/* chatpp.cpp
 * chat application, example for simple networking framework in C++
 *
 * Copyright 2022 Andre Adrian
 * License for this source code: 3-Clause BSD License 
 *
 * 28jun2022 adr: 3rd published version
 */

#include <libgen.h>     // basename()
#include "iomux.hpp"

/* ******************************************************** */
// Business logic

enum {
  BUFMAX = 1460,   // Ethernet packet size minus IPv4 TCP header size
};

class Chat_Client : public Client {
public:
  virtual int cb_read(SOCKET fd) override;
  static Conn* make() { return make_impl<Chat_Client>(); }
};

// callback client network read available
int Chat_Client::cb_read(SOCKET fdrecv) {
  assert(fdrecv >= 0 && fdrecv < FDMAX);
  char buf[BUFMAX];
  int readbytes = recv(fdrecv, buf, sizeof buf, 0);
  if (readbytes > 0 && readbytes != SOCKET_ERROR) {
    // Write all data out
    int writebytes = fwrite(buf, 1, readbytes, stdout);
    assert(writebytes == readbytes && "fwrite");
  }
  return readbytes;
}

class Chat_Server : public Server {
public:
  virtual int cb_read(SOCKET fd) override;
  static Conn* make() { return make_impl<Chat_Server>(); }
};

// callback server network read available
int Chat_Server::cb_read(SOCKET fdrecv) {
  assert(fdrecv >= 0 && fdrecv < FDMAX);
  char buf[BUFMAX];    // buffer for client data
  int readbytes = recv(fdrecv, buf, sizeof buf, 0);
  if (readbytes > 0 && readbytes != SOCKET_ERROR) {
    // we got some data from a client
    for (SOCKET fd = 0; fd < FDMAX; ++fd) {
      // send to everyone!
      if (FD_ISSET(fd, &fds)) {
        // except the listener and ourselves
        if (fd != sockfd && fd != fdrecv) {
          int writebytes = send(fd, buf, readbytes, 0);
          if (writebytes != readbytes) {
            perror("WW send");
          }
        }
      }
    }
  }
  return readbytes;
}

// callback keyboard polling
void cb_keyboard_poll(Conn* obj) {
  assert(obj != nullptr);
  if (_kbhit()) {   // very MS-DOS
    char buf[BUFMAX];
    char* rv = fgets(buf, sizeof buf, stdin);
    if (rv != nullptr) {
      send(Conn::get_sockfd(obj), buf, strlen(buf), 0);
    }
  }
  Timer::after(TIMEOUT, reinterpret_cast<cb_timer_t>(cb_keyboard_poll), obj);
}

int main(int argc, char* argv[]) {
  iomux_begin();

  if (argc < 4) {
    char* name = basename(argv[0]);
    fprintf(stderr,"usage server: %s s hostname port\n", name);
    fprintf(stderr,"usage client: %s c hostname port\n", name);
    fprintf(stderr,"example server IPv4: %s s 127.0.0.1 60000\n", name);
    fprintf(stderr,"example client IPv4: %s c 127.0.0.1 60000\n", name);
    fprintf(stderr,"example server IPv6: %s s ::1 60000\n", name);
    fprintf(stderr,"example client IPv6: %s c ::1 60000\n", name);
    exit(EXIT_FAILURE);
  }

  switch(argv[1][0]) {
  case 'c': {
    Conn* obj = Chat_Client::make();
    obj->connectRetry = 1;
    obj->open(argv[2], argv[3]);
    Timer::after(TIMEOUT, reinterpret_cast<cb_timer_t>(cb_keyboard_poll), obj);
  }
  break;
  case 's': {
    Conn* obj = Chat_Server::make();
    obj->open(argv[2], argv[3]);
  }
  break;
  default:
    fprintf(stderr,"EE %s: unexpected argument %s\n", FUNCTION, argv[1]);
    exit(EXIT_FAILURE);
  }

  Conn::event_loop();  // start inversion of control
  iomux_end();
  return 0;
}

The header file iomux.hpp is very simple. The real work is done in the operating system specific header files liomux.hpp and wiomux.hpp.

/* iomux.hpp
 */

#ifndef IOMUX_HPP_
#define IOMUX_HPP_

#ifdef _WIN32
#include "wiomux.hpp"
#else
#include "liomux.hpp"
#endif

#endif  // IOMUX_HPP_

The framework offers a "Timer class" and a "Connection class". The Connection class sub-classes into a "server class" and into a "client class". The business logic are sub-classes of "server class" and "client class". We have a little class hierarchy.

/* liomux.hpp
 * Simple networking framework in C++
 * Linux version
 * I/O Multiplexing (select) IPv4, IPv6, TCP Server, TCP client
 *
 * Copyright 2022 Andre Adrian
 * License for this source code: 3-Clause BSD License 
 *
 * 28jun2022 adr: 4th published version
 */

#ifndef LIOMUX_HPP_
#define LIOMUX_HPP_

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <libgen.h>     // basename()
#include <time.h>
#include <memory>       // make_unique, unique_ptr
#include <cstdint>
#include <climits>

// Linux
#include <errno.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <arpa/inet.h>

#define FUNCTION __PRETTY_FUNCTION__

enum {
  CONNMAX = 10,     // maximum number of Connection objects
  FDMAX = 64,       // maximum number of open file descriptors
  TIMEOUT = 40,     // select() timeout in milli seconds
  STRMAX = 80,      // maximum length of C-String
  TIMERMAX = 10,    // maximum number of Timer objects
  SOCKET_ERROR = INT_MAX,     // for MS-Windows compability
  INVALID_SOCKET = INT_MAX-1, // for MS-Windows compability
};

typedef int SOCKET; // for MS-Windows compability

// Copies a string with security enhancements
void strcpy_s(char* dest, size_t n, const char* src) {
  strncpy(dest, src, n-1);
  dest[n - 1] = '\0';
}

/** @brief Checks the console for keyboard input
 * @retval >0 keyboard input
 * @retval =0 no keyboard input
 */
int _kbhit(void) {
  fd_set fds;
  FD_ZERO(&fds);
  FD_SET(STDIN_FILENO, &fds);
  struct timeval tv = {0, 0};
  return select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv);
}

/* ******************************************************** */
// Timer object

using cb_timer_t = void (*)(void* obj);

class Timer {
  cb_timer_t cb_timer;  // cb_timer and obj are a closure
  void* obj;
  struct timespec ts;   // expire time
public:
  static int after(int interval, cb_timer_t cb_timer, void* obj);
  static void walk();
};

static Timer timers[TIMERMAX];  // Timer objects array

int Timer::after(int interval, cb_timer_t cb_timer, void* obj) {
  assert(interval >= 0);
  assert(cb_timer != nullptr);
  // no assert obj

  int id;
  for (id = 0; id < TIMERMAX; ++id) {
    if (nullptr == timers[id].cb_timer) {
      break;  // found a free entry
    }
  }
  assert (id < TIMERMAX && "timer array full");

  // convert interval in milliseconds to timespec
  struct timespec dts;
  dts.tv_nsec = (interval % 1000) * 1000000;
  dts.tv_sec = interval / 1000;
  struct timespec now;
  clock_gettime(CLOCK_MONOTONIC, &now);
  timers[id].cb_timer = cb_timer;
  timers[id].obj = obj;
  timers[id].ts.tv_nsec = (now.tv_nsec + dts.tv_nsec) % 1000000000;
  timers[id].ts.tv_sec = (now.tv_nsec + dts.tv_nsec) / 1000000000;
  timers[id].ts.tv_sec += (now.tv_sec + dts.tv_sec);
  /*
  printf("II %s now=%ld,%ld dt=%ld,%ld ts=%ld,%ld\n", FUNCTION,
    now.tv_sec, now.tv_nsec, dts.tv_sec, dts.tv_nsec,
    timers[i].ts.tv_sec, timers[i].ts.tv_nsec);
  */
  return id;
}

void Timer::walk() {
  // looking for expired timers
  for (int i = 0; i < TIMERMAX; ++i) {
    if (timers[i].cb_timer != nullptr) {
      struct timespec ts;
      clock_gettime(CLOCK_MONOTONIC, &ts);
      if ((ts.tv_sec > timers[i].ts.tv_sec) || (ts.tv_sec == timers[i].ts.tv_sec
          && ts.tv_nsec >= timers[i].ts.tv_nsec)) {
        Timer tmp = timers[i];
        // erase array entry because called function can overwrite this entry
        memset(&timers[i], 0, sizeof timers[i]);
        assert(tmp.cb_timer != nullptr);
        (*tmp.cb_timer)(tmp.obj);
      }
    }
  }
}

/* ******************************************************** */
// Connection object

class Conn {
  virtual void handle(SOCKET fd) = 0;
  virtual void connected() = 0;
protected:
  fd_set fds;       // read file descriptor set
  SOCKET sockfd;    // network port for client, listen port for server
  char hostname[STRMAX];
  char port[STRMAX];
  uint8_t isConnecting;   // non-blocking connect started, but no finished
public:
  uint8_t acceptOne;      // behavior modifier for server 
  uint8_t connectRetry;   // behavior modifier for client

  Conn() : sockfd(-1), isConnecting(0), acceptOne(0), connectRetry(0) {
    FD_ZERO(&fds);
  };
  virtual void open(const char* hostname, const char* port) = 0;
  virtual int cb_read(SOCKET fd) = 0;
  virtual void open1() = 0;
  virtual ~Conn() { };

  static SOCKET get_sockfd(Conn* obj);
  static const fd_set* get_fds(Conn* obj);
  static void event_loop();
};

static std::unique_ptr<Conn> conns[CONNMAX]; // Connection objects pointers array

SOCKET Conn::get_sockfd(Conn* obj) {
  assert(obj != nullptr);
  return obj->sockfd;
}

const fd_set* Conn::get_fds(Conn* obj) {
  assert(obj != nullptr);
  return &(obj->fds);
}

class Client : public Conn {
  virtual void handle(SOCKET fd) override;
  virtual void connected() override;
  void reopen();
public:
  virtual void open(const char* hostname, const char* port) override;
  virtual int cb_read(SOCKET fd) override = 0;
  static void open1_(Conn* obj);   // from C to C++ helper
  virtual void open1() override;
};

void Client::open(const char* hostname_, const char* port_) {
  assert(port_ != nullptr);
  assert(hostname_ != nullptr);
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port_, hostname_);

  FD_ZERO(&fds);

  strcpy_s(port, sizeof port, port_);
  strcpy_s(hostname, sizeof hostname, hostname_);
  open1();
}

void Client::open1() {
  struct addrinfo hints;
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  struct addrinfo* res;
  int rv = getaddrinfo(hostname, port, &hints, &res);
  if (rv != 0) {
    fprintf(stderr, "EE %s getaddrinfo: %s\n", FUNCTION, gai_strerror(rv));
    exit(EXIT_FAILURE);
  }

  // loop through all the results and connect to the first we can
  struct addrinfo* p;
  for (p = res; p != nullptr; p = p->ai_next) {
    sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (-1 == sockfd) {
      perror("WW socket");
      continue;
    }

    // UNPv2 ch. 15.4, 16.2 non-blocking connect
    int val = 1;
    rv = ioctl(sockfd, FIONBIO, &val);
    if (rv != 0) {
      perror("WW ioctl FIONBIO ON");
      close(sockfd);
      continue;
    }

    rv = connect(sockfd, p->ai_addr, p->ai_addrlen);
    if (rv != 0) {
      if (EINPROGRESS == errno) {
        isConnecting = 1;
      } else {
        perror("WW connect");
        close(sockfd);
        continue;
      }
    }

    break;  // exit loop after socket and connect were successful
  }

  assert(p != nullptr && "connect try");

  char dst[NI_MAXHOST];
  rv = getnameinfo(p->ai_addr, p->ai_addrlen, dst, sizeof dst, nullptr, 0, 0);
  assert(0 == rv && "getnameinfo");

  freeaddrinfo(res);

  // don't add sockfd to the fd_set, client_connect() will do

  printf("II %s: connect try to %s (%s) port %s socket %d\n", FUNCTION,
         hostname, dst, port, sockfd);
}

void Client::open1_(Conn* obj) {
  assert(obj != nullptr);
  obj->open1();
}

void Client::reopen() {
  close(sockfd);
  FD_CLR(sockfd, &fds);   // remove network fd
  sockfd = -1;
  if (connectRetry) {
    Timer::after(5000, reinterpret_cast<cb_timer_t>(Client::open1_), this);
    // ugly bug with after(0, ...
  } else {
    exit(EXIT_SUCCESS);
  }
}

void Client::handle(SOCKET fdrecv) {
  assert(fdrecv >= 0 && fdrecv < FDMAX);
  int rv = cb_read(fdrecv);
  if (rv < 1) {
    int optval = 0;
    socklen_t optlen = sizeof optval;
    int rv = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen);
    assert(0 == rv && "getsockopt SOL_SOCKET SO_ERROR");
    fprintf(stderr, "WW %s: connect fail to %s port %s socket %d rv %d: %s\n",
            FUNCTION, hostname, port, sockfd, rv, strerror(optval));
    reopen();
  }
}

void Client::connected() {
  isConnecting = 0;
  int optval = 0;
  socklen_t optlen = sizeof optval;
  int rv = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen);
  assert(0 == rv && "getsockopt SOL_SOCKET SO_ERROR");
  if (0 == optval) {
    FD_SET(sockfd, &fds);
    printf("II %s: connect success to %s port %s socket %d\n", FUNCTION,
           hostname, port, sockfd);
  } else {
    fprintf(stderr, "WW %s: connect fail to %s port %s socket %d: %s\n", FUNCTION,
            hostname, port, sockfd, strerror(optval));
    reopen();
  }
}

class Server : public Conn {
  virtual void handle(SOCKET fdrecv) override;
  virtual void connected() override { };  // dummy method
public:
  virtual void open(const char* hostname, const char* port) override;
  virtual int cb_read(SOCKET fdrecv) override = 0;
  virtual void open1() override { };  // dummy method
};

void Server::open(const char* hostname, const char* port) {
  assert(port != nullptr);
  // no assert hostname
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port, hostname);

  FD_ZERO(&fds);

  struct addrinfo hints;
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;

  struct addrinfo* res;
  int rv = getaddrinfo(hostname, port, &hints, &res);
  if (rv != 0) {
    fprintf(stderr, "EE %s getaddrinfo: %s\n", FUNCTION, gai_strerror(rv));
    exit(EXIT_FAILURE);
  }

  struct addrinfo* p;
  for(p = res; p != nullptr; p = p->ai_next) {
    sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (-1 == sockfd) {
      perror("WW socket");
      continue;
    }

    int yes = 1;
    rv = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    if (rv != 0) {
      perror("WW setsockopt SO_REUSEADDR");
      close(sockfd);
      continue;
    }

    rv = bind(sockfd, p->ai_addr, p->ai_addrlen);
    if (rv != 0) {
      perror("WW bind");
      close(sockfd);
      continue;
    }

    break;  // exit loop after socket and bind were successful
  }

  freeaddrinfo(res);

  assert(p != nullptr && "bind");

  rv = listen(sockfd, 10);
  assert(0 == rv && "listen");

  // add the listener to the master set
  FD_SET(sockfd, &fds);

  printf("II %s: listen on socket %d\n", FUNCTION, sockfd);
}

void Server::handle(SOCKET fdrecv) {
  assert(fdrecv >= 0 && fdrecv < FDMAX);
  if (fdrecv == sockfd) {
    // handle new connections
    struct sockaddr_storage remoteaddr; // client address
    socklen_t addrlen = sizeof remoteaddr;
    if (acceptOne) {
      // close old connections
      for (SOCKET fd = 0; fd < FDMAX; ++fd) {
        if (FD_ISSET(fd, &fds) && fd != sockfd) {
          close(fd);
          FD_CLR(fd, &fds);
        }
      }
    }
    // newly accept()ed socket descriptor
    SOCKET newfd = accept(sockfd, reinterpret_cast<struct sockaddr*>(&remoteaddr),
                       &addrlen);
    if (-1 == newfd) {
      perror("WW accept");
    } else {
      FD_SET(newfd, &fds); // add to master set
      char dst[NI_MAXHOST];
      int rv = getnameinfo(reinterpret_cast<struct sockaddr*>(&remoteaddr),
                           sizeof remoteaddr, dst, sizeof dst, nullptr, 0, 0);
      assert(0 == rv && "getnameinfo");
      printf("II %s: new connection %s on socket %d\n", FUNCTION, dst, newfd);
    }
  } else {
    int rv = cb_read(fdrecv);
    if (rv < 1) {
      printf("II %s: connection closed on socket %d\n", FUNCTION, fdrecv);
      close(fdrecv);
      FD_CLR(fdrecv, &fds);   // remove from fd_set
    }
  }
}

void Conn::event_loop() {
  for(;;) {
    // virtualization pattern: join all read fds into one
    fd_set read_fds;
    FD_ZERO(&read_fds);
    for (int i = 0; i < CONNMAX; ++i) {
      if (conns[i]) {
        for (SOCKET fd = 0; fd < FDMAX; ++fd) {
          if (FD_ISSET(fd, &conns[i]->fds)) {
            FD_SET(fd, &read_fds);
          }
        }
      }
    }
    // virtualization pattern: join all connect pending into one
    fd_set write_fds;
    FD_ZERO(&write_fds);
    for (int i = 0; i < CONNMAX; ++i) {
      if (conns[i] && conns[i]->isConnecting) {
        FD_SET(conns[i]->sockfd, &write_fds);
      }
    }

    struct timeval tv = {0, TIMEOUT * 1000};
    int rv = select(FDMAX, &read_fds, &write_fds, nullptr, &tv);
    if (-1 == rv && EINTR != errno) {
      perror("EE select");
      exit(EXIT_FAILURE);
    }

    if (rv > 0) {
      // looking for data to read available
      for (SOCKET fd = 0; fd < FDMAX; ++fd) {
        if (FD_ISSET(fd, &read_fds)) {
          for (int i = 0; i < CONNMAX; ++i) {
            if (conns[i] && FD_ISSET(fd, &conns[i]->fds)) {
              conns[i]->handle(fd);  // vtable "switch"
            }
          }
        }
      }
      // looking for connect pending success or fail
      for (int i = 0; i < CONNMAX; ++i) {
        if (conns[i] && FD_ISSET(conns[i]->sockfd, &write_fds)) {
          conns[i]->connected();
        }
      }
    }
    Timer::walk();
  }
}

template <class T>
Conn* make_impl() {
  for (int i = 0; i < CONNMAX; ++i) {
    if (nullptr == conns[i]) {
      conns[i] = std::make_unique<T>();
      return conns[i].get();
    }
  }
  assert(0 && "conns array full");
  return nullptr;
}

void iomux_begin() {
  // do nothing
}

void iomux_end() {
  // do nothing
}

#endif  // LIOMUX_HPP_

The MS-Windows implemenation is different in the area of asynchrounous connect. The Linux select() uses write fd_set to signal the "connecting success/failed" event, but MS-Windows select() uses exception fd_set for this event. There are some more differences. Linux can use file descriptors for check for console input, read from/write to the file system and read from/write to the network. MS-Windows uses the MS-DOS leftover _kbhit() for the first, stream IO for the second and SOCKET descriptors for the third. Worst of all, SOCKET descriptors are unsigned 64 bit integers. Somebody at Microsoft was thinking big.

/* wiomux.hpp
 * Simple networking framework in C++
 * MS-Windows version
 * I/O Multiplexing (select) IPv4, IPv6, TCP Server, TCP client
 *
 * Copyright 2022 Andre Adrian
 * License for this source code: 3-Clause BSD License 
 *
 * 28jun2022 adr: 4th published version
 */

#ifndef WIOMUX_HPP_
#define WIOMUX_HPP_

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <libgen.h>     // basename()
#include <time.h>
#include <memory>       // make_unique, unique_ptr
#include <cstdint>

// MS-Windows
#include <conio.h>      // _kbhit()
#include <winsock2.h>
#include <ws2tcpip.h>   // getaddrinfo()

#define FUNCTION __PRETTY_FUNCTION__

enum {
  CONNMAX = 10,     // maximum number of Connection objects
  FDMAX = 256,      // maximum number of open file descriptors
  TIMEOUT = 40,     // select() timeout in milli seconds
  STRMAX = 80,      // maximum length of C-String
  TIMERMAX = 10,    // maximum number of Timer objects
};

// get sockaddr, IPv4 or IPv6:
void* get_in_addr(struct sockaddr* sa) {
  assert(sa != nullptr);
  if (AF_INET == sa->sa_family) {
    return &(reinterpret_cast<struct sockaddr_in*>(sa)->sin_addr);
  } else {
    return &(reinterpret_cast<struct sockaddr_in6*>(sa)->sin6_addr);
  }
}

/* ******************************************************** */
// Timer object

using cb_timer_t = void (*)(void* obj);

class Timer {
  cb_timer_t cb_timer;  // cb_timer and obj are a closure
  void* obj;
  struct timespec ts;   // expire time
public:
  static int after(int interval, cb_timer_t cb_timer, void* obj);
  static void walk();
};

static Timer timers[TIMERMAX];  // Timer objects array

int Timer::after(int interval, cb_timer_t cb_timer, void* obj) {
  assert(interval >= 0);
  assert(cb_timer != nullptr);
  // no assert obj

  int id;
  for (id = 0; id < TIMERMAX; ++id) {
    if (nullptr == timers[id].cb_timer) {
      break;  // found a free entry
    }
  }
  assert (id < TIMERMAX && "timer array full");

  // convert interval in milliseconds to timespec
  struct timespec dts;
  dts.tv_nsec = (interval % 1000) * 1000000;
  dts.tv_sec = interval / 1000;
  struct timespec now;
  clock_gettime(CLOCK_MONOTONIC, &now);
  timers[id].cb_timer = cb_timer;
  timers[id].obj = obj;
  timers[id].ts.tv_nsec = (now.tv_nsec + dts.tv_nsec) % 1000000000;
  timers[id].ts.tv_sec = (now.tv_nsec + dts.tv_nsec) / 1000000000;
  timers[id].ts.tv_sec += (now.tv_sec + dts.tv_sec);
  /*
  printf("II %s now=%ld,%ld dt=%ld,%ld ts=%ld,%ld\n", FUNCTION,
    now.tv_sec, now.tv_nsec, dts.tv_sec, dts.tv_nsec,
    timers[i].ts.tv_sec, timers[i].ts.tv_nsec);
  */
  return id;
}

void Timer::walk() {
  // looking for expired timers
  for (int i = 0; i < TIMERMAX; ++i) {
    if (timers[i].cb_timer != nullptr) {
      struct timespec ts;
      clock_gettime(CLOCK_MONOTONIC, &ts);
      if ((ts.tv_sec > timers[i].ts.tv_sec) || (ts.tv_sec == timers[i].ts.tv_sec
          && ts.tv_nsec >= timers[i].ts.tv_nsec)) {
        Timer tmp = timers[i];
        // erase array entry because called function can overwrite this entry
        memset(&timers[i], 0, sizeof timers[i]);
        assert(tmp.cb_timer != nullptr);
        (*tmp.cb_timer)(tmp.obj);
      }
    }
  }
}

/* ******************************************************** */
// Connection object

class Conn {
  virtual void handle(SOCKET fd) = 0;
  virtual void connected() = 0;
protected:
  fd_set fds;       // read file descriptor set
  SOCKET sockfd;    // network port for client, listen port for server
  char hostname[STRMAX];
  char port[STRMAX];
  uint8_t isConnecting;   // non-blocking connect started, but no finished
public:
  uint8_t acceptOne;      // behavior modifier for server 
  uint8_t connectRetry;   // behavior modifier for client

  Conn() : sockfd(-1), isConnecting(0), acceptOne(0), connectRetry(0) {
    FD_ZERO(&fds);
  };
  virtual void open(const char* hostname, const char* port) = 0;
  virtual int cb_read(SOCKET fd) = 0;
  virtual ~Conn() { };

  static SOCKET get_sockfd(Conn* obj);
  static const fd_set* get_fds(Conn* obj);
  static void event_loop();
};

static std::unique_ptr<Conn> conns[CONNMAX]; // Connection objects pointers array

SOCKET Conn::get_sockfd(Conn* obj) {
  assert(obj != nullptr);
  return obj->sockfd;
}

const fd_set* Conn::get_fds(Conn* obj) {
  assert(obj != nullptr);
  return &(obj->fds);
}

class Client : public Conn {
  virtual void handle(SOCKET fd) override;
  virtual void connected() override;
  void open1();
public:
  virtual void open(const char* hostname, const char* port) override;
  virtual int cb_read(SOCKET fd) override = 0;
};

void Client::open(const char* hostname_, const char* port_) {
  assert(port_ != nullptr);
  assert(hostname_ != nullptr);
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port_, hostname_);

  FD_ZERO(&fds);

  strcpy_s(port, sizeof port, port_);
  strcpy_s(hostname, sizeof hostname, hostname_);
  open1();
}

void Client::open1() {
  struct addrinfo hints;
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  struct addrinfo* res;
  int rv = getaddrinfo(hostname, port, &hints, &res);
  if (rv != 0) {
    fprintf(stderr, "EE %s getaddrinfo: %d\n", FUNCTION, rv);
    exit(EXIT_FAILURE);
  }

  // loop through all the results and connect to the first we can
  struct addrinfo* p;
  for (p = res; p != nullptr; p = p->ai_next) {
    sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (INVALID_SOCKET == sockfd) {
      perror("WW socket");
      continue;
    }

    // UNPv2 ch. 15.4, 16.2 non-blocking connect
    unsigned long val = 1;
    rv = ioctlsocket(sockfd, FIONBIO, &val);
    if (rv != 0) {
      perror("WW ioctlsocket FIONBIO ON");
      closesocket(sockfd);
      continue;
    }

    rv = connect(sockfd, p->ai_addr, p->ai_addrlen);
    if (rv != 0) {
      if (WSAEWOULDBLOCK == WSAGetLastError()) {
        isConnecting = 1;
      } else {
        perror("WW connect");
        closesocket(sockfd);
        continue;
      }
    }

    break;  // exit loop after socket and connect were successful
  }

  assert(p != nullptr && "connect try");

  void* src = get_in_addr(reinterpret_cast<struct sockaddr*>(p->ai_addr));
  char dst[INET6_ADDRSTRLEN];
  inet_ntop(p->ai_family, src, dst, sizeof dst);

  freeaddrinfo(res);

  FD_SET(sockfd, &fds);

  printf("II %s: connect try to %s (%s) port %s socket %llu\n", FUNCTION,
         hostname, dst, port, sockfd);
}

void Client::connected() {
  closesocket(sockfd);
  FD_CLR(sockfd, &fds);   // remove network fd
  sockfd = -1;
  if (connectRetry) {
    open1();
  } else {
    exit(EXIT_SUCCESS);
  }
}

void Client::handle(SOCKET fdrecv) {
  assert(fdrecv < FDMAX);
  int rv = cb_read(fdrecv);
  if (rv < 1 || SOCKET_ERROR == rv) {
    // documentation conflict between
    // https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getsockopt
    // https://docs.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options
    unsigned long optval;
    int optlen = sizeof optval;
    int rv = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, 
                        reinterpret_cast<char*>(&optval), &optlen);
    assert(0 == rv && "getsockopt SOL_SOCKET SO_ERROR");
    fprintf(stderr, "WW %s: connect fail to %s port %s socket %llu rv %d: %s\n",
            FUNCTION, hostname, port, sockfd, rv, strerror(optval));
    connected();
  } else {
    isConnecting = 0;   // hack: connect successful after first good read()
  }
}

class Server : public Conn {
  virtual void handle(SOCKET fdrecv) override;
  virtual void connected() override { };  // dummy method
public:
  virtual void open(const char* hostname, const char* port) override;
  virtual int cb_read(SOCKET fdrecv) override = 0;
};

void Server::open(const char* hostname, const char* port) {
  assert(port != nullptr);
  assert(hostname != nullptr);
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port, hostname);

  FD_ZERO(&fds);

  struct addrinfo hints;
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;

  struct addrinfo* res;
  int rv = getaddrinfo(hostname, port, &hints, &res);
  if (rv != 0) {
    fprintf(stderr, "EE %s getaddrinfo: %d\n", FUNCTION, rv);
    exit(EXIT_FAILURE);
  }

  struct addrinfo* p;
  for(p = res; p != nullptr; p = p->ai_next) {
    sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (INVALID_SOCKET == sockfd) {
      perror("WW socket");
      continue;
    }

    // documentation conflict between
    // https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-setsockopt
    // https://docs.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options
    const unsigned long yes = 1;
    rv = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                    reinterpret_cast<const char*>(&yes), sizeof(yes));
    if (rv != 0) {
      perror("WW setsockopt SO_REUSEADDR");
      closesocket(sockfd);
      continue;
    }

    rv = bind(sockfd, p->ai_addr, p->ai_addrlen);
    if (rv != 0) {
      perror("WW bind");
      closesocket(sockfd);
      continue;
    }

    break;  // exit loop after socket and bind were successful
  }

  freeaddrinfo(res);

  assert(p != nullptr && "bind");

  rv = listen(sockfd, 10);
  assert(0 == rv && "listen");

  // add the listener to the master set
  FD_SET(sockfd, &fds);

  printf("II %s: listen on socket %llu\n", FUNCTION, sockfd);
}

void Server::handle(SOCKET fdrecv) {
  assert(fdrecv < FDMAX);
  if (fdrecv == sockfd) {
    // handle new connections
    struct sockaddr_storage remoteaddr; // client address
    socklen_t addrlen = sizeof remoteaddr;
    if (acceptOne) {
      // close old connections
      for (SOCKET fd = 0; fd < FDMAX; ++fd) {
        if (FD_ISSET(fd, &fds) && fd != sockfd) {
          closesocket(fd);
          FD_CLR(fd, &fds);
        }
      }
    }
    // newly accept()ed socket descriptor
    SOCKET newfd = accept(sockfd, reinterpret_cast<struct sockaddr*>(&remoteaddr),
                       &addrlen);
    if (INVALID_SOCKET == newfd) {
      perror("WW accept");
    } else {
      FD_SET(newfd, &fds); // add to master set
      void* src = get_in_addr(reinterpret_cast<struct sockaddr*>(&remoteaddr));
      char dst[INET6_ADDRSTRLEN];
      inet_ntop(remoteaddr.ss_family, src, dst, sizeof dst);
      printf("II %s: new connection %s on socket %llu\n", FUNCTION, dst, newfd);
    }
  } else {
    int rv = cb_read(fdrecv);
    if (rv < 1 || SOCKET_ERROR == rv) {
      printf("II %s: connection closed on socket %llu\n", FUNCTION, fdrecv);
      closesocket(fdrecv);
      FD_CLR(fdrecv, &fds);   // remove from fd_set
    }
  }
}

void Conn::event_loop() {
  for(;;) {
    // virtualization pattern: join all read fds into one
    fd_set read_fds;
    FD_ZERO(&read_fds);
    for (int i = 0; i < CONNMAX; ++i) {
      if (conns[i]) {
        for (SOCKET fd = 0; fd < FDMAX; ++fd) {
          if (FD_ISSET(fd, &conns[i]->fds)) {
            FD_SET(fd, &read_fds);
          }
        }
      }
    }
    // virtualization pattern: join all connect pending into one
    fd_set except_fds;
    FD_ZERO(&except_fds);
    for (int i = 0; i < CONNMAX; ++i) {
      if (conns[i] && conns[i]->isConnecting) {
        FD_SET(conns[i]->sockfd, &except_fds);
      }
    }

    struct timeval tv = {0, TIMEOUT * 1000};
    int rv = select(FDMAX, &read_fds, nullptr, &except_fds, &tv);
    if (SOCKET_ERROR == rv && WSAGetLastError() != WSAEINTR) {
      perror("EE select");
      exit(EXIT_FAILURE);
    }

    if (rv > 0) {
      // looking for data to read available
      for (SOCKET fd = 0; fd < FDMAX; ++fd) {
        if (FD_ISSET(fd, &read_fds)) {
          for (int i = 0; i < CONNMAX; ++i) {
            if (conns[i] && FD_ISSET(fd, &conns[i]->fds)) {
              conns[i]->handle(fd);  // vtable "switch"
            }
          }
        }
      }
      // looking for connect pending fail
      for (int i = 0; i < CONNMAX; ++i) {
        if (conns[i] && FD_ISSET(conns[i]->sockfd, &except_fds)) {
          conns[i]->connected();
        }
      }
    }
    Timer::walk();
  }
}

template <class T>
Conn* make_impl() {
  for (int i = 0; i < CONNMAX; ++i) {
    if (nullptr == conns[i]) {
      conns[i] = std::make_unique<T>();
      return conns[i].get();
    }
  }
  assert(0 && "conns array full");
  return nullptr;
}

void iomux_begin() {
  static WSADATA wsaData;
  int rv = WSAStartup(MAKEWORD(2, 2), &wsaData);
  assert(0 == rv && "WSAStartup");
}

void iomux_end() {
  WSACleanup();
}

#endif  // WIOMUX_HPP_

The line count (wc -l) of this source code is: 116 for chatpp.c, 481 for liomux.hpp and 448 for wiomux.hpp. I think, this is really a simple networking framework. The constant CONNMAX defines the maximum number of "TCP servers" and "TCP clients" in the application. TIMERMAX defines the maximum number of Timers and FDMAX defines the maximum number of open SOCKET descriptors.

There is a simple networking framework in C from me, see Simple networking framework in C

\$\endgroup\$
2
  • \$\begingroup\$ Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. \$\endgroup\$
    – pacmaninbw
    Jun 30, 2022 at 17:29
  • \$\begingroup\$ After an answer you ask a follow up question that links to this question with the updated code. \$\endgroup\$
    – pacmaninbw
    Jun 30, 2022 at 17:33

1 Answer 1

1
\$\begingroup\$

Make much more use of C++

Of course you will need to use POSIX functions to open sockets and read from and write to them, but a lot of your code is still using C functions where it could use C++ functions.

Instead of printf(), use std::cout and std::cerr, possibly in combination with std::format().

Instead of using arrays of char to store strings, use std::string, so you no longer need to use the unsafe strcpy() or the non-standard strcpy_s().

Error handling in C++ can be done in several ways. An easy one is to throw exceptions. Create your own exception type that derives from one of the existing ones, like std::runtime_error, so that you can do something like throw network_error("getaddrinfo() failed"). This will allow the caller to catch it if they want, and if they don't it will cause the program to be terminated.

C++ also comes with time handling functions, has lots of containers types and algorithms. Using these can greatly simplify your code. There is much more to C++ than just slapping a few classes here and there onto C code.

Timers the C++ way

As an example of how to make things more C++, let's look at the timers.

First, instead of having a function pointer that takes a void*, and having a void* obj that you pass to it, in effect forming a closure as you wrote in the comments, use std::function to store closures.

Second, instead of struct timespec, use the appropriate std::chrono type to store a timestamp. To avoid having to write long type names everywhere, you can create an alias.

Finally, since you want to be able to check which timers have expired, it would be nice to keep them somehow sorted. The first thing to make that happen is to make timers comparable based on their expiry time. You can overload operator< to do this.

Throw in a proper constructor and member functions read the expiry time and to trigger the callback, the result is:

using clock = std::chrono::steady_clock; // the equivalent of CLOCK_MONOTONIC

class Timer {
    clock::timestamp expire_time;
    std::function<void()> callback;

public:
    Timer(clock::timestamp expire_time, std::function<void()> callback):
        expire_time(expire_time), callback(callback) {}

    clock::timestamp get_expire_time() const {
        return expire_time;
    }

    void expire() {
        callback();
    }

    bool operator<(const Timer& other) {
        return expire_time > other.expire_time;
    }
};

Instead of having a fixed-length array, you can now store those timers in a std::priority_queue, and because of the operator< overload, it will sort the queue based in the expiry time. Note that because you can only access the largest element in a std::priority_queue, but we want the earliest expiry time, we had to invert the comparison inside operator<. Now you can do:

std::priority_queue<Timer> timers;

void after(clock::duration interval, std::function<void()> callback) {
    timers.emplace(clock::now() + interval, callback);
}

void walk_timers() {
    auto now = clock::now();

    while (!timers.empty() && timers.top().get_expire_time() < now) {
        timers.top().expire();
        timers.pop();
    }
}

And to use it, you would write something like:

static constexpr std::chrono::milliseconds TIMEOUT = 40;
...
case 'c': {
    Conn* obj = Chat_Client::make();
    obj->connectRetry = 1;
    obj->open(argv[2], argv[3]);
    after(TIMEOUT, [obj]{ cb_keyboard_poll(obj); });
}

A lambda expression is used here to capture obj and to call cb_keyboard_poll(obj) after the timeout expired. No casts were necessary. Also note how it is now clear from the code that TIMEOUT is in milliseconds, no need to comment this anywhere. No arbitrary limit on the number of timers that can be pending. Most importantly, much less code!

\$\endgroup\$
2
  • \$\begingroup\$ The socket functions recv() and send() work on C-strings. If I use C++ strings, I have to convert from one representation to the other representation. In the case of chat TCP server that is after recv() convert from C-string to C++-string and then before send() convert from C++-string to C-string. If you do some processing, this is a good approach. But if you are only moving data around, the conversion is questionable. \$\endgroup\$ Jun 30, 2022 at 19:39
  • \$\begingroup\$ Ah, recv() and send() don't work on C-strings though, they just work on buffers of any type. So it's fine to use a char buf[BUFMAX] there. When I mentioned C++ strings, I was thinking of the member variables hostname and port. \$\endgroup\$
    – G. Sliepen
    Jul 1, 2022 at 5:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.