目录

Linux应用开发之在线词典项目

Linux应用开发之在线词典项目

一、系统整体架构

该在线字典查询系统采用 C/S(客户端 - 服务器)架构,基于 TCP 协议实现可靠的网络通信,结合 SQLite 数据库进行数据持久化存储,同时依赖本地文本文件作为单词库来源,支持用户注册、登录、单词查询及历史记录管理等核心功能,并区分管理员(root)与普通用户权限。

二、核心数据结构

MSG 结构体(通信协议)


// 定义通信双方的信息结构体
typedef struct {
    int type;               // 消息类型(R=注册、L=登录、Q=查询、H=历史)
    char name[N];           // 用户名(N=32)
    char data[256];         // 存储密码、单词或服务器返回的消息
    int root;               // 标识是否为root用户(1是,0否)
} MSG;

// 消息类型宏定义
#define  R  1   // 用户注册
#define  L  2   // 用户登录
#define  Q  3   // 单词查询
#define  H  4   // 历史记录查询

代码解析

  • 该结构体是客户端与服务器之间通信的基础,统一了数据交换格式
  • type字段用于区分不同的业务请求类型
  • name字段存储用户名,在整个会话中保持一致
  • data字段是多功能字段,根据不同操作存储不同内容
  • root字段用于权限控制,区分管理员和普通用户

三、核心模块与功能

(一)客户端模块

客户端作为用户交互的入口,负责接收用户操作、与服务器通信并展示结果,主要包含以下功能子模块:

1. 网络初始化模块

int main(int argc, char *argv[]) {
    int sockfd;
    struct sockaddr_in serveraddr;
    MSG msg;

    // 检查命令行参数
    if(argc != 3) {
        printf("Usage:%s serverip  port.\n", argv[0]);
        return -1;
    }
    
    // 创建TCP套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("fail to socket");
        return -1;
    }
 
    // 初始化服务器地址结构体
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  // 服务器IP
    serveraddr.sin_port = htons(atoi(argv[2]));       // 服务器端口

    // 连接服务器
    if(connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        perror("connect failed");
        return -1;
    }
    
    // 进入主菜单循环
    // ...
}

代码解析

  • socket()函数创建 TCP 套接字,返回文件描述符
  • struct sockaddr_in用于存储服务器地址信息
  • inet_addr()将点分十进制 IP 转换为网络字节序
  • htons()将端口号转换为网络字节序
  • connect()向服务器发起连接请求,完成 TCP 三次握手
2. 用户注册功能

int do_register(int sockfd, MSG *msg) {
    msg->type = R;  // 设置消息类型为注册
    
    // 获取用户输入
    printf("Input name:");
    scanf("%s", msg->name);
    getchar();  // 吸收换行符
    
    printf("Input passwd:");
    scanf("%s", msg->data);
    
    // 发送注册信息到服务器
    if(send(sockfd, msg, sizeof(MSG), 0) < 0) {
        printf("fail to send.\n");
        return -1;
    }
    
    // 接收服务器响应
    if(recv(sockfd, msg, sizeof(MSG), 0) < 0) {
        printf("Fail to recv.\n");
        return -1;
    }
    
    // 显示注册结果
    printf("%s\n", msg->data);
    return 0;
}

代码解析

  • 函数首先设置消息类型为 R (注册)
  • 通过标准输入获取用户名和密码
  • 使用send()函数将注册信息发送到服务器
  • 使用recv()函数接收服务器返回的注册结果
  • 最后将结果显示给用户(成功或用户名已存在)
3. 用户登录功能

