用于套接字客户端通信的每个线程的状态机

时间:2019-01-24 05:45:11

标签: c multithreading sockets state-machine

我正在使用线程化TCP套接字服务器来处理多个套接字客户端连接。客户端可以与服务器异步连接和断开连接,连接后,客户端应以预定义的自定义数据包协议格式发送一些数据。
该协议定义了帧的开始(SOP)和帧的结束(EOP)。

我编写了一个C代码,以便为每个成功的客户端连接创建一个线程,该线程继续以预定义的数据包格式从客户端接收字节,该线程具有线程本地状态机,因为每个客户端可以异步连接因此每个客户的状态可能会有所不同。
下面是从客户端接收该数据并根据接收到的字节类型维护状态的线程:

static void *receive_handler(void *args) {

  struct thread_args *local_args = args;
  struct sockaddr_in6 *client_address = local_args->client_address;
  //struct itimerval timer_val;
  int32_t conn_fd = local_args->conn_fd;
  int32_t val_read = 0;
  int32_t resp_code = 0;
  uint32_t sendBuffLen = 0;
  int8_t buffer[BUFFER_SIZE] = { 0 };
  uint8_t RetBuff[1024] = { 0 };
  int8_t rx_addr_str[INET6_ADDRSTRLEN];
  int8_t byte = 0;
  int16_t idx = ePacketType;
  int16_t packet_len = 0;
  int16_t calculated_crc = 0, recv_crc = 0;
  uint16_t num_bytes = 0;

  memset(rx_addr_str, 0, INET6_ADDRSTRLEN);
  inet_ntop(AF_INET6, &(client_address->sin6_addr), rx_addr_str, INET6_ADDRSTRLEN);
  printf("\nRx Thread (%d) Created for %s\n", local_args->connection_no, rx_addr_str);

  int eState = eStart_Frame;

  memcpy(rx_Packet_Info[local_args->connection_no].inet6, rx_addr_str, INET6_ADDRSTRLEN);

  //timerclear(&timer_val.it_interval); /* zero interval means no reset of timer */
  //timerclear(&timer_val.it_value);
  //timer_val.it_value.tv_sec = 10; /* 10 second timeout */

  //(void) signal(SIGALRM, state_reset_handler);

  while (1) {

    if (eState != eChecksum_Verify) {
      val_read = -1;
      val_read = recv(conn_fd, &byte, sizeof(byte), 0);
      debug_printf(INFO, "Amount Read: %d Byte Rxd: 0x%x => 0x%X\n", val_read, (byte & 0xFF), byte);
      if (val_read <= 0) {
        if (parse_packet("ERR_DISCONNECT", rx_addr_str, local_args->connection_no) < 0) {
          debug_printf(ERR, "Error parsing packet: %s\n", strerror(errno));
        }
        debug_printf(ERR, "May be closed by client %s: %s\n", rx_addr_str, strerror(errno));
        debug_printf(ERR, "Exiting Rx Thread: ConnIdx: %d", num_connections);
        close(conn_fd);
        pthread_exit(NULL);
      }
    }

    switch (eState) {

      case eStart_Frame:
        debug_printf(DEBG, "Current State: %d\n", eState);
        if ((val_read > 0) && (byte & 0xFF) == SOP) {
          memset(buffer, 0, BUFFER_SIZE);
          val_read = -1;
          buffer[eSOP] = (byte & 0xFF);
          eState = eFrame_Len;
        }
        break;

      case eFrame_Len: {
        static char MSB_Rxd = 0;
        debug_printf(DEBG, "Current State: %d\n", eState);
        if (val_read > 0) {
          if (MSB_Rxd == 0) {
            buffer[ePacket_length] = byte;
            MSB_Rxd = 1;
          }
          else {
            buffer[ePacket_length + 1] = byte;
            eState = eFrame;
            num_bytes = 0;
            MSB_Rxd = 0;
            packet_len = (buffer[ePacket_length] & 0xFF << 8) | (buffer[ePacket_length + 1]);
            debug_printf(INFO, "Packet Length: %d : 0x%x 0x%x\n", packet_len,
                buffer[ePacket_length], buffer[ePacket_length + 1]);
          }
        }
      }
        break;

      case eFrame:
        debug_printf(DEBG, "Current State: %d\n", eState);
        num_bytes++;
        buffer[idx] = byte;
        if (num_bytes == packet_len) {
          eState = eEnd_Frame;
          debug_printf(DEBG, "Num bytes: 0x%x\n", num_bytes);
        }
        else {
          debug_printf(ERR, "Num bytes: 0x%x Pkt Len: 0x%x\n", num_bytes, packet_len);
        }
        idx++;
        break;

      case eEnd_Frame:
        debug_printf(ERR, "Current State: %d val read %d\n", eState, val_read);
        if ((val_read > 0) && (byte & 0xFF) == EOP) {
          val_read = -1;
          eState = eChecksum_Verify;
        }
        break;

      case eChecksum_Verify: {

        calculated_crc = crc_16(&buffer[ePacket_length], (num_bytes));
        recv_crc = buffer[num_bytes + 1] << 8 | (buffer[num_bytes + 2] & 0xFF);

        if (calculated_crc != recv_crc) {
          debug_printf(ERR, "CRC Error! CRC do not match!!\n");
          debug_printf(ERR, "Calculated CRC: 0x%X\nCRC Rxd: 0x%X\n", calculated_crc, recv_crc);
          resp_code = CRC_ERR;
          send(conn_fd, &resp_code, sizeof(resp_code), 0);
        }
        else {
          if (rx_Packet_Info[local_args->connection_no].packetUUID != NULL) {
            free(rx_Packet_Info[local_args->connection_no].packetUUID);
            rx_Packet_Info[local_args->connection_no].packetUUID = NULL;
          }

          rx_Packet_Info[local_args->connection_no].packetUUID = calloc(buffer[ePacketUUIDLen],
              sizeof(uint8_t));
          memcpy(rx_Packet_Info[local_args->connection_no].packetUUID, &buffer[ePacketUUID],
              buffer[ePacketUUIDLen]);
          rx_Packet_Info[local_args->connection_no].packetUUIDlength = buffer[ePacketUUIDLen];

          printf("\nRX-Thread-UUID %d: ConnNo: %d\n", buffer[ePacketUUIDLen],
              local_args->connection_no);
          for (char i = 0; i < buffer[ePacketUUIDLen]; i++) {
            printf("0x%x ", rx_Packet_Info[local_args->connection_no].packetUUID[i]);
          }
          printf("\n");
          if (parse_packet(buffer, rx_addr_str, local_args->connection_no) < 0) {
            debug_printf(ERR, "Error parsing packet: %s\n", strerror(errno));
          }
        }
        num_bytes = 0;
        eState = eStart_Frame;
        idx = ePacketType;
      }
        break;

      default:
        debug_printf(DEBG, "Invalid State!! Should not come here.\n");
        num_bytes = 0;
        eState = eStart_Frame;
        idx = ePacketType;
        break;
    }
  }

  return NULL;
}

