feat: 实现并完成核心功能测试套件
- 编译系统:支持通过 `make clean all` 进行全量编译,生成可执行文件 `omni_client`、`omni_server`、`omni_relay` 和 `omni_test`。 - 客户端-服务端文件传输:支持 TCP/UDP/KCP 协议,已验证文件收发功能(使用 `/tmp/input.bin` 作为测试文件)。 - 服务端指令驱动:服务端可通过控制台发送 ASCII 指令(如 `hello-client`)实时驱动客户端。 - 动态转发功能 (Relay):实现 UDP 协议下的动态目标切换,支持 `show` 查询和 `set` 命令实时修改转发目标(如从 9102 端口切换到 9103 端口)。 - 所有功能已在本地环境(127.0.0.1)通过完整流程验证。
This commit is contained in:
86
Makefile
Normal file
86
Makefile
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 默认本机编译器(可由环境变量覆盖,例如 CC=clang)。
|
||||||
|
CC ?= gcc
|
||||||
|
# ARM 交叉编译工具链前缀(可按本地环境替换)。
|
||||||
|
CROSS_COMPILE ?= arm-linux-gnueabihf-
|
||||||
|
ARM_CC ?= $(CROSS_COMPILE)gcc
|
||||||
|
|
||||||
|
# 产物目录(native: build/,arm: build/arm/)。
|
||||||
|
BUILD_DIR ?= build
|
||||||
|
|
||||||
|
# 编译参数:
|
||||||
|
# - CFLAGS: 优化级别、调试符号、告警、C 标准
|
||||||
|
# - CPPFLAGS: POSIX 特性宏,确保 clock_gettime 等接口可见
|
||||||
|
CFLAGS ?= -O2 -g -Wall -Wextra -std=c11
|
||||||
|
CPPFLAGS ?= -D_DEFAULT_SOURCE -D_POSIX_C_SOURCE=200809L
|
||||||
|
LDFLAGS ?=
|
||||||
|
# 链接 pthread(apps 中有多线程)。
|
||||||
|
LDLIBS ?= -lpthread
|
||||||
|
INCLUDES := -Iinclude
|
||||||
|
|
||||||
|
# 所有二进制共享的核心源码(网络核心 + 协议实现 + ikcp)。
|
||||||
|
COMMON_SRCS := \
|
||||||
|
src/core/network.c \
|
||||||
|
src/core/logger.c \
|
||||||
|
src/protocols/tcp_impl.c \
|
||||||
|
src/protocols/udp_impl.c \
|
||||||
|
src/protocols/kcp_impl.c \
|
||||||
|
src/protocols/ikcp.c
|
||||||
|
|
||||||
|
# 各应用入口源文件。
|
||||||
|
APP_TEST_SRC := src/apps/test_main.c
|
||||||
|
APP_CLIENT_SRC := src/apps/client_main.c
|
||||||
|
APP_SERVER_SRC := src/apps/server_main.c
|
||||||
|
APP_RELAY_SRC := src/apps/relay_main.c
|
||||||
|
|
||||||
|
# 将源文件映射到 BUILD_DIR 下的对象文件路径。
|
||||||
|
COMMON_OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(COMMON_SRCS))
|
||||||
|
TEST_OBJ := $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_TEST_SRC))
|
||||||
|
CLIENT_OBJ := $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_CLIENT_SRC))
|
||||||
|
SERVER_OBJ := $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_SERVER_SRC))
|
||||||
|
RELAY_OBJ := $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_RELAY_SRC))
|
||||||
|
|
||||||
|
# 默认构建目标:4 个可执行程序。
|
||||||
|
TARGETS := \
|
||||||
|
$(BUILD_DIR)/omni_test \
|
||||||
|
$(BUILD_DIR)/omni_client \
|
||||||
|
$(BUILD_DIR)/omni_server \
|
||||||
|
$(BUILD_DIR)/omni_relay
|
||||||
|
|
||||||
|
.PHONY: all arm clean help
|
||||||
|
|
||||||
|
# 本机构建入口。
|
||||||
|
all: $(TARGETS)
|
||||||
|
|
||||||
|
# 各可执行程序链接规则。
|
||||||
|
$(BUILD_DIR)/omni_test: $(COMMON_OBJS) $(TEST_OBJ)
|
||||||
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/omni_client: $(COMMON_OBJS) $(CLIENT_OBJ)
|
||||||
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/omni_server: $(COMMON_OBJS) $(SERVER_OBJ)
|
||||||
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/omni_relay: $(COMMON_OBJS) $(RELAY_OBJ)
|
||||||
|
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
|
# 通用编译规则:
|
||||||
|
# - 自动创建对象文件所在目录
|
||||||
|
# - 编译单个 .c 为 .o
|
||||||
|
$(BUILD_DIR)/%.o: %.c
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||||
|
|
||||||
|
# ARM 构建入口:通过子 make 覆盖 BUILD_DIR 与 CC。
|
||||||
|
arm:
|
||||||
|
$(MAKE) BUILD_DIR=build/arm CC=$(ARM_CC) all
|
||||||
|
|
||||||
|
# 清理构建目录。
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
# 便捷帮助信息。
|
||||||
|
help:
|
||||||
|
@echo "make -> build native binaries in build/"
|
||||||
|
@echo "make arm -> build ARM binaries in build/arm (arm-linux-gnueabihf-gcc)"
|
||||||
|
@echo "make clean -> remove build artifacts"
|
||||||
201
README.md
201
README.md
@@ -1,21 +1,192 @@
|
|||||||
|
# OmniSocket
|
||||||
|
|
||||||
|
统一的 TCP / UDP / KCP 传输框架,包含:
|
||||||
|
- 协议抽象层(`omni_init / omni_send / omni_recv`)
|
||||||
|
- 客户端:文件分片发送 + 异步接收服务端 ASCII 指令
|
||||||
|
- 服务端:接收并写文件 + 交互输入指令下发客户端
|
||||||
|
- 转发器:A->B 中转,支持运行时动态修改目标端口
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
OmniSocket/
|
OmniSocket/
|
||||||
├── include/
|
├── include/
|
||||||
│ ├── common.h # 全局定义:MsgHeader 结构体、错误码、宏定义
|
│ ├── common.h # MsgHeader(type,len,timestamp)、消息类型、通用宏
|
||||||
│ ├── network.h # 定义统一的协议接口 (omni_init, omni_send等)
|
│ ├── network.h # 统一协议接口定义
|
||||||
│ ├── kcp/ # 存放外部 KCP 源码 (ikcp.h, ikcp.c)
|
│ ├── kcp/ikcp.h # KCP 头文件
|
||||||
│ └── logger.h # 日志统计函数声明
|
│ └── logger.h # 日志与统计接口
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── protocols/
|
│ ├── protocols/
|
||||||
│ │ ├── tcp_impl.c # TCP 专用实现
|
│ │ ├── tcp_impl.c # TCP 实现(16字节头 + 粘包拆包)
|
||||||
│ │ ├── udp_impl.c # UDP 专用实现
|
│ │ ├── udp_impl.c # UDP 实现(sendto/recvfrom)
|
||||||
│ │ └── kcp_impl.c # KCP 专用实现(调用 ikcp.c)
|
│ │ ├── kcp_impl.c # KCP 实现(基于 UDP + ikcp)
|
||||||
|
│ │ └── ikcp.c # KCP 源码
|
||||||
│ ├── core/
|
│ ├── core/
|
||||||
│ │ ├── network.c # 协议分发逻辑(根据参数选 TCP/UDP/KCP)
|
│ │ ├── network.c # 协议工厂分发
|
||||||
│ │ └── logger.c # 延迟计算、吞吐量统计逻辑实现
|
│ │ └── logger.c # 性能统计日志
|
||||||
│ ├── apps/
|
│ └── apps/
|
||||||
│ │ ├── client_main.c # 客户端入口(文件读取、指令接收)
|
│ ├── client_main.c # 客户端入口
|
||||||
│ │ ├── server_main.c # 服务端入口(指令输入、数据接收)
|
│ ├── server_main.c # 服务端入口
|
||||||
│ │ └── relay_main.c # 转发器入口(中转逻辑)
|
│ ├── relay_main.c # 转发器入口
|
||||||
|
│ └── test_main.c # 简易协议连通性测试
|
||||||
|
├── scripts/
|
||||||
|
│ └── local_smoke_test.sh # 本机一键 smoke 测试
|
||||||
├── build/ # 编译产物目录
|
├── build/ # 编译产物目录
|
||||||
├── Makefile # 关键:支持 make server 和 make client_arm
|
├── Makefile
|
||||||
└── README.md # 运行指南与参数说明
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
### 本机构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
生成:
|
||||||
|
- `build/omni_client`
|
||||||
|
- `build/omni_server`
|
||||||
|
- `build/omni_relay`
|
||||||
|
- `build/omni_test`
|
||||||
|
|
||||||
|
### ARM 交叉编译
|
||||||
|
|
||||||
|
默认使用 `arm-linux-gnueabihf-gcc`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make arm
|
||||||
|
```
|
||||||
|
|
||||||
|
生成到 `build/arm/` 目录。
|
||||||
|
|
||||||
|
## 程序参数
|
||||||
|
|
||||||
|
### `omni_server`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_server -p tcp|udp|kcp -P <listen_port> -o <output_file> [-b <bind_ip>]
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 接收客户端发送的文件分片并写入 `output_file`
|
||||||
|
- 若在交互终端运行,可在标准输入输入 ASCII 文本并回发给客户端
|
||||||
|
- 输入 `quit` 可退出服务端交互循环
|
||||||
|
|
||||||
|
### `omni_client`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_client -p tcp|udp|kcp -H <server_ip> -P <server_port> -f <file> [-b <bind_port>] [-m <chunk_mtu>] [-w <wait_seconds|-1>]
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 读取 `file`,按 `chunk_mtu`(默认 1400)分片发送
|
||||||
|
- 发送结束后额外发送 `FILE_END` 控制包
|
||||||
|
- 后台线程持续接收并打印服务端 ASCII 指令
|
||||||
|
- `-w -1` 表示常驻模式,直到手动 `Ctrl+C`
|
||||||
|
|
||||||
|
### `omni_relay`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_relay -p tcp|udp|kcp -L <listen_port> -H <target_ip> -P <target_port>
|
||||||
|
```
|
||||||
|
|
||||||
|
标准输入支持命令:
|
||||||
|
- `set <ip> <port>`:动态修改转发目标
|
||||||
|
- `show`:显示当前目标
|
||||||
|
- `quit`:退出 relay
|
||||||
|
|
||||||
|
## 快速启动(本机)
|
||||||
|
|
||||||
|
先准备一个测试文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dd if=/dev/urandom of=/tmp/input.bin bs=1400 count=64
|
||||||
|
```
|
||||||
|
|
||||||
|
### TCP 直连(2 个终端)
|
||||||
|
|
||||||
|
终端 1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_server -p tcp -P 9000 -o /tmp/out_tcp.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
终端 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_client -p tcp -H 127.0.0.1 -P 9000 -f /tmp/input.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
校验:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmp -s /tmp/input.bin /tmp/out_tcp.bin && echo OK || echo FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
### UDP 直连(2 个终端)
|
||||||
|
|
||||||
|
终端 1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_server -p udp -P 9001 -o /tmp/out_udp.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
终端 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_client -p udp -H 127.0.0.1 -P 9001 -f /tmp/input.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
校验:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmp -s /tmp/input.bin /tmp/out_udp.bin && echo OK || echo FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
### KCP 直连(2 个终端)
|
||||||
|
|
||||||
|
终端 1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_server -p kcp -P 9002 -o /tmp/out_kcp.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
终端 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_client -p kcp -H 127.0.0.1 -P 9002 -f /tmp/input.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
校验:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmp -s /tmp/input.bin /tmp/out_kcp.bin && echo OK || echo FAIL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Relay 场景示例(3 个终端)
|
||||||
|
|
||||||
|
终端 1(最终接收端 B):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_server -p udp -P 9102 -o /tmp/out_relay.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
终端 2(relay):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_relay -p udp -L 9101 -H 127.0.0.1 -P 9102
|
||||||
|
```
|
||||||
|
|
||||||
|
终端 3(发送端 A):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
build/omni_client -p udp -H 127.0.0.1 -P 9101 -f /tmp/input.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
relay 终端可输入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
show
|
||||||
|
set 127.0.0.1 9103
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
* common.h
|
* common.h
|
||||||
* 全局公共定义:消息头、错误码、通用宏
|
|
||||||
|
. * 全局公共定义:消息头、错误码、通用宏
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef OMNISOCKET_COMMON_H
|
#ifndef OMNISOCKET_COMMON_H
|
||||||
#define OMNISOCKET_COMMON_H
|
#define OMNISOCKET_COMMON_H
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
/* 统一的 16 字节消息头(解决 TCP 粘包用) */
|
/* 统一的 16 字节消息头(应用层消息头) */
|
||||||
typedef struct MsgHeader {
|
typedef struct MsgHeader {
|
||||||
uint32_t magic; /* 固定魔数,用于快速校验 */
|
uint32_t type; /* 消息类型:文件块/控制指令等 */
|
||||||
uint32_t length; /* 后续负载长度(字节数) */
|
uint32_t len; /* 后续负载长度(字节数) */
|
||||||
uint64_t seq; /* 序列号或会话内消息 ID */
|
uint64_t timestamp; /* 发送时间戳(毫秒) */
|
||||||
} MsgHeader;
|
} MsgHeader;
|
||||||
|
|
||||||
#define MSG_HEADER_SIZE (sizeof(MsgHeader)) /* 16 字节 */
|
#define MSG_HEADER_SIZE (sizeof(MsgHeader)) /* 16 字节 */
|
||||||
#define MSG_MAGIC 0x4F4D4E49u /* 'OMNI' */
|
|
||||||
|
enum {
|
||||||
|
MSG_TYPE_FILE_CHUNK = 1,
|
||||||
|
MSG_TYPE_FILE_END = 2,
|
||||||
|
MSG_TYPE_COMMAND = 3,
|
||||||
|
MSG_TYPE_RAW = 100
|
||||||
|
};
|
||||||
|
|
||||||
|
#define OMNI_DEFAULT_MTU 1400u
|
||||||
|
|
||||||
/* 通用错误码(负数返回表示出错) */
|
/* 通用错误码(负数返回表示出错) */
|
||||||
enum {
|
enum {
|
||||||
@@ -35,6 +45,50 @@ static inline uint64_t omni_now_ms(void)
|
|||||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000u);
|
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000u);
|
||||||
}
|
}
|
||||||
|
// 64 位整数主机序与网络序转换工具
|
||||||
|
static inline uint64_t omni_bswap64(uint64_t x)
|
||||||
|
{
|
||||||
|
return ((x & 0x00000000000000FFull) << 56) |
|
||||||
|
((x & 0x000000000000FF00ull) << 40) |
|
||||||
|
((x & 0x0000000000FF0000ull) << 24) |
|
||||||
|
((x & 0x00000000FF000000ull) << 8) |
|
||||||
|
((x & 0x000000FF00000000ull) >> 8) |
|
||||||
|
((x & 0x0000FF0000000000ull) >> 24) |
|
||||||
|
((x & 0x00FF000000000000ull) >> 40) |
|
||||||
|
((x & 0xFF00000000000000ull) >> 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint64_t omni_htonll(uint64_t x)
|
||||||
|
{
|
||||||
|
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||||
|
return omni_bswap64(x);
|
||||||
|
#else
|
||||||
|
return x;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint64_t omni_ntohll(uint64_t x)
|
||||||
|
{
|
||||||
|
return omni_htonll(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void omni_msg_header_encode(MsgHeader *out_hdr,
|
||||||
|
uint32_t type,
|
||||||
|
uint32_t len,
|
||||||
|
uint64_t timestamp_ms)
|
||||||
|
{
|
||||||
|
out_hdr->type = htonl(type);
|
||||||
|
out_hdr->len = htonl(len);
|
||||||
|
out_hdr->timestamp = omni_htonll(timestamp_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void omni_msg_header_decode(const MsgHeader *net_hdr,
|
||||||
|
MsgHeader *host_hdr)
|
||||||
|
{
|
||||||
|
host_hdr->type = ntohl(net_hdr->type);
|
||||||
|
host_hdr->len = ntohl(net_hdr->len);
|
||||||
|
host_hdr->timestamp = omni_ntohll(net_hdr->timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
#endif /* OMNISOCKET_COMMON_H */
|
#endif /* OMNISOCKET_COMMON_H */
|
||||||
|
|
||||||
|
|||||||
32496
omni_logs.jsonl
Normal file
32496
omni_logs.jsonl
Normal file
File diff suppressed because it is too large
Load Diff
122
scripts/local_smoke_test.sh
Executable file
122
scripts/local_smoke_test.sh
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 本机一键 smoke 测试:
|
||||||
|
# - test1: TCP 直连 client -> server 文件一致性
|
||||||
|
# - test2: UDP client -> relay -> server,包含动态目标切换
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 根目录与构建产物目录。
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BUILD_DIR="$ROOT_DIR/build"
|
||||||
|
# 每次测试创建独立临时目录,避免互相污染。
|
||||||
|
TMP_DIR="$(mktemp -d /tmp/omnisocket-smoke.XXXXXX)"
|
||||||
|
|
||||||
|
# 随机选择一组端口,降低被系统中已有进程占用的概率。
|
||||||
|
BASE_PORT=$((20000 + (RANDOM % 20000)))
|
||||||
|
DIRECT_PORT="$BASE_PORT"
|
||||||
|
RELAY_PORT=$((BASE_PORT + 1))
|
||||||
|
SINK1_PORT=$((BASE_PORT + 2))
|
||||||
|
SINK2_PORT=$((BASE_PORT + 3))
|
||||||
|
|
||||||
|
# 记录后台进程 PID,统一在 cleanup 中回收。
|
||||||
|
PIDS=()
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
# 无论脚本成功/失败,都尽量回收子进程,避免残留占端口。
|
||||||
|
for pid in "${PIDS[@]:-}"; do
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
# 删除临时目录与中间文件。
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[smoke] %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_with_timeout() {
|
||||||
|
# 轮询等待某个 PID 退出,超时返回非 0。
|
||||||
|
# 参数:
|
||||||
|
# $1 pid
|
||||||
|
# $2 timeout_s
|
||||||
|
local pid="$1"
|
||||||
|
local timeout_s="$2"
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < timeout_s * 10; ++i)); do
|
||||||
|
if ! kill -0 "$pid" 2>/dev/null; then
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
log "ports direct=$DIRECT_PORT relay=$RELAY_PORT sink1=$SINK1_PORT sink2=$SINK2_PORT"
|
||||||
|
log "building native binaries"
|
||||||
|
# 统一从干净状态构建。
|
||||||
|
make -C "$ROOT_DIR" clean all >/dev/null
|
||||||
|
|
||||||
|
# 测试输入与输出文件路径。
|
||||||
|
INPUT_FILE="$TMP_DIR/input.bin"
|
||||||
|
DIRECT_OUT="$TMP_DIR/direct_out.bin"
|
||||||
|
RELAY1_OUT="$TMP_DIR/relay_sink1.bin"
|
||||||
|
RELAY2_OUT="$TMP_DIR/relay_sink2.bin"
|
||||||
|
|
||||||
|
# 准备随机输入文件(32 * 1400 = 44800 bytes)。
|
||||||
|
dd if=/dev/urandom of="$INPUT_FILE" bs=1400 count=32 status=none
|
||||||
|
|
||||||
|
log "test1: direct tcp client -> server"
|
||||||
|
# 启动 TCP 服务端接收文件。
|
||||||
|
"$BUILD_DIR/omni_server" -p tcp -P "$DIRECT_PORT" -o "$DIRECT_OUT" >"$TMP_DIR/direct_server.log" 2>&1 &
|
||||||
|
DIRECT_SERVER_PID=$!
|
||||||
|
PIDS+=("$DIRECT_SERVER_PID")
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# 启动客户端发送文件。
|
||||||
|
"$BUILD_DIR/omni_client" -p tcp -H 127.0.0.1 -P "$DIRECT_PORT" -f "$INPUT_FILE" -w 1 >"$TMP_DIR/direct_client.log" 2>&1
|
||||||
|
wait_with_timeout "$DIRECT_SERVER_PID" 10
|
||||||
|
# 校验接收文件与输入文件一致。
|
||||||
|
cmp -s "$INPUT_FILE" "$DIRECT_OUT"
|
||||||
|
log "test1 passed"
|
||||||
|
|
||||||
|
log "test2: udp relay forwarding with dynamic port switch"
|
||||||
|
# sink1:relay 初始目标(预期可能不再接收最终数据)。
|
||||||
|
"$BUILD_DIR/omni_server" -p udp -P "$SINK1_PORT" -o "$RELAY1_OUT" >"$TMP_DIR/relay_sink1.log" 2>&1 &
|
||||||
|
SINK1_PID=$!
|
||||||
|
PIDS+=("$SINK1_PID")
|
||||||
|
|
||||||
|
# sink2:relay 切换后的目标(最终校验对象)。
|
||||||
|
"$BUILD_DIR/omni_server" -p udp -P "$SINK2_PORT" -o "$RELAY2_OUT" >"$TMP_DIR/relay_sink2.log" 2>&1 &
|
||||||
|
SINK2_PID=$!
|
||||||
|
PIDS+=("$SINK2_PID")
|
||||||
|
|
||||||
|
# 预置 relay 控制命令:启动后立即切到 sink2。
|
||||||
|
CTRL_FILE="$TMP_DIR/relay_ctrl.txt"
|
||||||
|
printf 'set 127.0.0.1 %s\n' "$SINK2_PORT" >"$CTRL_FILE"
|
||||||
|
|
||||||
|
# 启动 relay(UDP 监听 RELAY_PORT)。
|
||||||
|
"$BUILD_DIR/omni_relay" -p udp -L "$RELAY_PORT" -H 127.0.0.1 -P "$SINK1_PORT" <"$CTRL_FILE" >"$TMP_DIR/relay.log" 2>&1 &
|
||||||
|
RELAY_PID=$!
|
||||||
|
PIDS+=("$RELAY_PID")
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# 客户端发送到 relay,由 relay 中转到目标 sink。
|
||||||
|
"$BUILD_DIR/omni_client" -p udp -H 127.0.0.1 -P "$RELAY_PORT" -f "$INPUT_FILE" -w 1 >"$TMP_DIR/relay_client.log" 2>&1
|
||||||
|
wait_with_timeout "$SINK2_PID" 10
|
||||||
|
# 校验 relay 最终接收端文件一致。
|
||||||
|
cmp -s "$INPUT_FILE" "$RELAY2_OUT"
|
||||||
|
|
||||||
|
if [[ -s "$RELAY1_OUT" ]]; then
|
||||||
|
# 如果 sink1 收到数据,通常是切换命令生效前的短暂窗口内到达。
|
||||||
|
log "warning: sink1 received data before switch (relay reconfiguration happened mid-flight)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# relay/sink1 不一定会自然退出,这里主动结束避免脚本挂住。
|
||||||
|
kill "$RELAY_PID" 2>/dev/null || true
|
||||||
|
wait "$RELAY_PID" 2>/dev/null || true
|
||||||
|
kill "$SINK1_PID" 2>/dev/null || true
|
||||||
|
|
||||||
|
log "test2 passed"
|
||||||
|
log "all smoke tests passed"
|
||||||
358
src/apps/client_main.c
Normal file
358
src/apps/client_main.c
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* client_main.c
|
||||||
|
* 客户端:读取大文件分片发送,同时后台接收服务端 ASCII 指令并打印
|
||||||
|
*
|
||||||
|
* 线程模型:
|
||||||
|
* - 主线程:读取文件并发送 FILE_CHUNK / FILE_END
|
||||||
|
* - 子线程:持续接收服务端 COMMAND 并打印
|
||||||
|
*
|
||||||
|
* 消息格式:
|
||||||
|
* - 每条业务消息为 [MsgHeader(16B) + payload]
|
||||||
|
* - MsgHeader 字段由 common.h 中的 encode/decode 统一处理
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "network.h"
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define CLIENT_FRAME_BUF_SIZE (MSG_HEADER_SIZE + 65536u)
|
||||||
|
|
||||||
|
typedef struct ClientRuntime {
|
||||||
|
/* 协议抽象层句柄。 */
|
||||||
|
OmniContext *ctx;
|
||||||
|
/* 线程共享运行标记:1=运行中,0=退出。 */
|
||||||
|
atomic_int running;
|
||||||
|
} ClientRuntime;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 进程级停止标记:
|
||||||
|
* - 收到 SIGINT/SIGTERM(例如 Ctrl+C)时置 1
|
||||||
|
* - 主线程据此触发收尾逻辑,保证线程/连接能优雅退出
|
||||||
|
*/
|
||||||
|
static volatile sig_atomic_t g_stop = 0;
|
||||||
|
|
||||||
|
static void on_signal(int signo)
|
||||||
|
{
|
||||||
|
(void)signo;
|
||||||
|
g_stop = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void install_signal_handlers(void)
|
||||||
|
{
|
||||||
|
struct sigaction sa;
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = on_signal;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
|
||||||
|
(void)sigaction(SIGINT, &sa, NULL);
|
||||||
|
(void)sigaction(SIGTERM, &sa, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(const char *prog)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage:\n"
|
||||||
|
" %s -p tcp|udp|kcp -H <server_ip> -P <server_port> -f <file>\n"
|
||||||
|
" [-b <bind_port>] [-m <chunk_mtu>] [-w <wait_seconds|-1>]\n",
|
||||||
|
prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static OmniProtocol parse_proto(const char *s)
|
||||||
|
{
|
||||||
|
/* 输入非法时回退到 TCP,方便本地默认测试。 */
|
||||||
|
if (!s) return OMNI_PROTO_TCP;
|
||||||
|
if (strcmp(s, "tcp") == 0) return OMNI_PROTO_TCP;
|
||||||
|
if (strcmp(s, "udp") == 0) return OMNI_PROTO_UDP;
|
||||||
|
if (strcmp(s, "kcp") == 0) return OMNI_PROTO_KCP;
|
||||||
|
return OMNI_PROTO_TCP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int send_app_message(OmniContext *ctx,
|
||||||
|
uint32_t type,
|
||||||
|
const void *payload,
|
||||||
|
uint32_t payload_len)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 统一应用层发包:
|
||||||
|
* 1) 组装业务头(网络字节序)
|
||||||
|
* 2) 拼接 payload
|
||||||
|
* 3) 通过 omni_send 一次发送整帧
|
||||||
|
*/
|
||||||
|
size_t total_len = MSG_HEADER_SIZE + (size_t)payload_len;
|
||||||
|
uint8_t *frame = (uint8_t *)malloc(total_len);
|
||||||
|
if (!frame) {
|
||||||
|
logger_log("ERROR", "client", "malloc_frame_failed len=%zu", total_len);
|
||||||
|
return OMNI_ERR_GENERIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgHeader hdr;
|
||||||
|
omni_msg_header_encode(&hdr, type, payload_len, omni_now_ms());
|
||||||
|
memcpy(frame, &hdr, MSG_HEADER_SIZE);
|
||||||
|
if (payload_len > 0 && payload) {
|
||||||
|
memcpy(frame + MSG_HEADER_SIZE, payload, payload_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t n = omni_send(ctx, frame, total_len);
|
||||||
|
free(frame);
|
||||||
|
|
||||||
|
if (n != (ssize_t)total_len) {
|
||||||
|
logger_log("ERROR", "client",
|
||||||
|
"omni_send_failed expect=%zu got=%zd type=%u",
|
||||||
|
total_len, n, (unsigned)type);
|
||||||
|
return OMNI_ERR_IO;
|
||||||
|
}
|
||||||
|
return OMNI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int decode_app_message(const uint8_t *frame,
|
||||||
|
size_t frame_len,
|
||||||
|
MsgHeader *out_hdr,
|
||||||
|
const uint8_t **out_payload)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 统一应用层解包:
|
||||||
|
* - 至少要有 16B 头
|
||||||
|
* - 头中 len 与总帧长度必须一致,避免越界/脏数据
|
||||||
|
*/
|
||||||
|
if (!frame || frame_len < MSG_HEADER_SIZE || !out_hdr || !out_payload) {
|
||||||
|
return OMNI_ERR_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgHeader net_hdr;
|
||||||
|
memcpy(&net_hdr, frame, MSG_HEADER_SIZE);
|
||||||
|
omni_msg_header_decode(&net_hdr, out_hdr);
|
||||||
|
|
||||||
|
if ((size_t)out_hdr->len + MSG_HEADER_SIZE != frame_len) {
|
||||||
|
return OMNI_ERR_IO;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_payload = frame + MSG_HEADER_SIZE;
|
||||||
|
return OMNI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *recv_thread_main(void *arg)
|
||||||
|
{
|
||||||
|
ClientRuntime *rt = (ClientRuntime *)arg;
|
||||||
|
uint8_t frame[CLIENT_FRAME_BUF_SIZE];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 显式启用可取消:主线程收尾时通过 pthread_cancel 打断阻塞 recv,
|
||||||
|
* 避免 UDP/KCP 场景下因长时间无回包导致 join 卡住。
|
||||||
|
*/
|
||||||
|
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||||
|
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
|
||||||
|
|
||||||
|
while (atomic_load(&rt->running)) {
|
||||||
|
ssize_t n = omni_recv(rt->ctx, frame, sizeof(frame));
|
||||||
|
if (n < 0) {
|
||||||
|
logger_log("ERROR", "client", "recv_failed n=%zd", n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
/* 0 在不同协议下可能代表“暂时无数据”或“对端关闭”,做短暂退避避免空转。 */
|
||||||
|
usleep(2 * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgHeader hdr;
|
||||||
|
const uint8_t *payload = NULL;
|
||||||
|
int rc = decode_app_message(frame, (size_t)n, &hdr, &payload);
|
||||||
|
if (rc != OMNI_OK) {
|
||||||
|
logger_log("ERROR", "client", "invalid_app_frame bytes=%zd rc=%d", n, rc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hdr.type == MSG_TYPE_COMMAND) {
|
||||||
|
/* COMMAND 约定为 ASCII 文本,做安全截断后打印。 */
|
||||||
|
char cmd[2048];
|
||||||
|
size_t cpy = hdr.len < (uint32_t)(sizeof(cmd) - 1) ? hdr.len : (sizeof(cmd) - 1);
|
||||||
|
memcpy(cmd, payload, cpy);
|
||||||
|
cmd[cpy] = '\0';
|
||||||
|
printf("[server-cmd] %s\n", cmd);
|
||||||
|
fflush(stdout);
|
||||||
|
} else {
|
||||||
|
/* 客户端当前只消费 COMMAND,其它类型保留日志便于调试。 */
|
||||||
|
logger_log("INFO", "client",
|
||||||
|
"recv_non_command type=%u len=%u",
|
||||||
|
(unsigned)hdr.type, (unsigned)hdr.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&rt->running, 0);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
install_signal_handlers();
|
||||||
|
|
||||||
|
/* 命令行参数默认值。 */
|
||||||
|
const char *proto_str = "tcp";
|
||||||
|
const char *server_ip = NULL;
|
||||||
|
const char *file_path = NULL;
|
||||||
|
int server_port = 0;
|
||||||
|
int bind_port = 0;
|
||||||
|
unsigned chunk_size = OMNI_DEFAULT_MTU;
|
||||||
|
int wait_seconds = 2;
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "p:H:P:f:b:m:w:")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'p':
|
||||||
|
proto_str = optarg;
|
||||||
|
break;
|
||||||
|
case 'H':
|
||||||
|
server_ip = optarg;
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
server_port = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
file_path = optarg;
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
bind_port = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
chunk_size = (unsigned)strtoul(optarg, NULL, 10);
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
wait_seconds = atoi(optarg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server_ip || server_port <= 0 || !file_path) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (chunk_size == 0 || chunk_size > 65536u) {
|
||||||
|
/* 约束 chunk 上限,避免一次申请/发送过大缓冲。 */
|
||||||
|
fprintf(stderr, "invalid chunk size: %u\n", chunk_size);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
FILE *fp = fopen(file_path, "rb");
|
||||||
|
if (!fp) {
|
||||||
|
perror("fopen");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
OmniProtocol proto = parse_proto(proto_str);
|
||||||
|
/* 客户端角色:对端地址由 -H/-P 指定。 */
|
||||||
|
OmniContext *ctx = omni_init(OMNI_ROLE_CLIENT, proto,
|
||||||
|
NULL, (uint16_t)bind_port,
|
||||||
|
server_ip, (uint16_t)server_port);
|
||||||
|
if (!ctx) {
|
||||||
|
fclose(fp);
|
||||||
|
fprintf(stderr, "omni_init failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientRuntime rt;
|
||||||
|
rt.ctx = ctx;
|
||||||
|
atomic_init(&rt.running, 1);
|
||||||
|
|
||||||
|
/* 启动异步接收线程(打印服务端指令)。 */
|
||||||
|
pthread_t recv_tid;
|
||||||
|
if (pthread_create(&recv_tid, NULL, recv_thread_main, &rt) != 0) {
|
||||||
|
perror("pthread_create");
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
fclose(fp);
|
||||||
|
omni_close(ctx);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *chunk = (uint8_t *)malloc(chunk_size);
|
||||||
|
if (!chunk) {
|
||||||
|
logger_log("ERROR", "client", "malloc_chunk_failed size=%u", chunk_size);
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
pthread_cancel(recv_tid);
|
||||||
|
pthread_join(recv_tid, NULL);
|
||||||
|
omni_close(ctx);
|
||||||
|
fclose(fp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t total_sent = 0;
|
||||||
|
/*
|
||||||
|
* 主发送循环:
|
||||||
|
* - 每次读取 chunk_size 字节
|
||||||
|
* - 发送 FILE_CHUNK
|
||||||
|
* - EOF 后发送 FILE_END
|
||||||
|
*/
|
||||||
|
while (atomic_load(&rt.running)) {
|
||||||
|
if (g_stop) {
|
||||||
|
logger_log("INFO", "client", "signal_received_stop_sending");
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
size_t nread = fread(chunk, 1, chunk_size, fp);
|
||||||
|
if (nread == 0) {
|
||||||
|
if (feof(fp)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ferror(fp)) {
|
||||||
|
logger_log("ERROR", "client", "fread_failed");
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nread > 0) {
|
||||||
|
int rc = send_app_message(ctx, MSG_TYPE_FILE_CHUNK, chunk, (uint32_t)nread);
|
||||||
|
if (rc != OMNI_OK) {
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
total_sent += nread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atomic_load(&rt.running)) {
|
||||||
|
/* 正常结束时发送 FILE_END,通知服务端落盘完成。 */
|
||||||
|
int rc = send_app_message(ctx, MSG_TYPE_FILE_END, NULL, 0);
|
||||||
|
if (rc != OMNI_OK) {
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger_log("INFO", "client", "file_transfer_done bytes=%llu",
|
||||||
|
(unsigned long long)total_sent);
|
||||||
|
free(chunk);
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 等待模式:
|
||||||
|
* - wait_seconds >= 0: 发送完成后最多等待 N 秒
|
||||||
|
* - wait_seconds < 0 : 常驻模式,直到 Ctrl+C(SIGINT)或连接异常
|
||||||
|
*/
|
||||||
|
if (wait_seconds < 0) {
|
||||||
|
logger_log("INFO", "client", "keepalive_mode=on press_ctrl_c_to_exit");
|
||||||
|
while (atomic_load(&rt.running) && !g_stop) {
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < wait_seconds && atomic_load(&rt.running) && !g_stop; ++i) {
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收尾顺序:先停接收线程,再关闭网络上下文。 */
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
pthread_cancel(recv_tid);
|
||||||
|
pthread_join(recv_tid, NULL);
|
||||||
|
omni_close(ctx);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
269
src/apps/relay_main.c
Normal file
269
src/apps/relay_main.c
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
/*
|
||||||
|
* relay_main.c
|
||||||
|
* 中转站:从 A 接收数据后立即转发到 B,支持运行时动态修改转发目标
|
||||||
|
*
|
||||||
|
* 并发模型:
|
||||||
|
* - 主线程:阻塞接收上游流量并转发到当前目标
|
||||||
|
* - 控制线程:读取 stdin 命令,动态切换目标地址
|
||||||
|
*
|
||||||
|
* 线程安全策略:
|
||||||
|
* - tx_ctx / target_ip / target_port 受 tx_mu 互斥锁保护
|
||||||
|
* - 主线程转发发送与控制线程切换目标不会并发踩内存
|
||||||
|
*
|
||||||
|
* 控制命令(stdin):
|
||||||
|
* set <ip> <port> 修改目标地址
|
||||||
|
* show 打印当前目标
|
||||||
|
* quit 退出
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "network.h"
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define RELAY_BUF_SIZE (MSG_HEADER_SIZE + 65536u)
|
||||||
|
|
||||||
|
typedef struct RelayState {
|
||||||
|
/* 当前 relay 工作协议。 */
|
||||||
|
OmniProtocol proto;
|
||||||
|
/* 上游接收上下文(通常是服务端角色)。 */
|
||||||
|
OmniContext *rx_ctx;
|
||||||
|
/* 下游发送上下文(通常是客户端角色,可动态替换)。 */
|
||||||
|
OmniContext *tx_ctx;
|
||||||
|
/* 保护 tx_ctx 与目标地址信息。 */
|
||||||
|
pthread_mutex_t tx_mu;
|
||||||
|
/* 运行标志。 */
|
||||||
|
atomic_int running;
|
||||||
|
/* 当前目标地址快照(用于 show 命令与日志)。 */
|
||||||
|
char target_ip[64];
|
||||||
|
uint16_t target_port;
|
||||||
|
} RelayState;
|
||||||
|
|
||||||
|
static void usage(const char *prog)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage:\n"
|
||||||
|
" %s -p tcp|udp|kcp -L <listen_port> -H <target_ip> -P <target_port>\n",
|
||||||
|
prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static OmniProtocol parse_proto(const char *s)
|
||||||
|
{
|
||||||
|
/* 非法输入回退 TCP。 */
|
||||||
|
if (!s) return OMNI_PROTO_TCP;
|
||||||
|
if (strcmp(s, "tcp") == 0) return OMNI_PROTO_TCP;
|
||||||
|
if (strcmp(s, "udp") == 0) return OMNI_PROTO_UDP;
|
||||||
|
if (strcmp(s, "kcp") == 0) return OMNI_PROTO_KCP;
|
||||||
|
return OMNI_PROTO_TCP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int relay_set_target(RelayState *st, const char *ip, uint16_t port)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 动态切换目标步骤:
|
||||||
|
* 1) 先建立新 tx_ctx(失败时保持旧目标不变)
|
||||||
|
* 2) 加锁替换指针与目标参数
|
||||||
|
* 3) 解锁后关闭旧 tx_ctx(避免持锁做慢操作)
|
||||||
|
*/
|
||||||
|
OmniContext *new_tx = omni_init(OMNI_ROLE_CLIENT, st->proto,
|
||||||
|
NULL, 0,
|
||||||
|
ip, port);
|
||||||
|
if (!new_tx) {
|
||||||
|
logger_log("ERROR", "relay", "connect_target_failed ip=%s port=%u",
|
||||||
|
ip, (unsigned)port);
|
||||||
|
return OMNI_ERR_IO;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&st->tx_mu);
|
||||||
|
OmniContext *old_tx = st->tx_ctx;
|
||||||
|
st->tx_ctx = new_tx;
|
||||||
|
snprintf(st->target_ip, sizeof(st->target_ip), "%s", ip);
|
||||||
|
st->target_port = port;
|
||||||
|
pthread_mutex_unlock(&st->tx_mu);
|
||||||
|
|
||||||
|
if (old_tx) {
|
||||||
|
omni_close(old_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger_log("INFO", "relay", "target_updated ip=%s port=%u", ip, (unsigned)port);
|
||||||
|
return OMNI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *control_thread_main(void *arg)
|
||||||
|
{
|
||||||
|
/* 控制线程负责解析 stdin 命令。 */
|
||||||
|
RelayState *st = (RelayState *)arg;
|
||||||
|
char line[256];
|
||||||
|
|
||||||
|
while (atomic_load(&st->running)) {
|
||||||
|
if (!fgets(line, sizeof(line), stdin)) {
|
||||||
|
/*
|
||||||
|
* 管道/重定向 EOF 时不要立刻退出 relay:
|
||||||
|
* - 清理 EOF 状态
|
||||||
|
* - 短暂休眠后继续循环
|
||||||
|
* 这样 relay 仍可继续处理主数据面转发。
|
||||||
|
*/
|
||||||
|
clearerr(stdin);
|
||||||
|
usleep(100 * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t len = strlen(line);
|
||||||
|
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||||
|
line[--len] = '\0';
|
||||||
|
}
|
||||||
|
if (len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(line, "quit") == 0) {
|
||||||
|
/* 通知主线程退出。 */
|
||||||
|
atomic_store(&st->running, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(line, "show") == 0) {
|
||||||
|
/* 在锁保护下读取目标快照,避免与 set 并发冲突。 */
|
||||||
|
pthread_mutex_lock(&st->tx_mu);
|
||||||
|
fprintf(stderr, "relay target: %s:%u\n",
|
||||||
|
st->target_ip[0] ? st->target_ip : "N/A",
|
||||||
|
(unsigned)st->target_port);
|
||||||
|
pthread_mutex_unlock(&st->tx_mu);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char ip[64];
|
||||||
|
unsigned port = 0;
|
||||||
|
if (sscanf(line, "set %63s %u", ip, &port) == 2 && port > 0 && port <= 65535u) {
|
||||||
|
/* 动态切目标。 */
|
||||||
|
relay_set_target(st, ip, (uint16_t)port);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "unknown command: %s\n", line);
|
||||||
|
fprintf(stderr, "commands: set <ip> <port> | show | quit\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
/* 命令行参数默认值。 */
|
||||||
|
const char *proto_str = "tcp";
|
||||||
|
const char *target_ip = NULL;
|
||||||
|
int listen_port = 0;
|
||||||
|
int target_port = 0;
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "p:L:H:P:")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'p':
|
||||||
|
proto_str = optarg;
|
||||||
|
break;
|
||||||
|
case 'L':
|
||||||
|
listen_port = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'H':
|
||||||
|
target_ip = optarg;
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
target_port = atoi(optarg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target_ip || listen_port <= 0 || target_port <= 0) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RelayState st;
|
||||||
|
memset(&st, 0, sizeof(st));
|
||||||
|
st.proto = parse_proto(proto_str);
|
||||||
|
atomic_init(&st.running, 1);
|
||||||
|
pthread_mutex_init(&st.tx_mu, NULL);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* rx_ctx 作为上游入口(server 角色):
|
||||||
|
* - TCP: 等待上游 connect
|
||||||
|
* - UDP/KCP: 绑定监听端口接收上游包
|
||||||
|
*/
|
||||||
|
st.rx_ctx = omni_init(OMNI_ROLE_SERVER, st.proto, NULL, (uint16_t)listen_port, NULL, 0);
|
||||||
|
if (!st.rx_ctx) {
|
||||||
|
fprintf(stderr, "relay: omni_init rx failed\n");
|
||||||
|
pthread_mutex_destroy(&st.tx_mu);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 初始目标连接失败不直接退出:
|
||||||
|
* - relay 可先启动数据入口
|
||||||
|
* - 后续通过 set 命令修复目标地址
|
||||||
|
*/
|
||||||
|
(void)relay_set_target(&st, target_ip, (uint16_t)target_port);
|
||||||
|
|
||||||
|
/* 启动控制面线程。 */
|
||||||
|
pthread_t ctrl_tid;
|
||||||
|
if (pthread_create(&ctrl_tid, NULL, control_thread_main, &st) != 0) {
|
||||||
|
perror("pthread_create");
|
||||||
|
omni_close(st.rx_ctx);
|
||||||
|
if (st.tx_ctx) omni_close(st.tx_ctx);
|
||||||
|
pthread_mutex_destroy(&st.tx_mu);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t buf[RELAY_BUF_SIZE];
|
||||||
|
while (atomic_load(&st.running)) {
|
||||||
|
/* 数据面:收上游 -> 转发下游。 */
|
||||||
|
ssize_t n = omni_recv(st.rx_ctx, buf, sizeof(buf));
|
||||||
|
if (n < 0) {
|
||||||
|
logger_log("ERROR", "relay", "recv_failed n=%zd", n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
/* 暂时无数据时短暂退避。 */
|
||||||
|
usleep(2 * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t m = OMNI_ERR_PARAM;
|
||||||
|
/* 发送上下文受锁保护,防止与 set 命令并发替换。 */
|
||||||
|
pthread_mutex_lock(&st.tx_mu);
|
||||||
|
if (st.tx_ctx) {
|
||||||
|
m = omni_send(st.tx_ctx, buf, (size_t)n);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&st.tx_mu);
|
||||||
|
|
||||||
|
if (m != n) {
|
||||||
|
logger_log("ERROR", "relay", "forward_failed in=%zd out=%zd", n, m);
|
||||||
|
} else {
|
||||||
|
logger_log("INFO", "relay", "forward_ok bytes=%zd", n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收尾:先停主循环,再依次释放 rx / 控制线程 / tx。 */
|
||||||
|
atomic_store(&st.running, 0);
|
||||||
|
omni_close(st.rx_ctx);
|
||||||
|
pthread_join(ctrl_tid, NULL);
|
||||||
|
|
||||||
|
pthread_mutex_lock(&st.tx_mu);
|
||||||
|
OmniContext *tx = st.tx_ctx;
|
||||||
|
st.tx_ctx = NULL;
|
||||||
|
pthread_mutex_unlock(&st.tx_mu);
|
||||||
|
if (tx) {
|
||||||
|
omni_close(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_destroy(&st.tx_mu);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
303
src/apps/server_main.c
Normal file
303
src/apps/server_main.c
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
* server_main.c
|
||||||
|
* 服务端:接收文件写盘;主线程监听键盘输入并发送 ASCII 指令到客户端
|
||||||
|
*
|
||||||
|
* 线程模型:
|
||||||
|
* - 接收线程:持续收业务帧,写入文件,直到 FILE_END
|
||||||
|
* - 主线程:在交互终端下读取 stdin,发送 COMMAND 给客户端
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 当 stdin 不是 TTY(例如被脚本后台拉起)时,主线程不做交互输入,
|
||||||
|
* 仅等待接收线程完成传输,便于自动化测试稳定运行。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "network.h"
|
||||||
|
#include "logger.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define SERVER_FRAME_BUF_SIZE (MSG_HEADER_SIZE + 65536u)
|
||||||
|
|
||||||
|
typedef struct ServerRuntime {
|
||||||
|
/* 协议抽象层句柄。 */
|
||||||
|
OmniContext *ctx;
|
||||||
|
/* 当前运行协议,用于区分 recv 返回 0 的语义(TCP=对端关闭)。 */
|
||||||
|
OmniProtocol proto;
|
||||||
|
/* 接收文件写入目标。 */
|
||||||
|
FILE *out_fp;
|
||||||
|
/* 全局运行标记。 */
|
||||||
|
atomic_int running;
|
||||||
|
/* 收到 FILE_END 后置 1。 */
|
||||||
|
atomic_int transfer_done;
|
||||||
|
/* 已成功写入的文件字节数。 */
|
||||||
|
uint64_t bytes_written;
|
||||||
|
} ServerRuntime;
|
||||||
|
|
||||||
|
static void usage(const char *prog)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage:\n"
|
||||||
|
" %s -p tcp|udp|kcp -P <listen_port> -o <output_file> [-b <bind_ip>]\n",
|
||||||
|
prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static OmniProtocol parse_proto(const char *s)
|
||||||
|
{
|
||||||
|
/* 输入不合法时回退到 TCP。 */
|
||||||
|
if (!s) return OMNI_PROTO_TCP;
|
||||||
|
if (strcmp(s, "tcp") == 0) return OMNI_PROTO_TCP;
|
||||||
|
if (strcmp(s, "udp") == 0) return OMNI_PROTO_UDP;
|
||||||
|
if (strcmp(s, "kcp") == 0) return OMNI_PROTO_KCP;
|
||||||
|
return OMNI_PROTO_TCP;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int send_app_message(OmniContext *ctx,
|
||||||
|
uint32_t type,
|
||||||
|
const void *payload,
|
||||||
|
uint32_t payload_len)
|
||||||
|
{
|
||||||
|
/* 与客户端保持一致的统一发包函数。 */
|
||||||
|
size_t total_len = MSG_HEADER_SIZE + (size_t)payload_len;
|
||||||
|
uint8_t *frame = (uint8_t *)malloc(total_len);
|
||||||
|
if (!frame) {
|
||||||
|
logger_log("ERROR", "server", "malloc_frame_failed len=%zu", total_len);
|
||||||
|
return OMNI_ERR_GENERIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgHeader hdr;
|
||||||
|
omni_msg_header_encode(&hdr, type, payload_len, omni_now_ms());
|
||||||
|
memcpy(frame, &hdr, MSG_HEADER_SIZE);
|
||||||
|
if (payload_len > 0 && payload) {
|
||||||
|
memcpy(frame + MSG_HEADER_SIZE, payload, payload_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t n = omni_send(ctx, frame, total_len);
|
||||||
|
free(frame);
|
||||||
|
|
||||||
|
if (n != (ssize_t)total_len) {
|
||||||
|
logger_log("ERROR", "server",
|
||||||
|
"omni_send_failed expect=%zu got=%zd type=%u",
|
||||||
|
total_len, n, (unsigned)type);
|
||||||
|
return OMNI_ERR_IO;
|
||||||
|
}
|
||||||
|
return OMNI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int decode_app_message(const uint8_t *frame,
|
||||||
|
size_t frame_len,
|
||||||
|
MsgHeader *out_hdr,
|
||||||
|
const uint8_t **out_payload)
|
||||||
|
{
|
||||||
|
/* 与客户端一致的统一解包校验。 */
|
||||||
|
if (!frame || frame_len < MSG_HEADER_SIZE || !out_hdr || !out_payload) {
|
||||||
|
return OMNI_ERR_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgHeader net_hdr;
|
||||||
|
memcpy(&net_hdr, frame, MSG_HEADER_SIZE);
|
||||||
|
omni_msg_header_decode(&net_hdr, out_hdr);
|
||||||
|
|
||||||
|
if ((size_t)out_hdr->len + MSG_HEADER_SIZE != frame_len) {
|
||||||
|
return OMNI_ERR_IO;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_payload = frame + MSG_HEADER_SIZE;
|
||||||
|
return OMNI_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *recv_thread_main(void *arg)
|
||||||
|
{
|
||||||
|
ServerRuntime *rt = (ServerRuntime *)arg;
|
||||||
|
uint8_t frame[SERVER_FRAME_BUF_SIZE];
|
||||||
|
|
||||||
|
/* 允许主线程在退出时取消本线程,避免阻塞 recv 导致无法收尾。 */
|
||||||
|
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||||
|
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
|
||||||
|
|
||||||
|
while (atomic_load(&rt->running)) {
|
||||||
|
ssize_t n = omni_recv(rt->ctx, frame, sizeof(frame));
|
||||||
|
if (n < 0) {
|
||||||
|
logger_log("ERROR", "server", "recv_failed n=%zd", n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
/*
|
||||||
|
* recv 返回 0 的语义依赖协议:
|
||||||
|
* - TCP: 对端连接关闭,接收线程可退出
|
||||||
|
* - UDP/KCP: 可能仅表示当前无可读数据,继续等待
|
||||||
|
*/
|
||||||
|
if (rt->proto == OMNI_PROTO_TCP) {
|
||||||
|
logger_log("INFO", "server", "tcp_peer_closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usleep(2 * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgHeader hdr;
|
||||||
|
const uint8_t *payload = NULL;
|
||||||
|
int rc = decode_app_message(frame, (size_t)n, &hdr, &payload);
|
||||||
|
if (rc != OMNI_OK) {
|
||||||
|
logger_log("ERROR", "server", "invalid_app_frame bytes=%zd rc=%d", n, rc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hdr.type == MSG_TYPE_FILE_CHUNK) {
|
||||||
|
/* 文件分片:直接按顺序落盘。 */
|
||||||
|
size_t nw = fwrite(payload, 1, hdr.len, rt->out_fp);
|
||||||
|
if (nw != hdr.len) {
|
||||||
|
logger_log("ERROR", "server", "fwrite_failed expect=%u got=%zu",
|
||||||
|
(unsigned)hdr.len, nw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rt->bytes_written += nw;
|
||||||
|
} else if (hdr.type == MSG_TYPE_FILE_END) {
|
||||||
|
/*
|
||||||
|
* 文件接收结束:
|
||||||
|
* - 仅置位 transfer_done
|
||||||
|
* - 不退出线程,让服务端在交互模式下继续保持长连接并可下发指令
|
||||||
|
*/
|
||||||
|
fflush(rt->out_fp);
|
||||||
|
atomic_store(&rt->transfer_done, 1);
|
||||||
|
logger_log("INFO", "server", "file_transfer_end bytes=%llu",
|
||||||
|
(unsigned long long)rt->bytes_written);
|
||||||
|
continue;
|
||||||
|
} else if (hdr.type == MSG_TYPE_COMMAND) {
|
||||||
|
/* 当前服务端不处理“来自客户端”的 COMMAND,仅记录日志。 */
|
||||||
|
logger_log("INFO", "server",
|
||||||
|
"recv_command_from_peer len=%u (ignored)",
|
||||||
|
(unsigned)hdr.len);
|
||||||
|
} else {
|
||||||
|
logger_log("INFO", "server",
|
||||||
|
"recv_unknown_type type=%u len=%u",
|
||||||
|
(unsigned)hdr.type, (unsigned)hdr.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_store(&rt->running, 0);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
/* 命令行参数默认值。 */
|
||||||
|
const char *proto_str = "tcp";
|
||||||
|
const char *bind_ip = NULL;
|
||||||
|
const char *output_path = NULL;
|
||||||
|
int listen_port = 0;
|
||||||
|
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt(argc, argv, "p:b:P:o:")) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'p':
|
||||||
|
proto_str = optarg;
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
bind_ip = optarg;
|
||||||
|
break;
|
||||||
|
case 'P':
|
||||||
|
listen_port = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'o':
|
||||||
|
output_path = optarg;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen_port <= 0 || !output_path) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *out_fp = fopen(output_path, "wb");
|
||||||
|
if (!out_fp) {
|
||||||
|
perror("fopen");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
OmniProtocol proto = parse_proto(proto_str);
|
||||||
|
/* 服务端角色:仅监听本地端口。 */
|
||||||
|
OmniContext *ctx = omni_init(OMNI_ROLE_SERVER, proto,
|
||||||
|
bind_ip, (uint16_t)listen_port,
|
||||||
|
NULL, 0);
|
||||||
|
if (!ctx) {
|
||||||
|
fclose(out_fp);
|
||||||
|
fprintf(stderr, "omni_init failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerRuntime rt;
|
||||||
|
rt.ctx = ctx;
|
||||||
|
rt.proto = proto;
|
||||||
|
rt.out_fp = out_fp;
|
||||||
|
rt.bytes_written = 0;
|
||||||
|
atomic_init(&rt.running, 1);
|
||||||
|
atomic_init(&rt.transfer_done, 0);
|
||||||
|
|
||||||
|
/* 启动接收线程处理文件写入主流程。 */
|
||||||
|
pthread_t recv_tid;
|
||||||
|
if (pthread_create(&recv_tid, NULL, recv_thread_main, &rt) != 0) {
|
||||||
|
perror("pthread_create");
|
||||||
|
omni_close(ctx);
|
||||||
|
fclose(out_fp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isatty(STDIN_FILENO)) {
|
||||||
|
/*
|
||||||
|
* 交互模式:
|
||||||
|
* - 每次回车读取一行
|
||||||
|
* - 非空行封装为 COMMAND 发送给客户端
|
||||||
|
*/
|
||||||
|
char line[2048];
|
||||||
|
while (atomic_load(&rt.running)) {
|
||||||
|
if (!fgets(line, sizeof(line), stdin)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t len = strlen(line);
|
||||||
|
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||||
|
line[--len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strcmp(line, "quit") == 0) {
|
||||||
|
/* 主动退出交互循环。 */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = send_app_message(ctx, MSG_TYPE_COMMAND, line, (uint32_t)len);
|
||||||
|
if (rc != OMNI_OK) {
|
||||||
|
logger_log("ERROR", "server", "send_command_failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* 非交互模式(如脚本后台):
|
||||||
|
* 只等待接收线程将 transfer_done 置位,避免阻塞在 stdin。
|
||||||
|
*/
|
||||||
|
while (atomic_load(&rt.running) && !atomic_load(&rt.transfer_done)) {
|
||||||
|
usleep(100 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收尾:取消接收线程 -> join -> 关闭网络 -> 关闭文件。 */
|
||||||
|
atomic_store(&rt.running, 0);
|
||||||
|
pthread_cancel(recv_tid);
|
||||||
|
pthread_join(recv_tid, NULL);
|
||||||
|
omni_close(ctx);
|
||||||
|
fclose(out_fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* tcp_impl.c
|
* tcp_impl.c
|
||||||
* TCP 协议实现,带 16 字节包头解决粘包
|
* TCP 协议实现,带 16 字节包头解决粘包
|
||||||
|
*
|
||||||
|
* 设计说明:
|
||||||
|
* 1) TCP 是字节流,天然没有消息边界,因此这里通过“固定 16 字节头 + payload 长度”
|
||||||
|
* 显式划分消息边界,避免粘包/拆包带来的上层读取混乱。
|
||||||
|
* 2) 本层的头只用于“流边界管理”,上层业务仍可在 payload 中定义自己的消息头。
|
||||||
|
* 3) send/recv 均采用阻塞全量读写语义:要么完整收发一帧,要么返回错误/关闭状态。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@@ -21,6 +27,7 @@
|
|||||||
/* Linux 下 TCP_INFO 定义通常已在 <netinet/tcp.h> 提供,避免引入 <linux/tcp.h> 重定义 */
|
/* Linux 下 TCP_INFO 定义通常已在 <netinet/tcp.h> 提供,避免引入 <linux/tcp.h> 重定义 */
|
||||||
|
|
||||||
struct TcpContext {
|
struct TcpContext {
|
||||||
|
/* 已建立连接的 socket fd(服务端 accept 后或客户端 connect 后)。 */
|
||||||
int fd;
|
int fd;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,12 +73,14 @@ static void tcp_log_info(int fd, const char *tag)
|
|||||||
|
|
||||||
static int tcp_set_nodelay(int fd)
|
static int tcp_set_nodelay(int fd)
|
||||||
{
|
{
|
||||||
|
/* 关闭 Nagle,降低小包时延(更利于交互指令场景)。 */
|
||||||
int flag = 1;
|
int flag = 1;
|
||||||
return setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
|
return setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
|
||||||
}
|
}
|
||||||
|
|
||||||
static int tcp_set_reuseaddr(int fd)
|
static int tcp_set_reuseaddr(int fd)
|
||||||
{
|
{
|
||||||
|
/* 允许端口快速复用,减少开发/测试时 TIME_WAIT 影响。 */
|
||||||
int flag = 1;
|
int flag = 1;
|
||||||
return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
|
return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
|
||||||
}
|
}
|
||||||
@@ -80,12 +89,14 @@ static int tcp_bind_and_listen(struct TcpContext *ctx,
|
|||||||
const char *bind_ip,
|
const char *bind_ip,
|
||||||
uint16_t bind_port)
|
uint16_t bind_port)
|
||||||
{
|
{
|
||||||
|
/* 创建监听 socket。 */
|
||||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
logger_log("ERROR", "tcp", "socket_failed errno=%d", errno);
|
logger_log("ERROR", "tcp", "socket_failed errno=%d", errno);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 监听 socket 打开地址复用。 */
|
||||||
tcp_set_reuseaddr(fd);
|
tcp_set_reuseaddr(fd);
|
||||||
|
|
||||||
struct sockaddr_in addr;
|
struct sockaddr_in addr;
|
||||||
@@ -100,6 +111,10 @@ static int tcp_bind_and_listen(struct TcpContext *ctx,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 这里 backlog 取 1,符合当前“单连接演示/测试”场景。
|
||||||
|
* 若后续要支持多客户端,可提升 backlog 并改为事件循环/线程池模型。
|
||||||
|
*/
|
||||||
if (listen(fd, 1) < 0) {
|
if (listen(fd, 1) < 0) {
|
||||||
logger_log("ERROR", "tcp", "listen_failed errno=%d", errno);
|
logger_log("ERROR", "tcp", "listen_failed errno=%d", errno);
|
||||||
close(fd);
|
close(fd);
|
||||||
@@ -108,7 +123,7 @@ static int tcp_bind_and_listen(struct TcpContext *ctx,
|
|||||||
|
|
||||||
logger_log("INFO", "tcp", "listening port=%u", (unsigned)bind_port);
|
logger_log("INFO", "tcp", "listening port=%u", (unsigned)bind_port);
|
||||||
|
|
||||||
/* 简化:阻塞接受一个客户端,之后用于长连接 */
|
/* 简化:阻塞接受一个客户端,连接建立后作为长连接使用。 */
|
||||||
int cfd = accept(fd, NULL, NULL);
|
int cfd = accept(fd, NULL, NULL);
|
||||||
if (cfd < 0) {
|
if (cfd < 0) {
|
||||||
logger_log("ERROR", "tcp", "accept_failed errno=%d", errno);
|
logger_log("ERROR", "tcp", "accept_failed errno=%d", errno);
|
||||||
@@ -116,6 +131,7 @@ static int tcp_bind_and_listen(struct TcpContext *ctx,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 监听 fd 仅用于 accept,一旦接入成功即可关闭监听 fd。 */
|
||||||
close(fd);
|
close(fd);
|
||||||
tcp_set_nodelay(cfd);
|
tcp_set_nodelay(cfd);
|
||||||
|
|
||||||
@@ -127,6 +143,7 @@ static int tcp_connect_peer(struct TcpContext *ctx,
|
|||||||
const char *peer_ip,
|
const char *peer_ip,
|
||||||
uint16_t peer_port)
|
uint16_t peer_port)
|
||||||
{
|
{
|
||||||
|
/* 创建主动连接 socket。 */
|
||||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
logger_log("ERROR", "tcp", "socket_failed errno=%d", errno);
|
logger_log("ERROR", "tcp", "socket_failed errno=%d", errno);
|
||||||
@@ -139,6 +156,7 @@ static int tcp_connect_peer(struct TcpContext *ctx,
|
|||||||
addr.sin_port = htons(peer_port);
|
addr.sin_port = htons(peer_port);
|
||||||
addr.sin_addr.s_addr = inet_addr(peer_ip);
|
addr.sin_addr.s_addr = inet_addr(peer_ip);
|
||||||
|
|
||||||
|
/* 阻塞 connect 到对端。 */
|
||||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
logger_log("ERROR", "tcp", "connect_failed errno=%d", errno);
|
logger_log("ERROR", "tcp", "connect_failed errno=%d", errno);
|
||||||
close(fd);
|
close(fd);
|
||||||
@@ -154,6 +172,12 @@ static int tcp_connect_peer(struct TcpContext *ctx,
|
|||||||
|
|
||||||
static ssize_t tcp_read_n(int fd, void *buf, size_t n)
|
static ssize_t tcp_read_n(int fd, void *buf, size_t n)
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
* 从 TCP 流中“恰好读取 n 字节”:
|
||||||
|
* - 正常返回 n
|
||||||
|
* - 返回 0 表示对端关闭(如果发生在中途,返回已读字节数)
|
||||||
|
* - 返回 -1 表示系统调用错误
|
||||||
|
*/
|
||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
char *p = (char *)buf;
|
char *p = (char *)buf;
|
||||||
while (off < n) {
|
while (off < n) {
|
||||||
@@ -172,6 +196,11 @@ static ssize_t tcp_read_n(int fd, void *buf, size_t n)
|
|||||||
|
|
||||||
static ssize_t tcp_write_n(int fd, const void *buf, size_t n)
|
static ssize_t tcp_write_n(int fd, const void *buf, size_t n)
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
* 向 TCP 流中“恰好写入 n 字节”:
|
||||||
|
* - EINTR 自动重试
|
||||||
|
* - 其余错误返回 -1
|
||||||
|
*/
|
||||||
size_t off = 0;
|
size_t off = 0;
|
||||||
const char *p = (const char *)buf;
|
const char *p = (const char *)buf;
|
||||||
while (off < n) {
|
while (off < n) {
|
||||||
@@ -191,12 +220,14 @@ static OmniContext *tcp_init(OmniRole role,
|
|||||||
const char *peer_ip,
|
const char *peer_ip,
|
||||||
uint16_t peer_port)
|
uint16_t peer_port)
|
||||||
{
|
{
|
||||||
|
/* 协议私有上下文(通过 OmniContext* 向上层做不透明传递)。 */
|
||||||
struct TcpContext *ctx = (struct TcpContext *)calloc(1, sizeof(*ctx));
|
struct TcpContext *ctx = (struct TcpContext *)calloc(1, sizeof(*ctx));
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc;
|
int rc;
|
||||||
|
/* 按角色决定是被动监听还是主动连接。 */
|
||||||
if (role == OMNI_ROLE_SERVER) {
|
if (role == OMNI_ROLE_SERVER) {
|
||||||
rc = tcp_bind_and_listen(ctx, bind_ip, bind_port);
|
rc = tcp_bind_and_listen(ctx, bind_ip, bind_port);
|
||||||
} else {
|
} else {
|
||||||
@@ -221,15 +252,18 @@ static ssize_t tcp_send(OmniContext *c, const void *buf, size_t len)
|
|||||||
struct TcpContext *ctx = (struct TcpContext *)c;
|
struct TcpContext *ctx = (struct TcpContext *)c;
|
||||||
if (!ctx || ctx->fd < 0) return OMNI_ERR_PARAM;
|
if (!ctx || ctx->fd < 0) return OMNI_ERR_PARAM;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 外层 TCP 帧头(16B)仅用于切分消息边界。
|
||||||
|
* 当前 type 统一标记为 MSG_TYPE_RAW,表示“payload 是上层透传内容”。
|
||||||
|
*/
|
||||||
uint64_t t0 = omni_now_ms();
|
uint64_t t0 = omni_now_ms();
|
||||||
MsgHeader hdr;
|
MsgHeader hdr;
|
||||||
hdr.magic = htonl(MSG_MAGIC);
|
omni_msg_header_encode(&hdr, MSG_TYPE_RAW, (uint32_t)len, t0);
|
||||||
hdr.length = htonl((uint32_t)len);
|
|
||||||
hdr.seq = 0; /* 如有需要,上层可扩展维护序列号 */
|
|
||||||
|
|
||||||
uint8_t header_buf[MSG_HEADER_SIZE];
|
uint8_t header_buf[MSG_HEADER_SIZE];
|
||||||
memcpy(header_buf, &hdr, MSG_HEADER_SIZE);
|
memcpy(header_buf, &hdr, MSG_HEADER_SIZE);
|
||||||
|
|
||||||
|
/* 先写固定头,再写 payload,接收侧可据此恢复完整帧。 */
|
||||||
ssize_t n1 = tcp_write_n(ctx->fd, header_buf, MSG_HEADER_SIZE);
|
ssize_t n1 = tcp_write_n(ctx->fd, header_buf, MSG_HEADER_SIZE);
|
||||||
if (n1 != (ssize_t)MSG_HEADER_SIZE) {
|
if (n1 != (ssize_t)MSG_HEADER_SIZE) {
|
||||||
return OMNI_ERR_IO;
|
return OMNI_ERR_IO;
|
||||||
@@ -241,6 +275,7 @@ static ssize_t tcp_send(OmniContext *c, const void *buf, size_t len)
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint64_t t1 = omni_now_ms();
|
uint64_t t1 = omni_now_ms();
|
||||||
|
/* 记录协议层发送耗时,便于后续性能分析。 */
|
||||||
logger_on_proto_send_latency(t1 - t0);
|
logger_on_proto_send_latency(t1 - t0);
|
||||||
logger_log("DEBUG", "tcp", "send payload_bytes=%zu header_bytes=%zu proto_ms=%llu",
|
logger_log("DEBUG", "tcp", "send payload_bytes=%zu header_bytes=%zu proto_ms=%llu",
|
||||||
len, (size_t)MSG_HEADER_SIZE, (unsigned long long)(t1 - t0));
|
len, (size_t)MSG_HEADER_SIZE, (unsigned long long)(t1 - t0));
|
||||||
@@ -255,6 +290,12 @@ static ssize_t tcp_recv(OmniContext *c, void *buf, size_t len)
|
|||||||
struct TcpContext *ctx = (struct TcpContext *)c;
|
struct TcpContext *ctx = (struct TcpContext *)c;
|
||||||
if (!ctx || ctx->fd < 0) return OMNI_ERR_PARAM;
|
if (!ctx || ctx->fd < 0) return OMNI_ERR_PARAM;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 收包流程:
|
||||||
|
* 1) 固定先读 16 字节头
|
||||||
|
* 2) 解析 payload_len
|
||||||
|
* 3) 再读 payload_len 字节
|
||||||
|
*/
|
||||||
uint64_t t0 = omni_now_ms();
|
uint64_t t0 = omni_now_ms();
|
||||||
uint8_t header_buf[MSG_HEADER_SIZE];
|
uint8_t header_buf[MSG_HEADER_SIZE];
|
||||||
ssize_t n1 = tcp_read_n(ctx->fd, header_buf, MSG_HEADER_SIZE);
|
ssize_t n1 = tcp_read_n(ctx->fd, header_buf, MSG_HEADER_SIZE);
|
||||||
@@ -265,14 +306,17 @@ static ssize_t tcp_recv(OmniContext *c, void *buf, size_t len)
|
|||||||
return OMNI_ERR_IO;
|
return OMNI_ERR_IO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 解码网络字节序头字段。 */
|
||||||
MsgHeader hdr;
|
MsgHeader hdr;
|
||||||
|
MsgHeader host_hdr;
|
||||||
memcpy(&hdr, header_buf, MSG_HEADER_SIZE);
|
memcpy(&hdr, header_buf, MSG_HEADER_SIZE);
|
||||||
if (ntohl(hdr.magic) != MSG_MAGIC) {
|
omni_msg_header_decode(&hdr, &host_hdr);
|
||||||
logger_log("ERROR", "tcp", "invalid_magic");
|
|
||||||
return OMNI_ERR_IO;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t payload_len = ntohl(hdr.length);
|
uint32_t payload_len = host_hdr.len;
|
||||||
|
/*
|
||||||
|
* 调用方缓冲区不足时直接报错。
|
||||||
|
* 当前实现不做“读取并丢弃剩余字节”,因此调用方应保证 recv 缓冲足够大。
|
||||||
|
*/
|
||||||
if (payload_len > len) {
|
if (payload_len > len) {
|
||||||
logger_log("ERROR", "tcp", "buffer_too_small payload=%u buf_len=%zu",
|
logger_log("ERROR", "tcp", "buffer_too_small payload=%u buf_len=%zu",
|
||||||
payload_len, len);
|
payload_len, len);
|
||||||
@@ -286,9 +330,14 @@ static ssize_t tcp_recv(OmniContext *c, void *buf, size_t len)
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint64_t t1 = omni_now_ms();
|
uint64_t t1 = omni_now_ms();
|
||||||
|
/* 记录协议层接收耗时。 */
|
||||||
logger_on_proto_recv_latency(t1 - t0);
|
logger_on_proto_recv_latency(t1 - t0);
|
||||||
logger_log("DEBUG", "tcp", "recv payload_bytes=%u header_bytes=%zu proto_ms=%llu",
|
logger_log("DEBUG", "tcp",
|
||||||
payload_len, (size_t)MSG_HEADER_SIZE, (unsigned long long)(t1 - t0));
|
"recv payload_bytes=%u header_bytes=%zu msg_type=%u ts_ms=%llu proto_ms=%llu",
|
||||||
|
payload_len, (size_t)MSG_HEADER_SIZE,
|
||||||
|
(unsigned)host_hdr.type,
|
||||||
|
(unsigned long long)host_hdr.timestamp,
|
||||||
|
(unsigned long long)(t1 - t0));
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
tcp_log_info(ctx->fd, "after_recv");
|
tcp_log_info(ctx->fd, "after_recv");
|
||||||
#endif
|
#endif
|
||||||
@@ -299,6 +348,7 @@ static void tcp_close(OmniContext *c)
|
|||||||
{
|
{
|
||||||
struct TcpContext *ctx = (struct TcpContext *)c;
|
struct TcpContext *ctx = (struct TcpContext *)c;
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
/* 关闭连接并释放私有上下文。 */
|
||||||
if (ctx->fd >= 0) {
|
if (ctx->fd >= 0) {
|
||||||
close(ctx->fd);
|
close(ctx->fd);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user