int do_login(int sockfd, MSG *msg) {
    msg->type = L;  // 设置消息类型为登录
    
    // 获取用户输入
    printf("Input name:");
    scanf("%s", msg->name);
    getchar();
    
    printf("Input passwd:");
    scanf("%s", msg->data);
    
    // 发送登录信息到服务器
    if(send(sockfd, msg, sizeof(MSG), 0) < 0) {
        printf("fail to send.\n");
        return -1;
    }
    
    // 接收服务器响应
    if(recv(sockfd, msg, sizeof(MSG), 0) < 0) {
        printf("Fail to recv.\n");
        return -1;
    }
    
    // 判断登录结果
    if(strncmp(msg->data, "OK", 3) == 0) {
        printf("Login ok!\n");
        // 判断是否为管理员
        if(strncmp(msg->name, "root", 4) == 0) {
            printf("hello %s!!!\n", msg->name);
            msg->root = 1;  // 标记为root用户
            return 1;       // 返回1表示管理员登录
        } else {
            printf("hello %s\n", msg->name);
            msg->root = 0;  // 标记为普通用户
            return 2;       // 返回2表示普通用户登录
        }
    } else {
        printf("%s\n", msg->data);
    }
    return 0;  // 登录失败
}

代码解析

  • 函数设置消息类型为 L (登录) 并获取用户输入
  • 发送登录信息到服务器并等待响应
  • 根据服务器返回结果判断登录是否成功
  • 区分管理员 (root) 和普通用户,设置不同权限标识
  • 通过返回值告知主程序登录结果,以便跳转至相应菜单
4. 单词查询功能

int do_query(int sockfd, MSG *msg) {
    msg->type = Q;  // 设置消息类型为查询
    puts("--------------");

    while(1) {
        printf("Input word:");
        scanf("%s", msg->data);
        getchar();
        
        // 输入#号退出查询
        if(strncmp(msg->data, "#", 1) == 0)
            break;
 
        // 发送查询请求
        if(send(sockfd, msg, sizeof(MSG), 0) < 0) {
            printf("Fail to send.\n");
            return -1;
        }
 
        // 接收查询结果
        if(recv(sockfd, msg, sizeof(MSG), 0) < 0) {
            printf("Fail to recv.\n");
            return -1;
        }
        
        // 显示单词释义
        printf("%s\n", msg->data);
    }	
    return 0;
}

代码解析

  • 函数设置消息类型为 Q (查询) 并进入循环
  • 用户可连续输入多个单词查询,输入 #退出
  • 每次查询都将单词通过send()发送到服务器
  • 接收服务器返回的单词释义并显示
  • 该函数采用循环设计,方便用户连续查询多个单词
5. 历史记录查询功能

// 普通用户查询历史
int do_history(int sockfd, MSG *msg) {
    msg->type = H;  // 设置消息类型为历史查询
    
    send(sockfd, msg, sizeof(MSG), 0);
    
    // 接收并显示历史记录
    while(1) {
        recv(sockfd, msg, sizeof(MSG), 0);
        if(msg->data[0] == '\0')  // 空字符串表示结束
            break;
        printf("%s\n", msg->data);
    }
    return 0;
}

// 管理员查询历史
int do_history_root(int sockfd, MSG *msg) {
    msg->type = H;  // 设置消息类型为历史查询
    strcpy(msg->name, "root");  // 确保是root身份
    
    send(sockfd, msg, sizeof(MSG), 0);
    
    // 接收并显示所有用户的历史记录
    while(1) {
        recv(sockfd, msg, sizeof(MSG), 0);
        if(msg->data[0] == '\0')  // 空字符串表示结束
            break;
        printf("%s\n", msg->data);
    }
    return 0;
}

代码解析

  • 两个函数分别处理普通用户和管理员的历史查询
  • 都设置消息类型为 H (历史查询)
  • 管理员版本明确设置用户名为 “root” 以获取全量历史
  • 通过循环接收服务器发送的历史记录,直到收到空字符串
  • 服务器采用逐条发送的方式传输历史记录
6.客户端 main 函数功能详解

