目录
1:数据库的准备
2:服务器的编写
3:客户端的编写
4:可能出现的问题总结
前言:
电子词典的实现要兼顾注册,登录,查找,查看历史以及退出功能。所以要思考服务端和客户端之间交互(即数据的传送)的类型,交互的是用户登录信息,还是单词信息。
看代码时要注意方法,我建议是先看完头文件(不必完全理解是什么东西可以结合后面的代码一起理解)再去看主函数,看主函数时看到哪个函数就跳转到哪个函数去看,看完这个函数,再回到主函数刚刚的位置继续向下阅读,从头看到尾的方式我想对于我这种初学者还是很难看懂的。
一:数据库的准备
首先,既然是电子词典,我们参考有道,必须具备一个数据库用来存储用户账户,用户密码,单词以及单词的示意。所以我这里选择创建一个数据库,在数据库中创建两个表,一个用来存放用户信息,一个用来存放单词信息,如图:
需要数据库的留言邮箱即可。
二:服务端的编写
服务端的编写,我们选择比较标准和规范的形式,采用多文件编程的方式,也就是头文件,函数,主函数分开的形式来书写。开始接触这种方式可能感觉比较耗费时间,但在较大工程时这种方法便于维护和修改。
头文件函数:head.h
#ifndef _HEAD_H_
#define _HEAD_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sqlite3.h>
#include <signal.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdbool.h>
//消息的类型
#define USER_REGISTER 10
#define USER_LOGIN 20
#define USER_WORD 30
#define USER_SUCCESS 40
#define USER_FAILURE 50
//__attribute__((__packed__))
//作用:告诉编译器在编译的过程中取消优化对齐。
// 方便我们在发送和接收数据的时候一个字节一个字节的排列
typedef struct
{
char _username[25]; //用户名
char _password[25]; //密码
}__attribute__((__packed__))user_t;
typedef struct
{
int type; //消息类型
int size; //消息大小
union
{
user_t uinfo; //用户信息
char _word[100];
}content;
//客户端填单词,服务端填写单词解释
#define word content._word
#define username content.uinfo._username
#define password content.uinfo._password
}__attribute__((__packed__))mhead_t;
//'\'表示多行链接上一行表示, #deifne ....do...while(0);
//表示封装成独立的语法单元,防止被语法错误。
//注意:'\'之后不要留空格,要不然编译会有警告
#define EXEC_SQL(db,sql,errmsg) do{\
if(sqlite3_exec(db,sql,NULL,NULL,&errmsg) < 0)\
{\
fprintf(stderr,"sqlite3 execl [%s] error : %s.\n",sql,errmsg);\
exit(EXIT_FAILURE);\
}\
}while(0);
#endif
头文件文件需要说明的点不多,但需要分析为啥要这样定义结构体,你们先看,独立思考,再看注解!
!先自己思考
!先自己思考
!先自己思考
!先自己。。
!。。
!。。
!。。
!。。
!。。
首先我们分析,作为头文件,服务端需要我们,客户端也需要我们,这就意味着定义的宏和结构体必须要能完善的表达出需求和完成需求后信息发送。所以开始的宏定义就相当于一个标志,它标志我们这条信息是什么信息,例如:在信息中填写 USER_REGISTER 就意味着这是调注册信息,是客户端告诉服务器需要注册的信号,其他的也是如此,登录标志,查找单词的标志,成功的标志,失败的标志。如果现在不好理解也不用着急,后面可以结合代码的成功和失败标志去理解。
再者我们定义的结构体,看似很复杂,其实很简单。我们一层层看会发现,不过是一个消息的结构体里面包含了消息类型,消息大小和一个共用体,共用体内放的是用户信息。
最后的宏定义是为了减少代码量,就是再用到时不用写那么长了。
服务器主函数:
#include "head.h"
void signal_handler(int signum)
{
waitpid(-1,NULL,WNOHANG);
return;
}
int init_tcp(char *ip,char *port)
{
int sockfd;
struct sockaddr_in server_addr;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("Fail to socket");
exit(EXIT_FAILURE);
}
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(port));
server_addr.sin_addr.s_addr = inet_addr(ip);
if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
perror("Fail to bind");
exit(EXIT_FAILURE);
}
listen(sockfd,5);
printf("listen....\n");
return sockfd;
}
//.server ip port db
//数据库中已经手动创建了2个表:user_table,word_table
//注:由于我们后面函数要传承,故这里的const应该去掉
int main(int argc, char *argv[])
{
int pid;
sqlite3 *pdb;
int listenfd,connect_fd;
int addr_len = sizeof(struct sockaddr);
struct sockaddr_in peer_addr;
if(argc < 4)
{
fprintf(stderr,"Usage : %s ip port system.db.\n",argv[0]);
exit(EXIT_FAILURE);
}
//探测子进程的改变状态,回收僵尸态子进程
if(signal(SIGCHLD,signal_handler) == SIG_ERR)
{
perror("Fail to signal");
exit(EXIT_FAILURE);
}
if(sqlite3_open(argv[3],&pdb) != SQLITE_OK)
{
fprintf(stderr,"sqlite3 open %s : %s.\n",argv[3],sqlite3_errmsg(pdb));
exit(EXIT_FAILURE);
}
//初始化tcp连接,得到监听套接字
listenfd = init_tcp(argv[1],argv[2]);
//提取客户段的链接请求,创建子进程和客户端交互
while(1)
{
if((connect_fd = accept(listenfd,(struct sockaddr *)&peer_addr,&addr_len)) < 0)
{
perror("Fail to accept");
exit(EXIT_FAILURE);
}
printf("IP:%s PORT:%d\n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port));
if((pid = fork()) < 0)
{
perror("Fail to fork");
exit(EXIT_FAILURE);
}
//创建子进程处理客户端的请求
if(pid == 0){
close(listenfd);
do_client(connect_fd,pdb);
}
close(connect_fd);
}
exit(EXIT_SUCCESS);
}
主函数这边是一个很基本的创建服务器网络连接和打开库的操作,向函数中传递套接字和库的指针,通过函数来完成操作才是重点,下面我们来看函数的编写。
函数:do_client.c
#include "head.h"
int do_register(int sockfd,sqlite3 *pdb,char *_username,char *_password)
{
char *errmsg;
char buf[1024];
char **dbresult;
int nrow = 0,ncolumn = 0;
char sql[1024] = {0};
mhead_t *head = (mhead_t *)buf;
sprintf(sql,"select * from user_table where NAME='%s';",_username);
if(sqlite3_get_table(pdb,sql,&dbresult,&nrow,&ncolumn,&errmsg) != 0)
{
fprintf(stderr,"sqlite3 get table error : %s.\n",errmsg);
exit(EXIT_FAILURE);
}
//没有这样的用户名
if(nrow == 0)
{
//录入数据库
bzero(sql,sizeof(sql));
sprintf(sql,"insert into user_table values('%s','%s');",_username,_password);
EXEC_SQL(pdb,sql,errmsg);
printf("ok ........\n");
head->type = USER_SUCCESS;
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
//注册失败,用户名存在
}else{
head->type = USER_FAILURE;
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
//表示未知
printf("???????\n");
}
//插入到数据库之后,释放dbresult结果
sqlite3_free_table(dbresult);
return 0;
}
//===============================================================================================
int log_in(int sockfd,sqlite3 *pdb,char *_username,char *_password)
{
char *errmsg;
char buf[1024];
char **dbresult;
int nrow = 0,ncolumn = 0;
char sql[1024] = {0};
mhead_t *head = (mhead_t *)buf;
sprintf(sql,"select * from user_table where NAME='%s' and PASSWORD='%s';",_username,_password);
if(sqlite3_get_table(pdb,sql,&dbresult,&nrow,&ncolumn,&errmsg) != 0)
{
fprintf(stderr,"sqlite3 get table error : %s.\n",errmsg);
exit(EXIT_FAILURE);
}
//没有这样的用户名或错误的用户名和密码
if(nrow == 0)
{
printf("系统没找到该用户,会提示其重新输入或注册\n");
head->type = USER_FAILURE;
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
//用户存在且正确
}else{
head->type = USER_SUCCESS;
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
//表示未知
printf("账户密码正确已给出提示\n");
}
//搜索完数据库之后,释放dbresult结果
sqlite3_free_table(dbresult);
return 0;
}
//================================================================================================
int select_word(int sockfd,sqlite3 *pdb,mhead_t *buf)
{
char *errmsg;//数据库错误消息
char **dbresult;//查询结果信息
int nrow = 0,ncolumn = 0;
int i,j,n,index=0;
char sql[1024] = {0};
char sql_2[1024] = {0};
mhead_t *head = (mhead_t *)buf;
head->word[strlen(head->word) - 1] = '\0';//不去不行,有空格无法识别
printf("接收到了%s",head->word);
sprintf(sql_2,"select * from dict_table");
sprintf(sql,"select word from dict_table where word= '%s'",head->word);
printf("sql:%s\n",sql);
if(sqlite3_get_table(pdb,sql,&dbresult,&nrow,&ncolumn,&errmsg) != 0)
{
fprintf(stderr,"sqlite3 get table error : %s.\n",errmsg);
exit(EXIT_FAILURE);
}
if(nrow == 0)//没找到单词
{
head->type = USER_FAILURE;
head->size = sizeof(mhead_t);
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
}else{
head->type = USER_WORD;
if(sqlite3_get_table(pdb,sql_2,&dbresult,&nrow,&ncolumn,&errmsg) != 0)
{
fprintf(stderr,"sqlite3 get table error : %s.\n",errmsg);
exit(EXIT_FAILURE);
}
for(i=0;i<=nrow;i++)
{
for(j=0;j<=ncolumn;j++)
{
if(strncmp(dbresult[index],head->word,sizeof(head->word))==0)
{
// head->word = dbresult[index+1];这样等于修改指针不行
strcpy(head->word,dbresult[index+1]);
// printf("%s",dbresult[index+1]);
i=nrow+1;
break;
}
index++;
}
}
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
//表示未知
printf("发送注解成功\n");
}
//插入到数据库之后,释放dbresult结果
sqlite3_free_table(dbresult);
return 0;
}
//================================================================================================
int do_client(int sockfd,sqlite3 *pdb)
{
int n;
int count = 0;
char buf[1024];
mhead_t *head = (mhead_t *)buf;
while(1)
{
count = 0;
//接收协议头
while(1)
{
n = recv(sockfd,buf + count,sizeof(mhead_t) - count,0);
if(n <= 0){
exit(EXIT_FAILURE);
}
count += n;
printf("count : %ld mhead_t : %ld\n",count,sizeof(mhead_t));
printf("%s\n",head->word);
if(count == sizeof(mhead_t))
break;
}
switch(head->type)
{
case USER_REGISTER://注册
do_register(sockfd,pdb,head->username,head->password);
break;
case USER_LOGIN://登录
log_in(sockfd,pdb,head->username,head->password);
break;
case USER_WORD://查询单词
select_word(sockfd,pdb,(mhead_t *)buf);
break;
defalut:
exit(EXIT_SUCCESS);
}
}
return 0;
}
(还以为csdn很智能帮我画了分割线,后来发现是我自己加的)
从主函数进入的函数是do_client(),进入后便是通过判断传递来的信息,选择对应的函数进行处理,为什么可以判断这就是头文件中所提及标志的作用,通过判断信息中夹带的标志去做出对应处理。从上到下三个函数分别是注册(do_register),登录(log_in),和查找单词(select_word)的函数。每个函数的写法其实大同小异,都是通过对比数据库内的信息做出相应判断。比如注册就是接收到客户端的信息后,查看数据库是否有对应信息,然后按情况分开处理,处理完成后发送相关提示告诉客户端完成了怎么样的处理。当然发送给客户端的信息也是需要标志的,比如注册完成后,发送给客户端的信息就要有成功的标志(USER_SUCCESS),注册失败就要有失败的标志(USER_FAILURE),要不然客户端收到信息,也不知道到底是成功还是失败,也就无法做出相应判断了。现在知道了服务器的运行规则,那么客户端也就手到擒来。
二:客户端的编写
客户端的头文件:
和服务器头文件一样,直接复制一份即可。
客户端的主函数和函数:
我这里并没有将主函数和对应函数分开,是方便去看整体,更好理解(我真的不是在偷懒)。
#include "head.h"
//用户提示界面1
void help_info1()
{
printf("\t-----------------------------------------------\n");
printf("\t| HENRY 在线辞典 |\n");
printf("\t|版本:0.0.1 |\n");
printf("\t|作者:HQYJ-22061班王某某 |\n");
printf("\t|功能: |\n");
printf("\t| [1] 登录 |\n");
printf("\t| [2] 注册 |\n");
printf("\t| [3] 退出 |\n");
printf("\t|注意:用户只有登录成功后才能进入查单词界面 |\n");
printf("\t------------------------------------------------\n");
return;
}
void help_info2()
{
printf("\t-----------------------------------------------\n");
printf("\t| HENRY 在线辞典 |\n");
printf("\t|版本:0.0.1 |\n");
printf("\t|作者:HQYJ-22061班王某某 |\n");
printf("\t|功能: |\n");
printf("\t| [1] 查询单词 |\n");
printf("\t| [2] 查询历史 |\n");
printf("\t| [3] 退出 |\n");
printf("\t|注意:用户只有登录成功后才能进入查单词界面 |\n");
printf("\t------------------------------------------------\n");
return;
}
//用户输入指令,供大家选择
enum{
LOGIN = 1, //登陆
REGISTER = 2, //注册
QUIT = 3, //退出
QUERY = 1, //查询单词
HISTORY = 2, //查询历史
};
//==================================定义一个链表,记录历史
struct history
{
char time_buf[64];
char word_buf[64];
char explain_buf[1024];
};
struct Node
{
struct history all_buf;
struct Node *next;
};
//================================================================
//链表的头插
void head_insert(struct Node **head_ref,char *new_val1,char *new_val2)
{
time_t t;
time(&t);//获得从1970年1月1日开始到现在有å少秒
struct Node *new1=NULL;
new1 = (struct Node *)malloc(sizeof(struct Node));
strcpy(new1->all_buf.time_buf,ctime(&t));
strcpy(new1->all_buf.word_buf,new_val1);
strcpy(new1->all_buf.explain_buf,new_val2);
printf("历史记录添加成功\n");
new1->next=*head_ref;//*q<===>head
*head_ref=new1;
}
bool isEmpty(struct Node *head_ref)//判断链表是否为空
{
return head_ref == NULL ? true :false;
}
void printfList(struct Node *head)
{
struct Node *p=head;
printf("============================================\n");
while(p != NULL)
{
printf("时间:%s ",head->all_buf.time_buf);
printf("单词:%s ",head->all_buf.word_buf);
printf("单词示意:%s ",head->all_buf.explain_buf);
putchar('\n');
p=p->next;
}
printf("============================================");
putchar('\n');
}
//================================================================
int init_tcp(char *ip,char *port)
{
int sockfd;
struct sockaddr_in server_addr;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("Fail to socket");
exit(EXIT_FAILURE);
}
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(port));
server_addr.sin_addr.s_addr = inet_addr(ip);
if(connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
perror("Fail to bind");
exit(EXIT_FAILURE);
}
return sockfd;
}
int do_register(int sockfd)
{
int n = 0;
int count = 0;
char buf[1024] = {0};
//定义发送的协议头
mhead_t *head = (mhead_t *)buf;
printf("\n您正在注册,请输入用户名和密码\n");
head->type = USER_REGISTER;
head->size = sizeof(mhead_t);
printf("Input username : ");
fgets(head->username,sizeof(head->username),stdin);
head->username[strlen(head->username) - 1] = '\0';
printf("Input password : ");
fgets(head->password,sizeof(head->password),stdin);
head->password[strlen(head->password) - 1] = '\0';
//发给服务器端
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
bzero(&buf,sizeof(buf));
while(1)
{
//接收数据,TCP是可靠的连接,若是数据
//未完全接收的话,可以在接收
n = recv(sockfd,buf + count,sizeof(mhead_t) - count,0);
if(n <= 0){
perror("Fail to send");
exit(EXIT_FAILURE);
}
//若是数据未发送完成,再次接收的时候可补充
count += n;
if(count == sizeof(mhead_t))
break;
}
if(head->type == USER_SUCCESS)
{
printf("\n恭喜您,注册成功!\n");
return 0;
}else{
printf("\n很遗憾,这个用户名已经被其它用户注册过了,请重新注册");
return -1;
}
}
//================================================================================================
int select_word(int sockfd,struct Node **head_his)
{
char buf[1024]={0};
char tmp_buf[1024]={0};
int count = 0;
int n;
ssize_t send_bytes;
//定义发送的协议头
mhead_t *head = (mhead_t *)buf;
head->type = USER_WORD;
head->size = sizeof(mhead_t);
printf("Input the word:");
fgets(head->word,sizeof(head->word),stdin);
strcpy(tmp_buf,head->word);//备份单词,用于链表插入
send_bytes=send(sockfd,head,sizeof(mhead_t),0);
if(send_bytes < 0)
{
perror("Fail to word send");
exit(EXIT_FAILURE);
}
bzero(&buf,sizeof(buf));
while(1)
{
//接收数据,TCP是可靠的连接,若是数据
//未完全接收的话,可以在接收
n = recv(sockfd,head + count,sizeof(mhead_t) - count,0);
if(n <= 0){
perror("Fail to send");
exit(EXIT_FAILURE);
}
//若是数据未发送完成,再次接收的时候可补充
count += n;
if(count == sizeof(mhead_t))
break;
}
if(head->type == USER_WORD)
{
printf("单词示意:%s\n",head->word);
head_insert(head_his,tmp_buf,head->word);//将单词和注释加入链表
return 0;
}else{
printf("\n很遗憾,没找到该单词\n");
return -1;
}
}
void history_menu(struct Node *head_his)
{
printf("进入查询模式\n");
// struct Node *head_his=NULL;//一定要初始化,不然段错误
printfList(head_his);
printf("查询完成,退出查询\n");
}
//===========================================================================================
int do_task_2(int sockfd)
{
int cmd;
struct Node *head_his=NULL;
while(1)
{
//提示界面帮助,用户选择
help_info2();
printf("\n\n请选择>");
scanf("%d",&cmd);
//吃掉回车键
getchar();
// QUERY = 1, //查询单词
// HISTORY = 2, //查询历史
switch(cmd)
{
case QUERY:
select_word(sockfd,&head_his);
break;
//用户登陆
case HISTORY:
history_menu(head_his);
break;
case QUIT:
exit(EXIT_SUCCESS);
default:
printf("Unknow cmd.\n");
continue;
}
}
return 0;
}
int log_in(int sockfd)
{
int n = 0;
int count = 0;
char buf[1024] = {0};
//定义发送的协议头
mhead_t *head = (mhead_t *)buf;
printf("\n您正在登录,请输入用户名和密码\n");
head->type = USER_LOGIN;
head->size = sizeof(mhead_t);
printf("Input username : ");
fgets(head->username,sizeof(head->username),stdin);
head->username[strlen(head->username) - 1] = '\0';
printf("Input password : ");
fgets(head->password,sizeof(head->password),stdin);
head->password[strlen(head->password) - 1] = '\0';
//发给服务器端
if(send(sockfd,buf,sizeof(mhead_t),0) < 0)
{
perror("Fail to send");
exit(EXIT_FAILURE);
}
bzero(&buf,sizeof(buf));
while(1)
{
//接收数据,TCP是可靠的连接,若是数据
//未完全接收的话,可以在接收
n = recv(sockfd,buf + count,sizeof(mhead_t) - count,0);
if(n <= 0){
perror("Fail to send");
exit(EXIT_FAILURE);
}
//若是数据未发送完成,再次接收的时候可补充
count += n;
if(count == sizeof(mhead_t))
break;
}
if(head->type == USER_SUCCESS)
{
printf("\n恭喜您,登录成功!\n");
do_task_2(sockfd);
return 0;
}else{
printf("\n账户或密码错误!!!\n");
return -1;
}
}
int do_task(int sockfd)
{
int cmd;
while(1)
{
//提示界面帮助,用户选择
help_info1();
printf("\n\n请选择>");
scanf("%d",&cmd);
//吃掉回车键
getchar();
switch(cmd)
{
//用户注册,我们先来写注册的函数
case REGISTER:
if(do_register(sockfd) < 0)
continue;
break;
//用户登陆
case LOGIN:
log_in(sockfd);
break;
case QUIT:
exit(EXIT_SUCCESS);
default:
printf("Unknow cmd.\n");
continue;
}
}
return 0;
}
//./client ip port
//由于后面要传递参数,故这里的const省略
int main(int argc, char *argv[])
{
int sockfd;
int addr_len = sizeof(struct sockaddr);
struct sockaddr_in peer_addr;
if(argc < 3)
{
fprintf(stderr,"Usage : %s argv[1] argv[2]\n",argv[0]);
exit(EXIT_FAILURE);
}
sockfd = init_tcp(argv[1],argv[2]);
do_task(sockfd);
return 0;
}
先看自己看看呗
。。
。。
。。
。。
。。
。。
。。
。。
。。
看完你发现,服务端和客户端其实没有太大差别,服务器是先接收,再在数据里里找,在发送信息给客户端;客户端是先发信息,再接收服务器给的回应。无非是交换了顺序,而且还不需要访问数据库。
四:可能会出现的问题总结
1:在客户端中select_word函数里有这样一条语句
strcpy(tmp_buf,head->word);//备份单词,用于链表插入
上面是最终修改的,下面是我第一次写的
tmp_buf = head->word;
思考一下,两者都能实现吗?两者功能相同吗?两者本质是什么?
、
、
、
、
答案揭晓,第二种方法完全错误,就不应该这样写。tmp_buf是一个以及初始化的字符数组,而head->word是指针指向的一个字符串,所以让一个以及初始化数组赋值一个字符串是行不通的,编译就会错误更不用考虑功能的实现了,所以我们使用strcpy,将前几位替换为head->word字符串。
2:在服务器中,对数据库进行查找时要注意,接收到的信息是个字符串,也就意味着其末尾会有“\0”这个玩意的存在,但在数据库中存放的单词末尾是没有“\0”的,所以有时候你发现好像程序完美,所以地方都是通的,但就是找不到对应单词就是这个原因,所以记得查找时删除掉末尾的“\0”
head->word[strlen(head->word) - 1] = '\0';//不去不行,有空格无法识别