3
\$\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. 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 liomux.h file implements the Linux specific version, the wiomux.h implements the MS-Windows specific version and chat.c 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.

/* chat.c
 * chat application, example for simple networking framework in C
 *
 * Copyright 2022 Andre Adrian
 * License for this source code: 3-Clause BSD License 
 *
 * 25jun2022 adr: 2nd published version
 */

#include <libgen.h>     // basename()
#ifdef _WIN32
#include "wiomux.h"
#else
#include "liomux.h"
#endif

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

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

// callback client read available
int cb_chat_client_read(Conn* obj, SOCKET fd) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(fd >= 0 && fd < FDMAX);
  char buf[BUFMAX];
  int readbytes = recv(fd, 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;
}

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

void keyboard_poll(Conn* obj) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  if (_kbhit()) {   // very MS-DOS
    char buf[BUFMAX];
    char* rv = fgets(buf, sizeof buf, stdin);
    assert(rv != NULL);
    send(obj->sockfd, buf, strlen(buf), 0);
  }
  after(TIMEOUT, (cb_timer_t)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 = conn_make();
    client_open(obj, argv[2], argv[3]);
    conn_add_cb(obj, cb_chat_client_read);
    after(TIMEOUT, (cb_timer_t)keyboard_poll, obj);
  }
  break;
  case 's': {
    Conn* obj = conn_make();
    server_open(obj, argv[2], argv[3]);
    conn_add_cb(obj, cb_chat_server_read);
  }
  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 framework offers a "Timer class" and a "Connection class". The Connection class sub-classes into a "server class" and into a "client class". Because C is not object oriented, these are only design ideas.

/* liomux.h
 * 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 
 *
 * 25jun2022 adr: 2nd published version
 */

#ifndef LIOMUX_H_
#define LIOMUX_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
#include <limits.h>

// 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 __func__

enum {
  CONNMAX = 10,     // maximum number of Connection objects
  FDMAX = 64,       // maximum number of open file descriptors
  CONN_SERVER = 1,  // TCP server connection object label
  CONN_CLIENT = 2,  // TCP client connection object label
  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

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

// 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, NULL, NULL, &tv);
}

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

typedef void (*cb_timer_t)(void* obj);

typedef struct {
  cb_timer_t cb_timer;  // cb_timer and obj are a closure
  void* obj;
  struct timespec ts;   // expire time
} Timer;

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

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

  int id;
  for (id = 0; id < TIMERMAX; ++id) {
    if (NULL == 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(void) {
  // looking for expired timers
  for (int i = 0; i < TIMERMAX; ++i) {
    if (timers[i].cb_timer != NULL) {
      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 != NULL);
        (*tmp.cb_timer)(tmp.obj);
      }
    }
  }
}

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

typedef struct Conn {
  fd_set fds;       // read file descriptor set
  int sockfd;       // network port for client, listen port for server
  int isConnecting; // non-blocking connect started, but no finished
  int typ;          // tells client or server object
  char hostname[STRMAX];
  char port[STRMAX];
  int (*cb_read)(struct Conn* obj, int fd);   // Callback Pointer
} Conn;

typedef int (*cb_read_t)(Conn* obj, int fd);  // Callback Pointer type

static Conn conns[CONNMAX];  // Connection objects array

Conn* conn_make(void) {
  for (int i = 0; i < CONNMAX; ++i) {
    if (0 == conns[i].typ) {
      return &conns[i];  // found a free entry
    }
  }
  assert(0 && "conn array full");
  return NULL;
}

void client_open(Conn* obj, const char* hostname, const char* port) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(port != NULL);
  assert(hostname != NULL);
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port, hostname);

  FD_ZERO(&obj->fds);
  obj->typ = CONN_CLIENT;

  strcpy_s(obj->port, sizeof obj->port, port);
  strcpy_s(obj->hostname, sizeof obj->hostname, hostname);
  void client_open1(Conn* obj);
  client_open1(obj);
}

void client_open1(Conn* obj) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  struct addrinfo hints;
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  struct addrinfo* res;
  int rv = getaddrinfo(obj->hostname, obj->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 != NULL; p = p->ai_next) {
    obj->sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (-1 == obj->sockfd) {
      perror("WW socket");
      continue;
    }

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

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

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

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

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

  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,
         obj->hostname, dst, obj->port, obj->sockfd);
}

