目录
闲话少说
此文章为在线英语词典实现,第一次基本独立做项目,大部分功能没问题,不足的是代码冗余有点多,还可能有些小BUG。
仅做记录并且分享自己学习Linux网络编程的过程,学艺不精,code新人,如有错误欢迎在评论区讨论、指正!,若文章内容对你有帮助,希望可以给我点个赞!
1. 思路构筑(想框架,理流程)
做完本次项目,进一步体会到思路构筑的重要性;若你向我一样经验不足,最好不要脑子一热就狂写一堆,到了项目做到一半发现要大改……说多都是泪。这部分多花时间一点也无妨。
先来看一下做该项目的大致思路:写好大致框架后,一个模块一个模块的添,模块边写边测试。“让服务器做所有事情” 这种想法是大忌。 有些事情让客户端去做会大大降低你项目编码的难度。二者如何分工,请看下文。
1.1 大致思路
1.1.1 功能模块
- 用户界面:告诉用户可以做些什么事情
- 账号操作:登录或注册
- 查询操作:查询单词或者成功查询的历史
1.1.2 服务器
- 一个数据库,数据库内三张表user, word, history ,分别存放用户,单词,和成功查询单词历史信息
- 接收请求
- 1.注册:获得用户 id pwd 并添加到 user 表中
- 2.登录:获得用户 id pwd 到 user 表中比对 id 和 pwd
- 3.查单词:获得用户输入单词,到 word 表中查找
- 4.查历史:根据用户 id,到 history 中查找记录
- 返回相应结果
1.1.3 客户端
- 用户界面:提示用户当前可以做什么事情
- 获得用户请求,并发送给服务器
- 接收回复结果,做相应处理
1.1.4 交互流程
该项目简单来说就是:用户只有在登录成功之后才可以进入查询环节;进入查询环节之后可以一直查询,直到用户在“查单词“环节输入 ”Q“ 之后才会回退到账号界面。
当然这并不灵活,是后续可以改进的点。
服务器启动的条件下,用户运行客户端,输入数字(1, 2, 3, 4)即可做对应的事情。
数字对应功能如下图所示
账号界面
查询界面
2. 代码
2.1 部分效果展示
登录成功
单词查询结果
历史查询结果,打印有点小问题
2.2 项目概况 readMe.txt
服务器端
- 主函数 server.c
- 功能函数 tool.c
- 编译命令: gcc server.c tool.c -lsqlite3 -o server
- 运行命令: ./server IP port
- 初次运行命令: ./server IP port num --> num 任意数字即可
- 词典数据文件:dict.txt
客户端
- 主函数 client.c
- 功能函数 tool.c
- 编译命令: gcc clien.c tool.c -lsqlite3 -o client
- 运行命令: ./client IP port
2.3.头文件 tool.h
包含了客户端和服务器需要用到的所有功能模块函数接口(并不是很好,最好客户端和服务器模块功能函数分开放)
#ifndef _TOOL_H_
#define _TOOL_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#include <wait.h>
#include <time.h>
#include <sqlite3.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define R 1 // register
#define L 2 // login
#define C 3 // check
#define H 4 // history
#define Q 5 // quit
#define GET_ID_ERR 6 //获取id 失败
#define GET_PWD_ERR 7 //获取pwd 失败
#define ID_ERR 8 // 用户id 错误
#define PWD_ERR 9 // 用户pwd 错误
//用户界面
void UI_account(void);
//查询界面
void UI_check(void);
// 初始化数据库,只是准备,后续使用仍需要手动打开
int database_init(char *dbname);
// 注册
int user_add(int connfd, sqlite3 *db);
// 登陆
int user_login(int connfd, sqlite3 *db);
// 查单词
int check(int connfd, sqlite3 *db, int id);
// 成功历史查询
int history(int connfd, sqlite3 *db, int id);
// 定义用户帐号函数,登陆成功返回用户id
int account(int connfd, sqlite3 *db,int mode);
// 行为函数
int operation(int connfd, sqlite3 *db, int mode, int id);
// 服务器初始化
int server_init(char *ip, char* port);
#endif
2.4 功能函数 tool.c
#include "tool.h"
//帐号界面
void UI_account(void){
char buf[512] = {0};
strcat(buf,"******************************************\n");
strcat(buf,"* *\n");
strcat(buf,"* ** 输入数字即可开始 ** *\n");
strcat(buf,"* *\n");
strcat(buf,"* 1:注册 2:登陆 *\n");
strcat(buf,"* *\n");
strcat(buf,"******************************************\n");
printf("%s", buf);
}
// 查询界面
void UI_check(void){
char buf[512] = {0};
strcat(buf,"******************************************\n");
strcat(buf,"* *\n");
strcat(buf,"* ** 输入数字即可开始 ** *\n");
strcat(buf,"* *\n");
strcat(buf,"* 3:查单词 4:历史查询 *\n");
strcat(buf,"* *\n");
strcat(buf,"******************************************\n");
printf("%s", buf);
}
// 初始化数据库,只是准备,后续使用仍需要手动打开
int database_init(char *dbname)
{
puts("--- 服务器初始化中,请稍候... ---");
char sql[256] = {0};
sqlite3 *db;
if(-1 == sqlite3_open(dbname, &db)){
printf("%s open error: %s\n", dbname, sqlite3_errmsg(db));
return -1;
}
/*------------------------------------------------------------------------*/
// 初始化 user(id int primary key, password int)
strcpy(sql, "create table if not exists user(id int primary key,\
password int)");
if(0 != sqlite3_exec(db, sql, NULL, NULL, NULL)){
printf("user create error: %s\n", sqlite3_errmsg(db));
return -1L;
}
/*------------------------------------------------------------------------*/
// 创建 word 数据库
strcpy(sql, "create table if not exists word(word str(15),\
meaning text)");
if(0 != sqlite3_exec(db, sql, NULL, NULL, NULL)){
printf("word create error: %s\n", sqlite3_errmsg(db));
return -1;
}
//词典文件上传到数据库
FILE *fp = fopen("dict.txt", "r+");
if(NULL == fp){
perror("dict fopen");
return -1;
}
char buf[64] = {0};//词条
char word[32] = {0}; //单词
while(fgets(buf, sizeof(buf), fp)){
//把单词从词条中提取出来
char *cut = strstr(buf, " ");
strncpy(word, buf, cut-buf);
//单词和词条添加到数据库word中
char sql[128] = {0};
sprintf(sql, "insert into word values('%s', '%s')",word, buf);
if(0 != sqlite3_exec(db, sql, NULL, NULL, NULL)){
printf("word insert: %s\n", sqlite3_errmsg(db));
return -1;
}
memset(buf, 0, sizeof(buf));
memset(word, 0, sizeof(word));
}
/*------------------------------------------------------------------------*/
// 初始化 history(id int primary key, word str(40), time text)
strcpy(sql, "create table if not exists history(id int,\
word str(40), time text)");
if(0 != sqlite3_exec(db, sql, NULL, NULL, NULL)){
printf("history create error: %s\n", sqlite3_errmsg(db));
return -1;
}
sqlite3_close(db);
return 0;
}
/*------------------------------------------------------------------------*/
// 注册
int user_add(int connfd, sqlite3 *db){
// id是主键,sqlite3会判断 id 唯一性,所以这里不用
int id, pwd;
char sql[128] = {0};
char buf[128] = {0};
// 接受数据,拼接sql语句
recv(connfd, (void*)&id, sizeof(id), 0);
recv(connfd, (void*)&pwd, sizeof(pwd), 0);
if(9 >= id)
return ID_ERR;// id 不合法
sprintf(sql, "insert into user values(%d,%d);", id, pwd);
if(0 != sqlite3_exec(db, sql, NULL, NULL, NULL)){
printf("add user error: %s\n", sqlite3_errmsg(db));
return ID_ERR;
}
// 注册成功
printf("用户id:%d pwd:%d 注册成功!\n",id, pwd);
return 0;
}
/*------------------------------------------------------------------------*/
// 登陆, 登陆成功返回用户id
int user_login(int connfd, sqlite3 *db){
int id, pwd;
char **result;
int row, col;
char buf[128] = {0};
char sql[128] = {0};
// 获取登陆用户输入的id pwd
int ret = recv(connfd, &id, sizeof(id), 0);
if(-1 == ret){
perror("获取登陆用户id 失败");
return GET_ID_ERR;
}else if(0 == ret){
printf("客户端退出\n");
}
ret = recv(connfd, &pwd, sizeof(pwd), 0);
if(-1 == ret){
perror("获取登陆用户pwd 失败");
return GET_PWD_ERR;
}else if(0 == ret){
printf("客户端退出\n");
}
//判断登陆id合法性
if(9 >= id){
return ID_ERR;//不合法
}
printf("已登陆用户-----id: %d, pwd: %d\n", id, pwd);
// 查看 user 表中是否有该用户
sprintf(sql, "select * from user where id = %d", id);
if(0 != sqlite3_get_table(db, sql, &result, &row, &col, NULL)){
printf("login user error: %s\n", sqlite3_errmsg(db));
return ID_ERR;//用户不存在
}
for(int i=1; i<=row; i++){
for(int j=0; j<col;j++){
//判断用户id
if(id == atoi(result[i * col + j])){
//判断用户密码
if(pwd == atoi(result[i*col+j+1])){
return id;
}else{
return PWD_ERR;
}
}
}
}
return -1;
}
/*------------------------------------------------------------------------*/
// 可查一次查单词, 且上传成功查询历史
// 返回值:正常查询发送信息给客户端,返回 0 ;出错 -1
int check(int connfd, sqlite3 *db, int id){
char **result;
int row, col;
char sql[128] = {0};
char buf[128] = {0};
// 提取 word 表的数据
strcpy(sql, "select * from word");
if(0 != sqlite3_get_table(db, sql, &result, &row, &col, NULL)){
return -1;
}
//获得用户单词信息
char input[32] = {0};
recv(connfd, input, sizeof(input), 0);
int input_len = strlen(input);
printf("客户端获得单词:%s\n", input);
// 得到Q退出
if(0 == strncmp(input, "Q", 1)){
return Q; //
}
//判断word
int index = col;//查询所用下标,因为 col 等于字段数
int flag = 0;//标记单词查询状态 0:未找到 1:找到
int max = row*col-2; //单词以及解释的总个数, 2 是字段数 word, meaning
for(int i=0; i<=row; i++){
if(1 == flag)//找到直接退出循环
break;
for(int j=0; j<col;j++){
//接收数据库数据(否则无法获取单词真正长度)
char word[20] = {0};
strcpy(word, result[index]);
int word_len = strlen(word);
//判断单词内容与单词长度
if(input_len == word_len && 0 == strncmp(input, result[index], strlen(input))){
// 成功查到发送解释 result[index+1]
sprintf(buf, "%s", result[index+1]);
send(connfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
/* -------------------上传历史记录--------------*/
// 获取查询时间
time_t tm;
time(&tm);
struct tm *tp = localtime(&tm);
char timmsg[32] = {0};
sprintf(timmsg,"%d/%d/%d %d:%d:%d\n", tp->tm_year+1900,
tp->tm_mon+1, tp->tm_mday, tp->tm_hour,
tp->tm_min, tp->tm_sec);
char sql[128] = {0};
sprintf(sql,"insert into history values(%d,'%s','%s')",id,word,timmsg);
if(-1 == sqlite3_exec(db, sql, NULL, NULL, NULL)){
return -1;
}
flag = 1;
break;
}
index+=2;
index %= max;//防止段错误
}
}
//循环退出时 flag 仍为 0 则未找到
if(0 == flag){
strcpy(buf, "单词未找到\n");
send(connfd, buf, strlen(buf), 0);
return 0;
}
return 0;
}
/*------------------------------------------------------------------------*/
// 成功查询的历史
int history(int connfd, sqlite3 *db, int id){
char sql[128] = {0};
char buf[128] = {0};
char **result;
int row, col;
sprintf(sql, "select * from history where id=%d", id);
if(-1 == sqlite3_get_table(db, sql, &result, &row, &col, NULL)){
sprintf(buf,"select history: %s\n", sqlite3_errmsg(db));
send(connfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
return -1;
}
int max = col * row + 3;// 3 是字段数
if(3 == max){// max 为3 该用户无成功查询记录
strcpy(buf, "NONE!");
send(connfd, buf, strlen(buf), 0);
return -1;
}else{
strcpy(buf,"\n---------------成功查询历史----------------\n");
send(connfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
for(int i=0; i<=row; i++){
for(int j=0; j<col; j++){ // j 从3 开始,不传字段名
sprintf(buf, "%s ", result[i*col+j]);
send(connfd, buf, strlen(buf), 0);
usleep(1000);
memset(buf, 0, sizeof(buf));
}
}
// 历史数据发送完成,发送结束信息
strcpy(buf, "DONE!");
send(connfd, buf, strlen(buf), 0);
}
return 0;
}
/*------------------------------------------------------------------------*/
// 帐号操作,函数注册成功返回 0,登陆成功返回id
int account(int connfd, sqlite3 *db, int mode){
int id;
char buf[128] = {0};
//注册
if(R == mode){
int ret = user_add(connfd, db);
if(0 == ret){
printf("新用户注册成功!\n");
}else if(ID_ERR){
return ID_ERR;
}
return 0;//注册成功
}else if(L ==mode){
// 登陆
int ret = user_login(connfd, db);
if(ID_ERR == ret){
return ID_ERR;//id不合法
}else if(PWD_ERR == ret){
return PWD_ERR;//密码错误
}else if(GET_ID_ERR == ret){
return GET_ID_ERR;
}else if(GET_PWD_ERR == ret){
return GET_PWD_ERR;
}
return ret;
}else{
return -1;
}
return 0;
}
int operation(int connfd, sqlite3 *db, int mode, int id){
if(C == mode){
// 查询
int ret = check(connfd, db, id);
if(-1 == ret){
return -1;
}else if(Q == ret){
return Q;
}
return 0;
}else if(H == mode){
// 历史
if(-1 == history(connfd, db, id))
return -1;
return 0;
}
}
/*------------------------------------------------------------------------*/
//服务器初始化
int server_init(char *ip, char* port)
{
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket");
return -1;
}
// 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(atoi(port));
if(-1 == bind(sockfd, (struct sockaddr *)&addr, sizeof(addr))){
perror("connect");
return -1;
}
// 监听
if(-1 == listen(sockfd, 10)){
perror("listen");
return -1;
}
printf("----------------服务启动!----------------\n");
printf("--- 等待客户端连接 ---\n");
return sockfd;
}
2.5 客户端 client.c
// -------------------------Client---------------------------
#include "tool.h"
int main(int argc, char *argv[]){
// 判断运行指令合法性
if(3 != argc){
printf("Input like: %s IP port\n", argv[0]);
return -1;
}
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket");
return -1;
}
// 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
if(-1 == connect(sockfd, (struct sockaddr *)&addr, sizeof(addr))){
perror("connect");
return -1;
}
while(1){
/*---------------------帐号操作----------------------*/
long mode = 0; //用户模式数
int ret = 0; //接收各种函数返回结果
char ui[512] = {0};//接收UI
char buf[128] = {0};//存储信息
// 调用帐号UI
UI_account();
// 发送mode
printf("输入您的模式选择数:\n");
scanf("%ld", &mode);
getchar();
send(sockfd,(void*)&mode, sizeof(mode), 0);
if(R == mode){
/*-----------------------注册---------------------*/
int id, pwd;
printf("输入您要注册的用户id(需至少为两位数)\n");
scanf("%d", &id);
send(sockfd, (void*)&id, sizeof(id), 0);
printf("输入您的密码\n");
scanf("%d", &pwd);
send(sockfd, (void*)&pwd, sizeof(pwd), 0);
printf("您的 id:%d pwd: %d\n", id, pwd);
recv(sockfd, (void*)&id, sizeof(id), 0); //接收回复
if(0 == id){
printf("注册成功\n");
continue;
}else if(-1 == id){
printf("注册失败\n");
continue;
}else if(ID_ERR == id){
printf("用户已存在\n");
continue;
}
}else if(L == mode){
/*-----------------------登陆---------------------*/
int id, pwd;
printf("输入登陆的用户id(需至少为两位数)\n");
scanf("%d", &id);
send(sockfd, (void*)&id, sizeof(id), 0);
printf("输入密码\n");
scanf("%d", &pwd);
send(sockfd, (void*)&pwd, sizeof(pwd), 0);
recv(sockfd, &id, sizeof(id), 0); //接收回复
if(-1 == id){
printf("登陆失败\n");
continue;
}else if(0 == id){
printf("您不能以 0 为用户名\n");
continue;
}else if(GET_ID_ERR == id){
printf("获取id失败\n");
continue;
}else if(GET_PWD_ERR == id){
printf("获取pwd失败\n");
continue;
}else if(ID_ERR == id){
printf("用户id不合法\n");
continue;
}else if(PWD_ERR == id){
printf("密码错误\n");
continue;
}else if(9 < id){
/*-------------------登陆成功, 查询开始--------------------*/
while(1){
// 调用查询UI
UI_check();
// 发送mode
printf("输入查询模式选择数:\n");
scanf("%ld", &mode);
getchar();
send(sockfd, (void*)&mode, sizeof(mode), 0);
if(C == mode){
/*----------------------查询-----------------------*/
//发送单词信息,输入的是Q则退出,当然Q也要发给服务器
printf("输入您要查询的单词,输入 Q 退出\n");
fgets(buf, sizeof(buf), stdin);
send(sockfd, buf, strlen(buf)-1, 0);
if(0 == strncmp(buf, "Q", 1)){
break;
}
memset(buf, 0, sizeof(buf));
//接受结果
char result[64] = {0};
recv(sockfd, result, sizeof(result), 0);
printf("%s\n", result);
continue;
}else if(H == mode){
while(1){
char buf[128] = {0};
int ret = recv(sockfd, buf, sizeof(buf), 0);
if(-1 == ret){
perror("history recv");
break;
}else if(0 == ret){
printf("客户端退出\n");
break;
}
// 查询成功
printf("%s", buf);
// 历史数据读取完成
if(0 == strncmp(buf, "DONE!", 5)){
printf("\n");
break;
}else if(0 == strncmp(buf, "NONE!", 5)){
//暂无历史数据
printf("\n");
break;
}
}
continue; //返回查询界面
}else{
puts("请输入正确的选项数");
continue;
}
}
}
}
}
}
2.6 服务器 server.c
// -------------------------Server----------------------------
#include "tool.h"
/* ----------------------------main------------------------------ */
int main(int argc, char *argv[])
{
if(3 > argc){
printf("参数缺失,您至少需要输入三个参数如:\n./server IP port\n");
puts("");
printf("第一次运行服务器,需额外输入任意数字:\n./server IP port num\n");
return -1;
}
// 若输入了模式(第一次运行服务器,没有创建数据库)
if(4 == argc)
database_init("data.db");
sqlite3* db;
if(0 != sqlite3_open("data.db", &db)){
printf("data base open: %s\n", sqlite3_errmsg(db));
return -1;
}
// 初始化服务器,得到监听套接字
int listenfd = server_init(argv[1], argv[2]);
if(-1 == listenfd)
return -1;
/*--------- 子进程处理用户交互 ----------*/
/*-----------------一个连接一个进程--------------------*/
while(1){
long mode = 0;// 接收用户模式
int ret = 0;//接受一些函数的运行结果
struct sockaddr_in client;
socklen_t len = sizeof(client);
int connfd = accept(listenfd, (struct sockaddr*)&client, &len);
if(-1 == connfd){
perror("connect");
return -1;
}
puts("客户端连接成功!");
pid_t pid = fork();
if(-1 == pid){
perror("fork");
return -1;
}else if(0 == pid){
while(1){
/* --------------------- 帐号操作 ----------------------*/
// 用户登陆操作信息
ret = recv(connfd, (void*)&mode, sizeof(mode), 0);
if(-1 == ret){
perror("recv mode");
break;
}else if(0 == ret){
printf("客户端退出\n");
break;
}
/* ------------------帐号操作 --------------------*/
// 帐号函数,登陆成功返回登陆id
int id = account(connfd, db, mode);
send(connfd, (void*)&id, sizeof(id), 0);//发送account的结果
if(9 < id){
/* ------------------ 登陆成功开始查询 --------------------*/
while(1){
// 接收mode
if(-1 == recv(connfd, (void*)&mode, sizeof(mode),0)){
perror("recv mode:");
exit(-1);
}else if(0 == ret){
printf("客户端退出\n");
exit(-1);
}
int ret = operation(connfd, db, mode, id);
if(-1 == ret){
break;
}else if(0 == ret){
continue;
}else if(Q == ret){
break;
}
}
}
}
}
close(connfd);
}
/* -------------------------- 父进程 ------------------------------*/
wait(NULL);
close(listenfd);
sqlite3_close(db);
exit(0);
}
制作者名单
- 文案:張嘉鑫
- 视图:張嘉鑫
- 代码:張嘉鑫
- 其余图片:来自网络
- 特别鸣谢:李伟老师
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/15c47ec15a42493da84acf15163331bd.gif#pic_center)