execve_hook/SOCKET_PROTOCOL.md

8.8 KiB
Raw Permalink Blame History

Socket 实时终端信息同步

概述

本项目实现了一个现代化的 Unix Socket 通信机制,支持 C 客户端和 Go 服务端之间的双向消息传递。特别地,实现了实时监听终端窗口大小变化并同步给服务端的功能。

架构设计

协议设计

采用基于消息头的结构化协议:

// 消息头16字节固定大小
typedef struct {
    uint32_t magic;       // 魔数 0x42534D54 ("BSMT")
    uint32_t type;        // 消息类型
    uint32_t payload_len; // 载荷长度(压缩后)
    uint32_t reserved;    // 低16位: 压缩标志; 高16位: 原始大小/256
} MessageHeader;

压缩标志reserved 字段)

#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))

消息类型

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 信号监听终端窗口大小变化:

// 信号处理器
static void handle_sigwinch(int sig) {
    g_window_size_changed = 1;
}

// 注册信号
struct sigaction sa;
sa.sa_handler = handle_sigwinch;
sigaction(SIGWINCH, &sa, NULL);

2. 独立监控线程(条件变量驱动)

启动专门的线程监控窗口变化,使用条件变量实现高效等待:

// 全局条件变量
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 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结构体

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环境变量)

编译和测试

编译

cd execve_hook
make clean
make

测试

  1. 启动 Go 服务端:
cd go_service
sudo ./build/bash_go_service-amd64 daemon
  1. 在另一个终端运行测试:
cd execve_hook
./build/test_client
  1. 在测试运行时调整终端窗口大小,观察服务端日志输出

技术特性

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 端(客户端)

// 初始化协议上下文并启用压缩
ProtocolContext ctx;
protocol_init(&ctx, COMPRESS_LZ4, 0);  // LZ4 快速模式

// 使用压缩写入消息
write_message_compressed(sock, &ctx, MSG_TYPE_TERMINAL_INPUT, data, len);

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)

编译时启用 LZ4C 端)

# 启用 LZ4 压缩支持
make LZ4=1

# 或者 DEBUG + LZ4
make DEBUG=1 LZ4=1

压缩统计

// 获取压缩统计
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 模式编译:

make DEBUG=1

这将输出详细的调试信息,包括:

  • 消息发送/接收详情
  • 窗口大小变化事件
  • 线程生命周期信息