void client_reopen(Conn* obj) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  close(obj->sockfd);
  FD_CLR(obj->sockfd, &obj->fds);   // remove network fd
  obj->sockfd = -1;
  after(5000, (cb_timer_t)client_open1, obj);
  // ugly bug with after(0, ...
}

void client_connect(Conn* obj) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  obj->isConnecting = 0;
  int optval = 0;
  socklen_t optlen = sizeof optval;
  int rv = getsockopt(obj->sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen);
  assert(0 == rv && "getsockopt SOL_SOCKET SO_ERROR");
  if (0 == optval) {
    FD_SET(obj->sockfd, &obj->fds);
    printf("II %s: connect success to %s port %s socket %d\n", FUNCTION,
           obj->hostname, obj->port, obj->sockfd);
  } else {
    fprintf(stderr, "WW %s: connect fail to %s port %s socket %d: %s\n", FUNCTION,
            obj->hostname, obj->port, obj->sockfd, strerror(optval));
    client_reopen(obj);
  }
}

void client_handle(Conn* obj, int fd) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(fd >= 0 && fd < FDMAX);
  assert(obj->cb_read != NULL);
  int rv = (*obj->cb_read)(obj, fd);
  if (rv < 1) {
    int optval = 0;
    socklen_t optlen = sizeof optval;
    int rv = getsockopt(obj->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, obj->hostname, obj->port, obj->sockfd, rv, strerror(optval));
    client_reopen(obj);
  }
}

void conn_add_cb(Conn* obj, cb_read_t cb_read) {
  assert(cb_read != NULL);
  obj->cb_read = cb_read;
}

void server_open(Conn* obj, const char* hostname, const char* port) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(port != NULL);
  // no assert hostname
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port, hostname);

  FD_ZERO(&obj->fds);
  obj->typ = CONN_SERVER;

  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 != NULL; p = p->ai_next) {
    obj->sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (-1 == obj->sockfd) {
      perror("WW socket");
      continue;
    }

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

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

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

  freeaddrinfo(res);

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

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

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

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

void server_handle(Conn* obj, int fd) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(fd >= 0 && fd < FDMAX);
  if (fd == obj->sockfd) {
    // handle new connections
    struct sockaddr_storage remoteaddr; // client address
    socklen_t addrlen = sizeof remoteaddr;
    // newly accept()ed socket descriptor
    int newfd = accept(obj->sockfd, (struct sockaddr*)&remoteaddr, &addrlen);
    if (-1 == newfd) {
      perror("WW accept");
    } else {
      FD_SET(newfd, &obj->fds);
      void* src = get_in_addr((struct sockaddr*)&remoteaddr);
      char dst[INET6_ADDRSTRLEN];
      inet_ntop(remoteaddr.ss_family, src, dst, sizeof dst);
      printf("II %s: new connection %s on socket %d\n", FUNCTION, dst, newfd);
    }
  } else {
    assert(obj->cb_read != NULL);
    int rv = (*obj->cb_read)(obj, fd);
    if (rv < 1) {
      printf("II %s: connection closed on socket %d\n", FUNCTION, fd);
      close(fd);
      FD_CLR(fd, &obj->fds); // remove from fd_set
    }
  }
}

void conn_event_loop(void) {
  for(;;) {
    // virtualization pattern: join all read fds into one
    fd_set read_fds = conns[0].fds;
    for (int i = 1; i < CONNMAX; ++i) {
      for (int 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].isConnecting) {
        FD_SET(conns[i].sockfd, &write_fds);
      }
    }

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

    if (rv > 0) {
      // looking for data to read available
      for (int fd = 0; fd < FDMAX; ++fd) {
        if (FD_ISSET(fd, &read_fds)) {
          for (int i = 0; i < CONNMAX; ++i) {
            if (FD_ISSET(fd, &conns[i].fds)) {
              switch (conns[i].typ) {
              case CONN_CLIENT:
                client_handle(&conns[i], fd);
                break;
              case CONN_SERVER:
                server_handle(&conns[i], fd);
                break;
              }
            }
          }
        }
      }
      // looking for connect pending success or fail
      for (int i = 0; i < CONNMAX; ++i) {
        if (FD_ISSET(conns[i].sockfd, &write_fds)) {
          client_connect(&conns[i]);
        }
      }
    }
    timer_walk();
  }
}

