C++ TCP 套接字通信 - 连接按预期工作,几秒钟后失败,没有收到新数据,read() 和 recv() 块

C++ TCP Socket communication - Connection is working as expected, fails after a couple of seconds, no new data is received and read() and recv() block

本文关键字:失败 新数据 recv read 数据 几秒 连接 通信 工作 C++ 套接字      更新时间:2023-10-16

我使用的是64位Ubuntu 16.04 LTS。就像我说的,我正在尝试与另一台设备建立TCP套接字连接。程序首先从套接字读取数据以初始化last_recorded_data变量(如下图所示,朝向myStartProcedure()的底部(,我知道这完全按预期工作。然后,程序的其余部分启动,由回调驱动。当我UPDATE_BUFFER_MS做一些更小的东西,比如 8 时,它会在几秒钟后失败。这个值的频率是所需的值,但是如果我为了测试目的(例如 500(将其变大,那么它的工作时间会更长一些,但最终也会以同样的方式失败。

失败如下:我尝试从中读取的设备始终每 8 毫秒发送一次数据,在此数据包中,保留前几个字节用于告诉客户端数据包的大小(以字节为单位(。在正常操作期间,接收的字节数和前几个字节描述的大小相等。但是,在read()调用开始阻止之前直接接收的数据包始终比预期大小小 24 字节,但数据包表示发送的数据包仍应为预期大小。下次尝试获取数据时,read()调用会阻止,并在超时时将errno设置为EAGAIN (Resource temporarily unavailable)

我尝试与Python应用程序与同一设备进行通信,但它没有遇到相同的问题。此外,我在另一台设备上尝试了这个C++应用程序,我看到了相同的行为,所以我认为这是我的问题。我的代码(简化(如下。如果您看到任何明显的错误,请告诉我,谢谢!!

#include <string>
#include <unistd.h>
#include <iostream>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define COMM_DOMAIN AF_INET
#define PORT        8008
#define TIMEOUT_SECS  3
#define TIMEOUT_USECS 0
#define UPDATE_BUFFER_MS 8
#define PACKET_SIZE_BYTES_MAX 1200
//
// Global variables
//
// Socket file descriptor
int socket_conn;
// Tracks the timestamp of the last time data was recorded
// The data packet from the TCP connection is sent every UPDATE_BUFFER_MS milliseconds
unsigned long last_process_cycle_timestamp;
// The most recently heard data, cast to a double
double last_recorded_data;
// The number of bytes expected from a full packet
int full_packet_size;
// The minimum number of bytes needed from the packet, as I don't need all of the data
int min_required_packet_size;
// Helper to cast the packet data to a double
union PacketAsFloat
{
unsigned char byte_values[8];
double decimal_value;
};
// Simple struct to package the data read from the socket
struct SimpleDataStruct
{
// Whether or not the struct was properly populated
bool valid;
// Some data that we're interested in right now
double important_data;
//
// Other, irrelevant members removed for simplicity
//
};
// Procedure to read the next data packet
SimpleDataStruct readCurrentData()
{
SimpleDataStruct data;
data.valid = false;
unsigned char socket_data_buffer[PACKET_SIZE_BYTES_MAX] = {0};
int read_status = read(socket_conn, socket_data_buffer, PACKET_SIZE_BYTES_MAX);
if (read_status < min_required_packet_size)
{
return data;
}
//for (int i = 0; i < read_status - 1; i++)
//{
//  std::cout << static_cast<int>(socket_data_buffer[i]) << ", ";
//}
//std::cout << static_cast<int>(socket_data_buffer[read_status - 1]) << std::endl;
PacketAsFloat packet_union;
for (int j = 0; j < 8; j++)
{
packet_union.byte_values[7 - j] = socket_data_buffer[j + 252];
}
data.important_data = packet_union.decimal_value;
data.valid          = true;
return data;
}
// This acts as the main entry point
void myStartProcedure(std::string host)
{
//
// Code to determine the value for full_packet_size and min_required_packet_size (because it can vary) was removed
// Simplified version is below
//
full_packet_size         = some_known_value;
min_required_packet_size = some_other_known_value;
//
// Create socket connection
//
if ((socket_conn = socket(COMM_DOMAIN, SOCK_STREAM, 0)) < 0)
{
std::cout << "socket_conn heard a bad value..." << std::endl;
return;
}
struct sockaddr_in socket_server_address;
memset(&socket_server_address, '0', sizeof(socket_server_address));
socket_server_address.sin_family = COMM_DOMAIN;
socket_server_address.sin_port   = htons(PORT);
// Create and set timeout
struct timeval timeout_chars;
timeout_chars.tv_sec  = TIMEOUT_SECS;
timeout_chars.tv_usec = TIMEOUT_USECS;
setsockopt(socket_conn, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_chars, sizeof(timeout_chars));
if (inet_pton(COMM_DOMAIN, host.c_str(), &socket_server_address.sin_addr) <= 0)
{
std::cout << "Invalid address heard..." << std::endl;
return;
}
if (connect(socket_conn, (struct sockaddr *)&socket_server_address, sizeof(socket_server_address)) < 0)
{
std::cout << "Failed to make connection to " << host << ":" << PORT << std::endl;
return;
}
else
{
std::cout << "Successfully brought up socket connection..." << std::endl;
}
// Sleep for half a second to let the networking setup properly
sleepMilli(500); // A sleep function I defined elsewhere
SimpleDataStruct initial = readCurrentData();
if (initial.valid)
{
last_recorded_data = initial.important_data;
}
else
{
// Error handling
return -1;
}
//
// Start the rest of the program, which is driven by callbacks
//
}
void updateRequestCallback()
{
unsigned long now_ns = currentTime(); // A function I defined elsewhere that gets the current system time in nanoseconds
if (now_ns - last_process_cycle_timestamp >= 1000000 * UPDATE_BUFFER_MS)
{
SimpleDataStruct current_data = readCurrentData();
if (current_data.valid)
{
last_recorded_data = current_data.important_data;
last_process_cycle_timestamp = now_ns;
}
else
{
// Error handling
std::cout << "ERROR setting updated data, SimpleDataStruct was invalid." << std:endl;
return;
}
}
}

编辑 #1

我应该每次都接收一定数量的字节,我希望read()的返回值也返回该值。但是,我刚刚尝试将PACKET_SIZE_BYTES_MAX的值更改为 2048,read()的返回值现在是 2048,此时它应该是设备发回的数据包的大小(不是 2048(。Python 应用程序还将最大值设置为 2048,其返回的数据包大小是正确的/预期的大小......

尝试注释掉超时设置。我从不使用它,也没有遇到你所说的问题。

// Create and set timeout
struct timeval timeout_chars;
timeout_chars.tv_sec  = TIMEOUT_SECS;
timeout_chars.tv_usec = TIMEOUT_USECS;
setsockopt(socket_conn, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_chars, sizeof(timeout_chars));

为避免阻塞,您可以将套接字设置为非块套接字,然后使用select()poll()获取更多数据。这两个函数都可以使用上面显示的超时。但是,对于非阻塞套接字,必须确保读取按预期工作。在许多情况下,您将获得部分读取,并且必须再次等待(select()poll()(才能获得更多数据。所以代码会稍微复杂一些。

socket_conn = socket(COMM_DOMAIN, SOCK_STREAM | SOCK_NONBLOCK, 0);

如果安全性是一个潜在的问题,我还会设置SOCK_CLOEXEC以防止子进程访问同一个套接字。

std::vector<struct pollfd> fds;
struct pollfd fd;
fd.fd = socket_conn;
fd.events = POLLIN | POLLPRI | POLLRDHUP; // also POLLOUT for writing
fd.revents = 0; // probably useless... (kernel should clear those)
fds.push_back(fd);
int64_t timeout_chars = TIMEOUT_SECS * 1000 + TIMEOUT_USECS / 1000;
int const r = poll(&fds[0], fds.size(), timeout_chars);
if(r < 0) { ...handle error(s)... }

假设标头大小已明确定义且永不更改,另一种方法是读取标头,然后使用标头信息读取其余数据。在这种情况下,您可以保留阻塞套接字而没有任何超时。从你的结构中,我不知道那会是什么。所以。。。让我们首先定义这样一个结构:

struct header
{
char sync[4];  // four bytes indicated a synchronization point
uint32_t size; // size of packet
...            // some other info
};

我放了一个"同步"字段。在TCP中,人们通常会添加这样的字段,因此,如果您忘记了自己的位置,则可以通过一次读取一个字节来寻求下一次同步。坦率地说,使用TCP,您永远不会遇到这样的传输错误。您可能会丢失连接,但永远不会丢失流中的数据(即TCP就像网络上的完美FIFO(。话虽如此,如果您正在开发关键任务软件,那么同步和校验和将非常受欢迎。

接下来,我们只read()标题。现在我们知道了这个数据包的确切大小,所以我们可以使用这个特定的大小,并在我们的数据包缓冲区中准确读取那么多字节:

struct header hdr;
read(socket_conn, &hdr, sizeof(hdr));
read(socket_conn, packet, hdr.size /* - sizeof(hdr) */);

显然,read()可能会返回错误,并且标头中的大小可能以大端定义(因此您需要交换 x86 处理器上的字节(。但这应该让你继续前进。

此外,如果在标头中找到的大小包括标头中的字节数,请确保在读取数据包的其余部分时减去该数量。


此外,以下内容是错误的:

memset(&socket_server_address, '0', sizeof(socket_server_address));

你的意思是用零清除结构,而不是字符零。尽管如果它连接起来,这意味着它可能无关紧要。只需使用0而不是'0'