【问题解析】我的客户端与服务器交互无响应了?

最近在工作上上手了一个QT的项目,里面包含了一个tftp的服务器,上手后我发现在同时用服务器传输多个文件的时候,会造成所有文件都传输失败,并且客户端再进行传输就无响应了?

由于现象比较难复现,并且最开始我并不清除是什么原因导致的无响应,且一个客户端无响应后会导致所有客户端都开始无响应。我一度以为是ip冲突了,尝试了许多方法都难以定义。于是选择观察多个文件传输时内存的一个变化,发现一旦失败就会造成内存泄漏,且内存泄露的大小和要传输的文件大小一致。那么此时定位就逐渐明确了,大致范围就在服务器传输数据的时候,从本地写入内存后未成功发送,造成了泄露。

下断点观察服务器后,发现了原有的服务器采用了阻塞的socket,并且没有重传机制和超时时间。造成一直recv从而使服务器阻塞了。下图是我的服务器的大致流程。

什么是阻塞IO和非阻塞IO可以去看我的【UNIX网络编程】5种I/O模型这篇文章,讲解了阻塞IO相关的问题。下面给大家讲一下我针对于这个服务器的解决办法。

select + 重传机制

嵌入式中常用的IO复用模型就是select,可以用于为服务器指定超时时间,避免无限等待。select的具体使用方法可以去看【UNIX网络编程】I/O多路复用这篇文章。

下面是一个服务器的示例:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_RETRIES 3
#define TIMEOUT_SEC 5
#define MAX_CLIENTS 10

/* 函数声明 */
int createServerSocket(int port);
int acceptClientConnection(int serverFd);
int waitForDataWithTimeout(int sockFd, int timeoutSec);
int receiveWithRetry(int sockFd, char* buffer, int bufferSize, int maxRetries, int timeoutSec);
void handleClient(int clientSock);

/* 创建服务器socket并绑定端口 */
int createServerSocket(int port) {
    int serverFd;
    struct sockaddr_in address;
    int opt = 1;

    /* 创建socket */
    if ((serverFd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        return -1;
    }

    /* 设置socket选项 */
    if (setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        close(serverFd);
        return -1;
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port);

    /* 绑定端口 */
    if (bind(serverFd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(serverFd);
        return -1;
    }

    /* 开始监听 */
    if (listen(serverFd, MAX_CLIENTS) < 0) {
        perror("listen");
        close(serverFd);
        return -1;
    }

    printf("Server listening on port %d\n", port);
    return serverFd;
}

/* 接受客户端连接 */
int acceptClientConnection(int serverFd) {
    struct sockaddr_in address;
    int addrLen = sizeof(address);
    int clientSock;

    if ((clientSock = accept(serverFd, (struct sockaddr*)&address, (socklen_t*)&addrLen)) < 0) {
        perror("accept");
        return -1;
    }

    printf("Client connected from %s:%d\n", 
           inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    return clientSock;
}

/* 使用select等待数据到达,带超时功能
 * 返回: 1-有数据可读, 0-超时, -1-错误 */
int waitForDataWithTimeout(int sockFd, int timeoutSec) {
    fd_set readFds;
    struct timeval tv;

    FD_ZERO(&readFds);
    FD_SET(sockFd, &readFds);

    tv.tv_sec = timeoutSec;
    tv.tv_usec = 0;

    int result = select(sockFd + 1, &readFds, NULL, NULL, &tv);
    
    if (result < 0) {
        perror("select error");
        return -1;
    } else if (result == 0) {
        return 0; /* 超时 */
    }

    return FD_ISSET(sockFd, &readFds) ? 1 : 0;
}

/* 带重传机制的数据接收函数 */
int receiveWithRetry(int sockFd, char* buffer, int bufferSize, int maxRetries, int timeoutSec) {
    int retryCount = 0;
    int bytesReceived;

    while (retryCount < maxRetries) {
        int ready = waitForDataWithTimeout(sockFd, timeoutSec);
        
        if (ready == 1) {
            bytesReceived = recv(sockFd, buffer, bufferSize - 1, 0);
            
            if (bytesReceived < 0) {
                perror("recv failed");
                return -1;
            } else if (bytesReceived == 0) {
                printf("Connection closed by peer\n");
                return 0;
            } else {
                buffer[bytesReceived] = '\0'; /* 确保字符串结束 */
                return bytesReceived;
            }
        } else if (ready == 0) {
            printf("Timeout, waiting for data... (%d/%d)\n", retryCount + 1, maxRetries);
            retryCount++;
        } else {
            return -1; /* select错误 */
        }
    }

    printf("Max retries exceeded for receiving data\n");
    return -2; /* 超过最大重试次数 */
}

/* 处理客户端连接 */
void handleClient(int clientSock) {
    char buffer[BUFFER_SIZE];
    int bytesReceived;

    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        
        bytesReceived = receiveWithRetry(clientSock, buffer, BUFFER_SIZE, MAX_RETRIES, TIMEOUT_SEC);
        
        if (bytesReceived > 0) {
            printf("Received: %s\n", buffer);
            
            /* 发送确认 */
            const char* ackMsg = "ACK";
            if (send(clientSock, ackMsg, strlen(ackMsg), 0) < 0) {
                perror("send ACK failed");
                break;
            }
            printf("Sent ACK\n");
            
        } else if (bytesReceived == 0) {
            printf("Client disconnected\n");
            break;
        } else if (bytesReceived == -2) {
            printf("Max retries exceeded, closing connection\n");
            break;
        } else {
            perror("Receive error");
            break;
        }
    }
}

/* 主函数 */
int main() {
    int serverFd, clientSock;

    serverFd = createServerSocket(PORT);
    if (serverFd < 0) {
        fprintf(stderr, "Failed to create server socket\n");
        return EXIT_FAILURE;
    }

    clientSock = acceptClientConnection(serverFd);
    if (clientSock < 0) {
        fprintf(stderr, "Failed to accept client connection\n");
        close(serverFd);
        return EXIT_FAILURE;
    }

    handleClient(clientSock);

    close(clientSock);
    close(serverFd);
    printf("Server shutdown\n");
    
    return EXIT_SUCCESS;
}
相关推荐
zhougl9962 小时前
Vuex 模块命名冲突:问题解析与完整解决方案
linux·服务器·apache
温宇飞2 小时前
内存异常
c++
爱丽_2 小时前
MyBatis动态SQL完全指南
服务器·sql·mybatis
怎么就重名了2 小时前
记录Qt的UDP通信丢包问题
开发语言·qt·udp
I · T · LUCKYBOOM3 小时前
1.Apache网站优化
linux·运维·服务器·网络·apache
追烽少年x3 小时前
Qt面试题合集(四)
qt
GHL2842710903 小时前
vmware中无法看到共享文件夹
linux·运维·服务器
我是伪码农3 小时前
注册表单提交加验证码功能
运维·服务器
GanGuaGua4 小时前
JsonRpc:手搓一个高性能Rpc服务(应用篇)
qt·网络协议·rpc