void iomux_begin(void) {
  // do nothing
}

void iomux_end(void) {
  // do nothing
}

#endif  // LIOMUX_H_

The MS-Windows implemenation is differnt 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.

/* wiomux.h
 * 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 
 *
 * 25jun2022 adr: 2nd published version
 */

#ifndef WIOMUX_H_
#define WIOMUX_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>

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

#define FUNCTION __func__

enum {
  CONNMAX = 10,     // maximum number of Connection objects
  FDMAX = 256,      // maximum number of open file descriptors
  CONN_SERVER = 1,  // TCP server connection object label
  CONN_CLIENT = 2,  // TCP client connection object label
  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 != NULL);
  if (AF_INET == sa->sa_family) {
    return &(((struct sockaddr_in*)sa)->sin_addr);
  } else {
    return &(((struct sockaddr_in6*)sa)->sin6_addr);
  }
}

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

typedef void (*cb_timer_t)(void* obj);

typedef struct  {
  cb_timer_t cb_timer;  // cb_timer and obj are a closure
  void* obj;
  struct timespec ts;   // expire time
} Timer;

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

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

  int id;
  for (id = 0; id < TIMERMAX; ++id) {
    if (NULL == 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(void) {
  // looking for expired timers
  for (int i = 0; i < TIMERMAX; ++i) {
    if (timers[i].cb_timer != NULL) {
      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 != NULL);
        (*tmp.cb_timer)(tmp.obj);
      }
    }
  }
}

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

typedef struct Conn {
  fd_set fds;       // read file descriptor set
  SOCKET sockfd;    // network port for client, listen port for server
  int isConnecting; // non-blocking connect started, but no finished
  int typ;          // tells client or server object
  char hostname[STRMAX];
  char port[STRMAX];
  int (*cb_read)(struct Conn* obj, SOCKET fd);   // Callback Pointer
} Conn;

typedef int (*cb_read_t)(Conn* obj, SOCKET fd);   // Callback Pointer type

static Conn conns[CONNMAX];  // Connection objects array

Conn* conn_make(void) {
  for (int i = 0; i < CONNMAX; ++i) {
    if (0 == conns[i].typ) {
      return &conns[i];  // found a free entry
    }
  }
  assert(0 && "conn array full");
  return NULL;
}

void client_open(Conn* obj, const char* hostname, const char* port) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(port != NULL);
  assert(hostname != NULL);
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port, hostname);

  FD_ZERO(&obj->fds);
  obj->typ = CONN_CLIENT;

  strcpy_s(obj->port, sizeof obj->port, port);
  strcpy_s(obj->hostname, sizeof obj->hostname, hostname);
  void client_open1(Conn* obj);
  client_open1(obj);
}

void client_open1(Conn* obj) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  struct addrinfo hints;
  memset(&hints, 0, sizeof hints);
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;

  struct addrinfo* res;
  int rv = getaddrinfo(obj->hostname, obj->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 != NULL; p = p->ai_next) {
    obj->sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (INVALID_SOCKET == obj->sockfd) {
      perror("WW socket");
      continue;
    }

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

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

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

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

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

  freeaddrinfo(res);

  FD_SET(obj->sockfd, &obj->fds);

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

void client_reopen(Conn* obj) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  closesocket(obj->sockfd);
  FD_CLR(obj->sockfd, &obj->fds);   // remove network fd
  obj->sockfd = -1;
  client_open1(obj);
}

void client_handle(Conn* obj, SOCKET fd) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(fd < FDMAX);
  assert(obj->cb_read != NULL);
  int rv = (*obj->cb_read)(obj, fd);
  if (rv < 1) {
    // 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(obj->sockfd, SOL_SOCKET, SO_ERROR, (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, obj->hostname, obj->port, obj->sockfd, rv, strerror(optval));
    client_reopen(obj);
  } else {
    obj->isConnecting = 0;  // hack: connect successful after first good read()
  }
}

void conn_add_cb(Conn* obj, cb_read_t cb_read) {
  assert(cb_read != NULL);
  obj->cb_read = cb_read;
}