int main (int argc ,char *argv[])
{
    int sockfd;                      // 客户端套接字文件描述符
    struct sockaddr_in  serveraddr;  // 服务器地址结构体
    int n;                           // 用于存储用户菜单选择
    MSG  msg;                        // 通信消息结构体

    // 检查命令行参数是否正确(格式:./client 服务器IP 端口)
    if(argc != 3)
    {
        printf("Usage:%s serverip  port.\n", argv[0]);
        return -1;
    }
    
    // 创建 TCP 套接字(IPv4 协议)
    if((sockfd = socket(AF_INET, SOCK_STREAM,0)) < 0) 
    {
        perror("fail to socket");
        return -1;
    }
    else
    {
        printf("create socket success!!!\n");
    }
 
    // 初始化服务器地址结构体
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;                     // IPv4 地址族
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);     // 服务器 IP 地址转换
    serveraddr.sin_port = htons(atoi(argv[2]));          // 服务器端口号转换

    // 连接服务器
    if(connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("connect failed");
        return -1;
    }
    else
    {
        printf("connect is success!!!\n");
    }

    // 一级菜单循环(注册/登录/退出)
    while(1)
    {
        printf("*****************************************************************\n");
        printf("* 1.register          2.login              3.quit               *\n");
        printf("*****************************************************************\n");
        printf("Please choose:");
        scanf("%d", &n);
        getchar();  // 吸收输入缓冲区中的换行符

        // 根据用户选择执行相应操作
        switch(n)
        {
            case 1:
                do_register(sockfd, &msg);  // 调用注册功能
                break;
            case 2:
                // 登录成功后根据用户类型跳转到不同菜单
                if(do_login(sockfd, &msg) == 1)
                {
                    goto root;  // 管理员菜单
                }
                else if(do_login(sockfd, &msg) == 2)
                {
                    goto user;  // 普通用户菜单
                }
                break;
            case 3:
                close(sockfd);  // 关闭套接字
                exit(0);        // 退出程序
                break;
            default:
                printf("Invalid data cmd.\n");  // 无效输入提示
        }
    }
 
// 普通用户二级菜单
user:
    while(1)
    {
        printf("*****************************************************\n");
        printf("* 1.query_word   2.history_record   3.quit          *\n");
        printf("*****************************************************\n");
        printf("Please choose:");
        scanf("%d", &n);
        getchar();
 
        switch(n)
        {
            case 1:
                do_query(sockfd, &msg);      // 单词查询
                break;
            case 2:
                do_history(sockfd, &msg);    // 历史记录查询
                break;
            case 3:
                close(sockfd);
                exit(0);
                break;
            default :
                printf("Invalid data cmd.\n");
        }
    }

// 管理员二级菜单
root:
    while(1)
    {
        memset(msg.name,0,32);
        strcpy(msg.name,"root");  // 确保管理员身份
        printf("*****************************************************\n");
        printf("* 1.query_word   2.history_record   3.quit          *\n");
        printf("*****************************************************\n");
        printf("Please choose:");
        scanf("%d", &n);
        getchar();
 
        switch(n)
        {
            case 1:
                do_query(sockfd, &msg);        // 单词查询
                break;
            case 2:
                do_history_root(sockfd, &msg); // 管理员历史记录查询
                break;
            case 3:
                close(sockfd);
                exit(0);
                break;
            default :
                printf("Invalid data cmd.\n");
        }
    }
 
    return 0;
}

