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

Linux终端C++聊天程序:实时更新stdout且不干扰stdin的实现方案

单机器多终端简易聊天程序实现方案

核心技术选型

  • 文件监听:采用Linux inotify API监听共享消息文件chat.txt的修改事件,替代轮询实现高效实时更新
  • 终端控制:通过termios API修改终端为非规范模式,实现字符级输入捕获,同时保留回显
  • 多路IO:用select实现同时监听用户输入和文件修改事件,避免阻塞单一任务
  • 输入保护:通过ANSI转义序列临时保存/恢复输入行状态,确保新消息到来时不打断用户输入

关键问题解决细节

1. 实时检测文件新增内容

使用inotify监听chat.txt的IN_MODIFY事件,当文件被修改时,读取自上次读取位置之后的新增内容:

  • 初始化inotify实例,添加对chat.txt的监听
  • 记录文件当前的偏移量(std::streampos),每次触发修改事件时,从该偏移量开始读取新内容
  • 处理文件被截断的情况(如> chat.txt清空文件),此时重置偏移量为0并重新读取全部内容

2. 不中断用户输入的消息更新

  • 将终端切换到非规范模式(关闭ICANON标志),这样输入字符会立即传递给程序,无需等待回车
  • 维护用户输入的缓存字符串,当有新消息需要打印时:
    1. 用\r将光标移到行首,再用\033[K清除当前行(避免覆盖输入)
    2. 打印新消息并换行
    3. 重新输出用户名提示符和用户已输入的缓存内容,将光标定位到输入末尾

3. 保留终端原生滚动功能

  • 绝对不使用clear或全屏幕清屏的ANSI序列
  • 所有消息追加打印到终端,依赖终端自身的滚动缓冲区保存历史记录,用户可通过终端原生的滚动按键(如Shift+PageUp)查看历史

4. 多用户支持与命令行参数

  • 程序启动时通过命令行参数指定用户名,如./chat alice
  • 输入行固定显示[用户名] -> 提示符
  • 写入消息时使用文件排他锁(flock),避免多进程同时写入导致的消息错乱

完整实现代码

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/inotify.h>
#include <sys/select.h>
#include <termios.h>
#include <sys/file.h>
#include <cctype>

// 全局变量保存终端原始属性,用于程序退出时恢复
struct termios orig_termios;
std::string username;
const char* CHAT_FILE = "chat.txt";
std::streampos last_pos = 0;

// 恢复终端原始模式
void restore_terminal() {
    tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
    std::cout << "\n";
}

// 读取文件新增内容并打印
void read_new_messages() {
    std::ifstream chat_file(CHAT_FILE, std::ios::in);
    if (!chat_file.is_open()) {
        return;
    }

    // 检查文件是否被截断
    chat_file.seekg(0, std::ios::end);
    std::streampos current_size = chat_file.tellg();
    if (current_size < last_pos) {
        last_pos = 0;
    }

    chat_file.seekg(last_pos);
    std::string line;
    while (std::getline(chat_file, line)) {
        std::cout << "\r\033[K" << line << "\n";
    }
    last_pos = chat_file.tellg();
    chat_file.close();
}

// 发送消息到文件
void send_message(const std::string& msg) {
    std::ofstream chat_file(CHAT_FILE, std::ios::app);
    if (!chat_file.is_open()) {
        return;
    }

    // 加排他锁,避免多进程写入冲突
    flock(fileno(chat_file.rdbuf()), LOCK_EX);
    chat_file << username << ": " << msg << "\n";
    flock(fileno(chat_file.rdbuf()), LOCK_UN);
    chat_file.close();
}

int main(int argc, char** argv) {
    // 处理命令行参数
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " <username>\n";
        return 1;
    }
    username = argv[1];

    // 保存终端原始属性,注册退出恢复函数
    tcgetattr(STDIN_FILENO, &orig_termios);
    atexit(restore_terminal);

    // 设置终端为非规范模式(cbreak),保留回显
    struct termios new_termios = orig_termios;
    new_termios.c_lflag &= ~ICANON; // 关闭规范模式
    new_termios.c_cc[VMIN] = 1;     // 至少读取1个字符
    new_termios.c_cc[VTIME] = 0;    // 无超时
    tcsetattr(STDIN_FILENO, TCSANOW, &new_termios);

    // 初始化inotify,监听chat.txt的修改事件
    int inotify_fd = inotify_init();
    if (inotify_fd == -1) {
        perror("inotify_init failed");
        return 1;
    }
    int watch_fd = inotify_add_watch(inotify_fd, CHAT_FILE, IN_MODIFY | IN_CREATE);
    if (watch_fd == -1) {
        perror("inotify_add_watch failed");
        close(inotify_fd);
        return 1;
    }

    // 初始化输入缓存
    std::string input_buf;
    std::cout << username << " -> " << std::flush;

    // 初始化select集合
    fd_set read_fds;
    int max_fd = std::max(STDIN_FILENO, inotify_fd);

    while (true) {
        FD_ZERO(&read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        FD_SET(inotify_fd, &read_fds);

        // 等待IO事件
        int ret = select(max_fd + 1, &read_fds, nullptr, nullptr, nullptr);
        if (ret == -1) {
            perror("select failed");
            break;
        }

        // 处理用户输入
        if (FD_ISSET(STDIN_FILENO, &read_fds)) {
            char c;
            ssize_t n = read(STDIN_FILENO, &c, 1);
            if (n <= 0) continue;

            if (c == '\n') {
                // 回车发送消息
                if (!input_buf.empty()) {
                    send_message(input_buf);
                    input_buf.clear();
                    std::cout << "\r\033[K" << username << " -> " << std::flush;
                } else {
                    std::cout << "\r\033[K" << username << " -> " << std::flush;
                }
            } else if (c == 127 || c == '\b') {
                // 退格键删除字符
                if (!input_buf.empty()) {
                    input_buf.pop_back();
                    std::cout << "\b \b" << std::flush;
                }
            } else if (isprint(c)) {
                // 可打印字符添加到缓存
                input_buf += c;
                std::cout << c << std::flush;
            }
        }

        // 处理文件修改事件
        if (FD_ISSET(inotify_fd, &read_fds)) {
            char buf[4096];
            ssize_t n = read(inotify_fd, buf, sizeof(buf));
            if (n <= 0) continue;

            // 读取新消息并恢复输入行
            read_new_messages();
            std::cout << username << " -> " << input_buf << std::flush;
        }
    }

    // 清理资源
    inotify_rm_watch(inotify_fd, watch_fd);
    close(inotify_fd);
    return 0;
}

编译与使用说明

  1. 编译:g++ chat.cpp -o chat -std=c++11
  2. 启动多个终端实例,分别指定用户名:
    • 终端1:./chat alice
    • 终端2:./chat bob
    • 终端3:./chat charlie
  3. 输入消息后回车发送,其他终端会实时收到新消息,且输入过程不会被打断

注意事项

  • 首次运行时如果chat.txt不存在,程序会自动创建(当第一个用户发送消息时)
  • 若程序异常崩溃,可手动执行reset命令恢复终端模式

内容的提问来源于stack exchange,提问作者Patrick O'Brien

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 19:39:53