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
    CommentedJun 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
    CommentedJun 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\$CommentedJun 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\$CommentedJul 1, 2022 at 5:32

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.