# Socket 实时终端信息同步 ## 概述 本项目实现了一个现代化的 Unix Socket 通信机制,支持 C 客户端和 Go 服务端之间的双向消息传递。特别地,实现了实时监听终端窗口大小变化并同步给服务端的功能。 ## 架构设计 ### 协议设计 采用基于消息头的结构化协议: ```c // 消息头(16字节,固定大小) typedef struct { uint32_t magic; // 魔数 0x42534D54 ("BSMT") uint32_t type; // 消息类型 uint32_t payload_len; // 载荷长度(压缩后) uint32_t reserved; // 低16位: 压缩标志; 高16位: 原始大小/256 } MessageHeader; ``` ### 压缩标志(reserved 字段) ```c #define MSG_FLAG_COMPRESSED 0x01 // 载荷已压缩 #define MSG_FLAG_COMPRESS_LZ4 0x02 // 使用 LZ4 压缩 #define MSG_FLAG_COMPRESS_HC 0x04 // 使用高压缩比模式 // 提取/构造 reserved 字段 #define GET_COMPRESS_FLAGS(reserved) ((reserved) & 0xFFFF) #define GET_ORIGINAL_SIZE_HINT(reserved) (((reserved) >> 16) * 256) #define MAKE_RESERVED(flags, orig_size) (((flags) & 0xFFFF) | (((orig_size) / 256) << 16)) ``` ### 消息类型 ```c typedef enum { MSG_TYPE_INIT = 1, // 初始化连接,发送命令信息 MSG_TYPE_WINDOW_SIZE_UPDATE = 2, // 终端窗口大小更新 MSG_TYPE_SERVER_RESPONSE = 3, // 服务器响应消息 MSG_TYPE_CLOSE = 4 // 关闭连接 } MessageType; ``` ## 核心功能 ### 1. 窗口大小实时监听(C端) 使用 SIGWINCH 信号监听终端窗口大小变化: ```c // 信号处理器 static void handle_sigwinch(int sig) { g_window_size_changed = 1; } // 注册信号 struct sigaction sa; sa.sa_handler = handle_sigwinch; sigaction(SIGWINCH, &sa, NULL); ``` ### 2. 独立监控线程(条件变量驱动) 启动专门的线程监控窗口变化,使用条件变量实现高效等待: ```c // 全局条件变量 static pthread_mutex_t g_winch_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t g_winch_cond = PTHREAD_COND_INITIALIZER; // 信号处理器中唤醒等待线程 static void handle_sigwinch(int sig) { g_window_size_changed = 1; pthread_cond_signal(&g_winch_cond); // 立即唤醒 } static void* window_monitor_thread(void* arg) { while (!g_should_exit) { pthread_mutex_lock(&g_winch_mutex); // 使用条件变量等待信号,带超时以便检查退出标志 while (!g_window_size_changed && !g_should_exit) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 1; // 1秒超时 pthread_cond_timedwait(&g_winch_cond, &g_winch_mutex, &ts); } pthread_mutex_unlock(&g_winch_mutex); if (g_window_size_changed) { g_window_size_changed = 0; send_terminal_info(sock, MSG_TYPE_WINDOW_SIZE_UPDATE); } } } ``` ### 3. Go服务端实时接收 服务端启动goroutine持续监听客户端消息: ```go go func() { for { msgType, payload, err := readMessage(conn) if err != nil { return } if msgType == MsgTypeWindowSizeUpdate { termInfo, _ := parseTerminalInfo(payload) params.TerminalInfo = *termInfo logging.Info("窗口大小已更新 - %dx%d", termInfo.Rows, termInfo.Cols) } } }() ``` ## 终端信息结构 ### 固定部分(C结构体) ```c typedef struct { uint32_t is_tty; // 是否为TTY uint16_t rows; // 行数 uint16_t cols; // 列数 uint16_t x_pixel; // X像素 uint16_t y_pixel; // Y像素 uint32_t has_termios; // 是否有termios属性 uint32_t input_flags; // termios输入标志 uint32_t output_flags; // termios输出标志 uint32_t control_flags; // termios控制标志 uint32_t local_flags; // termios本地标志 } TerminalInfoFixed; ``` ### 可变部分 - `term_type_len` + `term_type` (TERM环境变量) - `shell_type_len` + `shell_type` (SHELL环境变量) ## 编译和测试 ### 编译 ```bash cd execve_hook make clean make ``` ### 测试 1. 启动 Go 服务端: ```bash cd go_service sudo ./build/bash_go_service-amd64 daemon ``` 2. 在另一个终端运行测试: ```bash cd execve_hook ./build/test_client ``` 3. 在测试运行时调整终端窗口大小,观察服务端日志输出 ## 技术特性 ### 1. 线程安全 - 使用 `pthread_mutex` 保护共享的 socket 文件描述符 - 使用 `sig_atomic_t` 类型处理信号标志 ### 2. 优雅关闭 - 使用 channel 控制 goroutine 生命周期 - 通过 `MSG_TYPE_CLOSE` 消息通知客户端关闭 ### 3. 错误处理 - 魔数验证防止协议错误 - 完整的错误检查和日志记录 ### 4. 现代化设计 - 结构化消息协议 - 分离的消息读写函数 - 清晰的职责划分 ## 文件结构 ``` execve_hook/src/ ├── socket_protocol.h # 协议定义头文件 ├── socket_protocol.c # 协议实现 ├── compression.h # 压缩模块头文件 ├── compression.c # LZ4 压缩实现 ├── client.h # 客户端头文件 ├── client.c # 客户端实现(含SIGWINCH处理) └── ... go_service/internal/socket/ ├── protocol.go # Go 协议实现 ├── compression.go # Go 压缩支持 └── ... ``` ## 性能考虑 1. **事件驱动**:窗口监控线程使用条件变量等待 SIGWINCH 信号,零 CPU 占用 2. **即时响应**:窗口大小变化时立即唤醒处理,无延迟 3. **消息大小**:终端信息消息约 60-80 字节,网络开销小 4. **并发设计**:独立的监听线程不阻塞主流程 ## 数据压缩 ### 压缩算法 本协议支持 **LZ4** 压缩算法,具有以下特点: - 压缩速度:~500 MB/s - 解压速度:~1500 MB/s - 压缩比:约 2:1 到 3:1 - 内存占用:极低 ### 何时使用压缩 | 场景 | 是否推荐压缩 | 原因 | |------|-------------|------| | **本地 Unix Socket** | ❌ 不推荐 | 本地通信带宽充足,压缩反而增加 CPU 开销 | | **远程 TCP/网络通信** | ✅ 推荐 | 减少网络带宽,特别是在弱网环境 | | **大量终端输出** | ✅ 推荐 | 终端输出通常是文本,压缩效果好(3:1~5:1) | | **小消息(<64字节)** | ❌ 自动跳过 | 小数据压缩效果差,压缩后可能更大 | | **二进制数据** | ⚠️ 视情况 | 已压缩的数据(如图片)压缩效果差 | | **低延迟要求** | ✅ LZ4 适用 | LZ4 延迟极低(<1ms),适合实时场景 | | **CPU 受限环境** | ❌ 不推荐 | 压缩会消耗 CPU,嵌入式设备需权衡 | ### 压缩模式选择 | 模式 | 压缩比 | 速度 | 适用场景 | |------|--------|------|----------| | `COMPRESS_NONE` | 1:1 | 最快 | 本地通信 | | `COMPRESS_LZ4` | ~2.5:1 | 快 | 实时终端、网络通信 | | `COMPRESS_LZ4_HC` | ~3:1 | 中等 | 带宽敏感、可接受少量延迟 | ### 启用压缩 **C 端(客户端)**: ```c // 初始化协议上下文并启用压缩 ProtocolContext ctx; protocol_init(&ctx, COMPRESS_LZ4, 0); // LZ4 快速模式 // 使用压缩写入消息 write_message_compressed(sock, &ctx, MSG_TYPE_TERMINAL_INPUT, data, len); ``` **Go 端(服务端)**: ```go // 创建带压缩的连接 conn := socket.NewConnectionWithCompression(netConn, socket.CompressLZ4, 0) // 或者后期启用 conn.EnableCompression(socket.CompressLZ4, 0) // 使用自动解压读取 msgType, payload, err := socket.ReadMessageWithDecompression(conn) // 使用压缩写入 socket.WriteMessageCompressed(conn, compCtx, msgType, payload) ``` **编译时启用 LZ4(C 端)**: ```bash # 启用 LZ4 压缩支持 make LZ4=1 # 或者 DEBUG + LZ4 make DEBUG=1 LZ4=1 ``` ### 压缩统计 ```go // 获取压缩统计 bytesIn, bytesOut, compressCount, skipCount, ratio := conn.GetCompressionStats() fmt.Printf("压缩比: %d%%, 压缩次数: %d, 跳过: %d\n", ratio, compressCount, skipCount) ``` ### 典型压缩效果 | 数据类型 | 原始大小 | 压缩后 | 压缩比 | |----------|----------|--------|--------| | 终端文本输出 | 4KB | ~1KB | 4:1 | | ANSI 转义序列 | 1KB | ~400B | 2.5:1 | | 初始化消息 | 2KB | ~600B | 3.3:1 | | 小按键输入 | 10B | 跳过 | - | ## 扩展建议 1. **动态轮询间隔**:可以根据窗口变化频率动态调整超时时间 2. **去重机制**:连续相同的窗口大小可以不发送更新 3. ~~**压缩传输**~~:✅ 已完成 - 支持 LZ4 压缩 4. **心跳机制**:添加心跳消息检测连接状态 5. ~~**条件变量优化**~~:✅ 已完成 - 使用 `pthread_cond_t` 替代轮询,实现事件驱动 ## 调试 开启 DEBUG 模式编译: ```bash make DEBUG=1 ``` 这将输出详细的调试信息,包括: - 消息发送/接收详情 - 窗口大小变化事件 - 线程生命周期信息