Bootstrap

linux下网络编程综合项目——在线词典

目录

项目概述:

项目所实现的功能:

项目的实现

实现原理简单说明:

代码实现:

客户端client.c:

服务器端server.c:


项目概述:

在线词典,运用c语言与网络编程部分的知识来实现在线的英译英(因为提供的词典为全英文版),在客户端与服务器端连接成功后,客户端输入想查询的单词,服务器端会在词库中寻找这个单词的解释并发送给客户端,从而实现在线查询。

项目所实现的功能:

  1. 用户注册和登录验证 服务器端将用户信息和历史记录保存在数据库中。
  2. 客户端输入用户名和密码,服务器端在数据库中查找、匹配,返回结果
  3. 单词在线翻译 根据客户端输入的单词在字典文件中搜索
  4. 历史记录查询

项目实现图示:

项目的实现:

实现原理简单说明:

  1. 客户端的操作界面使用简单的打印来实现,通过客户的输入来选择所实现的功能
  2. 客户端与服务器端的通信使用socket套接字来实现
  3. 项目的并发由epoll函数实现
  4. 用户信息和查询记录分别存放在usr表与record表中,他们两个都建在 my.db 数据库下,usr表有两列,分别为name和pass,其中name为主键,使得其不能出现相同的用户名,这两列的数据类型都为string,record有name,data,word三列,word存放客户端查询过的单词,data为查询时的时间,数据库和表的创建还是比较简单的下面就直接拿来用了。
  5. 单词查询的过程使用文件IO来实现,通过客户端的输入与词典中的单词对比来判断是否查询到了这个单词,查询到后会把其解释发送给客户端

代码实现:

代码并没有进行很好的封装,只有客户端和服务器端两个.c文件,具体的原理见代码注释。

客户端client.c:

#include<stdio.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include<string.h>
#include<unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<stdlib.h>
//错误输出的宏定义
#define ERREXIT(msg) do{perror(msg);  \
	exit(EXIT_FAILURE); }while(0)

#define R 1    //注册 register
#define L 2    //登录 login
#define Q 3    //查询单词 query
#define H 4    //查询历史记录 history

//客户端与服务器端的通信结构体
typedef struct{

	int type;//存放消息类型,与上面定义的宏对应,服务器端通过判断它的值来做出相应的操作
	char name[32];//存放用户名
	char data[256];//存放所要发送的消息,比如密码、解释、报错等

}MSG;

