520 lines
17 KiB
C
520 lines
17 KiB
C
/**
|
||
* @file demo_main.c
|
||
* @brief TCP API 调用演示客户端 (一维/二维通用)
|
||
*
|
||
* 演示使用 QDXnetworkStack API 层的零拷贝发送与参数回调机制。
|
||
* 用户操作流程与原 tcp_c_demo 保持完全一致。
|
||
*/
|
||
|
||
#include "qdx_port.h"
|
||
#include "qdx_preprocess.h"
|
||
#include "qdx_protocol.h"
|
||
#include "qdx_tcp_logic.h"
|
||
|
||
#include <conio.h> /* _kbhit, _getch */
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <windows.h>
|
||
|
||
/* ============================================================
|
||
* 宏定义与全局变量
|
||
* ============================================================ */
|
||
|
||
/* 默认服务端连接参数(硬编码) */
|
||
#define DEFAULT_SERVER_IP "127.0.0.1"
|
||
#define DEFAULT_CONTROL_PORT 5511
|
||
#define DEFAULT_DATA_PORT 5512
|
||
|
||
/* ANSI 控制台颜色宏定义 */
|
||
#define CLR_RESET "\033[0m"
|
||
#define CLR_RED "\033[31m"
|
||
#define CLR_GREEN "\033[32m"
|
||
#define CLR_YELLOW "\033[33m"
|
||
#define CLR_BLUE "\033[34m"
|
||
#define CLR_MAGENTA "\033[35m"
|
||
#define CLR_CYAN "\033[36m"
|
||
#define CLR_DIM "\033[90m"
|
||
|
||
static uint8_t g_dimension_mode = 0; /* 0=1D, 1=2D */
|
||
static char g_matrix_dir[260] = {0};
|
||
|
||
/* 发送缓冲区(静态分配,避免 malloc) */
|
||
#define MAX_2D_PIXELS (256 * 256)
|
||
static uint16_t g_raw_matrix[MAX_2D_PIXELS];
|
||
|
||
/* API 要求的外部传输缓冲区,总大小 256KB,前置留出 1KB 给网络层附加头部 */
|
||
#define TX_BUFFER_TOTAL_CAPACITY (256 * 1024)
|
||
#define TX_BUFFER_HEAD_OFFSET 1024
|
||
static uint8_t g_tx_buffer[TX_BUFFER_TOTAL_CAPACITY];
|
||
static TcpTxBuffer_t g_api_tx_buffer;
|
||
|
||
/* ============================================================
|
||
* 辅助函数:十六进制打印
|
||
* ============================================================ */
|
||
static void print_hex(const uint8_t *data, int len) {
|
||
(void)data;
|
||
(void)len;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 模拟数据发送逻辑 (1D & 2D) 的前置声明
|
||
* ============================================================ */
|
||
static void simulate_send_2d_frame(uint32_t frameNum);
|
||
static void simulate_send_1d_frame(uint32_t frameNum);
|
||
|
||
/* ============================================================
|
||
* QDX API 回调函数实现
|
||
* ============================================================ */
|
||
|
||
/**
|
||
* @brief 上位机下发配置更新时的回调
|
||
*/
|
||
static void on_config_updated(const ConfigCommon_t *common,
|
||
const Config2D_t *cfg2d,
|
||
const Config1D_t *cfg1d) {
|
||
printf(CLR_CYAN "\n[API Callback] 收到最新配置" CLR_RESET "\n");
|
||
|
||
/* 打印通用配置 */
|
||
printf(" -> Common: Pipeline: %.*s, Type: %d, Mode: %d, Tag: %d, "
|
||
"Strictness: %d, Custom: %d\n",
|
||
16, common->PipelineId, common->PipelineType, common->WorkMode,
|
||
common->ConfigTag, common->StrictnessLevel, common->IsCustomMode);
|
||
|
||
/* 打印 2D 配置 */
|
||
printf(" -> 2D: Enabled: %d, Live: %d, DevId: %d, %dx%d, Fps: %d\n",
|
||
cfg2d->Enabled, cfg2d->IsLive, cfg2d->DeviceId, cfg2d->Width,
|
||
cfg2d->Height, cfg2d->Fps);
|
||
printf(" -> 2D: Mask: %d (Thresh: %d, %dx%d), Target: %dx%d\n",
|
||
cfg2d->MaskEnabled, cfg2d->MaskThreshold, cfg2d->MaskWidth,
|
||
cfg2d->MaskHeight, cfg2d->TargetWidth, cfg2d->TargetHeight);
|
||
|
||
/* 打印 1D 配置 */
|
||
printf(" -> 1D: Enabled: %d, RunMode: %d, TriggerType: %d, "
|
||
"BufferSize: %d\n",
|
||
cfg1d->Enabled, cfg1d->RunMode, cfg1d->TriggerType, cfg1d->BufferSize);
|
||
|
||
/* 核心:将配置下发给预处理算法库 */
|
||
Preprocess_Settings_Change(cfg2d, cfg1d, common);
|
||
printf(CLR_GREEN " -> [OK] 已将参数同步至预处理引擎" CLR_RESET "\n");
|
||
}
|
||
|
||
/**
|
||
* @brief 上位机反馈检测结果时的回调
|
||
*/
|
||
static void on_detection_result(uint32_t frameNumber, uint8_t resultStatus) {
|
||
printf(CLR_CYAN "[API Callback] 收到检测结果: Frame #%u, Result: %s" CLR_RESET
|
||
"\n",
|
||
frameNumber, resultStatus == 0 ? "OK" : "NG");
|
||
}
|
||
|
||
/**
|
||
* @brief 当服务端下发 TempFrame(请求回传当前帧)时的回调
|
||
*/
|
||
static void on_temp_frame_request(uint8_t is2dRequest) {
|
||
printf(CLR_CYAN
|
||
"[API Callback] 收到服务端 TempFrame 采集请求 (is2D=%d)" CLR_RESET
|
||
"\n",
|
||
is2dRequest);
|
||
|
||
static uint32_t simulated_frame_num = 1;
|
||
|
||
if (g_dimension_mode == 1) {
|
||
printf(CLR_MAGENTA " -> 模拟: 开始处理并回传 2D 阵列图像..." CLR_RESET
|
||
"\n");
|
||
simulate_send_2d_frame(simulated_frame_num++);
|
||
} else {
|
||
printf(CLR_MAGENTA " -> 模拟: 开始处理并回传 1D 阵列数据..." CLR_RESET
|
||
"\n");
|
||
simulate_send_1d_frame(simulated_frame_num++);
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 模拟数据发送逻辑 (1D & 2D)
|
||
* ============================================================ */
|
||
|
||
/**
|
||
* @brief 随机选取 2D 矩阵文件并解析
|
||
* 逻辑与原 demo 的 parse_temperature_matrix 完全一致
|
||
*/
|
||
static int load_random_2d_matrix(uint16_t *matrix, int max_pixels, int *out_w,
|
||
int *out_h) {
|
||
char pattern[300];
|
||
snprintf(pattern, sizeof(pattern), "%s*.txt", g_matrix_dir);
|
||
|
||
WIN32_FIND_DATAA fd;
|
||
HANDLE hFind = FindFirstFileA(pattern, &fd);
|
||
if (hFind == INVALID_HANDLE_VALUE)
|
||
return -1;
|
||
|
||
int count = 0;
|
||
do {
|
||
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
||
count++;
|
||
} while (FindNextFileA(hFind, &fd));
|
||
FindClose(hFind);
|
||
|
||
if (count == 0)
|
||
return -1;
|
||
|
||
int target = rand() % count;
|
||
hFind = FindFirstFileA(pattern, &fd);
|
||
int idx = 0;
|
||
char filepath[300] = {0};
|
||
|
||
do {
|
||
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
||
if (idx == target) {
|
||
snprintf(filepath, sizeof(filepath), "%s%s", g_matrix_dir,
|
||
fd.cFileName);
|
||
break;
|
||
}
|
||
idx++;
|
||
}
|
||
} while (FindNextFileA(hFind, &fd));
|
||
FindClose(hFind);
|
||
|
||
if (filepath[0] == '\0')
|
||
return -1;
|
||
|
||
static char file_buf[512 * 1024];
|
||
FILE *fp = fopen(filepath, "r");
|
||
if (!fp)
|
||
return -1;
|
||
|
||
int file_len = (int)fread(file_buf, 1, sizeof(file_buf) - 1, fp);
|
||
fclose(fp);
|
||
file_buf[file_len] = '\0';
|
||
|
||
char *pos = strstr(file_buf, "\"temperature\"");
|
||
if (!pos)
|
||
return -1;
|
||
pos = strchr(pos, '[');
|
||
if (!pos)
|
||
return -1;
|
||
pos++;
|
||
|
||
int width = 0, height = 0, total = 0, row_count = 0;
|
||
|
||
while (*pos != '\0') {
|
||
if (*pos == '[') {
|
||
row_count = 0;
|
||
pos++;
|
||
} else if (*pos == ']') {
|
||
if (row_count > 0) {
|
||
height++;
|
||
if (width == 0)
|
||
width = row_count;
|
||
row_count = 0;
|
||
} else {
|
||
break;
|
||
}
|
||
pos++;
|
||
} else if (*pos >= '0' && *pos <= '9') {
|
||
int val = 0;
|
||
while (*pos >= '0' && *pos <= '9') {
|
||
val = val * 10 + (*pos - '0');
|
||
pos++;
|
||
}
|
||
if (*pos == '.') {
|
||
pos++;
|
||
while (*pos >= '0' && *pos <= '9')
|
||
pos++;
|
||
}
|
||
if (total < max_pixels) {
|
||
matrix[total] = (uint16_t)val;
|
||
}
|
||
total++;
|
||
row_count++;
|
||
} else {
|
||
pos++;
|
||
}
|
||
}
|
||
|
||
*out_w = width;
|
||
*out_h = height;
|
||
return total > 0 ? total : -1;
|
||
}
|
||
|
||
/**
|
||
* @brief 模拟发送 2D 图像帧 (完整 API Zero-Copy 流水线)
|
||
*/
|
||
static void simulate_send_2d_frame(uint32_t frameNum) {
|
||
int w = 0, h = 0;
|
||
int total = load_random_2d_matrix(g_raw_matrix, MAX_2D_PIXELS, &w, &h);
|
||
if (total <= 0) {
|
||
printf(CLR_RED "[Error] 读取 2D 矩阵失败" CLR_RESET "\n");
|
||
return;
|
||
}
|
||
|
||
/* 1. 构造原始图像结构体 */
|
||
RawImageBuffer_t rawBuff = {.pData = g_raw_matrix,
|
||
.Width = (uint16_t)w,
|
||
.Height = (uint16_t)h,
|
||
.FrameNumber = frameNum};
|
||
|
||
/* 2. 重置发送缓冲包装器(确保偏移正确) */
|
||
g_api_tx_buffer.pBuffer = g_tx_buffer;
|
||
g_api_tx_buffer.TotalCapacity = TX_BUFFER_TOTAL_CAPACITY;
|
||
g_api_tx_buffer.HeadOffset = TX_BUFFER_HEAD_OFFSET;
|
||
g_api_tx_buffer.ValidPayloadLen = 0;
|
||
|
||
PreprocessResult_t resMeta = {0};
|
||
|
||
/* 3. 核心 API: 预处理执行。它会根据滑动窗口目标大小提取 ROI,
|
||
* 并写入我们提供的 g_tx_buffer[HeadOffset] 处。 */
|
||
if (Preprocess_Execute(&rawBuff, &g_api_tx_buffer, &resMeta) == 0) {
|
||
printf(CLR_MAGENTA
|
||
"[Data] 预处理完成: 提取出 %dx%d ROI, %.1f~%.1f°C" CLR_RESET "\n",
|
||
resMeta.ValidWidth, resMeta.ValidHeight, resMeta.MinTemp / 10.0f,
|
||
resMeta.MaxTemp / 10.0f);
|
||
|
||
/* 4. 核心 API: TCP 零拷贝图文打包与发送。
|
||
* 内部会在 HeadOffset 前面的 1KB 预留空间中拼装 18 字节包头,直接压入
|
||
* LwIP(WinSock) */
|
||
int8_t err = TcpLogic_BuildAndSendTemperatureFrame(
|
||
&g_api_tx_buffer, &resMeta, 0x01 /* TRIGGER */, 1 /* IS_2D */);
|
||
|
||
if (err == 0) {
|
||
printf(CLR_GREEN " -> [OK] TCP 帧已加入发送队列" CLR_RESET "\n");
|
||
} else {
|
||
printf(CLR_RED " -> [Fail] TCP 帧发送失败, 错误码: %d" CLR_RESET
|
||
"\n",
|
||
err);
|
||
}
|
||
} else {
|
||
printf(CLR_RED
|
||
"[Data] 预处理拒绝了本帧数据 (未达到阈值或参数异常)" CLR_RESET "\n");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 模拟发送 1D 温度帧
|
||
* 1D 模式下不需要滑动窗口图像处理,我们可以跳过 Preprocess_Execute,
|
||
* 直接组装 `PreprocessResult_t` 结构后提供给发送库。
|
||
*/
|
||
static void simulate_send_1d_frame(uint32_t frameNum) {
|
||
#define POINTS_COUNT 30
|
||
|
||
int16_t min_t = 32767;
|
||
int16_t max_t = -32768;
|
||
int32_t sum_t = 0;
|
||
|
||
g_api_tx_buffer.pBuffer = g_tx_buffer;
|
||
g_api_tx_buffer.TotalCapacity = TX_BUFFER_TOTAL_CAPACITY;
|
||
g_api_tx_buffer.HeadOffset = TX_BUFFER_HEAD_OFFSET;
|
||
|
||
uint8_t *dest = g_api_tx_buffer.pBuffer + g_api_tx_buffer.HeadOffset;
|
||
|
||
/* 模拟生成温度点 */
|
||
for (int i = 0; i < POINTS_COUNT; i++) {
|
||
uint16_t time_offset = (uint16_t)(i * 600 / (POINTS_COUNT - 1));
|
||
uint16_t temp = (uint16_t)(500 + rand() % 501); /* 50.0~100.0 */
|
||
|
||
int16_t t = (int16_t)temp;
|
||
if (t < min_t)
|
||
min_t = t;
|
||
if (t > max_t)
|
||
max_t = t;
|
||
sum_t += t;
|
||
|
||
/* 序列化到 buffer (按协议要求 TempPoint1D_t: 2字节时间偏移 +
|
||
* 2字节温度,小端存储) */
|
||
dest[0] = (uint8_t)(time_offset & 0xFF);
|
||
dest[1] = (uint8_t)((time_offset >> 8) & 0xFF);
|
||
dest[2] = (uint8_t)(temp & 0xFF);
|
||
dest[3] = (uint8_t)((temp >> 8) & 0xFF);
|
||
|
||
dest += 4;
|
||
}
|
||
|
||
g_api_tx_buffer.ValidPayloadLen = POINTS_COUNT * 4;
|
||
|
||
/* 构造预处理元数据欺骗网络库 */
|
||
PreprocessResult_t resMeta = {.pValidData = g_api_tx_buffer.pBuffer +
|
||
g_api_tx_buffer.HeadOffset,
|
||
.DataLength = g_api_tx_buffer.ValidPayloadLen,
|
||
.ValidWidth = POINTS_COUNT,
|
||
.ValidHeight = 1,
|
||
.MinTemp = min_t,
|
||
.MaxTemp = max_t,
|
||
.AvgTemp = (int16_t)(sum_t / POINTS_COUNT),
|
||
.RoiTemp = (int16_t)(sum_t / POINTS_COUNT),
|
||
.Status = 0,
|
||
.FrameNumber = frameNum};
|
||
|
||
printf(CLR_MAGENTA "[Data] 构造 1D 数据完成: %d 点, %.1f~%.1f°C" CLR_RESET
|
||
"\n",
|
||
POINTS_COUNT, min_t / 10.0f, max_t / 10.0f);
|
||
|
||
int8_t err = TcpLogic_BuildAndSendTemperatureFrame(
|
||
&g_api_tx_buffer, &resMeta, 0x01 /* TRIGGER */, 0 /* IS_1D */);
|
||
|
||
if (err == 0) {
|
||
printf(CLR_GREEN " -> [OK] 1D 帧已加入发送队列" CLR_RESET "\n");
|
||
} else {
|
||
printf(CLR_RED " -> [Fail] 网络发送失败, 错误码: %d" CLR_RESET "\n",
|
||
err);
|
||
}
|
||
|
||
#undef POINTS_COUNT
|
||
}
|
||
|
||
/* ============================================================
|
||
* 主函数 (控制台 UI 交互层)
|
||
* ============================================================ */
|
||
|
||
int main(void) {
|
||
/* 设置控制台支持 UTF-8 和 ANSI 转义 */
|
||
SetConsoleOutputCP(65001);
|
||
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
|
||
#endif
|
||
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||
DWORD dwMode = 0;
|
||
GetConsoleMode(hOut, &dwMode);
|
||
SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||
|
||
printf("========================================================\n");
|
||
printf("= TCP 透传客户端 API 演示版 (基于 QDXnetworkStack.c)\n");
|
||
printf("= 服务端: %s 控制: %d 数据: %d\n", DEFAULT_SERVER_IP,
|
||
DEFAULT_CONTROL_PORT, DEFAULT_DATA_PORT);
|
||
printf("========================================================\n\n");
|
||
|
||
/* Windows 下需要手动启动 Winsock */
|
||
WSADATA wsaData;
|
||
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
||
printf("[ERROR] WSAStartup 失败。\n");
|
||
return 1;
|
||
}
|
||
|
||
/* 交互式输入设备 ID */
|
||
int input_id = 0;
|
||
printf("请输入设备 ID (整数, 例如 101): ");
|
||
if (scanf("%d", &input_id) != 1 || input_id < 0 || input_id > 65535) {
|
||
printf("[ERROR] 无效。默认设为 101。\n");
|
||
input_id = 101;
|
||
}
|
||
printf("\n设备 ID 设置为: %d\n\n", input_id);
|
||
|
||
/* 清除遗留的回车换行 */
|
||
int c;
|
||
while ((c = getchar()) != '\n' && c != EOF) {
|
||
}
|
||
|
||
/* 选择维度 */
|
||
int dim_choice = 0;
|
||
printf("请选择维度模式:\n");
|
||
printf(" 1 - 一维模式 (随机温度数据)\n");
|
||
printf(" 2 - 二维模式 (从目录随机读取矩阵文件)\n");
|
||
printf("请输入 (1 或 2): ");
|
||
if (scanf("%d", &dim_choice) != 1 || (dim_choice != 1 && dim_choice != 2)) {
|
||
dim_choice = 1;
|
||
}
|
||
g_dimension_mode = (dim_choice == 2) ? 1 : 0;
|
||
printf("\n维度模式: %s\n\n", g_dimension_mode ? "二维 (2D)" : "一维 (1D)");
|
||
|
||
if (g_dimension_mode == 1) {
|
||
snprintf(g_matrix_dir, sizeof(g_matrix_dir), "../tcp_c_demo/src/2d_mask/");
|
||
printf("2D 矩阵文件目录: %s\n\n", g_matrix_dir);
|
||
}
|
||
|
||
/* 生成假 UUID */
|
||
uint8_t uuid[16] = {0};
|
||
srand((unsigned int)GetTickCount());
|
||
for (int i = 0; i < 16; i++) {
|
||
uuid[i] = (uint8_t)(rand() % 256);
|
||
}
|
||
|
||
/* ---------------------------------------------------------
|
||
* 重点:纯 API 调用区
|
||
* --------------------------------------------------------- */
|
||
printf(">>> 正在初始化 QDXnetworkStack 库架构...\n");
|
||
|
||
/* 1. 初始化预处理引擎 (分配 256x256 静态滑窗资源) */
|
||
Preprocess_Init(256, 256);
|
||
|
||
/* 2. 初始化网络栈 */
|
||
TcpLogic_Init(uuid, NULL);
|
||
|
||
/* 3. 注册业务回调 (配置更新、坏品通知、采集请求) */
|
||
TcpLogic_RegisterConfigCallback(on_config_updated);
|
||
TcpLogic_RegisterDetectionCallback(on_detection_result);
|
||
TcpLogic_RegisterTempFrameRequestCallback(on_temp_frame_request);
|
||
|
||
/* 4. [Hack Demo] 因为 API 库不包含直接设置当前 DevID
|
||
* 的接口(依赖服务端下发), 但为了 Demo 一开始能用指定的 101 ID
|
||
* 发起连接,我们手动造一个回调注入进去, 实际上在真实的 CH32 工程中,ID
|
||
* 会保存在 Flash 里面。这演示了库的工作原理。
|
||
*/
|
||
// 由于 g_TcpLogic 结构体对于外部是静态隐藏的,
|
||
// API 设计中如果要改变 DevID,规范应是等待协议 0x05 下发。
|
||
// 此处模拟真实情况:我们相信服务端会自动给我分发 ID,
|
||
// 所以不再强求立刻修改。库有默认 ID=101。
|
||
|
||
/* 5. 启动后台处理收发断连。
|
||
* 它内部通过 _beginthreadex 开了线程!
|
||
*/
|
||
TcpLogic_Start();
|
||
printf(">>> 库引擎以启动。后台线程已接管所有 Socket 收发。\n\n");
|
||
|
||
/* =========================================================
|
||
* 主业务测试循环
|
||
* ========================================================= */
|
||
printf("菜单 (上位机请求时会自动发送):\n");
|
||
printf(" [s] 立即触发一次发送 [c] 打印当前内存参数册\n");
|
||
printf(" [q] 退出 (Ctrl+C 也可)\n\n");
|
||
|
||
while (1) {
|
||
if (_kbhit()) {
|
||
int ch = _getch();
|
||
if (ch == 'q' || ch == 'Q') {
|
||
break;
|
||
} else if (ch == 's' || ch == 'S') {
|
||
printf("\n[Manual] 用户主动触发一帧。\n");
|
||
on_temp_frame_request(g_dimension_mode);
|
||
} else if (ch == 'c' || ch == 'C') {
|
||
ConfigCommon_t com;
|
||
Config2D_t c2d;
|
||
Config1D_t c1d;
|
||
if (TcpLogic_GetLatestConfig(&com, &c2d, &c1d) == 0) {
|
||
printf(CLR_GREEN
|
||
"\n[Info] 当前静态配置缓存中包含数据 (严苛度: %d)。" CLR_RESET
|
||
"\n",
|
||
com.StrictnessLevel);
|
||
/* 打印通用配置 */
|
||
printf(" -> Common: Pipeline: %.*s, Type: %d, Mode: %d, Tag: %d, "
|
||
"Strictness: %d, Custom: %d\n",
|
||
16, com.PipelineId, com.PipelineType, com.WorkMode,
|
||
com.ConfigTag, com.StrictnessLevel, com.IsCustomMode);
|
||
|
||
/* 打印 2D 配置 */
|
||
printf(
|
||
" -> 2D: Enabled: %d, Live: %d, DevId: %d, %dx%d, Fps: %d\n",
|
||
c2d.Enabled, c2d.IsLive, c2d.DeviceId, c2d.Width, c2d.Height,
|
||
c2d.Fps);
|
||
printf(" -> 2D: Mask: %d (Thresh: %d, %dx%d), Target: %dx%d\n",
|
||
c2d.MaskEnabled, c2d.MaskThreshold, c2d.MaskWidth,
|
||
c2d.MaskHeight, c2d.TargetWidth, c2d.TargetHeight);
|
||
|
||
/* 打印 1D 配置 */
|
||
printf(" -> 1D: Enabled: %d, RunMode: %d, TriggerType: %d, "
|
||
"BufferSize: %d\n",
|
||
c1d.Enabled, c1d.RunMode, c1d.TriggerType, c1d.BufferSize);
|
||
} else {
|
||
printf(CLR_YELLOW
|
||
"\n[Info] 当前静态配置缓存尚未收到上位机同步!" CLR_RESET
|
||
"\n");
|
||
}
|
||
}
|
||
}
|
||
Sleep(50);
|
||
}
|
||
|
||
printf("程序退出中...\n");
|
||
WSACleanup();
|
||
return 0;
|
||
}
|