FEATURED · 精选文章

C++ MFC封装Windows平台Traceroute:从ICMP协议到图形化网络诊断工具

发布时间 / 2026/8/23 10:08:31
来源 / 创域科博编辑部
栏目 / 资讯中心
C++ MFC封装Windows平台Traceroute:从ICMP协议到图形化网络诊断工具 1. 项目概述从网络诊断到代码实现做网络开发或者运维的朋友对tracert或traceroute这个命令肯定不陌生。当服务器连不上、网络延迟高的时候我们第一个想到的就是它敲下去看着那一行行跳转的IP和延迟心里大概就有谱了——是本地网关的问题还是中间某个骨干网节点抽风或者是目标服务器本身挂了。这个工具就像网络世界的“X光”能清晰地透视数据包从你的电脑到目标主机所走过的每一段路径。但是如果你不仅仅满足于使用命令行工具而是想把这个功能集成到自己的桌面应用、网络监控软件或者设备管理工具里呢比如你想做一个带图形界面的网络诊断工具箱或者需要一个后台服务自动追踪到多个服务器的路由并生成报告。这时候调用系统命令再解析文本输出就显得笨拙且不优雅了。我们需要的是在代码层面直接掌控路由跟踪的能力。这就是CTraceRoute封装项目存在的意义。它的核心目标就是利用 C 在 Windows 平台下基于 MFCMicrosoft Foundation Classes框架将底层的 ICMPInternet Control Message Protocol协议操作封装成一个可重用、易集成的 C 类。它把发送 ICMP 回显请求Echo Request、接收 ICMP 超时Time Exceeded和回显应答Echo Reply报文、计算往返时间RTT以及解析 IP 头获取上一跳地址这些复杂且平台相关的细节全部隐藏起来。开发者只需要实例化这个类调用一个像Trace()这样的方法传入目标主机名或IP就能以结构化的数据比如一个包含每跳IP、主机名和延迟的向量拿到跟踪结果从而轻松地在列表控件CListCtrl里展示、存入数据库或者触发进一步的逻辑。简单说这个项目是把一个系统级的网络诊断命令变成了你应用程序中的一个功能模块。它特别适合需要内嵌网络诊断功能的 Windows 桌面软件开发者尤其是那些熟悉或正在使用 MFC 进行传统 Win32 界面开发的朋友。即使你不直接用 MFC其核心的 Socket 和 ICMP 操作逻辑也具有很高的参考价值。2. 核心原理与协议深度解析要封装traceroute我们必须先吃透它的工作原理。它可不是简单地连续ping其巧妙之处在于利用了 IP 报文中的TTLTime To Live字段和 ICMP 协议的错误报告机制。2.1 TTL 与 ICMP 超时报文路由跟踪的基石IP 头中有一个 8 位的 TTL 字段初始值通常为 64 或 128。数据包每经过一个路由器即一跳该路由器就会将 TTL 值减 1。当 TTL 值减到 0 时路由器就不会再转发这个数据包而是会将其丢弃并向数据包的源地址发送一个ICMP 超时Time Exceeded报文类型 11代码 0。traceroute正是利用了这个机制它首先向目标发送一个 TTL1 的探测包通常是 ICMP Echo Request 或 UDP 到高端口。第一个路由器收到后TTL 减 1 变为 0于是丢弃该包并返回一个 ICMP 超时报文。源主机由此知道了第一个路由器的地址。接着发送 TTL2 的探测包它会到达第二个路由器后被丢弃并返回超时报文。以此类推逐步增加 TTL 值直到探测包最终到达目标主机。注意目标主机的反应不同。如果探测包是 ICMP Echo Request目标主机会回复一个ICMP 回显应答Echo Reply类型 0代码 0。收到这个报文traceroute就知道已经抵达终点停止发送。这也是 Windows 系统tracert命令默认使用的方式。2.2 选择探测协议ICMP vs UDP经典的 Unix/Linuxtraceroute默认使用 UDP 数据包目标端口从 33434 开始递增而 Windows 的tracert使用 ICMP Echo Request。在我们的封装中通常选择 ICMP原因如下一致性与ping的功能基础一致代码复用度高。防火墙穿透性虽然很多防火墙会屏蔽外部发来的 ICMP Echo但traceroute是探测路径上的路由器这些路由器的 ICMP 超时报文类型11出于网络诊断目的被放行的概率相对较高。且目标主机是否回复 Echo Reply 不影响路径探测直到最后一跳。实现简便在 Windows 下使用SOCK_RAW套接字可以直接构造和发送 ICMP 包逻辑清晰。2.3 Windows 下的实现挑战在 Linux 下你可以直接用socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)来收发 ICMP 包。但在 Windows 平台事情要复杂一些原始套接字权限创建原始套接字需要管理员权限。这对于一个需要普通用户运行的桌面应用来说是个障碍。不过对于发送 ICMP Echo我们有替代方案。ICMP.DLL 与IcmpSendEchoAPIWindows 提供了更高级的、无需管理员权限的 ICMP 功能。通过IcmpCreateFile,IcmpSendEcho,IcmpCloseHandle这一组 API我们可以方便地完成单次ping。但是标准的IcmpSendEcho是同步的且不提供 TTL 设置接口。异步操作与 TTL 控制为了实现traceroute我们需要设置 TTL这可以通过setsockopt函数设置IPPROTO_IP级别的IP_TTL选项来实现。即使使用IcmpSendEcho其内部也可能使用套接字但直接设置套接字选项更通用。接收中间路由器的 ICMP 超时报文IcmpSendEcho只能等待目标主机的 Echo Reply 或超时。要捕获路径上的超时报文我们必须使用原始套接字SOCK_RAW来监听所有 ICMP 报文并从中筛选出类型为 11超时的报文。这又回到了需要权限的问题。实操心得一个折中且稳定的实现方案是“混合模式”。使用原始套接字需处理权限问题或引导用户以管理员身份运行来接收所有 ICMP 响应。而对于发送可以使用普通的 UDP 套接字设置 TTL或 ICMP 原始套接字。考虑到兼容性和模仿tracert许多封装选择在拥有权限时使用原始套接字进行全流程控制而在普通权限下可以降级为仅能执行pingIcmpSendEcho的功能或者给出友好提示。我们的CTraceRoute封装通常预设为需要管理员权限以获得完整功能这是开发此类工具时需要向用户明确说明的一点。3. CTraceRoute 类的设计与封装要点一个好的封装应该职责清晰、接口简单、内部健壮。下面我们来拆解CTraceRoute类的设计。3.1 类接口设计// CTraceRoute.h class CTraceRoute { public: CTraceRoute(); virtual ~CTraceRoute(); // 核心跟踪方法 BOOL Trace(LPCTSTR lpszHost, int nMaxHops 30, int nTimeout 5000); // 获取结果 int GetHopCount() const; BOOL GetHopInfo(int nIndex, CString strIP, CString strHostName, DWORD dwRTT) const; // 状态与回调 void SetTimeout(int nMilliseconds); void SetMaxHops(int nHops); // 可以设计一个回调函数用于实时更新UI例如每发现一跳就通知 typedef void (CALLBACK* TRACEROUTE_CALLBACK)(int nHop, LPCTSTR lpszIP, LPCTSTR lpszHostName, DWORD dwRTT, LPVOID pParam); void SetCallback(TRACEROUTE_CALLBACK pfnCallback, LPVOID pParam NULL); // 辅助函数 static CString GetHostNameFromIP(const CString strIP); private: // 内部实现方法 BOOL SendICMPEcho(SOCKET s, const sockaddr_in destAddr, int nTTL, DWORD dwSeq, DWORD dwSendTime); BOOL ParseICMPResponse(char* pRecvBuf, int nLen, sockaddr_in fromAddr, DWORD dwSeq, int nType, int nCode, DWORD dwRoundTripTime); void AddHop(const CString strIP, const CString strHostName, DWORD dwRTT); // 成员变量 CArrayHopInfo, HopInfo m_arrHops; // 存储每一跳的信息 int m_nMaxHops; int m_nTimeout; TRACEROUTE_CALLBACK m_pfnCallback; LPVOID m_pCallbackParam; CRITICAL_SECTION m_csLock; // 多线程保护 }; struct HopInfo { CString strIP; CString strHostName; DWORD dwRTT; // 往返时间毫秒 BOOL bIsDestination; // 是否是目标主机 };3.2 核心流程实现拆解Trace方法是核心其内部逻辑如下初始化与解析目标BOOL CTraceRoute::Trace(LPCTSTR lpszHost, int nMaxHops, int nTimeout) { m_arrHops.RemoveAll(); m_nMaxHops nMaxHops; m_nTimeout nTimeout; // 1. 解析主机名或IP地址为 sockaddr_in sockaddr_in destAddr {0}; hostent* pHost gethostbyname(CT2A(lpszHost)); if (!pHost) return FALSE; destAddr.sin_family AF_INET; destAddr.sin_addr *((in_addr*)pHost-h_addr); // ... 错误处理 }创建原始套接字// 2. 创建用于接收的原始套接字 SOCKET recvSocket socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (recvSocket INVALID_SOCKET) { // 错误处理可能是权限不足 return FALSE; } // 设置接收超时 setsockopt(recvSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)nTimeout, sizeof(nTimeout));主循环TTL从1递增for (int nTTL 1; nTTL m_nMaxHops; nTTL) { // 3. 创建用于发送的套接字每次循环创建新的以便设置不同的TTL SOCKET sendSocket socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); setsockopt(sendSocket, IPPROTO_IP, IP_TTL, (char*)nTTL, sizeof(nTTL)); DWORD dwSeq GetCurrentThreadId() nTTL; // 生成一个序列号用于匹配请求和响应 DWORD dwSendTime 0; // 4. 发送ICMP Echo请求包 if (!SendICMPEcho(sendSocket, destAddr, nTTL, dwSeq, dwSendTime)) { closesocket(sendSocket); continue; // 发送失败尝试下一跳 } // 5. 接收响应 char recvBuf[1024] {0}; sockaddr_in fromAddr {0}; int fromLen sizeof(fromAddr); int nReceived recvfrom(recvSocket, recvBuf, sizeof(recvBuf), 0, (sockaddr*)fromAddr, fromLen); DWORD dwRTT GetTickCount() - dwSendTime; closesocket(sendSocket); // 关闭发送套接字 if (nReceived 0) { // 6. 解析响应包 int nType 0, nCode 0; DWORD dwReplyRTT 0; if (ParseICMPResponse(recvBuf, nReceived, fromAddr, dwSeq, nType, nCode, dwReplyRTT)) { CString strIP inet_ntoa(fromAddr.sin_addr); CString strHostName GetHostNameFromIP(strIP); // 计算最终RTT可能是直接回应的RTT也可能是超时报文返回的RTT DWORD dwFinalRTT (nType 0) ? dwReplyRTT : dwRTT; AddHop(strIP, strHostName, dwFinalRTT); // 调用回调更新UI if (m_pfnCallback) { m_pfnCallback(nTTL, strIP, strHostName, dwFinalRTT, m_pCallbackParam); } // 7. 判断是否到达目标 if (nType 0) { // ICMP Echo Reply break; // 跟踪完成 } // 否则是ICMP Time Exceeded继续下一跳 } } else { // 超时或接收错误记录为“*” AddHop(_T(*), _T(请求超时), 0); if (m_pfnCallback) { m_pfnCallback(nTTL, _T(*), _T(请求超时), 0, m_pCallbackParam); } } } closesocket(recvSocket); return m_arrHops.GetSize() 0; }3.3 关键细节构造与解析 ICMP 包SendICMPEcho和ParseICMPResponse是两个最核心的底层函数。构造 ICMP Echo 请求包 ICMP 报文是封装在 IP 报文中的数据。一个 Echo Request 报文的结构如下0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -------------------------------- | Type(8) | Code(0) | Checksum | -------------------------------- | Identifier | Sequence Number | -------------------------------- | Data... | --------------------------------我们需要在内存中填充这个结构计算校验和然后通过sendto发送。BOOL CTraceRoute::SendICMPEcho(SOCKET s, const sockaddr_in destAddr, int nTTL, DWORD dwSeq, DWORD dwSendTime) { // 构造ICMP包 char icmpPacket[sizeof(ICMP_HEADER) 32]; // 头部32字节数据 ICMP_HEADER* pIcmp (ICMP_HEADER*)icmpPacket; pIcmp-type 8; // Echo Request pIcmp-code 0; pIcmp-id (USHORT)GetCurrentProcessId(); // 用进程ID作为标识符 pIcmp-seq (USHORT)dwSeq; pIcmp-checksum 0; // 填充数据部分可以放时间戳等 memset(icmpPacket sizeof(ICMP_HEADER), A, 32); // 计算校验和 pIcmp-checksum CalculateChecksum((USHORT*)icmpPacket, sizeof(icmpPacket)); dwSendTime GetTickCount(); int nSent sendto(s, icmpPacket, sizeof(icmpPacket), 0, (sockaddr*)destAddr, sizeof(destAddr)); return (nSent sizeof(icmpPacket)); }解析 ICMP 响应包 接收到的数据是一个完整的 IP 数据包。我们需要跳过 IP 头长度可变需要根据IPHL字段计算。找到 IP 数据包内的 ICMP 报文。判断这个 ICMP 报文是ICMP 超时还是ICMP 回显应答。如果是 ICMP 超时报文其数据部分包含了触发它的原始 IP 包的头部和我们发送的 ICMP Echo 请求的一部分。我们需要从中提取出我们发送的包的标识符和序列号以匹配我们的请求。如果是 ICMP 回显应答直接检查标识符和序列号即可。BOOL CTraceRoute::ParseICMPResponse(char* pRecvBuf, int nLen, sockaddr_in fromAddr, DWORD dwSeq, int nType, int nCode, DWORD dwRoundTripTime) { // 1. 解析IP头 IP_HEADER* pIpHeader (IP_HEADER*)pRecvBuf; int nIpHeaderLen (pIpHeader-h_len_ver 0x0F) * 4; // IP头长度单位4字节 // 2. 定位到ICMP报文开始处 char* pIcmpData pRecvBuf nIpHeaderLen; ICMP_HEADER* pIcmpHeader (ICMP_HEADER*)pIcmpData; nType pIcmpHeader-type; nCode pIcmpHeader-code; if (nType 11 nCode 0) { // ICMP Time Exceeded // 3. 在超时报文的数据部分找到原始IP包 char* pOriginalIp pIcmpData sizeof(ICMP_HEADER); IP_HEADER* pOrigIpHeader (IP_HEADER*)pOriginalIp; int nOrigIpHeaderLen (pOrigIpHeader-h_len_ver 0x0F) * 4; // 4. 在原始IP包的数据部分找到我们发送的ICMP Echo请求头 char* pOriginalIcmp pOriginalIp nOrigIpHeaderLen; ICMP_HEADER* pOrigIcmpHeader (ICMP_HEADER*)pOriginalIcmp; // 5. 检查是否是我们发出的请求通过标识符和序列号 if (pOrigIcmpHeader-type 8 // 是我们发出的Echo Request pOrigIcmpHeader-id (USHORT)GetCurrentProcessId() pOrigIcmpHeader-seq (USHORT)dwSeq) { // 匹配成功这是一个有效的路径节点响应 dwRoundTripTime 0; // 对于超时报文使用外部计算的dwRTT return TRUE; } } else if (nType 0) { // ICMP Echo Reply // 直接检查标识符和序列号 if (pIcmpHeader-id (USHORT)GetCurrentProcessId() pIcmpHeader-seq (USHORT)dwSeq) { // 可以尝试从数据中提取发送时间戳来计算更精确的RTT这里简化处理 dwRoundTripTime 0; // 使用外部计算的dwRTT return TRUE; } } return FALSE; // 不是我们要找的响应包 }重要提示校验和计算、IP头结构定义IP_HEADER、ICMP_HEADER需要严格按照网络字节序和结构体对齐来定义。Windows SDK 中可能有现成的定义如ws2tcpip.h中的struct icmp和struct ip但需要注意兼容性和可移植性。自己定义时务必使用#pragma pack(1)确保单字节对齐。4. 在 MFC 项目中集成与界面展示有了核心的CTraceRoute类在 MFC 对话框中集成它就非常直观了。4.1 界面布局与控件绑定创建一个对话框资源添加以下控件编辑框IDC_EDIT_HOST用于输入目标主机或IP。按钮IDC_BTN_TRACE点击开始跟踪。列表控件IDC_LIST_RESULT以报告模式Report显示结果。添加四列“跳数”、“IP地址”、“主机名”、“延迟(ms)”。静态文本/进度条可选用于显示状态。使用 ClassWizard 为对话框创建类如CTraceRouteDlg并为控件关联变量。CString m_strHost关联IDC_EDIT_HOST。CListCtrl m_listResult关联IDC_LIST_RESULT。4.2 业务逻辑与线程处理网络操作是耗时的绝对不能放在 UI 主线程中执行否则界面会卡死。必须使用工作线程。在CTraceRouteDlg.cpp中// 定义线程参数结构 struct ThreadParam { CTraceRouteDlg* pDlg; CString strHost; }; // 静态线程函数 static UINT TraceRouteThread(LPVOID pParam) { ThreadParam* pThreadParam (ThreadParam*)pParam; CTraceRouteDlg* pDlg pThreadParam-pDlg; CString strHost pThreadParam-strHost; delete pThreadParam; // 记得释放内存 CTraceRoute tracer; // 设置回调用于在线程中更新UI需要跨线程 tracer.SetCallback(CTraceRouteDlg::TraceCallback, (LPVOID)pDlg-GetSafeHwnd()); pDlg-PostMessage(WM_USER_TRACE_STARTED); // 通知UI线程开始 BOOL bRet tracer.Trace(strHost); pDlg-PostMessage(WM_USER_TRACE_FINISHED, (WPARAM)bRet); // 通知UI线程结束 return 0; } // 回调函数由CTraceRoute在工作线程中调用 void CALLBACK CTraceRouteDlg::TraceCallback(int nHop, LPCTSTR lpszIP, LPCTSTR lpszHostName, DWORD dwRTT, LPVOID pParam) { HWND hWnd (HWND)pParam; if (::IsWindow(hWnd)) { // 打包数据发送消息到UI线程 HopData* pData new HopData; pData-nHop nHop; pData-strIP lpszIP; pData-strHostName lpszHostName; pData-dwRTT dwRTT; ::PostMessage(hWnd, WM_USER_HOP_FOUND, (WPARAM)pData, 0); } }自定义消息处理在对话框头文件中定义消息#define WM_USER_TRACE_STARTED (WM_USER 100) #define WM_USER_TRACE_FINISHED (WM_USER 101) #define WM_USER_HOP_FOUND (WM_USER 102) struct HopData { int nHop; CString strIP; CString strHostName; DWORD dwRTT; };在消息映射和实现中添加处理BEGIN_MESSAGE_MAP(CTraceRouteDlg, CDialogEx) ON_MESSAGE(WM_USER_TRACE_STARTED, CTraceRouteDlg::OnTraceStarted) ON_MESSAGE(WM_USER_TRACE_FINISHED, CTraceRouteDlg::OnTraceFinished) ON_MESSAGE(WM_USER_HOP_FOUND, CTraceRouteDlg::OnHopFound) END_MESSAGE_MAP() LRESULT CTraceRouteDlg::OnHopFound(WPARAM wParam, LPARAM lParam) { HopData* pData (HopData*)wParam; // 在列表控件中插入一行 int nIndex m_listResult.InsertItem(m_listResult.GetItemCount(), _T()); CString strHop; strHop.Format(_T(%d), pData-nHop); m_listResult.SetItemText(nIndex, 0, strHop); m_listResult.SetItemText(nIndex, 1, pData-strIP); m_listResult.SetItemText(nIndex, 2, pData-strHostName); if (pData-dwRTT 0) { CString strRTT; strRTT.Format(_T(%d ms), pData-dwRTT); m_listResult.SetItemText(nIndex, 3, strRTT); } else { m_listResult.SetItemText(nIndex, 3, _T(*)); } delete pData; // 释放内存 return 0; }按钮事件void CTraceRouteDlg::OnBnClickedBtnTrace() { UpdateData(TRUE); // 从控件更新变量 if (m_strHost.IsEmpty()) { AfxMessageBox(_T(请输入目标主机或IP地址)); return; } m_listResult.DeleteAllItems(); // 清空旧结果 GetDlgItem(IDC_BTN_TRACE)-EnableWindow(FALSE); // 禁用按钮防止重复点击 // 启动工作线程 ThreadParam* pParam new ThreadParam; pParam-pDlg this; pParam-strHost m_strHost; AfxBeginThread(TraceRouteThread, pParam); }4.3 提升用户体验的细节解析主机名GetHostNameFromIP函数可以使用getnameinfo或gethostbyaddr进行反向 DNS 查询但这是一个阻塞操作可能会很慢。建议在单独的线程中执行或者提供一个复选框让用户选择是否启用反向解析。超时处理CTraceRoute内部的接收超时SO_RCVTIMEO和整体的每跳超时m_nTimeout需要合理设置。太短容易误判为超时太长则整体跟踪时间过久。结果导出可以增加功能将m_arrHops中的结果导出为文本、CSV 或 HTML 格式。可视化高级功能可以尝试将路径在地图上进行可视化根据IP地理位置库但这需要额外的网络服务或本地数据库支持。5. 常见问题、调试技巧与性能优化在实际封装和使用过程中你会遇到各种各样的问题。下面是一些典型的坑和解决思路。5.1 权限问题与兼容性处理问题socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)失败错误代码 10013WSAEACCES。原因与解决Windows 上创建原始套接字需要管理员权限。方案一推荐但需提示用户在应用程序清单文件.manifest中增加requestedExecutionLevel为requireAdministrator。这样程序会直接以管理员身份运行。对于网络诊断工具这是合理的。requestedExecutionLevel levelrequireAdministrator uiAccessfalse/方案二功能降级捕获权限错误然后回退到使用IcmpCreateFile系列 API 仅实现ping功能并提示用户traceroute需要管理员权限。或者尝试使用 UDP 套接字SOCK_DGRAM进行跟踪这通常不需要管理员权限但行为可能与标准tracert有差异。5.2 接收不到响应或结果不全可能原因及排查防火墙拦截本地或路径上的防火墙可能丢弃了 ICMP 报文。这是最常见的原因。可以暂时关闭本地防火墙测试但无法控制中间节点。TTL 设置未生效确保在发送套接字上正确设置了IP_TTL选项并且是在sendto之前设置的。序列号匹配错误在ParseICMPResponse中检查标识符ID和序列号Sequence的匹配逻辑。确保发送和接收时使用的是相同的进程ID和序列号生成规则。注意网络字节序和主机字节序的转换htons,ntohs。缓冲区大小不足接收缓冲区recvBuf要足够大以容纳完整的 IP 包。建议至少 1024 字节。异步操作与超时recvfrom是阻塞的即使设置了SO_RCVTIMEO在多跳跟踪中如果某一跳完全无响应会导致该次接收阻塞整个超时时间。考虑使用select或WSAAsyncSelect实现异步 I/O以便更好地控制超时和取消。5.3 性能优化建议并行探测标准的traceroute每跳发送3个探测包以获取更稳定的 RTT。我们的循环是串行的发送-等待-发送下一跳。可以改为每跳同时发送3个包然后异步接收这样可以大幅缩短总跟踪时间。套接字复用目前每个 TTL 都创建新的发送套接字。可以考虑复用同一个发送套接字但需要在每次发送前用setsockopt重新设置IP_TTL。注意线程安全。DNS 解析异步化反向 DNS 查询IP-主机名非常耗时。可以将其放入线程池在获取到 IP 后立即开始解析而不阻塞主流程。等解析完成后再更新 UI。结果缓存对于经常跟踪的地址可以缓存路径结果短时间内再次跟踪时直接显示缓存并后台异步更新。5.4 增强健壮性内存管理确保所有动态分配的结构如ThreadParam,HopData在跨线程传递后都被正确释放防止内存泄漏。线程安全CTraceRoute类的m_arrHops等成员变量如果会被多个线程访问比如在跟踪过程中想实时读取结果需要使用临界区CRITICAL_SECTION或互斥量进行保护。错误处理对所有 Socket API、内存分配 API 的返回值进行严格检查并提供有意义的错误日志或用户提示。资源释放在Trace函数的任何错误退出路径上都要确保已经打开的套接字被正确关闭closesocket。封装一个稳定可靠的CTraceRoute类远不止是调用几个 API 那么简单。它涉及对网络协议的深刻理解、对操作系统特性的掌握、多线程编程的熟练运用以及对用户体验的细致考量。当你成功地将它集成到你的 MFC 应用中看着它流畅地绘出网络路径时你会觉得这些努力都是值得的。这个组件不仅能成为你工具箱中的利器其设计思想也能迁移到其他网络功能封装上比如端口扫描、网络带宽测试等。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