321 lines
8.8 KiB
Markdown
321 lines
8.8 KiB
Markdown
# 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
|
||
```
|
||
|
||
这将输出详细的调试信息,包括:
|
||
- 消息发送/接收详情
|
||
- 窗口大小变化事件
|
||
- 线程生命周期信息
|