客户端 main 函数核心功能解析

  1. 参数校验

    • 检查命令行参数数量是否正确(必须提供服务器 IP 和端口)
    • 如果参数不正确,输出正确用法并退出
  2. 网络初始化

    • 创建 TCP 套接字:socket(AF_INET, SOCK_STREAM, 0)
    • 初始化服务器地址结构体,设置 IP 地址和端口
    • 建立与服务器的连接:connect()函数
  3. 一级菜单控制

    • 提供注册、登录和退出三个选项
    • 使用while(1)实现循环显示菜单,直到用户选择退出
    • 根据用户输入调用相应功能函数(do_register()do_login()
  4. 用户权限控制

    • 登录成功后,根据返回值判断用户类型(管理员或普通用户)
    • 使用goto语句跳转到对应的二级菜单(userroot标签)
  5. 二级菜单功能

    • 普通用户:单词查询、个人历史记录查询、退出
    • 管理员:单词查询、所有用户历史记录查询、退出
    • 每个选项对应不同的功能函数
  6. 资源释放

    • 用户选择退出时,关闭套接字并调用exit(0)终止程序
    • 确保网络资源正确释放

(二)服务器模块

服务器作为业务逻辑和数据处理的核心,负责接收客户端请求、处理业务逻辑、操作数据库和单词库文件,主要包含以下功能子模块:

1. 服务器初始化模块

int main(int argc, const char *argv[]) {
    int sockfd;
    struct sockaddr_in serveraddr;
    sqlite3 *db;  // 数据库句柄
    int acceptfd;
    pid_t pid;

    // 检查命令行参数
    if(argc != 3) {
        printf("Usage:%s serverip  port.\n", argv[0]);
        return -1;
    }

    // 打开数据库
    if(sqlite3_open(DATABASE, &db) != SQLITE_OK) {
        printf("%s\n", sqlite3_errmsg(db));
        return -1;
    }
    
    // 创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("fail to socket");
        return -1;
    }
 
    // 初始化服务器地址
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    // 绑定地址
    if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        perror("fail to bind");
        return -1;
    }
 
    // 监听连接
    if(listen(sockfd, 5) < 0) {
        printf("fail to listen.\n");
        return -1;
    }
 
    // 忽略SIGCHLD信号,防止僵尸进程
    signal(SIGCHLD, SIG_IGN);
 
    // 循环接受客户端连接
    while(1) {
        if((acceptfd = accept(sockfd, NULL, NULL)) < 0) {
            perror("fail to accept");
            return -1;
        }
 
        // 创建子进程处理客户端请求
        if((pid = fork()) < 0) {
            perror("fail to fork");
            return -1;
        } else if(pid == 0) {  // 子进程
            close(sockfd);
            do_client(acceptfd, db);  // 处理客户端请求
        } else {  // 父进程
            close(acceptfd);
        }
    }
    return 0;
}

代码解析

  • 服务器初始化包括数据库打开、套接字创建、地址绑定和监听
  • sqlite3_open()打开或创建 SQLite 数据库
  • bind()将套接字与特定 IP 和端口绑定
  • listen()将套接字设为监听模式,准备接收连接
  • 服务器采用多进程模型,通过fork()为每个客户端创建子进程
  • signal(SIGCHLD, SIG_IGN)用于自动回收子进程资源,防止僵尸进程
2. 客户端请求处理模块

int do_client(int acceptfd, sqlite3 *db) {
    MSG msg;
    
    // 循环接收客户端消息
    while(recv(acceptfd, &msg, sizeof(msg), 0) > 0) {
        printf("type:%d\n", msg.type);
        // 根据消息类型分发处理
        switch(msg.type) {
            case R:
                do_register(acceptfd, &msg, db);
                break;
            case L:
                do_login(acceptfd, &msg, db);
                break;
            case Q:
                do_query(acceptfd, &msg, db);
                break;
            case H:
                do_history(acceptfd, &msg, db);
                break;
            default:
                printf("Invalid data msg.\n");
        }
    }
 
    printf("client exit.\n");
    close(acceptfd);
    exit(0);
    return 0;
}

代码解析

  • 这是子进程的核心函数,负责处理客户端的所有请求
  • 通过recv()循环接收客户端发送的消息
  • 根据 MSG 结构体中的type字段,将请求分发到相应的处理函数
  • 当客户端断开连接时,recv()返回 0,函数关闭套接字并退出
  • 这种设计实现了单一入口、多出口的请求处理模式
3. 注册处理功能

