使用asio::stream_descriptor异步读取raw模式stdin时崩溃及键码解析问题
我想用asio::stream_descriptor异步轮询stdin获取终端键盘事件,写了以下代码:
/* class tui contains a member 'stdin_' of type 'asio::stream_descriptor', initialized as such : stdin_{io_strand_,STDIN_FILENO} */ void tui::a_read_loop(){ stdin_.async_read_some(buffer_,[this](const auto& ec, size_t n){ if(ec==asio::error::operation_aborted);//canceled else{ //...treat input... a_read_loop();//chainloop } }); }
同时将终端设置为raw模式:
termattr.c_cflag|=CS8; termattr.c_iflag|=IGNBRK; termattr.c_iflag&=~(tcflag_t)(ICRNL|INLCR|IXOFF|IXON|IXANY); termattr.c_lflag&=~(tcflag_t)(ECHO|ECHOE|ECHOK|ECHONL|ICANON|IEXTEN|ISIG); termattr.c_oflag|=OPOST; termattr.c_cc[VMIN]=1; termattr.c_cc[VTIME]=0; tcsetattr(STDIN_FILENO,TCSAFLUSH,&termattr); std::setvbuf(stdin,nullptr,_IONBF,0);
代码大部分情况正常,但输入过多、处理不及时时会崩溃,抛出异常:
terminate called after throwing an instance of 'std::system_error' what(): cannot write to file: Resource temporarily unavailable
异常来自异步任务本身,会导致asio::thread_pool崩溃,排查后认为和asio::stream_descriptor遇到EAGAIN有关,尝试过多种non_blocking和native_non_blocking设置组合,高负载下仍报错。
另外还有一个问题:buffer_中有时会包含连续的两个键码,不知道如何解析并确定单个事件的正确长度,无法识别这类情况。
请问使用asio::stream_descriptor异步逐事件读取stdin的正确方式是什么?
一、解决EAGAIN导致的崩溃问题
你遇到的Resource temporarily unavailable本质是EAGAIN错误,但asio默认会将部分系统错误转换为异常抛出,尤其是在异步操作的内部处理中。要避免崩溃,需确保所有可能的错误都被正确捕获处理,同时调整流描述符的非阻塞设置和读取逻辑:
强制设置native_non_blocking为true
初始化stdin_后,必须将底层文件描述符设置为非阻塞模式,这是asio异步文件IO的必要条件:stdin_.native_non_blocking(true);注意不要混用
non_blocking()方法,native_non_blocking直接操作底层文件描述符的O_NONBLOCK标志,是异步IO的核心依赖。在回调中处理所有错误码
原代码只处理了operation_aborted,忽略了其他错误(比如EAGAIN),需要扩展错误处理逻辑,避免未处理的错误触发异常:void tui::a_read_loop(){ stdin_.async_read_some(buffer_,[this](const auto& ec, size_t n){ if(ec){ if(ec != asio::error::operation_aborted && ec.value() == EAGAIN){ // 资源暂时不可用,直接重新发起读操作 a_read_loop(); } // 其他错误可根据需求处理,比如日志输出 return; } //...treat input... a_read_loop();//chainloop }); }这里明确捕获
EAGAIN错误,直接重新发起异步读,阻止错误向上传播导致崩溃。确保strand的正确使用
你的stdin_绑定了io_strand_,要保证所有对stdin_的操作都在strand上下文中执行,避免并发操作引发异常。
二、解析连续键码的问题
终端raw模式下,特殊按键(比如方向键、功能键)会发送多字节序列(例如方向键上是\x1b[A),单字节则是普通字符。要正确解析单个键盘事件,需维护一个输入缓冲区,逐步累积字节直到组成完整的按键序列:
维护累积缓冲区
在tui类中添加成员变量std::vector<char> input_buffer_,用于暂存未解析的字节。分步骤解析输入
在异步读回调中,先将新读取的字节追加到input_buffer_,再尝试从缓冲区中解析完整的按键事件:// 在tui类中定义 std::vector<char> input_buffer_; void tui::process_input(const char* data, size_t n){ input_buffer_.insert(input_buffer_.end(), data, data + n); while(!input_buffer_.empty()){ size_t consumed = parse_key_event(input_buffer_); if(consumed == 0){ // 字节不足,无法组成完整事件,等待后续输入 break; } input_buffer_.erase(input_buffer_.begin(), input_buffer_.begin() + consumed); } } size_t tui::parse_key_event(const std::vector<char>& buf){ if(buf.empty()) return 0; // 普通ASCII字符(单字节) if(buf[0] != '\x1b'){ handle_key(buf[0]); return 1; } // 转义序列(多字节),比如方向键、功能键 if(buf.size() >= 2){ if(buf[1] == '['){ if(buf.size() >=3){ switch(buf[2]){ case 'A': handle_key(KEY_UP); return 3; case 'B': handle_key(KEY_DOWN); return 3; case 'C': handle_key(KEY_RIGHT); return 3; case 'D': handle_key(KEY_LEFT); return 3; // 其他功能键的转义序列可继续扩展 } } } } // 字节不足,返回0等待更多输入 return 0; }这种方式可以逐步累积输入字节,直到能解析出完整的单个键盘事件,避免一次性处理多个键码的问题。
适配终端转义序列差异
不同终端的转义序列可能略有不同(比如某些终端的方向键是\x1bOA而非\x1b[A),可根据目标终端调整解析逻辑,或使用ncurses等现成终端库简化键码解析。
内容的提问来源于stack exchange,提问作者kmft3kte

