首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >C中的简单网络框架

C中的简单网络框架
EN

Code Review用户
提问于 2022-06-25 09:58:53
回答 1查看 99关注 0票数 3

问:您对这个设计和实现有什么看法?这是一个简单的网络框架,用于IPv4、IPv6 TCP客户端和和MS。它使用了围绕系统调用select()的单线程解决方案,即IO多路复用。连接和网络读取是异步的,网络写入是同步的。liomux.h文件实现Linux特定版本,wiomux.h实现MS特定版本,chat.c实现聊天应用程序作为示例。

http://www.andreadrian.de/non-blocking_连接/index.html上有很多关于这个源代码的旧版本的信息。

代码语言:javascript
复制
/* 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      // 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;
}

该框架提供了一个"Timer类“和一个"Connection类”。将连接类子类转换为“服务器类”和“客户端类”。因为C不是面向对象的,所以这些只是设计思想。

代码语言:javascript
复制
/* 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 
#include 
#include 
#include 
#include 
#include 

// Linux
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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_

在异步连接方面,MS实现是不同的.Linux ()使用写fd_set来表示“连接成功/失败”事件,而()对此事件使用异常fd_set。还有一些不同之处。

代码语言:javascript
复制
/* 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 
#include 
#include 
#include 
#include 

// MS-Windows
#include       // _kbhit()
#include 
#include    // 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_

此源代码的行计数(wc -l)为: chat.c为109,liomux.h为443,wiomux.h为409。我认为,这真的是一个简单的网络框架。常量CONNMAX定义应用程序中"TCP服务器“和"TCP客户端”的最大数量。TIMERMAX定义最大定时器数,FDMAX定义打开文件描述符的最大数量。

在C++中有一个简单的网络框架,请参阅C++中的简单网络框架

EN

回答 1

Code Review用户

回答已采纳

发布于 2022-06-25 14:11:45

创建一个独立于平台的头文件

应用程序不必编写代码在liomux.h和wiomux.h之间进行选择,而是创建一个名为iomux.h的头文件,在内部执行此操作。这简化了应用程序,并且还允许您将来在框架中添加对其他平台的支持,而不必修改应用程序代码本身。

是一致的

我在您的代码中看到了很多不一致的地方。在你所做的事情中尽量保持一致。下面列出了一些不一致的事情:

  • 文档:一些函数看起来像是有适当的DO2文档,其他的只是一个简单的注释。试着用氧气记录一切。打开DO2中的警告,这样当您忘记记录某些函数、类型和参数时,它就会告诉您。
  • Typedefs:例如,typedef struct {...} TIMER;struct CONN_ {...}; typedef struct CONN_ CONN;。我建议您像这样声明结构:typedef struct FOO {...} FOO;。请注意,struct和它的typedef可以有完全相同的名称。
  • 错误处理:我看到assert()perror()exit(EXIT_FAILURE),有时甚至忘记处理错误。下面会有更多的介绍。
  • 命名:write_fds (带有下划线分隔词)与remoteaddr vs isConnecting

改进错误处理

确保检查可能返回错误的所有函数的返回值。这包括inet_ntop()close()clock_gettime()等。是的,这可能是非常不可能的,但对于一个您希望能够盲目依赖的框架来实际处理它可能遇到的所有可能的问题来说,这是很重要的。

只使用assert()检查编程错误。例如,assert(interval >= 0)after()中是很好的;如果使用负间隔调用它,那就是调用代码中的一个bug。但是,永远不要将它用于不是编程错误的东西。例如,assert(rv != NULL) in poll_keyboard()是错误的;如果stdin有问题,fgets()肯定会返回NULL (例如,程序运行的终端关闭了)。当使用assert()标志编译时,-DNDEBUG意味着成为一个非op,在生成版本时,它会自动设置在Meson和CMake这样的构建系统中。

当您确实遇到错误时,请确保使用fprintf()perror()将错误消息打印到D39D40。但是,也要确保您然后以最好的方式处理错误,千万不要忽视它们。不要调用exit();这会阻止应用程序以自己的方式处理错误,而是返回有意义的错误代码。在server_handle()中,您只需忽略返回错误的accept()。也许在某些情况下它是好的,比如如果是errno == EAGAIN,但是如果errno == EBADF,这意味着侦听套接字有问题,忽略它只会导致程序进入一个无限循环,认为有一个新的套接字可以接受,但是它会一次又一次地出现相同的错误。

命名事物

在命名函数、变量等时,可以改进以下几点:

  • 避免过于通用的名称。例如,obj。这是不是意味着它是个物体?但是什么样的对象呢?最好把它命名为connection
  • 通常只对宏和常量使用ALLCAPS名称,而不对结构使用ALLCAPS名称。
  • 一般来说,函数名应该是动词,变量名应该是名词。例如,应该将conn_factory()重命名为类似于new_connection()的东西。
  • 数组使用复数形式,所以不要使用conn[],而是编写connections[]

关于调试打印语句的

有几次将调试信息打印到stdout,比如在接受连接时打印消息,包括连接到的地址。虽然这在开发框架时可能很好,但对于链接到您的框架的应用程序来说,这将是非常烦人的。它还会给代码添加相当多的噪音。现在,我建议您从代码中删除所有这些printf("II...")语句,以及将地址转换为字符串的所有代码。了解如何使用适当的调试器(如GDB ),如果您想调试代码,可以避免在任何地方添加print()语句。

使用getnameinfo()将地址转换为字符串

与其使用inet_ntop(),不如使用getnameinfo()将地址转换为字符串。这样,您就不必处理IPv4和IPv6地址之间的差异。

不带参数的

函数应该是(void)

我看到你写像void timer_walk() {...}这样的函数。应该避免使用这种语法,而是编写void timer_walk(void) {...}。有关解释,请参见这个职位

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/277603

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档