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 函数核心功能解析
参数校验:
- 检查命令行参数数量是否正确(必须提供服务器 IP 和端口)
- 如果参数不正确,输出正确用法并退出
网络初始化:
- 创建 TCP 套接字:
socket(AF_INET, SOCK_STREAM, 0)
- 初始化服务器地址结构体,设置 IP 地址和端口
- 建立与服务器的连接:
connect()
函数
- 创建 TCP 套接字:
一级菜单控制:
- 提供注册、登录和退出三个选项
- 使用
while(1)
实现循环显示菜单,直到用户选择退出 - 根据用户输入调用相应功能函数(
do_register()
或do_login()
)
用户权限控制:
- 登录成功后,根据返回值判断用户类型(管理员或普通用户)
- 使用
goto
语句跳转到对应的二级菜单(user
或root
标签)
二级菜单功能:
- 普通用户:单词查询、个人历史记录查询、退出
- 管理员:单词查询、所有用户历史记录查询、退出
- 每个选项对应不同的功能函数
资源释放:
- 用户选择退出时,关闭套接字并调用
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 函数核心功能解析
参数校验:
- 检查命令行参数数量是否正确
- 确保提供了服务器 IP 地址和端口号
数据库初始化:
- 打开 SQLite 数据库(
my.db
) - 如果数据库打开失败,输出错误信息并退出
- 数据库用于存储用户信息和查询历史
- 打开 SQLite 数据库(
网络初始化:
- 创建 TCP 套接字:
socket(AF_INET, SOCK_STREAM, 0)
- 初始化并设置服务器地址结构体
- 绑定套接字到指定 IP 和端口:
bind()
- 将套接字设为监听模式:
listen()
,最大连接队列长度为 5
- 创建 TCP 套接字:
信号处理:
- 使用
signal(SIGCHLD, SIG_IGN)
忽略子进程结束信号 - 自动回收子进程资源,防止产生僵尸进程
- 使用
并发连接处理:
- 无限循环接受客户端连接:
accept()
- 每接受一个连接就创建一个子进程处理该客户端请求
- 子进程负责与客户端的所有交互(
do_client()
函数)
- 无限循环接受客户端连接:
资源管理:
- 子进程关闭监听套接字(不需要监听新连接)
- 父进程关闭与客户端通信的套接字(交给子进程处理)
- 实现并发处理多个客户端连接
(三)数据库设计
系统使用 SQLite3 数据库,包含两张表:
usr
表 - 存储用户信息CREATE TABLE IF NOT EXISTS usr ( name TEXT PRIMARY KEY, -- 用户名,主键 pass TEXT -- 密码 );
record
表 - 存储查询历史CREATE TABLE IF NOT EXISTS record ( name TEXT, -- 用户名 date TEXT, -- 查询时间 word TEXT -- 查询的单词 );