
简介基于C实现的局域网内主机监控系统是一份面向C课程设计与网络编程训练的完整工程。系统具备多主机桌面实时监控、远程控制鼠标/键盘与外设接口的能力同时提供1/4/8/24位色彩显示及多种图像压缩算法并集成消息传输、命令传输、文件传输和图形化操作界面功能覆盖监控交互主要环节。压缩包共114个文件包含13个cpp与15个h源代码文件、工程配置与可执行程序另有界面位图、图标、PDF说明等辅助材料整体大小约8.36MB。已有108人次学习浏览。资源中客户端与服务器端工程结构完整打包文件区分清晰便于运行演示或二次开发适合学习Socket通信、多线程、图像处理及远程控制原理的学生参考也可作为毕业设计或求职项目的基础模板。1. 用C写局域网主机监控图的不是性能而是可控和底盘做局域网主机监控用Python或Shell也能搭出能跑的版本但当目标网络里混杂着服役多年的Windows 7、旧Linux服务器、网络打印机和无agent能力的摄像头时最先崩掉的不是采集逻辑而是运行环境目标机器缺库、缺解释器或者运维团队根本不敢往生产内网里放一个带依赖的采集端。C版本的核心价值是把采集端编成一个单文件可执行程序扔到目标机器上解压即跑同时把ICMP、SNMP、TCP探活这些协议差异收敛到一个适配层里。这套系统的落地形态通常覆盖三类需求资产盘点时自动发现内网在线设备、日常巡检时盯着主机存活和关键端口、断电断网时快速定位是交换机还是末端设备失联。对五年以上工程师来说更值得读的是在线判定策略和超时参数背后的取舍。2. 先把监控协议选对ICMP、SNMP、ARP和TCP探测各管哪一段2.1 主机监控系统里协议选型的三个约束选协议之前先回答三个问题目标设备允不允许装agent监控器与目标之间隔不隔三层以及你要的只是在线状态还是CPU、内存这类真实指标。允许装agent就优先用主动上报但现实里大量网络打印机、老交换机、摄像头根本没有装agent的能力无代理探测因此成为局域网主机监控的标配。我一般会把探活结果分成三层记录ARP/ICMP负责主机层可达性TCP connect负责服务层连通性SNMP负责指标层数据。告警阈值建立在这三层之上而不是只盯一条ping命令的结果。三层模型的好处是排障时可以直接告诉值班人员“主机在线但服务端口不通”而不是一个笼统的掉线。2.2 ICMP探活最廉价但误判率最高的在线判断ICMP不需要目标机装任何东西发一个echo request就能确认网络栈活着成本在所有协议里最低。误判率也最高不少办公网的Windows防火墙默认不响应ping老式网络设备在高流量时段还会刻意丢弃ICMP报文以保护CPU离线列表跟着假阳性刷屏是常态也是最容易让监控系统失去信任的环节。运维日志里最常见的现象是连续ping四次只有一次超时单独拿这一次超时去判离线误报率会高到没法用。所以ICMP只适合做初筛和资产发现状态迁移必须让下一轮探测来确认。常见做法是把超时设在1500到3000毫秒并且连续两轮失败再迁移到离线状态。算法层面的优化点不在这重试策略和超时设计才是决定稳定性的地方。对于服役超过五年的老设备建议放宽超时而不是单纯提高重试次数因为低端设备的协议栈处理慢频繁重试反而加剧它的负担。2.3 SNMP真正能拿指标的协议代价是开启成本SNMP走UDP 161端口用community字符串做最低限度的访问控制返回的不是“活着没”而是接口流量、CPU占用、内存余量、系统版本。Linux下开snmpd只改一个配置文件Windows要在“启用或关闭Windows功能”里勾上SNMP服务再配置团体名和允许访问的源地址。这个门槛决定了无agent场景下SNMP通常先覆盖服务器和网络设备而不是办公终端。C侧走net-snmp时有三个参数容易搞混snmp_timeout是单次请求超时snmp_retries是重试次数两者乘积才是单个目标最坏情况下的等待时间。很多人把retries设成默认的5内网目标一多SNMP轮次会比TCP探活慢一个数量级。常见做法是retries压到2timeout保持1000到1500毫秒性价比最高。SNMP的community字符串不要用public即使在内网也要单独设一个读字符串并绑定监控服务器的源IP。2.4 ARP探活与TCP端口探测的适用边界ARP只能在同一广播域内工作对整个子网发request拿到回包说明二层可达适合资产盘点阶段快速扫网段。跨网段必须经过三层网关所以多网段网络得一段一段扫而且部分安全设备会抑制ARP扫描结果偏保守。ARP不适合做持续监控广播量在几百台规模下会挤压正常业务流量我一般只在每天凌晨跑一次全量发现。TCP connect比ICMP更接近业务实际。一台文件服务器ICMP通但445端口握手失败说明服务停了或防火墙策略变了反过来445通而ping不通的情况也很常见。最终在线认定应该以关键端口为准。局域网里常用探活端口是22、80、443、3389、445配合内网自己起的文件传输页面、打印共享和业务Web服务再按实际端口补进去。探活对象的选取原则是只选目标上必定开放且防火墙放行的端口宁少勿多。2.5 用适配器模式封装协议差异四种协议的信息粒度、端口和成本都不一样探测逻辑不能散落在监控主流程里。常见做法是定义探测基类ICMP、SNMP、TCP各自实现一个派生类配置里用probe_type字符串选择主流程完全不感知协议细节。enum class HostStatus { Unknown, Online, Offline }; struct HostTarget { std::string ip; int port 80; std::string community; // 仅 SNMP 探测使用 int timeout_ms 2000; }; class IProbe { public: virtual ~IProbe() default; virtual HostStatus check(const HostTarget target) const 0; virtual const char* name() const 0; }; class TcpConnectProbe : public IProbe { public: HostStatus check(const HostTarget target) const override { int fd socket(AF_INET, SOCK_STREAM, 0); bool ok nonblocking_connect(fd, target, target.timeout_ms); close(fd); return ok ? HostStatus::Online : HostStatus::Offline; } const char* name() const override { return tcp_connect; } };代码说明check的语义是同步阻塞直到得到结果内部超时由target.timeout_ms控制不阻塞主流程因为调用方在worker线程里。name()用在日志和配置文件里运维侧看到probe_typetcp_connect就能定位到TcpConnectProbe这个实现。各协议选型对比如下这个表可以直接写进方案文档探测方式依赖条件能确认的结论不能确认的结论主要代价ICMP echo目标允许回显网络栈在线服务是否可访问防火墙丢弃导致误报SNMP get目标开启snmpd且community正确CPU/内存/接口流量等指标应用层是否健康老设备兼容性差ARP request同一广播域二层可达跨网段存活状态广播压力TCP connect目标端口对外开放特定服务在线服务内部健康度目标侧留连接日志表格里的四行基本覆盖了无agent场景下能用的全部手段。实际落地时很少只用一种协议通常TCP连不上时回退到ICMPICMP也不通再回退到ARP按这个顺序把探测结果逐层降级最后综合出一个可信状态。3. 用非阻塞socket和多线程轮询撑起采集骨架3.1 选select还是epoll局域网规模下的实际差异几百到几千台设备的局域网连接数和事件量根本到不了select的上限。select单进程默认可监视1024个fdLinux上可以改内核或换poll但都不是重点。我选epoll不是因为高并发而是它的水平触发语义和“一批socket一次回收”的轮询模式匹配得很好。典型的事件循环骨架长这样struct epoll_event ev, events[64]; int epfd epoll_create1(0); ev.events EPOLLIN; ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); while (running) { int n epoll_wait(epfd, events, 64, 1000); if (n 0) { // 超时醒来执行周期性的离线状态检查 run_scheduled_tasks(); continue; } for (int i 0; i n; i) { handle_event(events[i].data.fd); } }epoll_wait的timeout设成1000毫秒每秒醒一次处理定时任务就不需要额外维护一个定时器线程。这里要留意的是n0并不能说明网络空闲它只是表示这一秒没有新事件定时任务仍然要独立记录上次执行时间避免因为事件频繁触发而饿死。3.2 非阻塞connect里最容易错的三处TCP探活最怕connect卡在握手阶段所以必须非阻塞。第一处易错connect返回-1且errno等于EINPROGRESS这只是表示连接还在继续不是失败。第二处易错select之后不能看了“可写”就默认成功因为对端RST时socket同样可写必须用getsockopt读SO_ERROR判断真实结果。第三处易错select超时要基于单调时钟重新计算Linux的select调用后tv会被改写Windows反而不会改依赖返回值写出来的代码在两个平台行为不一致。bool nonblocking_connect(int fd, const HostTarget target, int timeout_ms) { sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(static_castuint16_t(target.port)); inet_pton(AF_INET, target.ip.c_str(), addr.sin_addr); int flags fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); int rc connect(fd, reinterpret_castsockaddr*(addr), sizeof(addr)); if (rc 0) return true; if (errno ! EINPROGRESS) return false; timeval tv{}; tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; fd_set wfds; FD_ZERO(wfds); FD_SET(fd, wfds); rc select(fd 1, nullptr, wfds, nullptr, tv); if (rc 0) return false; int err 0; socklen_t len sizeof(err); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, err, len) 0) return false; return err 0; }参数说明timeout_ms建议在1500到3000之间。对同一目标上的多端口探活timeout可以并发执行而不是相加socket用完直接close没必要恢复阻塞标志。getsockopt返回0且err为0才算连接建立如果err是ECONNREFUSED目标主机的网络栈是活的但端口没人监听这个状态要保留给上层做服务级告警不能简单当成主机离线。3.3 线程池与扫描轮次不要让一次慢探测拖垮整轮设400台设备、每台超时2秒串行探测一轮就是800秒中途任何状态变化都反映不进来。常见做法是把目标列表按IP排序切成片每片一个worker线程线程数配成参数而不是用hardware_concurrency自动推导因为容器里看到的核数是宿主机的自动推导会被严重误导。线程数通常取4到8同一IP的多个端口放在同一个worker里避免两台目标机争抢同一个fd池。三种探测要错峰启动ARP全量扫描5分钟一轮SNMP采集1分钟一轮TCP探活30秒一轮启动时刻加一个0到5秒的随机偏移防止同时突刺把交换机的CPU打断。线程池里的任务获取用条件变量或环形队列都可以局域网监控场景下无锁队列带来的收益可以忽略代码简洁更重要。3.4 结果落盘攒批还是逐条写几十台设备顺序写SQLite足够几百台设备还要保留历史曲线时SQLite的写放大就会拖慢采集线程。常见做法是内存里保留最新状态快照状态变更以批量事务刷进SQLite历史曲线单独交给时间序列库。这样采集线程对磁盘的依赖降到最低崩了也不丢当前快照。sqlite3_stmt* stmt nullptr; sqlite3_prepare_v2(db, INSERT INTO host_snapshot(ip, status, latency_ms, ts) VALUES(?1, ?2, ?3, strftime(%s,now));, -1, stmt, nullptr); sqlite3_exec(db, BEGIN, nullptr, nullptr, nullptr); for (const auto rec : snapshot) { sqlite3_bind_int(stmt, 1, rec.ip_int); sqlite3_bind_int(stmt, 2, static_castint(rec.status)); sqlite3_bind_int(stmt, 3, rec.latency_ms); if (sqlite3_step(stmt) ! SQLITE_DONE) { // 记录错误继续处理下一条 } sqlite3_reset(stmt); } sqlite3_exec(db, COMMIT, nullptr, nullptr, nullptr); sqlite3_finalize(stmt);说明一条事务包含几百条INSERT时提交时间远小于逐条autocommitlatency_ms用int保存采样精度到毫秒就够用。批量写入的批次上限建议控制在1000条以内避免单次事务持锁时间过长挡着查询端。4. 实战在Linux下把采集端编出来并接到Web页面4.1 编译环境与最小依赖开发环境用VSCode配gtasks.json里能直接复用下面这行编译命令。SNMP支持依赖net-snmp开发库机器上没装就把probe_snmp.cpp排除掉其余模块依然能编译运行。g -stdc17 -O2 -Wall -pthread \ main.cpp probe_tcp.cpp probe_icmp.cpp store.cpp \ -o hostmon逻辑说明-O2对网络程序足够了-O3不会带来可感知的提升-pthread必须显式加否则链接pthread库时会有undefined reference。Linux部署时把这个二进制拷过去就能跑这就是C做采集端最省心的地方不需要在目标机器上准备任何解释器或运行时。4.2 从/proc里读系统指标CPU、内存、负载无agent探测拿不到目标机的CPU和内存但监控机自己也要被监控或者可以在目标机上放一个agent版采集端。对于能读/proc的Linux目标CPU利用率最容易算错因为/proc/stat里是累计值必须两次采样做差。struct CpuStat { unsigned long long idle, total; }; bool read_cpu_stat(CpuStat out) { std::ifstream f(/proc/stat); std::string line; if (!std::getline(f, line)) return false; std::istringstream ss(line); std::string tag; unsigned long long user, nice, system, idle, iowait; unsigned long long irq, softirq, steal; ss tag user nice system idle iowait irq softirq steal; out.idle idle iowait; out.total user nice system idle iowait irq softirq steal; return true; } double cpu_usage_percent() { CpuStat a, b; if (!read_cpu_stat(a)) return -1.0; std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (!read_cpu_stat(b)) return -1.0; unsigned long long dt b.total - a.total; if (dt 0) return 0.0; return 100.0 * (1.0 - static_castdouble(b.idle - a.idle) / static_castdouble(dt)); }说明500毫秒采样间隔在低负载机器上已经能稳定出数值间隔更短会看到明显噪声。iowait算进idle是Linux对CPU“空闲但等待I/O”的定义想单独观察I/O压力就分别取idle和iowaitdt等于0说明两次读取发生在同一个时钟节拍内直接返回0比除零安全。内存指标里MemAvailable比MemFree更接近应用可用内存因为它考虑了page cache可回收的部分。用MemTotal减MemAvailable做分子直接看MemFree会高估使用率这是新手上路最容易踩的偏差。4.3 用最简单的HTTP接口把状态暴露给浏览器采集端要能自证“我看到的和你看到的一样”挂一个只读HTTP接口最省事。手拼JSON的好处是不引入框架生产环境换成cpp-httplib这类单头文件库差别只是代码量。std::string make_json(const std::vectorHostRecord records) { std::ostringstream oss; oss {\hosts\:[; bool first true; for (const auto r : records) { if (!first) oss ,; first false; oss {\ip\:\ r.ip \, \status\: static_castint(r.status) , \latency_ms\: r.latency_ms }; } oss ]}; return oss.str(); }HTTP响应要用write一次性写完整包Content-Length必须和body字节数严格一致哪怕差一个换行浏览器解析JSON也会失败。局域网内直接访问http://监控机IP:8080/hosts就能看到结果前端页面用fetch轮询或者把这份JSON接到内网已有的资产页面上不需要独立的UI体系。4.4 三个必调的监控参数采集端跑起来后真正决定监控系统能不能用的就是后面这几个参数参数建议值依据probe_interval30s配合连续两轮失败判定离线确认延迟控制在1分钟以内connect_timeout_ms1500办公网正常RTT在1到20毫秒1500毫秒足够宽offline_confirm_rounds2抵消单次丢包带来的假阳性scan_threads4到8不要依赖hardware_concurrency容器里不准确如果做的是agent模式probe_interval可以压到5秒因为agent主动上报不消耗交换机CPU也不需要每次建连握手。参数改动后至少跑满一个离线/恢复周期再评估效果只跑几分钟就下结论每次的调整都会变成随机调参。4.5 编译、权限和探测不到目标时的排错顺序编译报错先看是不是缺net-snmp头文件没有SNMP需求直接去掉probe_snmp.cpp。运行报permission denied基本是ICMP raw socket需要root或cap_net_raw权限用sudo跑或setcap给二进制授权。探测不到目标时先在本机手动ping和nc -zv验证再查监控机与目标之间的防火墙策略。Windows主机“网络发现”开着但探活不到网络位置类型多半是公用网络公用档位下防火墙默认丢弃所有探测报文切到专用网络即可解决。内网部分机器能扫到部分扫不到优先怀疑交换机端口隔离或VLAN划分而不是监控程序本身。排查顺序固定下来以后大多数问题五分钟内能定位。5. 进阶跨平台适配与误报压制的三个技巧5.1 Windows下的ICMP探测用IcmpSendEchoLinux下ICMP探测要开raw socketWindows上更省事的做法是直接调IcmpSendEcho它来自iphlpapi.dll不需要额外权限#include winsock2.h #include iphlpapi.h #pragma comment(lib, iphlpapi.lib) HANDLE hIcmp IcmpCreateFile(); char send_buf[32] {0}; char reply_buf[sizeof(ICMP_ECHO_REPLY) 32] {0}; DWORD result IcmpSendEcho( hIcmp, inet_addr(192.168.1.10), send_buf, sizeof(send_buf), nullptr, reply_buf, sizeof(reply_buf), 1500); bool online (result ! 0); IcmpCloseHandle(hIcmp);参数说明最后一个1500是超时毫秒result为0说明没有回显应答要区分“目标不可达”和“超时”读ICMP_ECHO_REPLY里的Status字段。Windows部署包要带上对应架构的Visual C Redistributable否则老Windows机器上会直接弹缺DLL。TCP探活部分在Windows上几乎不用改把非阻塞connect的错误码从errno换成WSAGetLastError即可。5.2 误报压制的三个技巧第一多轮确认。状态迁移设计成confirmed_online、suspect_offline、confirmed_offline三态至少连续两轮失败才判离线恢复只需要一轮成功。这样交换机重启、局域网瞬时大流量造成的“ping四次有一次超时”不会抖成告警。第二同网段分批轮询。扫描不要对整段地址全量爆发而是切成小段顺序执行段间留几十毫秒间隔。交换机ARP表和路由器缓存不会被瞬间刷爆上报网络设备的性能数据也更稳。第三端口权重判定。多端口探活时按权重判断80、443、22这些关键端口全部存活才算在线只有一个端口响应而其他端口全超时先别急着判离线更可能是服务进程异常或防火墙策略收紧了这本身就是监控系统该暴露给运维的信息。5.3 用构造故障来验收监控系统搭建完成后在受控机器上模拟一次真实掉线比直接看界面有没有数据有价值得多。iptables丢包比直接关网卡更接近真实宕机的网络表现而且不碰目标机的远程管理通道# 监控机上模拟目标主机断网 iptables -A INPUT -s 192.168.1.10 -j DROP # 观察状态迁移到离线后恢复 iptables -D INPUT -s 192.168.1.10 -j DROP验收时固定记录两个数状态迁移延迟和48小时内误报次数。状态迁移延迟应当约等于probe_interval乘以offline_confirm_rounds明显偏大说明探测队列里有积压偏小说明离线判定条件太宽松。误报占比超过3%就回头检查是不是扫描周期太短、超时参数太紧或网段里有设备周期性休眠。把这两个数作为基线写进部署文档后面每次调参都拿它对照系统才算从“能跑”推到“敢用”。本文还有配套的精品资源点击获取