//注册的实现
int do_register(int fd,MSG *msg){
	
	msg->type = R;//消息类型为R,后面不做解释
		
	printf("please input name:");
	scanf("%s",msg->name);//输入要注册的姓名并传给通信结构体的name,
	getchar();//获取垃圾字符
	printf("please input data:");
	scanf("%s",msg->data);//输入密码并传给通信结构体的data

    //将msg发送给服务器端
	if((send(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("send");

    //接收服务器端的反馈,注册是否成功
	if((recv(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("recv");

    //打印服务器端的反馈信息
	printf("%s\n",msg->data);
	

	return 0;
}
//登录的实现,与注册基本一致
int do_login(int fd ,MSG *msg){
	msg->type = L;

	printf("please input name:");
	scanf("%s",msg->name);
	getchar();
	printf("please input data:");
	scanf("%s",msg->data);


	if((send(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("send");

	if((recv(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("recv");


    //因为客户端登陆成功后就会进入到查找单词的界面
    //所以我们要判断服务器端返回的消息是否是登录成功
    //当服务器端发送OK给客户端时表示登录成功,这时函数返回1,
    //主函数就会实现对应操作来跳转到查询界面
    //若不是OK则表示注册失败,返回0并打印错误信息
	if((strncmp(msg->data,"OK",3)) == 0){
		printf("%s\n",msg->data);
		return 1;
	}else{
		printf("%s\n",msg->data);
		return 0;
	}



}

//查询的实现
int do_query(int fd , MSG *msg){
	
	msg->type = Q;
    //循环的查询单词,当输入#时退出单词查询
	while(1){
	printf("please input the word:");
	scanf("%s",msg->data);
	getchar();

	if((strncmp(msg->data,"#",1) == 0))
		break;
	if((send(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("send");

	if((recv(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("recv");

	printf("%s\n",msg->data);
	}
	

	return 0;

}

//历史记录查询的实现
int history_record(int fd , MSG *msg){
	msg->type = H;


	if((send(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("send");
	
	while(1){


	if((recv(fd,msg,sizeof(MSG),0)) < 0)
		ERREXIT("recv");
    //当服务器端查询完所有record表中的信息后我们让他返回\0
    //所以当服务器端传回的消息为\0时表示查询完成
	if(msg->data[0] == '\0'){
		printf("record finish!\n");
		break;
	}
	else
		printf("%s\n",msg->data);
	}
	return 0;

}

int main(int argc, const char *argv[])
{
    //使用命令行传参,第二个参数为ip地址,第三个参数为端口号
	if(argc != 3){
		printf("too few num\n");
		return -1;
	}
	MSG msg;
	int fd,choosenum;
    //socket使用时所需要的一些初始化
	struct sockaddr_in addr;
	socklen_t socklen = sizeof(addr);
	bzero(&addr,socklen);
	addr.sin_port = htons(atoi(argv[2])); 
	addr.sin_family = AF_INET;
	if (inet_aton(argv[1], &addr.sin_addr) == 0) {
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}

	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd < 0)
		ERREXIT("socket");

	if((connect(fd,(struct sockaddr *)&addr,socklen))<0)
		ERREXIT("connect");

    //打印登录界面
	while(1){

		printf("***********************************************************************\n");
		printf("* 1.register          2.login     		         3.quit               *\n");
		printf("***********************************************************************\n");
		printf("Please choose:");
        //通过客户端输入判断需要进行的操作
		scanf("%d",&choosenum);
		getchar();
		switch(choosenum){

		case 1:
			do_register(fd,&msg);
			break;
		case 2:
            //当登录函数返回值为1时表示登录成功,需要跳转到查询界面
			if((do_login(fd,&msg)) > 0)
				goto login;
			break;
		case 3:
			printf("Exit Success!\n");
			close(fd);
			return 0;
			break;
		default:
			printf("please input true num!\n");


		}
	}
//查询界面
login:
		while(1){
			printf("************************************************************\n");
			printf("* 1.query_word   2.history_record   3.quit       		   *\n");
			printf("************************************************************\n");
			printf("Please choose:");
			scanf("%d",&choosenum);
			getchar();
			switch(choosenum){
			case 1:
				do_query(fd,&msg);
				break;
			case 2:
				history_record(fd,&msg);
				break;
			case 3:
				printf("Exit Success!\n");
				close(fd);
				return 0;
				break;
			default:
				printf("please input true num!\n");


			}




		}
	
	return 0;
}

服务器端server.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sqlite3.h>
#include <signal.h>
#include <time.h>
#include <sys/epoll.h>

//下面这些定义都与客户端相同,不多做解释

#define  R  1   // user - register
#define  L  2   // user - login
#define  Q  3   // user - query
#define  H  4   // user - history
#define MAXEVENTS 1024
#define  DATABASE  "my.db"
#define ERREXIT(msg) do{ perror(msg); \
	exit(EXIT_FAILURE) ;}while(0)
// 定义通信双方的信息结构体
typedef struct {
	int type;
	char name[32];
	char data[256];
}MSG;


//一些方法的声明
int do_client(int acceptfd, sqlite3 *db);
void 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 history_callback(void* arg,int f_num,char** f_value,char** f_name);
int do_searchword(int acceptfd, MSG *msg, char word[]);
int get_date(char *date);


int main(int argc, const char *argv[])
{

	int sockfd,epfd;
	int i,nfds;
	MSG msg;
	struct sockaddr_in  serveraddr;
	sqlite3 *db;
	socklen_t socklen = sizeof(struct sockaddr_in);

	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;
	}
	else
	{
		printf("open DATABASE success.\n");
	}
    //服务器端的socket操作

	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(argv[1]);
	serveraddr.sin_port = htons(atoi(argv[2]));



	if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in)) < 0)
	{
		perror("fail to bind.\n");
		return -1;
	}

	// 将套接字设为监听模式
	if(listen(sockfd, 5) < 0)
	{
		printf("fail to listen.\n");
		return -1;
	}
    //多路复用的实现,epoll函数
	struct epoll_event epl,eplevent[MAXEVENTS] = {};
	epfd = epoll_create(10);
	if(epfd < 0)
		ERREXIT("epoll_create");
	epl.events = EPOLLIN;
	epl.data.fd = sockfd;

	if((epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&epl)) < 0)
		ERREXIT("epoll_ctl");

	while(1)
	{
        //epoll实现,不懂可以看我前面关于epoll的文章
		nfds = epoll_wait(epfd,eplevent,MAXEVENTS,-1);
		if (nfds<0)
			ERREXIT("epoll_wait");
		for(i = 0;i<nfds;i++){
			if(eplevent[i].data.fd == sockfd){
				int acceptfd = accept(sockfd, (struct sockaddr *)&serveraddr,&socklen);
				if(acceptfd < 0)
					ERREXIT("accept");
				epl.events = EPOLLIN;
				epl.data.fd = acceptfd;
            //打印是哪个客户端进行了连接
				printf("[%s,%d]has connect\n",inet_ntoa(serveraddr.sin_addr),ntohs(serveraddr.sin_port));

				if((epoll_ctl(epfd,EPOLL_CTL_ADD,acceptfd,&epl)) < 0)
					ERREXIT("epoll_ctl");
			}else{
				
                //接受客户端发送的通信结构体并判断其类型
				if(recv(eplevent[i].data.fd, &msg, sizeof(MSG), 0) > 0)
				{
					int acceptfd = eplevent[i].data.fd;
					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");
					}

				}else{
                //当有客户端退出时所要执行的操作
				printf("client exit.\n");
				close(eplevent[i].data.fd);
				epoll_ctl(epfd,EPOLL_CTL_DEL,eplevent[i].data.fd,&epl);
				}

			}
		}
	}

	close(sockfd);
	close(epfd);
	return 0;
}


//注册的实现
void do_register(int acceptfd, MSG *msg, sqlite3 *db)
{
	char * errmsg;
	char sql[128];
    //字符串拼接,把想要执行的sql语句拼接到sql数组中,别忘记分号
	sprintf(sql, "insert into usr values('%s', %s);", msg->name, msg->data);
	//服务器端打印执行的语句,主要用来测试
    printf("%s\n",sql);
    //执行sql语句的函数,具体解释见我前面关于sqlite的api的文章
	if(sqlite3_exec(db,sql, NULL, NULL, &errmsg) != SQLITE_OK)
	{
        //将执行成功或失败分别对应的信息存入msg的data中并发送给客户端
		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 ;
	}

}
//注册的实现
int do_login(int acceptfd, MSG *msg , sqlite3 *db)
{
	char sql[128] = {};
	char *errmsg;
	int nrow;
	int ncloumn;
	char **resultp;

	sprintf(sql, "select * from usr where name = '%s' and pass = '%s';", msg->name, msg->data);
    //此函数用来获得表的数据,nrow表示有几行,ncloumn表示有几列
	if(sqlite3_get_table(db, sql, &resultp, &nrow, &ncloumn, &errmsg)!= SQLITE_OK)
	{
		printf("%s\n", errmsg);
		return -1;
	}
	else
	{
		printf("get_table ok!\n");
	}

	// 查询成功,数据库中拥有此用户
	if(nrow == 1)
	{
        //发送OK给客户端,客户端接收到后会去跳转到查询界面
		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;
}

//查找单词函数的实现
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.\n");
		strcpy(msg->data, "Failed to open dict.txt");
		send(acceptfd, msg, sizeof(MSG), 0);
		return -1;
	}

	//打印出,客户端要查询的单词
	len = strlen(word);//求所要查询的单词的长度
	printf("%s , len = %d\n", word, len);

	//读文件,来查询单词,遍历整个词库来寻找
	while(fgets(temp, 512, fp) != NULL)
	{

        //比较要查找的单词每个字母的ASCII码值是否与词库中的单词相同
        //能继续寻找的条件只有当词库中的单词的ASCII码值<要查询的单词,
        //当词库中的单词的每个字母都与所要查询的单词相同且单词长度也相同时表示找到了
        //若要查询的单词>词库中的单词时,则表示这个单词再词库中时不存在的
        
		result = strncmp(temp,word,len);
        //词库的单词对应的字母的值比要查找的小,继续查找
		if(result < 0)
		{
			continue;
		}
        //词库的单词对应的字母比要查找的大,又或者他们俩的每个值都相等但是词库中的词长度>len
        //例如 想查单词d,词库中的词的值与d相等的只有比他长的,所以就没有这个单词;
        //result>0则表示要查询的单词在词库里是不存在的,直接跳出循环打印错误消息即可,这里选择返回0让调用这个方法的函数来进行后续操作
		if(result > 0 || ((result == 0) && (temp[len]!=' ')))
		{
			break;
		}
     
		// 执行到这个位置表示找到了所要查询的单词
        //我们的逻辑时只需要打印其注释即可,而词库中单词和注释之间使用空格隔开的
        //这里我们定义一个指针p,让他指向我们get出来的信息加上这个单词的长度,
        //这样这个指针就指向了这个单词的末尾,然后我们删除掉单词与注释之间的空格
        //这样我们的指针p的首地址就是temp这个字符串数组从注释开始的位置,然后将其拷贝到msg的data中发送即可
		p = temp + len; //  abandon   v.akdsf dafsjkj 
		//	printf("found word:%s\n", p);
		while(*p == ' ')
		{
			p++;
		}

		// 找到了注释,跳跃过所有的空格
		strcpy(msg->data, p);
		printf("found word:%s\n", msg->data);

		// 注释拷贝完毕之后,应该关闭文件
		fclose(fp);
		return 1;
	}

	fclose(fp);

	return 0;
}

//获取当前时间,详细使用我前面的文章也有
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, tp->tm_mon+1, tp->tm_mday, 
			tp->tm_hour, tp->tm_min , tp->tm_sec);
	printf("get date:%s\n", date);

	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;

	//拿出msg结构体中,要查询的单词
	strcpy(word, msg->data);

	found = do_searchword(acceptfd, msg, word);
	printf("查询一个单词完毕.\n");

	// 表示找到了单词,那么此时应该将 用户名,时间,单词,插入到历史记录表中去。
	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
		{
			printf("Insert record done.\n");
		}

	}
	else  //表示没有找到
	{
		strcpy(msg->data, "Not found!");
	}

	// 将查询的结果,发送给客户端
	send(acceptfd, msg, sizeof(MSG), 0);

	return 0;
}

// 查询历史记录的回调函数,并且需要将历史记录发送给客户端
// arg是要操作的文件描述符,num是要查询的表的列数,value与name是数组,
//value用来存放列的值的地址,name用来存放列的名称
int history_callback(void* arg,int f_num,char** f_value,char** f_name)
{
	// record表的结构是:name  , date  , word 
    //所以value[0]表示name这列的元素,value[1]和value[2]分别表示date和word列中的元素
	int acceptfd;
	MSG msg;

	acceptfd = *((int *)arg);
    
    //发送查询的信息,因为客户端时登陆成功才能查询,所以不需要发送用户名
    //所以value[0]中的用户名信息不需要发送,直接发value[1]和value[2]即可
	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;

	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);
	}
	else
	{
		printf("Query record done.\n");
	}

    //因为客户端需要循环读取服务器端查询的信息,所以在查询完成时应该通知客户端不需要再读了
	// 所以所有的记录查询发送完毕之后,给客户端发出一个结束信息
	msg->data[0] = '\0';

	send(acceptfd, msg, sizeof(MSG), 0);

	return 0;
}

Makefile和词典的.txt文件就不展示了,一个自己写一个在网上淘吧

;