我的问题是,如果说在接收到帧开始之后客户端卡住并且无法发送帧长或完整帧直到帧结束,该如何重置状态机?

我认为的一种方法是实现计时器回调,但是我不确定如何跟踪多个线程的状态机。 有人可以建议在这种情况下该怎么办,或者如果我做错了什么?

1 个答案:

答案 0 :(得分:1)

如果我正确地解析了问题,则您是在询问如何妥善处理连接的客户端未及时发送数据的情况-即它已发送了消息的第一部分,但(由于网络问题,客户端错误或其他原因)永远不会发送其余的消息,从而使服务器端I / O线程在recv()调用中长时间/无限期地处于阻塞状态。

如果是这样,首先要问的是:这真的有问题吗?如果每个连接都有自己的线程,则阻塞一个特定的线程/连接不会对其他线程造成任何问题,因为它们彼此独立地执行。所以也许您可以完全忽略这个问题?

但是,更可能的答案是,忽略此问题还不够好,因为随后出现了一些不容易忽略的问题:(a)如果太多的客户端连接“冻结”了怎么办?同时?一两个停滞的TCP连接/线程不是什么大问题,但是如果相同的问题不断发生,最终您将耗尽资源以生成更多的线程或TCP连接,然后服务器将无法正常运行。并且(b)如果服务器进程现在要退出怎么办? (即,由于服务器的用户向其发送了SIGINT中断或类似消息)如果无限期地阻塞一个或多个线程,则服务器不可能以及时且受控的方式退出,因为主线程需要等待所有TCP线程必须先退出,然后才能清理进程范围的资源,并且任何阻塞的线程都不会退出很长时间(如果有的话)。

因此,假设需要解决问题 ,我发现解决该问题的最可靠方法就是永远不要阻塞recv()(或send() )。相反,请确保将每个套接字置于非阻塞模式,并仅在select()调用中使线程的while循环阻塞。这样做会使您的状态机更加复杂(因为它现在必须同时处理部分发送和部分接收),但是补偿的好处是线程现在可以更好地控制其自身的阻塞行为。特别是,您可以告诉select()在一定时间后始终返回,无论如何,(更好)您可以告诉select()在任意数量的套接字中有字节准备就绪时返回在上面阅读。这意味着,如果您的主线程想要退出,则可以使用pipe()socketpair()向每个TCP线程和TCP线程(假定在{{1 }},等待来自其客户端或管道/套接字对套接字的数据)将立即从select()返回,看到主线程已发送了一个字节,并立即退出进行了响应。

这应该足够了-以我的经验,如果可以避免的话,最好不要施加固定的超时时间,因为很难预测所有情况下的网络性能,以及任何经验法则提出来(例如“一个客户端在5秒钟之内不能发送完整的消息,必须断开”)很可能是错误的,并且如果您尝试执行该规则,最终将出现假阳性问题。最好只让每个客户端花费其想要/需要的时间,同时还具有一种机制,通过该机制,主线程可以在必要时(例如在服务器进程关闭期间)请求特定的客户端线程立即退出。如果活动的TCP线程过多,并且您希望在生成更多TCP / IP线程之前先修剪掉一些旧的/不活动的线程)