void server_open(Conn* obj, const char* hostname, const char* port) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(port != NULL);
  assert(hostname != NULL);
  printf("II %s: port=%s hostname=%s\n", FUNCTION, port, hostname);

  FD_ZERO(&obj->fds);
  obj->typ = CONN_SERVER;

  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 != NULL; p = p->ai_next) {
    obj->sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (INVALID_SOCKET == obj->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(obj->sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes,
                    sizeof(yes));
    if (rv != 0) {
      perror("WW setsockopt SO_REUSEADDR");
      closesocket(obj->sockfd);
      continue;
    }

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

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

  freeaddrinfo(res);

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

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

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

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

void server_handle(Conn* obj, SOCKET fd) {
  assert(obj >= &conns[0] && obj < &conns[CONNMAX]);
  assert(fd < FDMAX);
  if (fd == obj->sockfd) {
    // handle new connections
    struct sockaddr_storage remoteaddr; // client address
    socklen_t addrlen = sizeof remoteaddr;
    // newly accept()ed socket descriptor
    SOCKET newfd = accept(obj->sockfd, (struct sockaddr*)&remoteaddr, &addrlen);
    if (INVALID_SOCKET == newfd) {
      perror("WW accept");
    } else {
      FD_SET(newfd, &obj->fds); // add to master set
      void* src = get_in_addr((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 {
    assert(obj->cb_read != NULL);
    int rv = (*obj->cb_read)(obj, fd);
    if (rv < 1 || SOCKET_ERROR == rv) {
      printf("II %s: connection closed on socket %llu\n", FUNCTION, fd);
      closesocket(fd);
      FD_CLR(fd, &obj->fds); // remove from fd_set
    }
  }
}

void conn_event_loop(void) {
  for(;;) {
    // virtualization pattern: join all read fds into one
    fd_set read_fds = conns[0].fds;
    for (int i = 1; i < CONNMAX; ++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].isConnecting) {
        FD_SET(conns[i].sockfd, &except_fds);
      }
    }

    struct timeval tv = {0, TIMEOUT * 1000};
    int rv = select(FDMAX, &read_fds, NULL, &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 (FD_ISSET(fd, &conns[i].fds)) {
              switch (conns[i].typ) {
              case CONN_CLIENT:
                client_handle(&conns[i], fd);
                break;
              case CONN_SERVER:
                server_handle(&conns[i], fd);
                break;
              }
            }
          }
        }
      }
      // looking for connect pending fail
      for (int i = 0; i < CONNMAX; ++i) {
        if (FD_ISSET(conns[i].sockfd, &except_fds)) {
          client_reopen(&conns[i]);
        }
      }
    }
    timer_walk();
  }
}

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

void iomux_end(void) {
  WSACleanup();
}

#endif  // WIOMUX_H_

The line count (wc -l) of this source code is: 109 for chat.c, 443 for liomux.h and 409 for wiomux.h. 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 file descriptors.

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

\$\endgroup\$
5
  • \$\begingroup\$ Your iomux_begin() and iomux_end() functions are empty in the Linux variant. why? \$\endgroup\$
    – Mast
    Commented Jun 25, 2022 at 10:30
  • 1
    \$\begingroup\$ To Mast: MS-Windows needs this "start POSIX networking" stuff. Because the chat.c source code shall be the same for Linux and MS-Windows, I choose that solution. Linux can do POSIX networking "out of the box". \$\endgroup\$ Commented Jun 25, 2022 at 12:15
  • \$\begingroup\$ That's a surprisingly sane approach. Seems like you got a good start. \$\endgroup\$
    – Mast
    Commented Jun 25, 2022 at 12:23
  • 1
    \$\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
    Commented Jun 26, 2022 at 13:38
  • 2
    \$\begingroup\$ Please be more careful with your edits in the future. As noted by pacmaninbw answer invalidation is not allowed on Code Review. \$\endgroup\$
    – Peilonrayz
    Commented Jun 26, 2022 at 21:57

1 Answer 1

1
\$\begingroup\$

Create a platform-independent header file

Instead of an application having to write code to choose between liomux.h and wiomux.h, create a header file named iomux.h that does this internally. This simplifies the application, and also allows you to add support for other platforms to your framework in the future without having to modify the application code itself.

Be consistent

I see a great deal of inconsistency in your code. Try to be as consistent as possible in the things you are doing. Here is a list of some of the things that are not consistent:

  • Documentation: some functions look like they have proper Doxygen documentation, others just have a simple comment. Try to document everything using Doxygen. Turn on warnings in Doxygen so it will tell you when you forgot to document certain functions, types and parameters.
  • Typedefs: for example, typedef struct {...} TIMER; versus struct CONN_ {...}; typedef struct CONN_ CONN;. I recommend you declare structs like so: typedef struct FOO {...} FOO;. Note that the struct and its typedef can have exactly the same name.
  • Error handling: I see assert(), perror(), exit(EXIT_FAILURE), and sometimes forgetting to handle errors at all. More on this below.
  • Naming: write_fds (with an underscore separating words) vs remoteaddr vs isConnecting.

Improve error handling

Make sure you check the return value of all functions that might return an error. That includes things like inet_ntop(), close(), clock_gettime() and so on. Yes it might be very unlikely, but it is important for a framework that you want to be able to blindly rely upon to actually handle all possible issues it can run into.

Use assert() only to check for programming bugs. So for example, assert(interval >= 0) in after() is fine; if it is ever called with a negative interval, that's a bug in the calling code. However, never use this for things that are not programming bugs. For example, assert(rv != NULL) in poll_keyboard() is wrong; it is certainly possible that fgets() will return NULL if something is wrong with stdin (for example, the terminal the program is running in is closed). assert() is meant to become a no-op when compiled with the -DNDEBUG flag, which is automatically set in build systems like Meson and CMake when you make release builds.

When you do encounter an error, make sure you print an error message to stdout using fprintf() or perror(). However, also make sure you then handle the error in the best possible way, never ignore them. Don't call exit(); this prevents the application from dealing with the error in its own way, instead return a meaningful error code. In server_handle(), you just ignore accept() returning an error. Perhaps it is fine in some cases, like if errno == EAGAIN, but if errno == EBADF, that means there is something wrong with the listening socket, and ignoring it will just cause your program to go into an infinite loop thinking there is a new socket to accept, but it will get the same error over and over.

Naming things

There are a few things that can be improved when naming functions, variables and so on:

  • Avoid overly generic names. For example obj. Does that mean it's an object? But what kind of object? It is better to name it connection.
  • It is common to use ALLCAPS names only for macros and constants, but not for structs.
  • In general, function names should be verbs, variable names should be nouns. For example, conn_factory() should be renamed to something like new_connection().
  • Use the plural for arrays, so instead of conn[], write connections[].

About debug print statements

There are several times you print debugging information to stdout, like when accepting a connection you print a message including the address connected to. While that might be nice while developing the framework, this will be very annoying for an application that is linking to your framework. It also adds quite a bit of noise to your code. I suggest that at this point, you remove all those printf("II...") statements from your code, as well as all the code that converts addresses to strings. Learn how to use a proper debugger like GDB if you want to debug your code, it avoids you having to add print() statements everywhere.

Use getnameinfo() to convert addresses to strings

Instead of using inet_ntop(), prefer to use getnameinfo() to convert addresses to strings. This way, you don't have to deal with the differences between IPv4 and IPv6 addresses.

Functions that take no arguments should be (void)

I see you write functions like void timer_walk() {...}. That syntax should be avoided, write void timer_walk(void) {...} instead. See this post for an explanation.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks. One first answer: getnameinfo() does not work with MS-Windows, as gai_strerror() is not working either. Maybe I just don't know enough MS-Windows ... Another answer: "Remove the II printf" is good advice. I should use some logging that I can configure, like syslog(). Because: you can not debug networking programs in a debugger, at least not if these programs work with timeouts in the range of seconds. \$\endgroup\$ Commented Jun 25, 2022 at 15:53
  • \$\begingroup\$ I don't see why getnameinfo() wouldn't work on Windows. I'm not sure about gai_strerror(), maybe you have to use gai_strerrorA(). Debuggers are much more advanced than you think, you can make them log something and continue each time a breakpoint is hit, or even call other functions in your program. \$\endgroup\$
    – G. Sliepen
    Commented Jun 25, 2022 at 16:14

Your Answer

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

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