











效果图

工程目录

直接上完整代码
MyCServer.h
#include <stdio.h> //标准输入输出 终端/文件 I/O 基础 #include <stdlib.h> //通用工具与内存管理 进程控制、随机数、字符串转换 #include <string.h> //C 字符串与内存操作 不处理动态分配,仅操作已分配内存 #include <ctype.h> //字符属性判断与转换 安全处理 ASCII/扩展字符 #include <locale.h> //本地化/国际化设置 影响数字格式、字符排序、ctype 行为 #include <stdarg.h> //可变参数函数支持 实现类似 printf 的自定义函数 #include <curses.h> //终端文本 UI(TUI)Linux 通常用 ncurses,Windows 用 PDCurses #include <time.h> // 必须包含时间头文件 #include <sys/time.h> // 提供 gettimeofday() #include <unistd.h> // 提供 usleep() /*====================== 显示常量 ======================*/ #define SKIP_LINE 3 // 显示起始行 #define LOOP_TIME 1000 // 定时器周期 (ms) 1000毫秒 /*====================== 全局变量声明 ======================*/ extern int ymax, xmax; // 屏幕尺寸 extern int ypos, xpos; // 光标位置 extern int bSetTime; // 定时器标志 extern pthread_t Th; // 定时器线程 /*====================== 函数声明 ======================*/ int InitSys(void); void ExitSys(void); void InitWin(void); void UpInitWin(void); void OnTimer(void); void SetTimer(pthread_t *pTh); void KillTimer(pthread_t Th); void *timer_sr(void *timer_func);
MyCServer.c
#include "MyCServer.h" // 主头文件(包含 TRUNK_CHANNEL 等结构体定义和系统头文件) #include "Logger.h" // 日志模块 /*====================== 全局变量 ======================*/ // 此处为全局变量实际分配内存 int ymax, xmax; // 屏幕尺寸 int ypos, xpos; // 光标位置 int bSetTime = 0; // 定时器标志 pthread_t Th = 0; // 定时器线程(初始化为 0,表示未创建) //底层使用全局缓冲区,多线程同时写屏会导致乱码。互斥锁是轻量级且必要的保护。 pthread_mutex_t g_scr_mutex = PTHREAD_MUTEX_INITIALIZER; // 保护 ncurses 屏幕刷新 int main(void){ printf("Hello World\n");//// 此时只在内存中绘制 // 初始化日志 Log_Init(); Log_Write("======================================="); Log_Write("======== MyCServer v1.88 启动 =========="); Log_Write("======================================="); if (InitSys() != 0) { Log_Write("[MyCServer][错误] 系统初始化失败"); getch(); clear(); refresh(); endwin(); Log_Close(); return -1; } Log_Write("[MyCServer][信息] 系统初始化成功"); // 主循环:等待用户按键退出 int nTmp = getch(); while (tolower(nTmp) != 'q') { nTmp = getch(); } Log_Write("[MyCServer][信息] 用户退出,关闭系统"); ExitSys(); return 0; } /*====================== 系统初始化 ======================*/ int InitSys(void) { int nEnableTimer = 1; // Timer 开关:1=启动,其他值=不启动 // 1. 初始化界面 InitWin(); // 2. 根据开关决定是否启动定时器 if (nEnableTimer == 1) { SetTimer(&Th); } //已经读配置文件等一系统初始化工作 Log_Write("系统初始化完成==="); refresh(); //将内存中绘制好的“虚拟屏幕”一次性输出到物理终端上。 return 0; } /*====================== 系统退出 ======================*/ void ExitSys(void) { Log_Write("正在关闭系统..."); clear();//清空当前窗口(通常是整个终端屏幕),并将光标移动到左上角 (0,0) //它不会立刻清屏,而是将窗口标记为“需要完全重绘”。必须调用 refresh() 后, //清屏效果才会真正显示到终端。 refresh(); // 停止定时器(如果已启动) if (bSetTime == 1) { KillTimer(Th); Log_Write("[MyCServer][信息] Timer定时器已停止"); } endwin();//标准退出清理函数。它的核心作用是安全关闭 ncurses 模式,并将终端完整恢复到程序运行前的原始状态。 //与initscr()成对出现 } /*====================== 窗口初始化 ======================*/ void InitWin(void) { // setlocale 必须在 initscr() 之前调用 if (setlocale(LC_ALL, "") == NULL) { setlocale(LC_ALL, "zh_CN.UTF-8"); } //// 初始化 ncurses if (initscr() == NULL) { perror("Initscr"); return; } // 强制下次刷新时清除整个屏幕 clearok(stdscr, TRUE); cbreak(); // 禁用行缓冲 noecho(); // 不显示输入字符 curs_set(0); // 隐藏光标 UpInitWin(); } void UpInitWin(void) { getmaxyx(stdscr, ymax, xmax); // 清除屏幕并重新绘制边框 clear(); box(stdscr, ACS_VLINE, ACS_HLINE); // 绘制标题 mvhline(1, 1, ' ', xmax - 2); mvprintw(1, 30, "MyCServer v1.88 ("); addch('q' | A_REVERSE); printw("-退出)"); getyx(stdscr, ypos, xpos); // 绘制表头 mvhline(2, 1, ' ', xmax - 2); mvprintw(2, 3, "CH 状态 来电号码 通道信息"); // 清除通道显示区域 for (int i = SKIP_LINE; i < ymax - 2; i++) { mvhline(i, 1, ' ', xmax - 2); } refresh(); move(ypos, xpos); refresh(); } /* * 模块名称: 软件定时器管理模块 * 功能描述: 基于 POSIX 线程实现一个周期性执行的软件定时器。 * 通过创建独立线程循环执行回调函数,并利用系统时间测量与补偿, * 尽量保证回调函数以固定的周期 (LOOP_TIME) 触发。 * 依赖宏: LOOP_TIME (定时器目标周期,单位:毫秒) * 依赖类型: LP_V_V (应为回调函数指针类型,如 typedef void (*LP_V_V)(void);) * 依赖变量: bSetTime (全局运行状态标志,控制线程循环与退出) */ /*====================== 定时器回调 ======================*/ /** * @brief 定时器周期触发的业务回调函数 * @note 1. 此函数的执行耗时必须远小于 LOOP_TIME,否则会导致周期累积延迟甚至“跑飞”。 * 2. 避免在此函数中执行阻塞型操作(如 sleep、网络请求、大内存分配等)。 * 3. LOOP_TIME 1000 timer_sr 依赖此宏计算休眠时间。1000ms 保证回调每秒触发一次 */ void OnTimer(void) { //move(ypos, xpos); // 示例:更新坐标/位置信息 //refresh(); // 示例:刷新显示/渲染界面 time_t now = time(NULL); struct tm tm_info; localtime_r(&now, &tm_info); // 线程安全的时间转换 char time_str[32]; strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &tm_info); // 🔒 锁定屏幕操作区,防止与主线程 getch()/refresh() 并发冲突 pthread_mutex_lock(&g_scr_mutex); // 动态计算打印列坐标(第一行右侧),避免覆盖标题 int col_pos = (xmax > 24) ? xmax - 24 : 1; // 末尾补两个空格,用于覆盖旧时间字符串可能残留的字符 mvprintw(1, col_pos, "[%s] ", time_str); refresh(); // 立即输出到物理终端 pthread_mutex_unlock(&g_scr_mutex); } /*====================== 定时器管理 ======================*/ /** * @brief 创建并启动定时器线程 * @param pTh [out] 传出参数,用于保存新创建的线程 ID * @note 需确保 pTh 指向有效内存。多次调用此函数会创建多个定时器线程, * 建议在外部做好状态控制,避免重复创建。 */ void SetTimer(pthread_t *pTh) { bSetTime = 1; // 置位运行标志,使 timer_sr 中的 while 循环得以执行 // 创建线程:默认属性(可连接),线程入口为 timer_sr,将 OnTimer 作为参数传入 pthread_create(pTh, NULL, timer_sr, (void *)OnTimer); } /** * @brief 安全停止并销毁定时器线程 * @param Th [in] 待销毁的线程 ID * @note 采用“标志位退出 + 强制取消 + 资源回收”的组合策略,确保线程安全终止。 */ void KillTimer(pthread_t Th) { if (Th == 0) return; // 线程未创建或 ID 无效,直接返回 if (bSetTime == 1) { pthread_cancel(Th); // 发送线程取消请求(唤醒可能处于 usleep 的线程) bSetTime = 0; // 清除运行标志,破坏 timer_sr 的循环条件 pthread_join(Th, NULL); // 阻塞等待线程完全退出,回收线程资源 } } //定义一个新类型 LP_V_V,它代表“一个无参数、无返回值的函数指针 typedef void (*LP_V_V)(void);//定义函数指针类型别名的标准写法 /*====================== 定时器工作线程 ======================*/ /** * @brief 定时器线程主体函数 * @param timer_func [in] 用户传入的周期回调函数指针 * @return 线程退出状态指针 * * @逻辑流程: * 1. 记录执行回调前的系统时间 * 2. 执行用户回调函数 * 3. 记录执行后的系统时间,计算实际耗时 * 4. 计算剩余时间 = 目标周期 - 实际耗时 * 5. 若剩余时间 > 0,则休眠补足;否则直接进入下一轮周期 */ void *timer_sr(void *timer_func) { struct timeval old, now; long ns, nus; // 将通用 void 指针强转为实际的回调函数类型 LP_V_V tm_sr = (LP_V_V)timer_func; // 循环执行条件:受全局标志 bSetTime 控制,KillTimer 中置 0 后退出 while (bSetTime) { gettimeofday(&old, NULL); // 获取回调执行前的系统时间 (秒, 微秒) tm_sr(); // 执行用户回调函数 gettimeofday(&now, NULL); // 获取回调执行后的系统时间 // 计算回调函数实际执行耗时 ns = now.tv_sec - old.tv_sec; nus = now.tv_usec - old.tv_usec; // 处理微秒借位:若 now.tv_usec < old.tv_usec,需从秒位借 1 秒 (1000000 微秒) while (nus < 0) // 此处实际只需 if 即可,while 同样安全 { ns--; nus += 1000 * 1000; } // 计算本周期剩余需休眠的时间 (LOOP_TIME 为毫秒,需统一转为微秒) long remaining_us = LOOP_TIME * 1000 - (ns * 1000 * 1000 + nus); // 若剩余时间大于 0,则休眠补足周期;否则说明回调耗时已超周期,直接跳至下一轮 if (remaining_us > 0) { usleep(remaining_us); // 微秒级休眠。此函数为 POSIX 标准的“可取消点”, // 此时调用 pthread_cancel 可立即唤醒线程。 } } return (void *)0; // 线程正常退出 }
Logger.h
/* * ============================================================================ * Logger.h - 日志模块头文件 * ============================================================================ */ #ifndef LOGGER_H #define LOGGER_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/stat.h> #include <pthread.h> /*====================== 日志配置 ======================*/ #define LOG_BASE_PATH "./logs" // 日志根目录 #define LOG_MAX_SIZE (5 * 1024 * 1024) // 单个日志文件最大大小 (5MB) #define LOG_PATH_MAX 512 // 路径缓冲区大小 #define LOG_BUFFER_MAX 1024 // 日志内容缓冲区大小 #define LOG_TIME_FORMAT_MAX 64 // 时间戳格式缓冲区大小 /*====================== 日志句柄结构 ======================*/ typedef struct { FILE *fp; // 当前日志文件指针 char szFilePath[LOG_PATH_MAX]; // 当前日志文件路径 time_t tLastCheck; // 上次检查时间 long lFileSize; // 当前文件大小 int nLastHour; // 上次小时 int nLastDay; // 上次日期 } LOG_HANDLE; /*====================== 全局变量声明 ======================*/ extern LOG_HANDLE g_Log; // 全局日志句柄 extern pthread_mutex_t g_LogMutex; // 日志互斥锁 /*====================== 函数声明 ======================*/ /** * 初始化日志系统 * 创建日志目录并打开初始日志文件 */ void Log_Init(void); /** * 写入日志 * @param szFormat 格式化字符串 (支持 printf 风格格式) * @param ... 可变参数 */ void Log_Write(const char *szFormat, ...); /** * 检查并切换日志文件 (按小时或文件大小轮转) */ void Log_CheckAndRotate(void); /** * 关闭日志系统 * 关闭日志文件并释放资源 */ void Log_Close(void); /** * 获取当前日志文件路径 * @param szPath 输出路径缓冲区 * @param size 缓冲区大小 */ void Log_GetFilePath(char *szPath, size_t size); /** * 获取文件大小 * @param szPath 文件路径 * @return 文件大小 (字节) */ long Log_GetFileSize(const char *szPath); /** * 创建目录 (递归) * @param szPath 目录路径 */ void Log_CreateDir(const char *szPath); #endif /* SH_LOG_H */
Logger.c
/* * ============================================================================ * Logger.c - 日志模块实现 * ============================================================================ */ #include "Logger.h" #include <stdarg.h> // va_list, va_start, va_end /*====================== 类型定义 ======================*/ #ifndef BOOL_DEFINED #define BOOL_DEFINED typedef int BOOL; #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif #endif /*====================== 全局变量定义 ======================*/ LOG_HANDLE g_Log; // 全局日志句柄 pthread_mutex_t g_LogMutex; // 日志互斥锁 /*====================== 日志函数实现 ======================*/ /** * 创建目录(递归) * @param szPath 目录路径 */ void Log_CreateDir(const char *szPath) { char szDir[LOG_PATH_MAX]; char *p = szDir; strncpy(szDir, szPath, sizeof(szDir) - 1); szDir[sizeof(szDir) - 1] = '\0'; for (p += 1; *p; p++) { if (*p == '/') { *p = '\0'; mkdir(szDir, 0755); *p = '/'; } } mkdir(szDir, 0755); } /** * 获取当前日志文件路径 * 格式:./logs/YYYYMMDD/YYYYMMDD_HH.log * @param szPath 输出路径缓冲区 * @param size 缓冲区大小 */ void Log_GetFilePath(char *szPath, size_t size) { time_t tNow = time(NULL); struct tm *tmNow = localtime(&tNow); snprintf(szPath, size, "%s/%04d%02d%02d/%04d%02d%02d_%02d.log", LOG_BASE_PATH, tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday, tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday, tmNow->tm_hour); } /** * 获取文件大小 * @param szPath 文件路径 * @return 文件大小 (字节),失败返回 0 */ long Log_GetFileSize(const char *szPath) { struct stat st; if (stat(szPath, &st) == 0) { return st.st_size; } return 0; } /** * 初始化日志系统 * 创建日志目录并打开初始日志文件 */ void Log_Init(void) { memset(&g_Log, 0, sizeof(LOG_HANDLE)); pthread_mutex_init(&g_LogMutex, NULL); // 创建日志根目录 Log_CreateDir(LOG_BASE_PATH); // 打开日志文件 Log_CheckAndRotate(); } /** * 检查并切换日志文件 (按小时或文件大小轮转) * 触发条件: * 1. 小时变化 * 2. 日期变化 * 3. 文件大小超过 5MB */ void Log_CheckAndRotate(void) { time_t tNow = time(NULL); struct tm *tmNow = localtime(&tNow); char szNewPath[LOG_PATH_MAX]; // 检查是否需要切换文件(小时变化或文件过大) BOOL bNeedRotate = FALSE; if (g_Log.fp == NULL) { bNeedRotate = TRUE; } else if (g_Log.nLastHour != tmNow->tm_hour || g_Log.nLastDay != tmNow->tm_mday) { bNeedRotate = TRUE; // 小时或日期变化 } else if (g_Log.lFileSize >= LOG_MAX_SIZE) { bNeedRotate = TRUE; // 文件超过 5MB } if (!bNeedRotate) { return; } // 关闭旧文件 if (g_Log.fp != NULL) { fclose(g_Log.fp); g_Log.fp = NULL; } // 生成新文件路径 Log_GetFilePath(szNewPath, sizeof(szNewPath)); // 创建日期目录 char szDirPath[LOG_PATH_MAX]; time_t tNow2 = time(NULL); struct tm *tmNow2 = localtime(&tNow2); snprintf(szDirPath, sizeof(szDirPath), "%s/%04d%02d%02d", LOG_BASE_PATH, tmNow2->tm_year + 1900, tmNow2->tm_mon + 1, tmNow2->tm_mday); Log_CreateDir(szDirPath); // 打开新文件 g_Log.fp = fopen(szNewPath, "a"); if (g_Log.fp != NULL) { strncpy(g_Log.szFilePath, szNewPath, sizeof(g_Log.szFilePath) - 1); g_Log.lFileSize = Log_GetFileSize(szNewPath); g_Log.nLastHour = tmNow->tm_hour; g_Log.nLastDay = tmNow->tm_mday; g_Log.tLastCheck = tNow; } } /** * 写入日志 * @param szFormat 格式化字符串 (支持 printf 风格格式) * @param ... 可变参数 */ void Log_Write(const char *szFormat, ...) { va_list args; char szBuffer[LOG_BUFFER_MAX]; char szTime[LOG_TIME_FORMAT_MAX]; time_t tNow = time(NULL); struct tm *tmNow = localtime(&tNow); // 生成时间戳 snprintf(szTime, sizeof(szTime), "%04d-%02d-%02d %02d:%02d:%02d", tmNow->tm_year + 1900, tmNow->tm_mon + 1, tmNow->tm_mday, tmNow->tm_hour, tmNow->tm_min, tmNow->tm_sec); // 生成日志内容 va_start(args, szFormat); int nLen = vsnprintf(szBuffer, sizeof(szBuffer), szFormat, args); va_end(args); // 加锁 pthread_mutex_lock(&g_LogMutex); // 检查是否需要切换文件 Log_CheckAndRotate(); // 写入日志 if (g_Log.fp != NULL) { fprintf(g_Log.fp, "[%s] %s\n", szTime, szBuffer); fflush(g_Log.fp); g_Log.lFileSize += nLen + strlen(szTime) + 3; } // 解锁 pthread_mutex_unlock(&g_LogMutex); } /** * 关闭日志系统 * 关闭日志文件并释放资源 */ void Log_Close(void) { pthread_mutex_lock(&g_LogMutex); if (g_Log.fp != NULL) { fclose(g_Log.fp); g_Log.fp = NULL; } pthread_mutex_unlock(&g_LogMutex); pthread_mutex_destroy(&g_LogMutex); }
Makefile
# ========================================== # 编译器与变量定义 # 原生单 *.c 文件的的编译命令 gcc MyCServer.c -o MyCServer # 有Makefile文件的 清理并编译 make clean && make # 运行 ./MyCServer # ========================================== # 定义 C 语言编译器为 gcc CC = gcc # 定义 C++ 编译器为 g++ (虽然本文件主要编译 .c,但预留变量是个好习惯) CXX = g++ # 编译选项 (CFLAGS) # -Wall : 开启几乎所有警告,帮助发现潜在代码问题 # -O2 : 开启二级优化,提高程序运行速度 # -D_XOPEN_SOURCE_EXTENDED : 宏定义,启用 X/Open 标准扩展,通常用于支持宽字符和高级终端功能 # -D_GNU_SOURCE : 宏定义,启用 GNU C 库的扩展功能,确保能使用一些非标准的系统调用 CFLAGS = -Wall -O2 -D_XOPEN_SOURCE_EXTENDED -D_GNU_SOURCE # 链接选项 (LDFLAGS) # -lpthread : 链接 pthread 库,支持多线程编程 # -lshpa3 : 链接 shpa3 库 (可能是你的私有库或特定硬件库) # -lncursesw : 链接 ncursesw 库,'w' 代表 wide-character,用于在终端支持中文和宽字符显示 # -lstdc++ : 链接 C++ 标准库 (因为可能混编了 C++ 代码或依赖库是用 C++ 写的) # -lzmq : 链接 ZeroMQ 库,用于高性能网络消息通信 LDFLAGS = -lpthread -lshpa3 -lncursesw -lstdc++ -lzmq # ========================================== # 2. 目标文件与源文件定义 # ========================================== # 最终生成的可执行文件名 #TARGET = ShPbxServer TARGET = MyCServer # 源文件列表 (所有 .c 文件) #SRCS = ShPbxServer.c Logger.c ZeroMQServer.c ShBoard.c ShMemRecord.c ZeroMQAudioPush.c SRCS = MyCServer.c Logger.c # 目标文件列表 (自动将 SRCS 中的 .c 替换为 .o) # 例如: ShPbxServer.c -> ShPbxServer.o OBJS = $(SRCS:.c=.o) # ========================================== # 3. 伪目标声明 # ========================================== # 声明 .PHONY 告诉 make,all 和 clean 不是文件名,而是动作标签 # 这样可以避免当前目录下有名为 "all" 或 "clean" 的文件时产生冲突 .PHONY: all clean install uninstall # ========================================== # 4. 默认目标 # ========================================== # all 是默认执行的第一个目标,它依赖于 $(TARGET) # 执行 make 时,会先尝试构建 $(TARGET) all: $(TARGET) # ========================================== # 5. 链接规则 (生成可执行文件) # ========================================== # 规则:生成 $(TARGET) 依赖于所有的 $(OBJS) $(TARGET): $(OBJS) # 注意:下面这行开头必须是一个 Tab 键,不能是空格! # 执行链接命令:将所有的 .o 文件链接在一起,生成最终的可执行文件 $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(LDFLAGS) # ========================================== # 6. 编译规则 (生成 .o 文件) # ========================================== # 模式规则:告诉 make 如何将任意 .c 文件编译成 .o 文件 # % 是通配符,表示匹配任意文件名 %.o: %.c # 执行编译命令: # -c 表示只编译不链接 # $< 代表第一个依赖文件 (即源文件 .c) # $@ 代表目标文件 (即 .o 文件) $(CC) $(CFLAGS) -c $< -o $@ # ========================================== # 7. 清理规则 # ========================================== # 清理构建产物 clean: # 删除所有的 .o 中间文件和最终的可执行文件 # -f 参数表示强制删除,不提示确认 rm -f $(OBJS) $(TARGET) # ========================================== # 8. 安装与卸载 (可选) # ========================================== # 安装目标:将编译好的程序复制到系统路径,使其可以在任意目录运行 install: $(TARGET) cp $(TARGET) /usr/local/bin/ # 卸载目标:删除系统路径中的程序 uninstall: rm -f /usr/local/bin/$(TARGET) #================几个关键点解释=================== #1、-lncursesw 的作用: #你的程序中使用了 ncursesw 而不是普通的 ncurses。这说明你的程序界面需要显示中文或者特殊符号。如果编译报错找不到库,通常是因为系统没装 libncursesw-dev。 #2、$< 和 $@: #这是 Makefile 的自动变量。 #$<:代表依赖列表中的第一个文件(在这里是 .c 源文件)。 #$@:代表目标文件(在这里是 .o 对象文件)。 #使用它们可以让规则通用,不用为每个文件单独写一行编译命令。 #3、Tab 键缩进: #在 Makefile 中,所有命令(如 $(CC)... 或 rm...)前面必须使用 Tab 键 进行缩进,不能使用空格。这是新手最容易遇到的报错原因(missing separator)。 #4、$(SRCS:.c=.o): #这是一个字符串替换函数。它会自动把 SRCS 变量里所有的 .c 后缀替换成 .o,这样你添加新源文件时,只需要改 SRCS 列表,不用手动去改 OBJS。 #================================================
进入工程目录
执行 清理且编译 命令
运行命令

此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。