void do_register(int acceptfd, MSG *msg, sqlite3 *db) {
    char *errmsg;
    char sql[512];
    
    // 构建插入用户信息的SQL语句
    sprintf(sql, "insert into usr values('%s', '%s');", msg->name, msg->data);
    printf("%s\n", sql);

    // 执行SQL语句
    if(sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
        printf("%s\n", errmsg);
        strcpy(msg->data, "usr name already exist.");
    } else {
        printf("client register ok!\n");
        strcpy(msg->data, "OK!");
    }
 
    // 发送注册结果给客户端
    if(send(acceptfd, msg, sizeof(MSG), 0) < 0) {
        perror("fail to send");
        return;
    }
    return;
}

代码解析

  • 函数构建 SQL 插入语句,将用户名和密码存入 usr 表
  • 使用sqlite3_exec()执行 SQL 语句
  • 如果用户名已存在(主键冲突),返回 “usr name already exist.”
  • 注册成功则返回 “OK!”
  • 最后将结果通过send()函数发送给客户端
4. 登录处理功能

int do_login(int acceptfd, MSG *msg, sqlite3 *db) {
    char sql[512] = {};
    char *errmsg;
    int nrow;       // 结果行数
    int ncloumn;    // 结果列数
    char **resultp; // 结果集

    // 构建查询SQL语句
    sprintf(sql, "select * from usr where name = '%s' and pass = '%s';", 
            msg->name, msg->data);
    printf("%s\n", sql);

    // 执行查询
    if(sqlite3_get_table(db, sql, &resultp, &nrow, &ncloumn, &errmsg) != SQLITE_OK) {
        printf("%s\n", errmsg);
        return -1;
    }
 
    // 判断查询结果
    if(nrow == 1) {  // 找到匹配的用户
        strcpy(msg->data, "OK");
        send(acceptfd, msg, sizeof(MSG), 0);
        return 1;
    }
 
    if(nrow == 0) {  // 未找到匹配的用户
        strcpy(msg->data, "usr/passwd wrong.");
        send(acceptfd, msg, sizeof(MSG), 0);
    }
    return 0;
}

代码解析

  • 函数构建查询 SQL,检查用户名和密码是否匹配
  • 使用sqlite3_get_table()执行查询并获取结果
  • nrow变量存储查询结果的行数
  • 如果找到一条匹配记录(nrow == 1),登录成功,返回 “OK”
  • 如果没有找到匹配记录(nrow == 0),返回 “usr/passwd wrong.”
  • 结果通过send()函数发送给客户端
5. 单词查询处理功能

// 查询单词并返回释义
int do_searchword(int acceptfd, MSG *msg, char word[]) {
    FILE *fp;
    int len = 0;
    char temp[512] = {};
    int result;
    char *p;
 
    // 打开字典文件
    if((fp = fopen("dict.txt", "r")) == NULL) {
        perror("fail to fopen");
        strcpy(msg->data, "Failed to open dict.txt");
        send(acceptfd, msg, sizeof(MSG), 0);
        return -1;
    }
 
    len = strlen(word);
    // 循环查找单词
    while(fgets(temp, 512, fp) != NULL) {
        result = strncmp(temp, word, len);
        
        if(result < 0)  // 单词在当前行之后
            continue;
        if(result > 0 || (result == 0 && temp[len] != ' '))  // 未找到
            break;
        
        // 找到单词,提取释义
        p = temp + len;
        while(*p == ' ')  // 跳过空格
            p++;
        strcpy(msg->data, p);
        printf("found word:%s\n", msg->data);
        fclose(fp);
        return 1;
    }
    
    // 未找到单词
    strcpy(msg->data, "Not found!");
    printf("Not found!\n");
    fclose(fp);
    return 0;
}

