项目介绍

描述:
通过C/S架构实现在线词典,用户在客户端可以注册,登陆,然后可以查询单词,并且保存自己的单词查询记录。

知识点:

  • c语言进阶 Linux基础
  • C/S架构
  • 进程
  • sqlite3数据库
  • 时间函数
  • Makefile

效果图:


客户端

创建一个dict_client文件夹,存放客户端代码

client.h

#ifndef CLIENT_H#define CLIENT_H#include #include #include #include #include #include #include #include #include #define N 32#define R 1 //user - register#define L 2 //user - login#define Q 3 //user - query#define H 4 //user - history//定义通信双方的信息结构体typedef struct{int type;char name[N];char data[256]; //password或word信息}MSG;int do_register(int sockfd, MSG *msg);int do_login(int sockfd, MSG *msg);int do_query(int sockfd, MSG *msg);int do_history(int sockfd, MSG *msg);int client_connect(char *ip, int port);#endif

cli_dict.c

#include "client.h"int do_register(int sockfd, MSG *msg){msg->type = R;printf("Input name:");scanf("%s", msg->name);getchar();printf("Input password:");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;}//ok or user alread existprintf("%s\n", msg->data);return 0;}int do_login(int sockfd, MSG *msg){msg->type = L;printf("Input name:");scanf("%s", msg->name);getchar();printf("Input password:");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("OK\n");return 1;}else{printf("%s\n", msg->data);}return 0;}int do_query(int sockfd, MSG *msg){puts("查询--------------");char name[10]={0};strcpy(name,msg->name);while(1){msg->type = Q;strcpy(msg->name,name);printf("Input word[quit:#]:");scanf("%s", msg->data);//printf("%s %s\n",msg->name,msg->data);//客户端输入# 返回上一级菜单if(strncmp(msg->data,"#",1)==0)break;//将要查询的单词发送给服务器if(send(sockfd, msg, sizeof(MSG), 0)<0){printf("Fail to send.\n");return -2;}//等待接收服务器传递回来的单词注释信息if(recv(sockfd, msg, sizeof(MSG), 0) <0){printf("Fail to recv.\n");return -2;}printf("%s\n", msg->data);}return 0;}int do_history(int sockfd, MSG *msg){msg->type = H;send(sockfd,msg,sizeof(MSG),0);printf("%s的查找记录--------\n", msg->name);//接收服务器传递回来的历史记录信息while(1){recv(sockfd,msg,sizeof(MSG),0);if('\0'==msg->data[0])break;//打印历史记录信息printf("%s\n",msg->data);}return 0;}int client_connect(char *ip, int port){int sockfd;struct sockaddr_in serveraddr;//创建流式套接字if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){perror("fail to socket.\n");return -1;}//给服务器地址初始化bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(ip);serveraddr.sin_port = htons(port);//连接服务器if(connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0){perror("fail to connect");return -1;}return sockfd;}

main.c

#include "client.h"int main(int argc, char *argv[]){ int n,m;char dart1[32],dart2[32];MSG msg;if(argc != 3){printf("Usage:%s serverip port\n", argv[0]);return -1;}int sockfd = client_connect(argv[1], atoi(argv[2]));//一级菜单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 next;}break;case 3:close(sockfd);exit(0);break;default:printf("Invalid data cmd.\n");n = 0;break;//若没有break,输入aaa会执行3次switch}}//二级菜单next:while(1){printf("###########################################\n");printf("*1.query2.history 3.quit*\n");printf("###########################################\n");printf("Please choose:");getchar();scanf("%d", &m);switch(m){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");break;//若没有break,输入aaa会执行3次switch}}return 0;} 

Makefile

创建Makefile 文件,也可以不做这步,只是多敲几串命令而已,Makefile最大优点就是直接输入make命令就能编译文件,也挺方便。

cli: main.c cli_dict.cgcc *.c -o cli

服务器端

sqlite3

需要在linux中安装sqlite3,没有安装的输入这两条命令

sudo apt-get install sqlite3
sudo apt-get install libsqlite3-dev

然后创建数据库 sqlite3 dictionaryOL.db;
在sqlite3 shell中创建表

