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:
nnbcccscdscdsc
2026-03-13 22:39:41 +08:00
parent 4d475f8c92
commit 7ecd8a4ef4
9 changed files with 34061 additions and 152 deletions

86
Makefile Normal file
View 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 ?=
# 链接 pthreadapps 中有多线程)。
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
View File

@@ -1,21 +1,192 @@
# OmniSocket
统一的 TCP / UDP / KCP 传输框架,包含:
- 协议抽象层(`omni_init / omni_send / omni_recv`
- 客户端:文件分片发送 + 异步接收服务端 ASCII 指令
- 服务端:接收并写文件 + 交互输入指令下发客户端
- 转发器A->B 中转,支持运行时动态修改目标端口
## 目录结构
```text
OmniSocket/
├── include/
│ ├── common.h # 全局定义:MsgHeader 结构体、错误码、宏定义
│ ├── network.h # 定义统一协议接口 (omni_init, omni_send等)
│ ├── kcp/ # 存放外部 KCP 源码 (ikcp.h, ikcp.c)
│ └── logger.h # 日志统计函数声明
│ ├── common.h # MsgHeader(type,len,timestamp)、消息类型、通用宏
│ ├── network.h # 统一协议接口定义
│ ├── kcp/ikcp.h # KCP 头文件
│ └── logger.h # 日志统计接口
├── src/
│ ├── protocols/
│ │ ├── tcp_impl.c # TCP 专用实现
│ │ ├── udp_impl.c # UDP 专用实现
│ │ ── kcp_impl.c # KCP 专用实现(调用 ikcp.c
│ │ ├── tcp_impl.c # TCP 实现16字节头 + 粘包拆包)
│ │ ├── udp_impl.c # UDP 实现sendto/recvfrom
│ │ ── kcp_impl.c # KCP 实现(基于 UDP + ikcp
│ │ └── ikcp.c # KCP 源码
│ ├── core/
│ │ ├── network.c # 协议分发逻辑(根据参数选 TCP/UDP/KCP
│ │ └── logger.c # 延迟计算、吞吐量统计逻辑实现
── apps/
├── client_main.c # 客户端入口(文件读取、指令接收)
├── server_main.c # 服务端入口(指令输入、数据接收)
── relay_main.c # 转发器入口(中转逻辑)
│ │ ├── network.c # 协议工厂分发
│ │ └── logger.c # 性能统计日志
── apps/
├── client_main.c # 客户端入口
├── server_main.c # 服务端入口
── relay_main.c # 转发器入口
│ └── test_main.c # 简易协议连通性测试
├── scripts/
│ └── local_smoke_test.sh # 本机一键 smoke 测试
├── build/ # 编译产物目录
├── Makefile # 关键:支持 make server 和 make client_arm
└── README.md # 运行指南与参数说明
├── Makefile
└── 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
```
终端 2relay
```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
```

View File

@@ -1,23 +1,33 @@
/*
* common.h
* 全局公共定义:消息头、错误码、通用宏
. * 全局公共定义:消息头、错误码、通用宏
*/
#ifndef OMNISOCKET_COMMON_H
#define OMNISOCKET_COMMON_H
#include <arpa/inet.h>
#include <stdint.h>
#include <time.h>
/* 统一的 16 字节消息头(解决 TCP 粘包用 */
/* 统一的 16 字节消息头(应用层消息头 */
typedef struct MsgHeader {
uint32_t magic; /* 固定魔数,用于快速校验 */
uint32_t length; /* 后续负载长度(字节数) */
uint64_t seq; /* 序列号或会话内消息 ID */
uint32_t type; /* 消息类型:文件块/控制指令等 */
uint32_t len; /* 后续负载长度(字节数) */
uint64_t timestamp; /* 发送时间戳(毫秒) */
} MsgHeader;
#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 {
@@ -35,6 +45,50 @@ static inline uint64_t omni_now_ms(void)
clock_gettime(CLOCK_MONOTONIC, &ts);
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 */

32496
omni_logs.jsonl Normal file

File diff suppressed because it is too large Load Diff

122
scripts/local_smoke_test.sh Executable file
View 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"
# sink1relay 初始目标(预期可能不再接收最终数据)。
"$BUILD_DIR/omni_server" -p udp -P "$SINK1_PORT" -o "$RELAY1_OUT" >"$TMP_DIR/relay_sink1.log" 2>&1 &
SINK1_PID=$!
PIDS+=("$SINK1_PID")
# sink2relay 切换后的目标(最终校验对象)。
"$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"
# 启动 relayUDP 监听 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
View 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+CSIGINT或连接异常
*/
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
View 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
View 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;
}

