基于C语言开发带交互式菜单的TCP Telnet服务器技术咨询
嘿,我懂你这种从零到一的迷茫——从简单的文本收发跳到带交互式菜单的Telnet服务器,确实得迈过几个小坎儿。别慌,咱们把这个需求拆成一个个你能搞定的小模块,一步步来实现。
核心先搞明白:Telnet与终端控制
Telnet本质是基于TCP的,但和你之前写的简单文本交互不同,它要处理终端控制序列:比如用户按箭头键时,客户端发的不是普通字符,而是ESC[A(上箭头)、ESC[B(下箭头)这类转义码;同时服务器要通过ANSI转义序列控制客户端终端的显示(清屏、光标移动、高亮选中项)。
另外,默认Telnet客户端会发一堆选项协商报文(比如WILL/WONT),咱们得简单处理下,不然会收到乱码或者客户端卡住。
第一步:搭好基础TCP服务器框架
这个你应该熟,先把能和Telnet建立连接、收发文本的基础架子搭起来。先做单客户端版本,后续再扩展多客户端:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <time.h> #include <popen.h> #define PORT 2323 // 不用默认23,避免权限问题 void send_menu(int client_socket, char **menu, int menu_size, int selected); void handle_selection(int client_socket, int selected); void handle_client(int client_socket); int main() { int server_fd, new_socket; struct sockaddr_in address; int opt = 1; socklen_t addrlen = sizeof(address); // 创建socket if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 允许端口复用 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // 绑定端口 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_fd, 3) < 0) { perror("listen"); exit(EXIT_FAILURE); } printf("Waiting for Telnet connections on port %d...\n", PORT); // 接受客户端连接 if ((new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen)) < 0) { perror("accept"); exit(EXIT_FAILURE); } printf("Client connected!\n"); handle_client(new_socket); close(new_socket); close(server_fd); return 0; }
第二步:处理Telnet控制序列与菜单导航
接下来写handle_client函数,核心是识别箭头键的转义序列,更新选中的菜单项,然后重新绘制菜单:
void handle_client(int client_socket) { char buf[1024]; int n; int selected = 0; // 当前选中的菜单项索引 char *menu[] = {"Option 1: View Server Time", "Option 2: Check Uptime", "Option 3: Exit"}; int menu_size = sizeof(menu)/sizeof(menu[0]); // 先发送初始菜单 send_menu(client_socket, menu, menu_size, selected); while (1) { n = read(client_socket, buf, sizeof(buf)-1); if (n <= 0) { printf("Client disconnected\n"); break; } buf[n] = '\0'; // 处理Telnet选项协商(简单忽略或回复DONT) if (buf[0] == 0xFF) { // Telnet命令开头是0xFF // 回复DONT拒绝所有选项,避免客户端卡住 char dont_response[] = "\xFF\xFC\x01\xFF\xFC\x03"; // 示例:拒绝ECHO和SUPPRESS GO AHEAD send(client_socket, dont_response, strlen(dont_response), 0); continue; } // 处理箭头键转义序列 if (buf[0] == 0x1B) { // ESC开头 if (n >= 3 && buf[1] == '[' && buf[2] == 'A') { // 上箭头:选中项减1,边界判断 if (selected > 0) selected--; send_menu(client_socket, menu, menu_size, selected); } else if (n >= 3 && buf[1] == '[' && buf[2] == 'B') { // 下箭头:选中项加1 if (selected < menu_size-1) selected++; send_menu(client_socket, menu, menu_size, selected); } } else if (buf[0] == '\r' || buf[0] == '\n') { // 回车:处理选中的选项 handle_selection(client_socket, selected); if (selected == menu_size-1) { // 退出选项 send(client_socket, "Goodbye!\n", strlen("Goodbye!\n"), 0); break; } // 回到菜单 send_menu(client_socket, menu, menu_size, selected); } // 忽略其他普通字符,菜单只接受箭头和回车 } }
第三步:用ANSI转义序列绘制交互式菜单
send_menu函数负责向客户端发送终端控制码,清屏、移动光标、高亮选中项:
void send_menu(int client_socket, char **menu, int menu_size, int selected) { // 清屏 + 光标回到左上角 char clear_screen[] = "\033[2J\033[H"; send(client_socket, clear_screen, strlen(clear_screen), 0); // 发送菜单标题 char title[] = "=== My Telnet Server Menu ===\nUse arrow keys to navigate, Enter to select\n\n"; send(client_socket, title, strlen(title), 0); // 逐个渲染菜单项 for (int i = 0; i < menu_size; i++) { if (i == selected) { // 高亮选中项(加粗) char highlight_on[] = "\033[1m"; char highlight_off[] = "\033[0m"; send(client_socket, highlight_on, strlen(highlight_on), 0); send(client_socket, menu[i], strlen(menu[i]), 0); send(client_socket, highlight_off, strlen(highlight_off), 0); } else { send(client_socket, menu[i], strlen(menu[i]), 0); } send(client_socket, "\n", 1, 0); } }
第四步:处理选中后的交互逻辑
handle_selection函数根据用户选择执行对应操作,比如显示时间、查看系统运行时间:
void handle_selection(int client_socket, int selected) { char response[1024]; char buf[10]; // 用来接收用户回车 switch(selected) { case 0: // 显示服务器当前时间 time_t now = time(NULL); snprintf(response, sizeof(response), "\nCurrent Server Time: %s\nPress Enter to return...", ctime(&now)); send(client_socket, response, strlen(response), 0); read(client_socket, buf, sizeof(buf)); // 等待用户按回车 break; case 1: // 查看系统运行时间 FILE *fp = popen("uptime", "r"); if (fp) { send(client_socket, "\nServer Uptime:\n", strlen("\nServer Uptime:\n"), 0); char line[256]; while (fgets(line, sizeof(line), fp)) { send(client_socket, line, strlen(line), 0); } pclose(fp); } else { send(client_socket, "\nFailed to get uptime\n", strlen("\nFailed to get uptime\n"), 0); } send(client_socket, "\nPress Enter to return...", strlen("\nPress Enter to return..."), 0); read(client_socket, buf, sizeof(buf)); break; case 2: // 退出逻辑在主循环处理 break; default: send(client_socket, "\nInvalid selection\n", strlen("\nInvalid selection\n"), 0); read(client_socket, buf, sizeof(buf)); break; } }
几个关键注意事项
- Telnet选项协商:如果客户端发一堆
0xFF开头的报文,一定要处理(比如回复DONT),不然客户端可能会卡住。 - 多客户端支持:上面是单客户端版本,要支持多客户端的话,给每个
accept的连接开一个线程或者子进程即可。 - 输入缓冲区拼接:有时候箭头键的转义序列会被拆分成多次读取,需要维护一个缓冲区来拼接不完整的序列(比如先读到
ESC,下次读[A再合并判断)。 - 安全性:如果要执行系统命令,绝对别直接用用户输入,最好限制在预定义的安全命令列表里,避免被恶意利用。
- 终端兼容性:ANSI转义序列在Linux/macOS终端、PuTTY、Windows Terminal都能正常工作,但Windows默认的Telnet可能需要开启ANSI支持。
内容的提问来源于stack exchange,提问作者micsza
相关产品推荐
相关产品推荐

