ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++ WinSock实现Telnet相似功能:远程登录客户端编程详解

VC++ WinSock实现Telnet相似功能:远程登录客户端编程详解 简介面向Visual C开发者的Telnet客户端实现代码包演示如何利用Winsock接口编写类似Windows Telnet的远程登录程序。适合网络编程初学者、需要借鉴TCP/IP交互逻辑的开发者以及希望以最小工程示例理解远程登录原理的爱好者。压缩包共18个文件大小仅14KB以7个头文件与6个C源文件为主体另含工程文件、资源脚本和示例文本结构简明便于在VC环境中直接查阅、编译。已有272人学习。代码中封装了套接字收发与Telnet协议解析模块覆盖WSAStartup初始化、socket创建、远程连接、IAC命令交互及资源清理等关键环节且对错误处理和缓冲区管理有相应处理。借助包内说明可快速掌握Winsock编程骨架并在此基础上扩展命令历史、自动补全、颜色编码等交互能力也可进一步改造成多线程或安全登录工具。1. 先分清 Telnet 的“一样”与“相似”这个 VC 小工具究竟要做什么“vc基本实现了Windows中Telnet相似的功能”这句话我第一眼看到就知道不是在说“把完整 Telnet 协议栈搬进 VC”而是用 Visual C 加 WinSock写一个能连、能登录、能敲命令、能看输出的最小客户端。用到这个需求的场景很具体手里有一批只有 Telnet 管理口的路由器、光猫、嵌入式开发板想批量登录抓配置或者要在自己的 Windows 工具里内嵌一个远程命令行面板不想每次弹 cmd 去敲系统自带的 telnet。所以“相似”这个词是关键——它意味着可以砍掉终端类型协商、窗口尺寸更新这些繁琐的协议细节但 TCP 连接、收发循环、换行符、回显这几个底座一块都不能少。这个方向适合三种人做设备运维脚本的、用 VC 练 WinSock 但不想只写个 ping 的、以及给老项目补远程调试功能的。下面这套做法从 WinSock 初始化开始一路写到能跑的收发循环和排错清单。2. WinSock 最小 TCP 骨架初始化到断开的每一步2.1 为什么是 WinSock而不是 WinInet 或 C# 的 TcpClientWindows 上和网络相关的接口不少但真正适合做“Telnet 相似功能”的只有 WinSock。WinInet 和 WinHTTP 都是为 HTTP 族协议封装的拿它们做裸 TCP 属于拧着来C# 的 TcpClient 虽然写起来快但老设备批量处理工具经常要跑在没有 .NET 环境的机器上。更重要的是Telnet 排错时你要看的错误码、超时行为、半包现象在 WinSock 这一层才看得到。系统自带的 telnet.exe 其实是微软在命令行里实现好了的完整客户端你没法把它嵌入自己的循环里做自动登录所以“相似功能”必须自己用 socket 重写。从 VC6 的老工程到今天的 Visual Studio这套代码都能编译只需要确保链接 ws2_32.lib。我的做法是用#pragma comment(lib, ws2_32.lib)直接补上免去在工程配置里找依赖的麻烦。项目生成选项用“控制台应用程序”最省事调试时 printf 比任何 UI 都好使。2.2 初始化到连接WSAStartup、socket、connect 的职责边界WinSock 和普通 C 库函数最大的区别是用任何 socket 函数之前必须先调用 WSAStartup 加载协议栈退出前要 WSACleanup 释放。很多刚上手的人只调 WSAStartup 不调 WSACleanup短时间看不出问题但程序反复开关连接后会越跑越慢。#include winsock2.h #include ws2tcpip.h #include stdio.h #pragma comment(lib, ws2_32.lib) SOCKET ConnectHost(const char* host, int port) { WSADATA wsaData; // 向系统申请 2.2 版本的 WinSock老系统也能兼容 if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return INVALID_SOCKET; } SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return INVALID_SOCKET; } struct sockaddr_in addr { 0 }; addr.sin_family AF_INET; addr.sin_port htons((u_short)port); // 点分十进制 IP 的直接写法适合工具类软件 addr.sin_addr.S_un.S_addr inet_addr(host); if (addr.sin_addr.S_un.S_addr INADDR_NONE) { printf(Invalid IP: %s\n, host); closesocket(sock); WSACleanup(); return INVALID_SOCKET; } if (connect(sock, (struct sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(connect failed: %d\n, WSAGetLastError()); closesocket(sock); WSACleanup(); return INVALID_SOCKET; } return sock; }这段代码有几个参数要说明。AF_INET指 IPv4如果设备只支持 IPv6 要改用AF_INET6并换一套 sockaddr_in6 结构SOCK_STREAM是面向连接的流式套接字Telnet 基于 TCP 所以必须用它IPPROTO_TCP明确传输层协议。htons负责把端口号从主机字节序转成网络字节序端口 23 可以直接写但端口来自配置字符串时最容易忘转换。inet_addr在较新 SDK 里会提示用InetPton替代但老项目里保留它没有功能问题。connect 返回 0 只代表 TCP 三次握手完成不代表对端就是 Telnet 服务这个区分在后面第 4 章会专门讲。一个容易踩的细节是如果程序里只调用一次 ConnectHost那每次都要先 WSAStartup 再 WSACleanup会白白消耗几十毫秒。正确做法是在 main 里只启动一次 WinSock把 ConnectHost 改造成“不重复初始化”的版本。我习惯用全局变量记录启动状态函数开头先判断再决定要不要调 WSAStartup。2.3 收发职责划分为什么不能在主线程里 recv连接建立只是开始。Telnet 客户端要同时做两件事把键盘输入发给远端把远端返回打印到屏幕。如果按顺序先读键盘再收网络那么远端主动推送的数据比如欢迎 banner、定时日志会一直滞留在系统缓冲区直到你敲一次回车才显示。解决这个问题最常见的方案是开一个专门的接收线程。#include process.h // 接收线程只负责把远端数据打印到 stdout unsigned int __stdcall RecvThread(void* param) { SOCKET sock (SOCKET)param; char buf[4096]; while (1) { int n recv(sock, buf, sizeof(buf) - 1, 0); if (n 0) break; // 0 表示对端关闭SOCKET_ERROR 表示出错 buf[n] \0; printf(%s, buf); fflush(stdout); // 避免输出停在缓冲区里不刷新 } return 0; }recv是阻塞调用线程没数据时挂起等待不占 CPU。sizeof(buf)取 4096 是因为一屏文本通常不超过这个量但“通常”不等于“必然”真遇到设备一次性吐出几万字符日志时这个函数会把它切成多段打印画面上看是连续的但如果要做文本解析就会踩第 5 章说的半包问题。fflush(stdout)是必须的控制台在重定向到文件时尤其依赖这一句不然日志会缺尾巴。主线程那边只需要循环读键盘、调用 send。send 本身在数据量小时很快返回不需要单独线程。注意这个骨架还没有做任何 Telnet 协议协商只是把系统 telnet 的“裸 TCP 收发”部分先立住。2.4 正常断开与异常断开shutdown 的时机关闭连接不能直接 closesocket。如果接收线程正阻塞在 recv 上你先 closesocket 再等线程线程不一定立刻退出而且 WSACleanup 可能在一个还在使用 socket 的线程上被调用造成句柄泄漏。规范顺序是先shutdown(sock, SD_BOTH)让 recv 和 send 都返回失败线程自然退出然后closesocket(sock)释放资源最后等线程结束再WSACleanup。shutdown(sock, SD_BOTH); // 通知对端和本端都不再收发 closesocket(sock); WaitForSingleObject(hThread, 3000); // 等线程退出给 3 秒余量 WSACleanup();shutdown 的第二个参数有三个选择SD_RECEIVE 只禁止接收SD_SEND 只禁止发送SD_BOTH 全部禁止。在与设备交互的场景里用户输入“quit”后应该先给设备一点时间把剩余输出发完所以我通常先 SD_SEND 表示“我不再发数据了”等 recv 返回 0 后再 closesocket。这种做法能避免设备端收到 TCP RST 而丢失最后的日志。3. 把交互做成像 Telnet回显、行缓冲与换行符处理3.1 系统 telnet 命令怎么用差在哪一步在命令行里敲telnet 192.168.1.1 23很多人都会但你想自动化时就发现使唤不动它没有参数可以传用户名密码没有接口读取返回内容连超时时间都改不了。这就是“相似功能”存在的价值。观察系统 telnet 的行为你会发现它做了三件核心事本地逐字符读取键盘、把回车转换成正确的换行序列发送、把收到的数据直接显示。这三件事就是我说的“相似”底子。至于终端类型、窗口大小这些协商大多数嵌入式设备根本不依赖砍掉反而更稳。3.2 关掉控制台行缓冲逐字符还是逐行Windows 控制台默认有一个行缓冲机制ReadFile读键盘时只有按下回车才返回并且把回车带回给程序这对“逐行发命令”的自动化脚本够用但不像 telnet。真正的 telnet 客户端是每敲一个字符就发一个字符远端设备才能提供“输入密码时不回显”这类交互体验。要做到这一点必须用SetConsoleMode关闭控制台的 ENABLE_LINE_INPUT。HANDLE hStdin GetStdHandle(STD_INPUT_HANDLE); DWORD mode 0; GetConsoleMode(hStdin, mode); // 去掉行缓冲和本地回显改为按键即返回 mode ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); SetConsoleMode(hStdin, mode); char ch; DWORD n 0; while (ReadFile(hStdin, ch, 1, n, NULL) n 1) { // ch 里拿到的是原始按键方向键会变成两个字符的扫描码 printf(%c, ch); // 本地回显显示用户敲了什么 send(sock, ch, 1, 0); // 立刻发给远端 }关掉行缓冲的代价是 CtrlC、方向键、退格键都变成了原始按键序列程序要自己处理。逐字符模式适合需要“输入密码不回显”的场景但如果你要做的工具本质是“批量执行命令并抓结果”逐行模式反而更可靠——按行发送能减少粘包干扰也方便记录日志。我的建议是先默认逐行发送把命令封装成 SendLine将来需要模拟终端时再换逐字符不要让第一版就背上终端模拟的包袱。3.3 换行符转换设备只认 \n控制台给的是 \r\n这条是对“similar”还是“一模一样”的分水岭。Windows 记事本里的换行是 CRLFLinux 设备通常只认 LF而 Telnet 协议在传输层把 CRLF 当标准。直接用gets()拿到的字符串末尾带一个\nWindows 控制台还会在它前面补一个\r。把这个字符串原样 send 出去有的设备会多出一个^M回车符有的设备则完全无响应。兼容做法是发送前统一清理。void SendLine(SOCKET sock, const char* line) { char buf[2048]; memset(buf, 0, sizeof(buf)); strncpy(buf, line, sizeof(buf) - 2); // 去掉末尾的回车换行防止多敲 int len (int)strlen(buf); while (len 0 (buf[len - 1] \n || buf[len - 1] \r)) { buf[--len] \0; } // 按 Linux 嵌入式设备最通用的 \n 发送 buf[len] \n; buf[len 1] \0; send(sock, buf, len 1, 0); }这段代码的关键是“先修剪再补一个”。很多初学者只修剪不补发连上设备后敲命令没反应其实就是设备在等一个终止符。反过来如果设备只认\r\n多见于老交换机把buf[len] \n改成\r\n即可。我一般把换行模式做成全局变量连接阶段根据设备类型配置比每次调用都判断要省心。3.4 最小主循环连接、收发、退出一气呵成组合 2.2、2.3 和 SendLine就是一个能用的最小 telnet 相似客户端。主循环用 fgets 读整行匹配本地命令就先拦截普通内容走 SendLine。接收线程负责打印远端回显这样你每发一条命令输出立刻能被看到。int main() { SOCKET sock ConnectHost(192.168.1.1, 23); if (sock INVALID_SOCKET) return 1; HANDLE hThread (HANDLE)_beginthreadex(NULL, 0, RecvThread, (void*)sock, 0, NULL); char line[2048]; while (fgets(line, sizeof(line), stdin) ! NULL) { if (strncmp(line, quit, 4) 0) break; // 本地命令不发往远端 SendLine(sock, line); } shutdown(sock, SD_SEND); // 告诉远端不再发送 while (recv(sock, line, sizeof(line), 0) 0); // 把剩余输出收完 shutdown(sock, SD_BOTH); closesocket(sock); WSACleanup(); return 0; }这段循环有一个取舍收尾时用while (recv(...) 0);可能阻塞很久如果对端不关闭连接就一直收不完。更稳的办法是接收线程里收到用户输入 QUIT 后主线程只等一个固定超时比如 2 秒就强制关闭。超时设置的具体写法在第 4 章 select 一节里给。4. 连接体检端口通不通、服务是不是 Telnet4.1 对应“telnet ip 端口 命令怎么看通不通”的判断逻辑网上一搜“telnet ip 端口 命令怎么看通不通”绝大多数答案让你去控制面板开启 telnet 功能然后敲telnet 192.168.1.1 23界面变成全黑就是通。这个判断方式没问题缺点是一次只能试一个地址端口不通时还要干等。用自己写的 ConnectHost 函数判断更直接connect 返回 0 表示 TCP 握手成功返回 SOCKET_ERROR 再结合 WSAGetLastError 区分原因。下面是实际运维中最常碰到的几个错误码。WSAGetLastError数值含义常见原因WSAETIMEDOUT10060连接超时防火墙丢弃包、地址不可达WSAECONNREFUSED10061连接被拒绝端口没监听对端回了 RSTWSAENETUNREACH10065网络不可达路由不通、IP 配置错WSAEHOSTUNREACH10065主机不可达对端网关返回 ICMP 不可达排查顺序很重要先看 10061多半是服务没起来再看 10065查网段和路由最后才是 10060因为超时范围最大最常见的是虚拟机网络类型不对或设备访问控制列表拦了。把 connect 的返回值和错误码打印出来比 black-box 的 telnet.exe 直观得多。4.2 非阻塞 connect 加 select把判断控制在几秒内系统 telnet 在端口不通时能卡住几十秒原因是 connect 阻塞期间跟随系统 TCP 超时。要自己做超时控制不能简单调 connect要把 socket 先设成非阻塞connect 会立刻返回再用 select 等待可写事件。int ConnectWithTimeout(const char* host, int port, int seconds) { SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock INVALID_SOCKET) return -1; struct sockaddr_in addr { 0 }; addr.sin_family AF_INET; addr.sin_port htons((u_short)port); addr.sin_addr.S_un.S_addr inet_addr(host); u_long nonblock 1; ioctlsocket(sock, FIONBIO, nonblock); // 切到非阻塞模式 int ret connect(sock, (struct sockaddr*)addr, sizeof(addr)); if (ret SOCKET_ERROR) { int err WSAGetLastError(); if (err ! WSAEWOULDBLOCK) { // 地址错误这类立即失败 closesocket(sock); return -1; } } fd_set wset; FD_ZERO(wset); FD_SET(sock, wset); struct timeval tv { seconds, 0 }; ret select(0, NULL, wset, NULL, tv); // 等待 socket 可写 if (ret 0) { closesocket(sock); // 超时或出错端口大概率不通 return -1; } int err 0; int len sizeof(err); getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)err, len); if (err ! 0) { // 握手失败的具体原因 printf(connect error: %d\n, err); closesocket(sock); return -1; } u_long blocking 0; ioctlsocket(sock, FIONBIO, blocking); // 恢复阻塞方便后续 recv return (int)sock; }几个参数值得解释。FIONBIO的值为 1 时启动非阻塞connect 立即返回WSAEWOULDBLOCK10035表示握手在后台进行。select 的第三个参数传wset是等待“可写”TCP 握手完成时 socket 会变得可写。超时时间我一般设 2 到 5 秒小于 1 秒容易在局域网内误判。最后一定要getsockopt(SO_ERROR)确认一次因为 select 报告可写时connect 可能已经失败但事件集里同时带了错误标记。这套写法的用处不只是判断端口通不通。后续收发也可以改成 select 加超时防止设备断网后 recv 永远阻塞线程退不出来。把超时时间做成参数是第 2 章说的“不让界面卡死”的完整版。4.3 连上了没反应banner 与 Telnet 协商序列的辨识connect 成功但屏幕上没有字这是“telnet 测试不显示数据”的高频场景。连接建立不等于服务是 Telnet。Telnet 服务端一般会在连接后主动发一屏 banner并紧跟一段 IAC 协商序列如果收进来的第一个字节不是 0xFF 也不是可打印文本那很可能对面是 SSH、串口透传或某个自定义协议。用 recv 把初始数据读出来看特征char banner[512]; // 设置 2 秒超时收不到也继续 u_long mode 0; // 配合 select 做读超时 fd_set rset; FD_ZERO(rset); FD_SET(sock, rset); struct timeval tv { 2, 0 }; if (select(sock 1, rset, NULL, NULL, tv) 0) { int n recv(sock, banner, sizeof(banner) - 1, 0); banner[n] \0; // 打印十六进制和原始文本 for (int i 0; i n; i) printf(%02X , (unsigned char)banner[i]); printf(\n%s\n, banner); }如果十六进制开头是FF FD 1F之类说明服务端在等客户端回应终端类型协商如果开头是SSH-2.0-OpenSSH之类说明你连错了端口23 被映射成了 SSH如果完全没有任何数据且后续命令也石沉大海通常是设备管理口不是 Telnet 而是 Web 或私有协议。这一步能帮你快速决定“要不要继续往 Telnet 方向调”而不是对着黑屏浪费时间。5. 最容易翻车的 5 个细节协商、粘包与控制台乱码排查5.1 屏幕上冒出“255 251 1”这类乱码现象连接光猫或交换机后在欢迎语前后出现ÿý之类的怪字符有时还成串出现界面变得没法看。原因这是远程设备发来的 Telnet 协议协商包。0xFF 是 IAC解释命令标记后面跟的是 WILL、DO 等协商指令比如FF FD 01表示“希望客户端支持回显”。把这些字节直接按文本打印自然成了乱码。解决在接收线程里加一个协商过滤函数读到 0xFF 就跳过后续两到三个字节。这里“跳过后面的字节”这个做法其实不完全规范但绝大多数设备的协商序列很短且不回应也不会导致功能异常。做过滤时要注意 IAC 可以连续出现而且被转义的 0xFF 用序列FF FF表示过滤函数要保留后一个 0xFF。一个简单实现如下// 把 recv 拿到的原始字节过滤掉 IAC 协商普通数据留在 out 里 int StripIac(const char* in, int len, char* out) { int n 0; int i 0; while (i len) { unsigned char c (unsigned char)in[i]; if (c 0xFF) { // 进入 IAC 序列 i; if (i len) break; unsigned char cmd (unsigned char)in[i]; if (cmd 0xFF) { // 转义的普通 0xFF out[n] (char)0xFF; } else if (cmd 0xFB) { i 2; // WILL/WONT/DO/DONT 后面带一个选项号 } i; } else { out[n] (char)c; i; } } return n; }5.2 输入密码时回显两次或者干脆看不见现象连接 Linux 设备时敲密码屏幕出现两串同样的字符连接某些光猫时敲密码完全没反应像键盘失效。原因第一个情况是“双重回显”设备开了远程回显本地控制台也开了回显。第二个情况正相反设备端关闭了远程回显但本地控制台的 ECHO_INPUT 也被关了你输入的字符全被吞掉。解决本地控制台默认带 ECHO_INPUT如果目标是 Linux 设备最简单方案是关闭本地回显让设备决定显示什么。用SetConsoleMode去掉ENABLE_ECHO_INPUT即可。如果目标是某些不协商回显的老设备本地回显反而能帮你确认键盘输入有没有被发出去。我把这个做成一个开关参数默认“自动”即收到远程的 ECHO 协商时自动切换。关键点是telnet 协议本身不默认关闭本地回显新旧设备的行为可能相反。调试策略是在程序里维护一个echoLocal全局变量收到FF FD 01对方要求我们回显时置 true收到FF FB 01对方自己要回显时置 false再配合控制台模式切换。这条逻辑虽然小却是“相似”功能给用户观感提升最大的一处。5.3 设备一次输出几万字符读到半截命令就“粘”住了现象执行display current-config这类命令时程序没有一次把内容显示完而是先打印一半卡住等几秒再吐一段更麻烦的是对输出做字符串匹配时永远匹配不上。原因TCP 是字节流没有消息边界。recv 返回的字节数完全由网络决定一次可能返回半行也可能一次返回三百行。把网络数据按“完整一行”处理是错误假设。解决自己维护一个接收缓冲区把 recv 的内容先拼进去遇到\n才把完整的一行交给上层处理。下面这个结构是处理粘包和半包的通用做法。char g_buf[16384]; int g_len 0; // 把新数据追加到缓冲区碰到换行就取出一行 void AppendAndSplit(SOCKET sock, char* outLine, int maxLine) { int n recv(sock, g_buf g_len, sizeof(g_buf) - g_len - 1, 0); if (n 0) return; g_len n; g_buf[g_len] \0; char* p strchr(g_buf, \n); if (p) { int lineLen (int)(p - g_buf 1); if (lineLen maxLine) { memcpy(outLine, g_buf, lineLen); outLine[lineLen] \0; } // 已消费的数据移出缓冲区剩余部分下次继续拼 memmove(g_buf, p 1, g_len - lineLen); g_len - lineLen; g_buf[g_len] \0; } }这里的坑是缓冲区大小。g_buf取 16384 是因为很多设备的单行配置不超过这个量但极端情况下一行 JSON 日志也可能超所以调用处要处理缓冲区满但还没见到\n的情况。memmove代替memcpy是因为源和目标区域重叠用 memcpy 会出错。实际工程里我常常直接把这套逻辑封装成一个 C 类接收线程每读到一个\n就触发一个回调解析代码就清爽得多。5.4 用域名或主机名连接时卡死返回的端口还是 0现象用ConnectHost(router.local, 23)时程序直接返回 Invalid IP或者手输了一个端口字符串0023连上后发命令对方没反应。原因inet_addr只认点分十进制不解析域名。端口则可能是在某处用了atoi(0023)后得到 23但更常见的是htons参数本身已经是网络字节序转了两次导致端口错乱。解决需要域名支持就换getaddrinfo它同时处理了 IPv4 和 IPv6返回struct addrinfo链表直接传给 connect 更标准。struct addrinfo hints { 0 }, *result NULL; hints.ai_family AF_UNSPEC; hints.ai_socktype SOCK_STREAM; hints.ai_protocol IPPROTO_TCP; char portStr[8]; sprintf(portStr, %d, port); int rc getaddrinfo(host, portStr, hints, result); if (rc ! 0 || result NULL) { printf(getaddrinfo failed: %d\n, rc); return -1; } // 遍历链表逐个尝试 connect成功后跳出注意 getaddrinfo 是阻塞解析在域名服务器不通时会卡得比较久解决办法是把解析放到工作线程或干脆在 UI 上提示“解析中”。对于这个工具我一般直接要求用户填 IP域名解析交给上层代码瘦客户端要保持简单。端口处理上养成“字符串转数字后立刻 htons数据结构里始终存网络字节序”的习惯能省掉不少排查时间。5.5 中文输出乱码远程显示不全现象连接 Linux 设备执行命令返回里的中文全是或花码有时候连英文制表符都错位。原因Windows 控制台默认代码页是 GBK936而远程设备输出的是 UTF-8。recv 收到的是原始字节printf 按 GBK 解释 UTF-8 的中文字节自然全乱。解决最省事的办法是在程序入口把控制台输出代码页设成 UTF-8SetConsoleOutputCP(CP_UTF8);这行对 Win10 及以上的系统有效。老系统如果还乱就只能在接收线程里做 UTF-8 到 GBK 的转换用WideCharToMultiByte或MultiByteToWideChar配合中转。另一个做法是让设备端切换到 GBK locale但大部分网络设备不支持所以改自己程序更现实。需要注意的是SetConsoleOutputCP改了之后printf 的英文不受影响但如果你自己的源码里写了中文字符串字面量要保证源文件保存为 UTF-8否则编译出的可执行文件里字符串本身是乱的。6. 验证与无人值守把“相似”压实成可靠工具做一个“相似”功能最忌讳的是连上了就觉得完事。我一般用三步验证先和系统 telnet 对拍再跑一遍自动命令序列最后检查日志。第一步同一台设备、同一串命令分别用系统 telnet 和自己程序跑一遍把输出存成文件做 diff。系统 telnet 会留下本地回显自己的程序如果没有逐字符模式就没有回显所以 diff 前要先把回显行过滤掉。第二步把命令“enable、configure terminal、show running-config”写进一个文本文件程序按行读取并发送每行发送后等待输出的“#”或“”提示符再发下一行。这个“等提示符”的逻辑是自动化登录的关键sleep 固定时间不可靠因为在设备繁忙时 500 毫秒可能不够空闲时又白白浪费两秒。第三步记录原始收发日志这是你在第 5 章排查协商、粘包问题时的第一手资料。进阶方向是把程序改造成无人值守模式启动参数传入 IP、端口、用户名、密码和命令文件路径程序自动登录、逐条发命令、把结果写进文件。这样配合 Windows 计划任务就能实现“每天早上 8 点自动备份路由器配置”这类真实运维需求。日志文件我建议一行只记一类数据前缀-表示本机发出前缀-表示远端原始数据这条约定是血泪教训换来的否则隔天回看根本分不清是设备回显还是本地回显。我做这类工具的一个习惯是从第一版起就把原始收发日志开关留好默认关闭、出问题再打开文件按日期滚动。别觉得这功能小等你在生产环境里追一个偶发的乱码问题时就知道凌晨三点还有日志可查是多么幸运。希望这些从连接到底层协议、从超时到粘包的写法能帮到你少走几趟我走过的弯路。本文还有配套的精品资源点击获取
返回列表