资讯动态

VC++实现Windows Telnet客户端:Winsock协议协商与收发实战

发布时间:2026/10/9 14:16:52 来源:尧图企业网站定制
简介这份资源面向具备一定C基础、希望深入理解TCP/IP网络编程的开发者核心是用Visual C实现与Windows Telnet相似功能的客户端程序。包内共18个文件以7个h头文件与6个cpp源文件为主体分别承担套接字收发、协议解析、主程序逻辑等模块职责另含dsw、dsp工程文件、clw类向导文件、rc资源脚本及一份txt说明压缩包约14KB结构紧凑便于直接编译学习。内容围绕Winsock库展开涉及WSAStartup初始化、socket创建、gethostbyname或getaddrinfo解析地址、connect建立连接以及send与recv收发数据、IAC命令处理、closesocket与WSACleanup资源释放等关键环节并留有命令历史、自动补全、颜色编码等扩展空间。目前已有272人学习下载适合作为网络编程练手项目帮助读者掌握面向连接通信流程与Windows套接字接口的实际运用。1. 从 VC 里长出一个 Telnet为什么老工程师还在折腾这件事在 Windows 上做设备调试绕不开一个场景手头只有一台装着 VC 的机器要连串口服务器、连光猫、连工控板对方只认 Telnet 那套协商协议。现成的终端工具要么装不上要么在老旧系统上跑不起来于是「用 VC 基本实现 Windows 中 Telnet 相似的功能」就成了一个很实际的需求——不是复刻一个 PuTTY而是用 Winsock 把 TCP 连接、IAC 选项协商、字符收发这三件事做扎实让 VC 编译出来的小程序能顶上一个轻量终端。这件事适合两类人一类是在 VC 6 或 VS 老版本环境里维护工控、网络设备上位机的工程师需要把 Telnet 能力嵌进自己的程序另一类是刚接触网络编程、想找一个「协议不复杂但足够真实」的练手项目的人。它解决的核心问题很明确不依赖外部终端自己控制连接、协商和数据显示。下面按「协议怎么立住 → 代码怎么落地 → 坑在哪 → 怎么验证」推一遍能直接抄去改。2. Telnet 协议在 VC 里怎么立住协商模型与 Winsock 选型2.1 先搞清楚 Telnet 不是「裸 TCP」很多人第一次写 Telnet 客户端直接connect完就send/recv结果连上设备后屏幕一片乱码或者干脆卡死。原因在于 Telnet 在 TCP 之上还有一层选项协商双方用IAC0xFF开头的三字节序列互相告知「我支持什么、你支持什么」。最常见的几条是命令值含义IAC255命令引导字节WILL251我方将启用某选项WONT252我方不启用某选项DO253请求对方启用某选项DONT254请求对方禁用某选项SB250子协商开始SE240子协商结束选项码里最常碰到的是ECHO(1)、SUPPRESS_GO_AHEAD(3)、TERMINAL_TYPE(24)、NAWS(31)。设备发来IAC DO ECHO意思是「你回显吧」你如果不懂把这几个字节当普通字符显示出来就是一堆ÿûÿû的乱码。所以第一件事收到数据先做状态机解析把 IAC 序列从普通数据里剥出来。2.2 Winsock 版本选择别一上来就用最新VC 6 自带的是 Winsock 1.1ws2_32.lib需要手动加。我的习惯是统一用 Winsock 2.2因为WSAStartup之后的getaddrinfo、非阻塞ioctlsocket都好用。初始化代码// TelnetClient.cpp : Winsock 初始化与连接 #include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) SOCKET g_sock INVALID_SOCKET; bool InitAndConnect(const char* host, unsigned short port) { WSADATA wsa; // 请求 2.2 版本MAKEWORD 低字节主版本、高字节次版本 if (WSAStartup(MAKEWORD(2, 2), wsa) ! 0) return false; // 用 getaddrinfo 兼容 IPv4/IPv6比 gethostbyname 更稳 addrinfo hints { 0 }, *res nullptr; hints.ai_family AF_UNSPEC; hints.ai_socktype SOCK_STREAM; hints.ai_protocol IPPROTO_TCP; char portStr[8]; _snprintf(portStr, sizeof(portStr), %u, port); if (getaddrinfo(host, portStr, hints, res) ! 0) return false; g_sock socket(res-ai_family, res-ai_socktype, res-ai_protocol); if (g_sock INVALID_SOCKET) { freeaddrinfo(res); return false; } if (connect(g_sock, res-ai_addr, (int)res-ai_addrlen) SOCKET_ERROR) { closesocket(g_sock); g_sock INVALID_SOCKET; freeaddrinfo(res); return false; } freeaddrinfo(res); return true; }逻辑说明WSAStartup必须在任何 socket 调用之前且要和WSACleanup配对getaddrinfo返回的是链表这里只用第一个结果生产代码应遍历直到connect成功。参数上MAKEWORD(2,2)是版本协商传1,1也能跑但拿不到新 API。端口默认 23调试光猫、串口服务器时常被改成 2323 或 8023这个要按设备实际填。2.3 协商状态机把 IAC 从数据流里剥出来核心是一个逐字节的状态机状态在「普通数据」和「收到 IAC」之间切换。下面这段是精简版够用且好懂// 协商状态机处理 IAC 命令普通字节回填到 out enum { ST_DATA, ST_IAC, ST_OPT } g_state ST_DATA; unsigned char g_optCmd 0; void ParseTelnet(const unsigned char* in, int len, std::string out) { for (int i 0; i len; i) { unsigned char c in[i]; switch (g_state) { case ST_DATA: if (c 255) g_state ST_IAC; // IAC else out.push_back((char)c); // 普通数据 break; case ST_IAC: if (c 255) { out.push_back((char)255); g_state ST_DATA; } // 转义 255 else if (c 251 c 254) { g_optCmd c; g_state ST_OPT; } else g_state ST_DATA; // 其他命令先忽略 break; case ST_OPT: HandleOption(g_optCmd, c); // 回应 WILL/DO 等 g_state ST_DATA; break; } } }逻辑说明ST_IAC里遇到第二个 255 表示「数据里的 255」要原样输出遇到 251~254 进入选项状态读一个选项码后交给HandleOption。参数上HandleOption里通常对DO ECHO回WILL ECHO对DO SGA回WILL SGA其余一律回WONT/DONT避免设备等你回应而卡住。这一步做对乱码问题基本消失。3. 把收发和显示跑起来线程、缓冲与最小可运行程序3.1 收数据必须独立线程别在主线程 recvTelnet 是长连接recv会阻塞。如果放在 UI 线程里界面直接假死。常见做法是开一个接收线程循环recv把解析后的数据投递到主线程刷新。VC 6 里没有 lambda用全局缓冲加自定义消息最省事// 接收线程阻塞 recv解析后投递到 UI #define WM_TELNET_DATA (WM_USER 100) HWND g_hWnd NULL; DWORD WINAPI RecvThread(LPVOID) { char buf[4096]; while (g_sock ! INVALID_SOCKET) { int n recv(g_sock, buf, sizeof(buf), 0); if (n 0) break; // 0对端关闭0错误 std::string plain; ParseTelnet((unsigned char*)buf, n, plain); if (!plain.empty()) { // 拷贝到堆PostMessage 后由 UI 线程释放 std::string* p new std::string(plain); PostMessage(g_hWnd, WM_TELNET_DATA, 0, (LPARAM)p); } } return 0; }逻辑说明recv返回 0 表示对端正常关闭负值要WSAGetLastError看是不是WSAEWOULDBLOCK非阻塞模式下正常。参数上缓冲区 4096 是经验值太小会频繁触发协商解析太大在低速设备上反而增加延迟。用PostMessage而不是直接操作控件是因为跨线程碰 UI 是 VC 里最经典的翻车点。3.2 发送侧回车要发 CR LF不是 LF设备端大多按\r\n判断一行结束。你只发\n对方可能一直等\r表现就是「命令敲了没反应」。发送函数bool SendLine(const char* text) { if (g_sock INVALID_SOCKET) return false; std::string line(text); line \r\n; // Telnet 行结束用 CR LF int sent send(g_sock, line.c_str(), (int)line.size(), 0); return sent (int)line.size(); }逻辑说明send返回值可能小于请求长度尤其大包严格实现要循环发完。参数上\r\n是 Telnet 的 NVT 规范个别设备只认\r遇到不响应就换\r试。这一步没有玄学就是按对端脾气来。3.3 最小可运行程序的组装顺序把上面几块拼起来主流程是InitAndConnect→ 创建接收线程 → 进入消息循环 → 收到WM_TELNET_DATA就追加到编辑框 → 用户输入走SendLine。VC 6 里建一个基于对话框的 MFC 工程即可g_hWnd在OnInitDialog里赋值。编译时确认链接了ws2_32.lib否则报一堆unresolved external。跑通后连telnet 192.168.1.1那类设备登录名密码按设备实际填能看到回显和提示符就说明协商和收发都对了。4. 避坑与排查那些让程序「看着能跑其实不对」的细节4.1 现象连上就满屏ÿûÿû命令发出去没回显原因协商状态机没写或者HandleOption里对DO ECHO回了DONT设备认为你不回显于是它也不回显你看到的全是原始 IAC 字节。解决确认ParseTelnet在recv之后第一时间调用且对DO ECHO、DO SGA回WILL。用十六进制打印原始流看到FF FB 01IAC WILL ECHO就说明协商在走。4.2 现象程序在别人机器上能跑在自己机器上connect直接失败原因防火墙拦了出站或者目标端口写错。VC 6 编译的程序默认没有清单某些系统上会被安全软件当可疑进程。解决先用系统自带telnet或Test-NetConnection确认端口通不通再排查代码。参数上connect失败后一定WSAGetLastErrorWSAETIMEDOUT和WSAECONNREFUSED是两回事前者是网络不通后者是端口没开服务。4.3 现象接收线程里直接更新编辑框偶尔崩溃原因跨线程操作 MFC 控件MFC 对象有线程亲和性。解决一律PostMessage投递到 UI 线程处理堆上分配的数据在消息处理函数里delete。这是血泪经验别图省事直接SetWindowText。4.4 现象中文显示成乱码原因Telnet 本身不管编码设备发的是 GBK 还是 UTF-8 取决于设备。解决接收缓冲按字节存显示前统一转成目标编码。VC 6 里用MultiByteToWideChar指定代码页别默认按 ANSI 猜。参数上代码页 936 是 GBK65001 是 UTF-8按设备手册选。4.5 现象长时间挂机后连接悄悄断开原因中间网络设备清理空闲连接或者对端超时。解决加心跳定期发IAC NOPFF F1或一个空行。注意心跳别太频繁30 秒一次足够太密反而被设备当异常流量。这个没有后悔药断了只能重连所以重连逻辑要写。5. 进阶把协商做全、把连接做稳的验证方法协商做全的意思是支持子协商SB ... SE典型是TERMINAL_TYPE和NAWS窗口大小。设备发IAC SB TERMINAL_TYPE IAC SE你要回IAC SB TERMINAL_TYPE IS VT100 IAC SE。这段代码比基础状态机多一个ST_SB状态收到SE才结束。不做也能连但有些设备会一直重发浪费带宽。验证方法我一般分三层第一层用Wireshark抓包过滤tcp.port 23看协商序列是否符合预期IAC开头的包一眼能认出来第二层用系统telnet连同一台设备对比行为差异差异点就是你的 bug第三层写一个回环测试本机开一个监听 socket自己连自己发固定字节序列断言解析输出。第三层最省事不依赖真实设备。一个具体技巧把ParseTelnet的输入输出打成日志文件格式是「原始十六进制 → 解析后文本」跑一天下来所有协商组合都能覆盖到。这个日志在排查「设备偶尔不回显」这类玄学问题时特别有用比盯着屏幕猜强得多。连接稳定性上SO_KEEPALIVE可以开但 Windows 默认 2 小时才探测实际意义不大还是自己写应用层心跳靠谱。重连策略用指数退避1 秒、2 秒、4 秒封顶 30 秒避免设备重启时被你的程序打爆。我自己维护这套代码的习惯是协商状态机单独一个文件收发线程单独一个文件UI 只负责显示和输入三层之间用消息和缓冲解耦。这样下次换个设备、加个选项只动状态机那一块不会牵一发动全身。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