【知识总结】命令行系统服务端实现原理
命令行系统总框架命令行系统由客户端-服务器架构由命令行客户端以及命令行服务器组成。客户端功能以可执行文件的形式呈现用户直接调用接受用户在屏幕中的键入并发送用户的命令行信息给服务端最后接受服务端的回显并打印。服务端功能首先注册命令行接受客户端的命令行信息并解析处理最后给客户端发送回显信息。客户端与服务器端的通讯方式使用AF_UNIX套接字在同一主机上通信使用文件系统路径作为地址进程通过路径名进行连接。其流程如下图UserClientServerSocketFile用户键入命令行键入命令行信息并发送打开绑定文件 (AF_UNIX)确认连接发送数据 (通过 Socket)处理数据返回处理结果打印服务器返回的结果服务器连接失败错误 (文件不存在)提示错误alt[链接成功][连接失败]UserClientServerSocketFile命令行系统服务端处理流程全局变量与命令行注册首先要存储所有支持的命令及其对应的处理函数使用一个全局变量来存储这些信息。| | | | --- | --- | | 1 | typedef void (*CommandHandler)(int client_sock, char* arg); | | 2 | | | 3 | typedef struct { | | 4 | char command[MAX_CMD_LEN]; // 命令名称 | | 5 | CommandHandler handler; // 命令处理函数 | | 6 | } CommandEntry; | | 7 | | | 8 | CommandEntry command_table[MAX_COMMANDS]; // 全局命令表 |需要提供一个函数来注册命令以及绑定对应的处理函数命令刚注册函数command_register 。使用全局变量command_count记录当前注册的命令数量避免数组越界。| | | | --- | --- | | 1 | int command_count 0; // 当前注册的命令数量 | | 2 | void register_command(const char* command, CommandHandler handler) { | | 3 | if (command_count MAX_COMMANDS) { | | 4 | strncpy(command_table[command_count].command, command, MAX_CMD_LEN); | | 5 | command_table[command_count].handler handler; | | 6 | command_count; | | 7 | } else { | | 8 | fprintf(stderr, Command table is full!\n); | | 9 | } | | 10 | } |服务器启动与连接创建并绑定AF_UNIX套接字| | | | --- | --- | | 1 | int server_socket socket(AF_UNIX, SOCK_STREAM, 0); | | 2 | struct sockaddr_un server_addr; | | 3 | memset(server_addr, 0, sizeof(server_addr)); | | 4 | server_addr.sun_family AF_UNIX; | | 5 | strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1); | | 6 | unlink(SOCKET_PATH); // 删除旧的套接字文件 | | 7 | if (bind(server_socket, (struct sockaddr *)server_addr, sizeof(server_addr)) -1) { | | 8 | perror(Failed to bind socket); | | 9 | close(server_socket); | | 10 | exit(EXIT_FAILURE); | | 11 | } | | 12 | listen(server_socket, 5); |创建Epoll实例并将监听套接字添加到Epoll实例| | | | --- | --- | | 1 | int epfd epoll_create1(0); | | 2 | if (epfd -1) { | | 3 | perror(epoll_create1); | | 4 | exit(EXIT_FAILURE); | | 5 | } | | 6 | struct epoll_event ev; | | 7 | ev.events EPOLLIN; | | 8 | ev.data.fd server_socket; | | 9 | epoll_ctl(epfd, EPOLL_CTL_ADD, server_socket, ev); |监听事件并处理epoll模型一般会添加新连接,从而监控这个新连接的套接字,但是在这个命令行系统中客户端只发送一次请求服务器回复一次后关闭连接在accept后直接读取数据并处理之后发送命令行回显然后关闭连接。| | | | --- | --- | | 1 | while (1) { | | 2 | struct epoll_event events[1024]; | | 3 | int nready epoll_wait(epfd, events, 1024, -1); | | 4 | for (int i 0; i nready; i) { | | 5 | int fd events[i].data.fd; | | 6 | if (fd server_socket) { // 新客户端连接 | | 7 | int client_socket accept(server_socket, NULL, NULL); | | 8 | process_request(fd); | | 9 | // 关闭 | | 10 | close(client_socket); | | 11 | } | | 12 | } | | 13 | } | | 14 | | | 15 | // 将一个已经打开的文件描述符与一个文件流关联起来从而可以使用标准I/O库函数如 fprintf、fscanf、fgets 等来操作文件。 | | 16 | void process_request(int client_socket) { | | 17 | FILE *client_stream fdopen(client_socket, a); | | 18 | if (!client_stream) { | | 19 | perror(fdopen failed); | | 20 | return; | | 21 | } | | 22 | | | 23 | char buffer[MAX_COMMAND_LENGTH]; | | 24 | if (fgets(buffer, sizeof(buffer), client_stream) NULL) { | | 25 | fprintf(stderr, Failed to read from client\n); | | 26 | fclose(client_stream); | | 27 | return; | | 28 | } | | 29 | | | 30 | // 查找并执行命令 | | 31 | for (int j 0; j command_count; j) { | | 32 | if (strncmp(buffer, command_table[j].command, strlen(command_table[j].command)) 0) { | | 33 | command_table[j].handler(events[i].data.fd,buffer); | | 34 | break; | | 35 | } | | 36 | } | | 37 | | | 38 | fclose(client_stream); | | 39 | } |处理与发送打印信息需要定义一个结构体 CommandContext用于存储命令的上下文信息,put_format 函数用于格式化响应字符串并保存到 CommandContext 结构体中。| | | | --- | --- | | 1 | typedef struct CommandContext { | | 2 | char *response; | | 3 | size_t length; | | 4 | size_t allocated; | | 5 | } CommandContext; | | 6 | | | 7 | void put_format(CommandContext *ctx, const char *fmt, ...) { | | 8 | va_list args; | | 9 | va_start(args, fmt); | | 10 | vsnprintf(ctx-response, sizeof(ctx-response), fmt, args); | | 11 | va_end(args); | | 12 | } |write_str_to_socket函数用于将响应字符串通过套接字发送给客户端。| | | | --- | --- | | 1 | void write_str_to_socket(CommandContext *ctx, int client_socket) { | | 2 | write(client_socket, ctx-response, strlen(ctx-response)); | | 3 | } |提前注册好的处理函数,简单发送一个hello信息首先在处理函数中初始化上下文然后简要打印的回显通过put_format保存在回显上下文中最后通过write_str_to_socket发送信息| | | | --- | --- | | 1 | void handle_echo(int client_sock, char* arg) { | | 2 | CommandContext ctx {0}; | | 3 | put_format(ctx,hello!!) | | 4 | write_str_to_socket(ctx, client_socket); | | 5 | } |流程图全局处理函数注册表 命令行处理函数 命令行处理函数注册 创建Epoll实例并绑定AF_UNIX套接字 Epoll等待事件 接受客户端连接 解析客户端命令行数据 从注册表找到对应函数处理 初始化打印上下文 存储回显信息到上下文 发送回显信息到客户端 关闭文件流和文件描述符