You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows下C语言Socket通信:select/send/recv异常行为排查

C语言TCP服务器-客户端PING/PONG异常问题分析与解决

问题背景

开发C语言服务器-客户端程序,需求为:客户端连接服务器后,服务器定时向客户端发送PING消息。测试中遇到select、send和recv的异常行为。

服务器核心代码

while (1) {
    read_set = set; write_set = set; error_set = set; timeout = ref_timeout;
    // Windows平台下select第一个参数为NULL
    int result = select(NULL, &read_set, &write_set, &error_set, &timeout);
    if (result < 0) {
        puts("Error select");
        return 1;
    }
    if (result == 0) puts("timed out");
    else {
        for (int index = 0; index < 2; index++) {
            SOCKET i = sockets[index];
            if (FD_ISSET(i, &error_set)) {
                printf("%zu error\n", i);
                FD_CLR(i, &set);
                closesocket(i);
            }

            if (FD_ISSET(i, &write_set)) {
                const char* ping = "PING";
                int size = send(i, ping, strlen(ping)+1, 0);
                printf("%d bytes sent\n", size);
                if (size < 0) {
                    puts("Error send");
                    closesocket(i);
                    FD_CLR(i, &set);
                }
                send(i, NULL, 0, 0);
            }

            if (FD_ISSET(i, &read_set)) {
                if (i == listen_socket){
                    struct sockaddr addr;
                    int addrlen;
                    SOCKET new_socket = accept(listen_socket, &addr, &addrlen);
                    FD_SET(new_socket, &set);
                    if (new_socket > biggest) biggest = new_socket;
                    printf("new conn %zu\n", new_socket);
                    sockets[1] = new_socket;
                }
                else {
                    char msg[100];
                    memset(msg, 0, sizeof(msg));
                    if (recv(i, msg, sizeof(msg), 0) < 0) {
                        closesocket(i);
                        FD_CLR(i, &set);
                    }
                    else {
                        printf("%zu %s\n", i, msg);
                    }
                }
            }
        }
    }
}

客户端核心代码

char buffer[100];
const char* pong = "PONG";
for (int i = 0; i < 3; i++) {
    memset(buffer, 0, sizeof(buffer));
    int result = recv(client_socket, buffer, sizeof(buffer), 0);
    if (result < 0) {
        puts("something went wrong receiving");
        closesocket(client_socket);
        return 1;
    }
    if (send(client_socket, pong, strlen(pong)+1, 0) < 0) {
        puts("something went wrong sending");
        return 1;
    }
    printf("msg: %s\n", buffer);
    Sleep(10000);
}

预期与实际行为

  • 预期行为:客户端阻塞在recv时,服务器select检测到客户端socket处于write_set,发送5字节PING消息;接收客户端的PONG后,阻塞直到客户端下一次准备接收,定时重复该流程。
  • 实际异常:第一次recv后,服务器持续发送5字节PING消息,虽能正确接收PONG,但无法停止发送;杀死客户端进程后,send立即失败。

疑问

这是Windows平台特性吗?还是对select的用法理解有误?如何让客户端告知服务器暂时不接收消息?


问题分析与解决

1. 异常原因:对select的write_set逻辑理解错误

这不是Windows平台特性,是对select机制的误用:

  • select的write_set触发条件是:socket的发送缓冲区有可用空间(默认情况下,只要连接正常且缓冲区未满,就会处于可写状态)。
  • 客户端Sleep期间,TCP连接保持正常,服务器的发送缓冲区很快就会被清空(客户端TCP栈会接收数据并存入接收缓冲区,直到缓冲区满),所以select会持续检测到客户端socket可写,导致服务器不断执行send操作。
  • 服务器代码中每次循环都把write_set设置为包含所有socket的set,包括监听socket(监听socket不需要写操作,完全没必要加入write_set),进一步放大了问题。

2. 核心修正方案:实现真正的定时发送逻辑

要实现“定时发送PING”,不能依赖write_set触发发送,而是要利用select的timeout来控制定时:

  • 将ref_timeout设置为你需要的定时间隔(比如5秒)。
  • 当select返回0(即超时触发)时,遍历所有客户端socket,执行PING发送操作。
  • 平时循环中只把需要处理读事件的socket加入read_set,不需要写入时不要把客户端socket加入write_set。

示例修正后的服务器定时触发逻辑:

while (1) {
    read_set = set;
    // 不需要写入时,write_set初始化为空
    FD_ZERO(&write_set);
    error_set = set;
    timeout = ref_timeout;

    int result = select(NULL, &read_set, &write_set, &error_set, &timeout);
    if (result < 0) {
        puts("Error select");
        return 1;
    }

    // 超时触发:执行定时PING发送
    if (result == 0) {
        puts("timed out, sending PING to clients...");
        // 遍历所有客户端socket(排除监听socket)
        for (int index = 0; index < 2; index++) {
            SOCKET i = sockets[index];
            if (i == listen_socket) continue;
            if (FD_ISSET(i, &set)) { // 确保socket仍在活跃集合中
                const char* ping = "PING";
                int size = send(i, ping, strlen(ping)+1, 0);
                printf("%d bytes sent to %zu\n", size, i);
                if (size < 0) {
                    puts("Error send");
                    closesocket(i);
                    FD_CLR(i, &set);
                }
            }
        }
    }
    else {
        // 处理读事件和错误事件(保留原有逻辑,移除write_set自动发送)
        for (int index = 0; index < 2; index++) {
            SOCKET i = sockets[index];
            if (FD_ISSET(i, &error_set)) {
                printf("%zu error\n", i);
                FD_CLR(i, &set);
                closesocket(i);
            }

            if (FD_ISSET(i, &read_set)) {
                if (i == listen_socket){
                    struct sockaddr addr;
                    int addrlen = sizeof(struct sockaddr); // 必须初始化addrlen
                    SOCKET new_socket = accept(listen_socket, &addr, &addrlen);
                    FD_SET(new_socket, &set);
                    if (new_socket > biggest) biggest = new_socket;
                    printf("new conn %zu\n", new_socket);
                    sockets[1] = new_socket;
                }
                else {
                    char msg[100];
                    memset(msg, 0, sizeof(msg));
                    int recv_len = recv(i, msg, sizeof(msg), 0);
                    if (recv_len < 0) {
                        closesocket(i);
                        FD_CLR(i, &set);
                    }
                    else if (recv_len == 0) {
                        // 客户端主动关闭连接
                        printf("%zu disconnected\n", i);
                        closesocket(i);
                        FD_CLR(i, &set);
                    }
                    else {
                        printf("%zu %s\n", i, msg);
                    }
                }
            }
        }
    }
}

3. 客户端告知服务器暂停接收的实现方式

在应用层协议中设计控制逻辑:

  • 客户端发送"PAUSE"消息,服务器收到后暂停定时PING发送;
  • 客户端发送"RESUME"消息,服务器恢复发送;
  • 客户端Sleep前发送"PAUSE",Sleep结束后发送"RESUME"。

示例客户端修改:

char buffer[100];
const char* pong = "PONG";
const char* pause = "PAUSE";
const char* resume = "RESUME";
for (int i = 0; i < 3; i++) {
    memset(buffer, 0, sizeof(buffer));
    int result = recv(client_socket, buffer, sizeof(buffer), 0);
    if (result < 0) {
        puts("something went wrong receiving");
        closesocket(client_socket);
        return 1;
    }
    if (send(client_socket, pong, strlen(pong)+1, 0) < 0) {
        puts("something went wrong sending");
        return 1;
    }
    printf("msg: %s\n", buffer);
    
    // 告知服务器暂停发送
    send(client_socket, pause, strlen(pause)+1, 0);
    Sleep(10000);
    // 告知服务器恢复发送
    send(client_socket, resume, strlen(resume)+1, 0);
}

服务器端处理读事件时增加控制消息判断:

// ... 原有读事件处理逻辑
else {
    char msg[100];
    memset(msg, 0, sizeof(msg));
    int recv_len = recv(i, msg, sizeof(msg), 0);
    if (recv_len < 0) {
        closesocket(i);
        FD_CLR(i, &set);
    }
    else if (recv_len == 0) {
        printf("%zu disconnected\n", i);
        closesocket(i);
        FD_CLR(i, &set);
    }
    else {
        printf("%zu %s\n", i, msg);
        // 处理控制消息
        static int client_paused[2] = {0};
        if (strcmp(msg, "PAUSE") == 0) {
            client_paused[index] = 1;
        }
        else if (strcmp(msg, "RESUME") == 0) {
            client_paused[index] = 0;
        }
    }
}

定时发送PING时检查暂停状态:

// 超时触发发送PING时
if (client_paused[index] == 0) { // 仅当未暂停时发送
    const char* ping = "PING";
    int size = send(i, ping, strlen(ping)+1, 0);
    // ... 后续处理
}

4. 其他代码优化点

  • accept调用时必须初始化addrlen为sizeof(struct sockaddr),否则可能导致未定义行为;
  • 移除无用的send(i, NULL, 0, 0)调用;
  • 处理recv返回0的情况(客户端主动关闭连接);
  • 不要用固定大小的sockets数组,改用动态链表或可扩容数组管理客户端socket,避免覆盖问题。

内容的提问来源于stack exchange,提问作者Fred Esch

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 14:24:52