# 改变user表结构CREATE TABLE user(id INTEGER PRIMARY KEY,name char unique,pwd char);# 创建dict 存放字典内容(单词,含义)CREATE TABLE dict(dict_id int primary key,word char,mean char);# 统计字典里有多个意思的单词select word from dict group by word having count(*)>=2;# 创建record 记录用户所查询过的单词(姓名,时间,单词)CREATE TABLE record(name char,date char,word char);

dict表需要自己插入单词和含义。

server.h

#ifndef SERVER_H#define SERVER_H#include #include #include #include #include #include #include #include #include #include #include //时间函数#include #include #define N 32#define R 1 //user - register#define L 2 //user - login#define Q 3 //user - query#define H 4 //user - history//数据库#define DATABASE "dictionaryOL.db"//定义通信双方的信息结构体typedef struct{int type;char name[N];char data[256]; //password或word信息}MSG;int do_client(int acceptfd,sqlite3 *db);int do_register(int acceptfd, MSG *msg,sqlite3 *db);int do_login(int acceptfd, MSG *msg,sqlite3 *db);int do_query(int acceptfd, MSG *msg,sqlite3 *db);int do_history(int acceptfd, MSG *msg,sqlite3 *db);int search_callback(void *para,int f_num,char **f_value,char **f_name);int history_callback(void *para,int f_num,char **f_value,char **f_name);void get_date(char *date);int do_client(int acceptfd,sqlite3 *db);int server_init(char *ip, int port);#endif

ser_dict.c

#include "server.h"int do_register(int acceptfd, MSG *msg,sqlite3 *db){char *errmsg;char sql[512];sprintf(sql,"insert into user values(null,\"%s\",\"%s\");",msg->name,msg->data);printf("%s\n", 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 -1;}return 0;}int do_login(int acceptfd, MSG *msg,sqlite3 *db){char sql[512] = {0};char *errmsg;int nrow;int ncloumn;char **resultp;sprintf(sql,"select * from user where name='%s' and pwd='%s';",msg->name,msg->data);printf("%s\n", sql);if(sqlite3_get_table(db,sql,&resultp,&nrow,&ncloumn,&errmsg)){printf("%s\n", errmsg);return -1;}if(nrow == 1){//查询成功,有此用户strcpy(msg->data,"OK");send(acceptfd,msg,sizeof(MSG),0);return 1;}else{//密码或者用户名错误strcpy(msg->data,"user/password/worng");send(acceptfd,msg,sizeof(MSG),0);return 0;}return 1;}int do_query(int acceptfd, MSG *msg,sqlite3 *db){printf("正在查询....");char word[256]={0};char mean[100]={0};int found=0;char date[255]={0};char sql[1024]={0};char *errmsg;//msg结构体中要查询的单词strcpy(word,msg->data);printf("%s\n",msg->data);//把单词存在数据库 找到返回1sprintf(sql,"select mean from dict where word=\'%s\'",word);printf("%s\n", sql);int rc=sqlite3_exec(db,sql,search_callback,mean,&errmsg);if(rc!=SQLITE_OK){fprintf(stderr,"失败:%s\n",sqlite3_errmsg(db));return -1;}if(strlen(mean)!=0){strcpy(msg->data,mean);found = 1;}printf("查询一个单词完毕\n");if(1==found)//找到单词,同时将用户名,时间,单词,放到历史记录表中{memset(sql,0,sizeof(sql));//需要获取系统时间get_date(date);sprintf(sql,"insert into record values( '%s','%s',\"%s\")",msg->name,date,word);printf("%s\n", sql);if(SQLITE_OK!=sqlite3_exec(db,sql,NULL,NULL,&errmsg)){printf("%s\n",errmsg);return -1;}else{printf("Insert record done\n");}}else //没有找到{strcpy(msg->data,"not found");}//将查询结果,发送给客户端send(acceptfd,msg,sizeof(MSG),0);return 0;}int do_history(int acceptfd, MSG *msg,sqlite3 *db){char sql[128]={0};char *errmsg;sprintf(sql,"select * from record where name = '%s'",msg->name);//查询数据库if(SQLITE_OK!=sqlite3_exec(db,sql,history_callback,(void *)&acceptfd,&errmsg)){printf("%s\n",errmsg);}else{printf("已查看历史\n");}//所有的记录查询发送完毕后给客户端发送结束信息msg->data[0]='\0';send(acceptfd,msg,sizeof(MSG),0);return 0;}int search_callback(void *para,int f_num,char **f_value,char **f_name){strcat((char*)para,f_value[0]);printf("mean: %s\n",(char*)para);return 0;}int history_callback(void *para,int f_num,char **f_value,char **f_name){int acceptfd =*(int*)para;MSG msg;sprintf(msg.data,"%s: %s",f_value[2],f_value[1]);send(acceptfd,&msg,sizeof(MSG),0);return 0;}void get_date(char *date){//获取时间time_t t;struct tm *p;time(&t);p = localtime(&t);sprintf(date,"%d-%d-%d%d:%d:%d",p->tm_year+1900,p->tm_mon+1,p->tm_mday,p->tm_hour,p->tm_min,p->tm_sec);}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("Invaliddata msg.\n");}}close(acceptfd);exit(0);return 0;}int server_init(char *ip, int port){int sockfd;struct sockaddr_in serveraddr; //创建流式套接字if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){perror("fail to socket.\n");return -1;}//给服务器地址初始化bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");serveraddr.sin_port = htons(8008);//绑定客户端if(bind(sockfd, (struct sockaddr *)&serveraddr,sizeof(serveraddr))<0){perror("fail to bind.\n");return -1;}//将套接字设为监听模式if(listen(sockfd, 5) < 0){printf("Fail to listen.\n");return -1;}//处理僵尸进程signal(SIGCHLD, SIG_IGN);printf("服务器成功运行--------\n");return sockfd;}