// 处理查询请求并记录历史
int do_query(int acceptfd, MSG *msg, sqlite3 *db) {
    char word[64];
    int found = 0;
    char date[128] = {};
    char sql[128] = {};
    char *errmsg;

    strcpy(word, msg->data);
    // 查找单词
    found = do_searchword(acceptfd, msg, word);
    
    // 如果找到单词,记录查询历史
    if(found == 1) {
        get_date(date);  // 获取当前时间
        // 插入历史记录
        sprintf(sql, "insert into record values('%s', '%s', '%s')", 
                msg->name, date, word);
        if(sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
            printf("%s\n", errmsg);
            return -1;
        }
    } else {
        strcpy(msg->data, "Not found!");
    }
 
    // 发送查询结果
    send(acceptfd, msg, sizeof(MSG), 0);
    return 0;
}

代码解析

  • 该功能由两个函数协作完成:do_searchword()do_query()

  • do_searchword()负责从 dict.txt 文件中查找单词:

    • 利用字典文件按字母序排列的特点,使用 strncmp () 比较
    • 找到匹配的单词后提取释义部分
    • 未找到则返回 “Not found!”
  • do_query()负责整体流程控制:

    • 调用do_searchword()查找单词
    • 如果找到单词,调用get_date()获取当前时间
    • 将查询记录(用户名、时间、单词)插入 record 表
    • 最后将查询结果发送给客户端
6. 历史记录处理功能

// 历史记录查询回调函数
int history_callback(void* arg, int f_num, char**f_value, char** f_name) {
    int acceptfd = *((int *)arg);
    MSG msg;
    
    // 格式化历史记录
    sprintf(msg.data, "%s , %s", f_value[1], f_value[2]);
    send(acceptfd, &msg, sizeof(MSG), 0);
    return 0;
}

// 处理历史记录查询
int do_history(int acceptfd, MSG *msg, sqlite3 *db) {
    char sql[128] = {};
    char *errmsg;
    
    // 根据用户权限构建不同的查询SQL
    if(msg->root == 1) {  // 管理员查询所有记录
        sprintf(sql, "select * from record");
    } else {  // 普通用户仅查询自己的记录
        sprintf(sql, "select * from record where name = '%s'", msg->name);
    }
    
    // 执行查询,使用回调函数处理结果
    if(sqlite3_exec(db, sql, history_callback, (void *)&acceptfd, &errmsg) != SQLITE_OK) {
        printf("%s\n", errmsg);
    }
    
    // 发送结束标志
    msg->data[0] = '\0';
    send(acceptfd, msg, sizeof(MSG), 0);
    return 0;
}

代码解析

  • 该功能由do_history()和回调函数history_callback()组成

  • do_history()根据用户权限构建不同的查询 SQL:

    • 管理员 (root) 查询所有用户的历史记录
    • 普通用户只能查询自己的历史记录
    • 使用sqlite3_exec()执行查询,指定回调函数处理结果
  • history_callback()负责处理每条查询结果:

    • 将记录格式化(时间 + 单词)
    • 逐条发送给客户端
    • 所有记录发送完成后,发送空字符串作为结束标志
7. 辅助功能:获取当前时间

int get_date(char *date) {
    time_t t;
    struct tm *tp;
 
    time(&t);  // 获取当前时间
    tp = localtime(&t);  // 转换为本地时间
 
    // 格式化时间字符串
    sprintf(date, "%d-%d-%d %d:%d:%d", 
            tp->tm_year + 1900,  // 年份需要加1900
            tp->tm_mon + 1,      // 月份从0开始,需要加1
            tp->tm_mday,
            tp->tm_hour,
            tp->tm_min,
            tp->tm_sec);
    printf("get date:%s\n", date);
    return 0;
}

代码解析

  • 该函数用于获取当前系统时间并格式化为字符串
  • time(&t)获取当前时间的秒数(从 1970-01-01 00:00:00 开始)
  • localtime(&t)将时间转换为本地时间的结构体表示
  • 格式化字符串为 “YYYY-MM-DD HH:MM:SS” 格式
  • 注意年份需要加 1900,月份需要加 1(因为 tm_mon 从 0 开始)
