You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:32:41