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

C语言中实现无缓冲回合制Socket通信的技术问题求助

单线程回合制游戏终端输入缓冲问题解决方案

问题背景

我开发了一款单线程、一对一客户端-服务器架构的回合制游戏,采用non-blocking I/O实现回合机制,通信基于stdout和stdin完成。目前整体运行正常,但存在阻塞I/O的缓冲问题:客户端先行回合时,服务器在客户端操作期间仍处于运行状态,此时服务器终端输入的任意内容会被缓冲,直到客户端完成动作后才会被客户端接收,不符合预期。网上Socket相关资料较少,想咨询是否遗漏了某些函数或功能。

客户端代码片段

while (1){
    memset(buffer, 0, BUFFER_SIZE);
    printf("Enter a message for the server: ");
    fgets(buffer, BUFFER_SIZE, stdin);
    buffer[strlen(buffer)-1] = '\0'; 
    send(buffer, connfd);
    // sending 
}
memset(buffer, 0, BUFFER_SIZE);
read(connfd, buffer, BUFFER_SIZE);
// responding

服务器代码片段

while (1) {
    memset(buffer, 0, BUFFER_SIZE);
    read(connfd, buffer, BUFFER_SIZE);
    // responding since client goes first

    // Create msg
    char message[BUFFER_SIZE];
    while (1) {
        memset(message, 0, BUFFER_SIZE);
        printf("Enter a message for the client: ");
        fgets(message, BUFFER_SIZE, stdin);
        message[strlen(message)-1] = '\0';
        send(buffer, connfd);
        // sending
    }
}

注:自定义的send是封装函数,经解析后调用C标准库send();read使用C标准库内置的read()。


核心问题分析

  1. 单线程串行阻塞:当前代码中fgets(终端输入)和read(Socket读取)都是阻塞调用,单线程下同一时间只能处理一个操作,导致另一方的输入被积压缓冲。
  2. 终端行缓冲机制:终端默认是行缓冲模式,输入内容直到按下回车才会传递给程序,进一步加剧了输入延迟。
  3. 回合逻辑缺失状态控制:没有明确的回合状态标记,导致双方可能同时触发输入操作,输入顺序混乱。

具体解决步骤

1. 禁用终端行缓冲

修改终端属性,关闭行缓冲和回显(可选),让输入字符立即被程序读取:

#include <termios.h>
#include <unistd.h>

// 禁用行缓冲
void disable_line_buffering() {
    struct termios tty;
    tcgetattr(STDIN_FILENO, &tty);
    tty.c_lflag &= ~(ICANON | ECHO); // ICANON关闭行缓冲,ECHO关闭输入回显
    tcsetattr(STDIN_FILENO, TCSANOW, &tty); // 立即生效
}

// 程序退出前恢复终端属性,避免终端异常
void restore_line_buffering() {
    struct termios tty;
    tcgetattr(STDIN_FILENO, &tty);
    tty.c_lflag |= ICANON | ECHO;
    tcsetattr(STDIN_FILENO, TCSANOW, &tty);
}

在程序初始化时调用disable_line_buffering(),退出前(比如信号处理函数中)调用restore_line_buffering()。

2. 用select实现I/O多路复用

单线程下同时监听终端stdin和Socket的可读事件,避免阻塞在单一操作上。示例框架:

#include <sys/select.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    // ... 初始化Socket、禁用终端缓冲等操作 ...
    int connfd = ...; // 已建立的Socket描述符
    char buffer[BUFFER_SIZE];
    bool my_turn = false; // 客户端初始设为true,服务器设为false

    // 设置Socket为non-blocking模式
    int flags = fcntl(connfd, F_GETFL, 0);
    fcntl(connfd, F_SETFL, flags | O_NONBLOCK);

    fd_set read_fds;
    struct timeval tv;

    while (1) {
        FD_ZERO(&read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        FD_SET(connfd, &read_fds);
        int max_fd = connfd > STDIN_FILENO ? connfd : STDIN_FILENO;

        // 设置超时(100ms,可根据需求调整)
        tv.tv_sec = 0;
        tv.tv_usec = 100000;

        int ret = select(max_fd + 1, &read_fds, NULL, NULL, &tv);
        if (ret == -1) {
            perror("select error");
            break;
        }

        // 处理终端输入(仅当前回合时处理)
        if (FD_ISSET(STDIN_FILENO, &read_fds) && my_turn) {
            char c;
            ssize_t n = read(STDIN_FILENO, &c, 1);
            if (n > 0) {
                if (c == '\n') {
                    // 输入完成,发送消息
                    send(buffer, connfd);
                    memset(buffer, 0, BUFFER_SIZE);
                    my_turn = false; // 结束当前回合
                } else {
                    // 收集输入字符
                    strncat(buffer, &c, 1);
                    printf("%c", c); // 手动回显,因为关闭了ECHO
                }
            }
        }

        // 处理Socket消息
        if (FD_ISSET(connfd, &read_fds)) {
            memset(buffer, 0, BUFFER_SIZE);
            ssize_t n = read(connfd, buffer, BUFFER_SIZE);
            if (n > 0) {
                printf("\nReceived: %s\n", buffer);
                my_turn = true; // 收到消息后开始自己的回合
                printf("Enter your response: ");
                fflush(stdout); // 刷新输出,确保提示立即显示
            } else if (n == 0) {
                printf("Connection closed by peer\n");
                break;
            } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                perror("read error");
                break;
            }
        }
    }

    restore_line_buffering();
    close(connfd);
    return 0;
}

3. 严格控制回合状态

通过my_turn布尔变量标记当前是否为本方回合:

  • 客户端启动时设置my_turn = true,允许输入
  • 客户端发送消息后设置my_turn = false,等待服务器响应
  • 服务器收到客户端消息后设置my_turn = true,允许输入
  • 服务器发送响应后设置my_turn = false,等待客户端下一轮

这样能保证同一时间只有一方可以输入,避免交叉操作导致的缓冲问题。

4. 刷新输出缓冲

使用printf后,调用fflush(stdout)确保提示信息立即显示在终端,避免因为输出缓冲导致用户看不到输入提示。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 04:17:34