main.c

#include "server.h"int main(int argc, char *argv[]){ int sockfd;MSG msg;sqlite3 *db;pid_t pid;int acceptfd;struct sockaddr_in cddr={0};socklen_t len = sizeof(cddr);//打开数据库if(sqlite3_open(DATABASE, &db)!=SQLITE_OK){printf("%s\n", sqlite3_errmsg(db));return -1;} sockfd = server_init("0.0.0.0", 8008);while(1){//套接字 客户端地址 客户端大小if((acceptfd = accept(sockfd,(void *)&cddr,&len))<0){perror("fail to accept");return -1;}//解析客户端ip 和 port 需要用到inet_ntoa() ntohs()转换printf("IP:%s PORT:%d connected.\n",inet_ntoa(cddr.sin_addr),ntohs(cddr.sin_port));pid = fork();if(pid < 0){perror("fail to fork");return -1;}else if(pid == 0){//处理客户端具体的消息close(sockfd);do_client(acceptfd, db);}else{//父进程,用来接收客户端请求close(acceptfd);}}return 0;} 

Makefile

ser: main.c ser_dict.cgcc main.c ser_dict.c -o ser -lsqlite3

一定要注意gcc前面是Tab键,否则会报错,记得加上 -lsqlite3 连接一下库

编译运行

直接make就能编译,真的方便。若报警告不管,报错不能生成 cli 执行文件 或 ser 执行文件

# 先把服务器端打开./ser# 再把客户端打开,如果你有多台电脑互联也可以试试其他电脑ip./cli 127.0.0.1 8008# 若不想注册可用 用户名:bob密码:123登录。

思考与总结

服务器端代码步骤:
1.建立套接字 2.设置ip和端口号 3.绑定套接字 4.监听 5.接收客户端请求 6.创建子进程进行通信
客户端代码步骤:
1.建立套接字 2.设置ip和端口号 3.连接服务器 4.与服务器进行通信
数据库:
sqlite3_op()打开数据库 这里有一个 sqlite3 *db数据库操作句柄
sqlite3_exec()用来执行sql语句,里面有一个回调函数,基本上只有查询的时候用到。
sqlite3_get_table()参数char **resultp获取的就是他的结果 用resultp[1]这种形式访问,就像二维数组用一维的方式访问一样。

在客户端菜单界面用到switch语句,在输入scanf之后需要用getchar()带走脏字符,这是一个循环,若不这样做,下一次会读取到这个脏字符,但是还是没有完全解决。fgets也不好。

可以同时登陆一个账号,未解决变量作用域问题。

sprintf()会输入无线长度的字符串,不得直接使用无长度限制的字符拷贝函数。不应直接使用legacy的字符串拷贝、输入函数,如strcpy、strcat、sprintf、wcscpy、mbscpy等,这些函数的特征是:可以输出一长串字符串,而不限制长度。如果环境允许,应当使用其_s安全版本替代,或者使用n版本函数(如:snprintf,vsnprintf)。