View File

@@ -1,6 +1,12 @@
/*
* tcp_impl.c
* TCP 协议实现,带 16 字节包头解决粘包
*
* 设计说明:
* 1) TCP 是字节流,天然没有消息边界,因此这里通过“固定 16 字节头 + payload 长度”
* 显式划分消息边界,避免粘包/拆包带来的上层读取混乱。
* 2) 本层的头只用于“流边界管理”,上层业务仍可在 payload 中定义自己的消息头。
* 3) send/recv 均采用阻塞全量读写语义:要么完整收发一帧,要么返回错误/关闭状态。
*/
#include "common.h"
@@ -21,6 +27,7 @@
/* Linux 下 TCP_INFO 定义通常已在 <netinet/tcp.h> 提供,避免引入 <linux/tcp.h> 重定义 */
struct TcpContext {
/* 已建立连接的 socket fd服务端 accept 后或客户端 connect 后)。 */
int fd;
};
@@ -66,12 +73,14 @@ static void tcp_log_info(int fd, const char *tag)
static int tcp_set_nodelay(int fd)
{
/* 关闭 Nagle降低小包时延更利于交互指令场景。 */
int flag = 1;
return setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
}
static int tcp_set_reuseaddr(int fd)
{
/* 允许端口快速复用,减少开发/测试时 TIME_WAIT 影响。 */
int flag = 1;
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,
uint16_t bind_port)
{
/* 创建监听 socket。 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
logger_log("ERROR", "tcp", "socket_failed errno=%d", errno);
return -1;
}
/* 监听 socket 打开地址复用。 */
tcp_set_reuseaddr(fd);
struct sockaddr_in addr;
@@ -100,6 +111,10 @@ static int tcp_bind_and_listen(struct TcpContext *ctx,
return -1;
}
/*
* 这里 backlog 取 1符合当前“单连接演示/测试”场景。
* 若后续要支持多客户端,可提升 backlog 并改为事件循环/线程池模型。
*/
if (listen(fd, 1) < 0) {
logger_log("ERROR", "tcp", "listen_failed errno=%d", errno);
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);
/* 简化:阻塞接受一个客户端,之后用于长连接 */
/* 简化:阻塞接受一个客户端,连接建立后作为长连接使用。 */
int cfd = accept(fd, NULL, NULL);
if (cfd < 0) {
logger_log("ERROR", "tcp", "accept_failed errno=%d", errno);
@@ -116,6 +131,7 @@ static int tcp_bind_and_listen(struct TcpContext *ctx,
return -1;
}
/* 监听 fd 仅用于 accept一旦接入成功即可关闭监听 fd。 */
close(fd);
tcp_set_nodelay(cfd);
@@ -127,6 +143,7 @@ static int tcp_connect_peer(struct TcpContext *ctx,
const char *peer_ip,
uint16_t peer_port)
{
/* 创建主动连接 socket。 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
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_addr.s_addr = inet_addr(peer_ip);
/* 阻塞 connect 到对端。 */
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
logger_log("ERROR", "tcp", "connect_failed errno=%d", errno);
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)
{
/*
* 从 TCP 流中“恰好读取 n 字节”:
* - 正常返回 n
* - 返回 0 表示对端关闭(如果发生在中途,返回已读字节数)
* - 返回 -1 表示系统调用错误
*/
size_t off = 0;
char *p = (char *)buf;
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)
{
/*
* 向 TCP 流中“恰好写入 n 字节”:
* - EINTR 自动重试
* - 其余错误返回 -1
*/
size_t off = 0;
const char *p = (const char *)buf;
while (off < n) {
@@ -191,12 +220,14 @@ static OmniContext *tcp_init(OmniRole role,
const char *peer_ip,
uint16_t peer_port)
{
/* 协议私有上下文(通过 OmniContext* 向上层做不透明传递)。 */
struct TcpContext *ctx = (struct TcpContext *)calloc(1, sizeof(*ctx));
if (!ctx) {
return NULL;
}
int rc;
/* 按角色决定是被动监听还是主动连接。 */
if (role == OMNI_ROLE_SERVER) {
rc = tcp_bind_and_listen(ctx, bind_ip, bind_port);
} else {
@@ -221,15 +252,18 @@ static ssize_t tcp_send(OmniContext *c, const void *buf, size_t len)
struct TcpContext *ctx = (struct TcpContext *)c;
if (!ctx || ctx->fd < 0) return OMNI_ERR_PARAM;
/*
* 外层 TCP 帧头16B仅用于切分消息边界。
* 当前 type 统一标记为 MSG_TYPE_RAW表示“payload 是上层透传内容”。
*/
uint64_t t0 = omni_now_ms();
MsgHeader hdr;
hdr.magic = htonl(MSG_MAGIC);
hdr.length = htonl((uint32_t)len);
hdr.seq = 0; /* 如有需要,上层可扩展维护序列号 */
omni_msg_header_encode(&hdr, MSG_TYPE_RAW, (uint32_t)len, t0);
uint8_t header_buf[MSG_HEADER_SIZE];
memcpy(header_buf, &hdr, MSG_HEADER_SIZE);
/* 先写固定头,再写 payload接收侧可据此恢复完整帧。 */
ssize_t n1 = tcp_write_n(ctx->fd, header_buf, MSG_HEADER_SIZE);
if (n1 != (ssize_t)MSG_HEADER_SIZE) {
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();
/* 记录协议层发送耗时,便于后续性能分析。 */
logger_on_proto_send_latency(t1 - t0);
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));
@@ -255,6 +290,12 @@ static ssize_t tcp_recv(OmniContext *c, void *buf, size_t len)
struct TcpContext *ctx = (struct TcpContext *)c;
if (!ctx || ctx->fd < 0) return OMNI_ERR_PARAM;
/*
* 收包流程:
* 1) 固定先读 16 字节头
* 2) 解析 payload_len
* 3) 再读 payload_len 字节
*/
uint64_t t0 = omni_now_ms();
uint8_t 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;
}
/* 解码网络字节序头字段。 */
MsgHeader hdr;
MsgHeader host_hdr;
memcpy(&hdr, header_buf, MSG_HEADER_SIZE);
if (ntohl(hdr.magic) != MSG_MAGIC) {
logger_log("ERROR", "tcp", "invalid_magic");
return OMNI_ERR_IO;
}
omni_msg_header_decode(&hdr, &host_hdr);
uint32_t payload_len = ntohl(hdr.length);
uint32_t payload_len = host_hdr.len;
/*
* 调用方缓冲区不足时直接报错。
* 当前实现不做“读取并丢弃剩余字节”,因此调用方应保证 recv 缓冲足够大。
*/
if (payload_len > len) {
logger_log("ERROR", "tcp", "buffer_too_small payload=%u buf_len=%zu",
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();
/* 记录协议层接收耗时。 */
logger_on_proto_recv_latency(t1 - t0);
logger_log("DEBUG", "tcp", "recv payload_bytes=%u header_bytes=%zu proto_ms=%llu",
payload_len, (size_t)MSG_HEADER_SIZE, (unsigned long long)(t1 - t0));
logger_log("DEBUG", "tcp",
"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__
tcp_log_info(ctx->fd, "after_recv");
#endif
@@ -299,6 +348,7 @@ static void tcp_close(OmniContext *c)
{
struct TcpContext *ctx = (struct TcpContext *)c;
if (!ctx) return;
/* 关闭连接并释放私有上下文。 */
if (ctx->fd >= 0) {
close(ctx->fd);
}