8.服务器端 main 函数功能详解

int main(int argc, const char *argv[])
{
    int sockfd;                       // 服务器监听套接字
    struct sockaddr_in  serveraddr;   // 服务器地址结构体
    sqlite3 *db;                      // SQLite 数据库句柄
    int acceptfd;                     // 与客户端通信的套接字
    pid_t pid;                        // 进程 ID

    // 检查命令行参数是否正确(格式:./server 服务器IP 端口)
    if(argc != 3)
    {
        printf("Usage:%s serverip  port.\n", argv[0]);
        return -1;
    }

    // 打开 SQLite 数据库
    if(sqlite3_open(DATABASE, &db) != SQLITE_OK)
    {
        printf("%s\n", sqlite3_errmsg(db));
        return -1;
    }
    else
    {
        printf("open DATABASE success.\n");
    }

    // 创建 TCP 套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM,0)) < 0)
    {
        perror("fail to socket");
        return -1;
    }
 
    // 初始化服务器地址结构体
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    // 绑定套接字到指定 IP 和端口
    if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("fail to bind");
        return -1;
    }
 
    // 将套接字设为监听模式,最大连接队列长度为 5
    if(listen(sockfd, 5) < 0)
    {
        printf("fail to listen.\n");
        return -1;
    }
 
    // 忽略 SIGCHLD 信号,防止子进程变成僵尸进程
    signal(SIGCHLD, SIG_IGN);
 
    // 循环接收客户端连接
    while(1)
    {
        // 接受客户端连接请求
        if((acceptfd = accept(sockfd, NULL, NULL)) < 0)
        {
            perror("fail to accept");
            return -1;
        }
 
        // 创建子进程处理客户端请求
        if((pid = fork()) < 0)
        {
            perror("fail to fork");
            return -1;
        }
        // 子进程:处理客户端请求
        else if(pid == 0)
        {
            close(sockfd);  // 子进程关闭监听套接字
            do_client(acceptfd, db);  // 处理客户端请求
        }
        // 父进程:继续接受新连接
        else
        {
            close(acceptfd);  // 父进程关闭与客户端通信的套接字
        }
    }
    return 0;
}

服务器端 main 函数核心功能解析

  1. 参数校验

    • 检查命令行参数数量是否正确
    • 确保提供了服务器 IP 地址和端口号
  2. 数据库初始化

    • 打开 SQLite 数据库(my.db
    • 如果数据库打开失败,输出错误信息并退出
    • 数据库用于存储用户信息和查询历史
  3. 网络初始化

    • 创建 TCP 套接字:socket(AF_INET, SOCK_STREAM, 0)
    • 初始化并设置服务器地址结构体
    • 绑定套接字到指定 IP 和端口:bind()
    • 将套接字设为监听模式:listen(),最大连接队列长度为 5
  4. 信号处理

    • 使用signal(SIGCHLD, SIG_IGN)忽略子进程结束信号
    • 自动回收子进程资源,防止产生僵尸进程
  5. 并发连接处理

    • 无限循环接受客户端连接:accept()
    • 每接受一个连接就创建一个子进程处理该客户端请求
    • 子进程负责与客户端的所有交互(do_client()函数)
  6. 资源管理

    • 子进程关闭监听套接字(不需要监听新连接)
    • 父进程关闭与客户端通信的套接字(交给子进程处理)
    • 实现并发处理多个客户端连接

(三)数据库设计

系统使用 SQLite3 数据库,包含两张表:

  1. usr表 - 存储用户信息

    
    CREATE TABLE IF NOT EXISTS usr (
        name TEXT PRIMARY KEY,  -- 用户名,主键
        pass TEXT               -- 密码
    );
  2. record表 - 存储查询历史

    
    CREATE TABLE IF NOT EXISTS record (
        name TEXT,  -- 用户名
        date TEXT,  -- 查询时间
        word TEXT   -- 查询的单词
    );