8.8 KiB
8.8 KiB
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
测试
- 启动 Go 服务端:
cd go_service
sudo ./build/bash_go_service-amd64 daemon
- 在另一个终端运行测试:
cd execve_hook
./build/test_client
- 在测试运行时调整终端窗口大小,观察服务端日志输出
技术特性
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 压缩支持
└── ...
性能考虑
- 事件驱动:窗口监控线程使用条件变量等待 SIGWINCH 信号,零 CPU 占用
- 即时响应:窗口大小变化时立即唤醒处理,无延迟
- 消息大小:终端信息消息约 60-80 字节,网络开销小
- 并发设计:独立的监听线程不阻塞主流程
数据压缩
压缩算法
本协议支持 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)
编译时启用 LZ4(C 端):
# 启用 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 | 跳过 | - |
扩展建议
- 动态轮询间隔:可以根据窗口变化频率动态调整超时时间
- 去重机制:连续相同的窗口大小可以不发送更新
压缩传输:✅ 已完成 - 支持 LZ4 压缩- 心跳机制:添加心跳消息检测连接状态
条件变量优化:✅ 已完成 - 使用pthread_cond_t替代轮询,实现事件驱动
调试
开启 DEBUG 模式编译:
make DEBUG=1
这将输出详细的调试信息,包括:
- 消息发送/接收详情
- 窗口大小变化事件
- 线程生命周期信息