Linux终端C++聊天程序:实时更新stdout且不干扰stdin的实现方案
单机器多终端简易聊天程序实现方案
核心技术选型
- 文件监听:采用Linux
inotifyAPI监听共享消息文件chat.txt的修改事件,替代轮询实现高效实时更新 - 终端控制:通过
termiosAPI修改终端为非规范模式,实现字符级输入捕获,同时保留回显 - 多路IO:用
select实现同时监听用户输入和文件修改事件,避免阻塞单一任务 - 输入保护:通过ANSI转义序列临时保存/恢复输入行状态,确保新消息到来时不打断用户输入
关键问题解决细节
1. 实时检测文件新增内容
使用inotify监听chat.txt的IN_MODIFY事件,当文件被修改时,读取自上次读取位置之后的新增内容:
- 初始化inotify实例,添加对
chat.txt的监听 - 记录文件当前的偏移量(
std::streampos),每次触发修改事件时,从该偏移量开始读取新内容 - 处理文件被截断的情况(如
> chat.txt清空文件),此时重置偏移量为0并重新读取全部内容
2. 不中断用户输入的消息更新
- 将终端切换到非规范模式(关闭
ICANON标志),这样输入字符会立即传递给程序,无需等待回车 - 维护用户输入的缓存字符串,当有新消息需要打印时:
- 用
\r将光标移到行首,再用\033[K清除当前行(避免覆盖输入) - 打印新消息并换行
- 重新输出用户名提示符和用户已输入的缓存内容,将光标定位到输入末尾
- 用
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; }
编译与使用说明
- 编译:
g++ chat.cpp -o chat -std=c++11 - 启动多个终端实例,分别指定用户名:
- 终端1:
./chat alice - 终端2:
./chat bob - 终端3:
./chat charlie
- 终端1:
- 输入消息后回车发送,其他终端会实时收到新消息,且输入过程不会被打断
注意事项
- 首次运行时如果
chat.txt不存在,程序会自动创建(当第一个用户发送消息时) - 若程序异常崩溃,可手动执行
reset命令恢复终端模式
内容的提问来源于stack exchange,提问作者Patrick O'Brien
相关产品推荐
相关产品推荐

