TCP云词典项目

1.项目描述

        TCP云词典项目是一个基于TCP协议的网络应用程序,旨在提供在线的词典查询功能。用户可以通过客户端与服务器建立连接,并在客户端输入要查询的单词,服务器将返回相应的释义和解释,而且应该支持多个客服端。

项目主要分为客户端和服务器两部分,其主要功能如下:

  1. 客户端功能:

    • 与服务器建立TCP连接。
    • 账号注册及出错反馈。
    • 用户登录成功或失败反馈
    • 登录成功将提供用户界面,接收用户输入的单词。
    • 将用户输入的请求发送给服务器。
    • 接收并显示服务器返回的结果,包括词义、解释等。
    • 提供查询历史记录等。
    • 提供菜单选项,例如退出、返回等。
  2. 服务器功能:

    • 采用io多路复用,监听指定的端口,等待一个或多个客户端连接。
    • 接收客户端发送的请求。
    • 使用数据库,建立单词表和用户信息表
    • 根据请求查询相应的词义和解释。
    • 将查询结果发送给客户端。
    • 根据客服端输入的请求,反馈结果给客服。

项目使用C语言编写,并涉及以下关键技术点:

  • 使用socket库创建和管理套接字,实现客户端和服务器的网络通信。
  • 客户端使用TCP协议与服务器建立连接,并发送请求和接收响应。
  • 客服端使用多线程或者多进程分别处理键盘输入或者服务器的响应。
  • 服务器使用io多路复用来处理多个客户端的并发请求。
  • 使用数据库存储用户的账号密码,并设定key值。
  • 使用数据库存储词典数据,服务器根据客户端请求查询对应的数据。
  • 使用适当的数据结构来组织和存储词典数据,以便快速查询。
  • 使用链表等数据结构管理每个用户的查找记录,以便用户查看。
  • 实现用户友好的界面,并提供错误处理和异常情况处理机制。

2.项目介绍

2.1原理

  1. 客户端功能:

    • 与服务器建立TCP连接。
    • 账号注册及出错反馈。
    • 用户登录成功或失败反馈
    • 登录成功将提供用户界面,接收用户输入的单词。
    • 将用户输入的请求发送给服务器。
    • 接收并显示服务器返回的结果,包括词义、解释等。
    • 提供查询历史记录等。
    • 提供菜单选项,例如退出、返回等。
  2. 服务器功能:

    • 采用io多路复用,监听指定的端口,等待一个或多个客户端连接。
    • 接收客户端发送的请求。
    • 使用数据库,建立单词表和用户信息表
    • 根据请求查询相应的词义和解释。
    • 将查询结果发送给客户端。
    • 根据客服端输入的请求,反馈结果给客服。
  3. 通信协议:

    • 服务器和客户端之间使用TCP协议进行通信,以保证稳定可靠的连接。
    • 客户端发送的查询请求和服务器发送的响应数据都通过socket进行传输。
    • 使用“全满缓存”,防止TCP引起的沾包。
  4. 数据存储:

    • 服务器需要有一个词典数据库来存储词义和解释等数据。
    • 服务器需要有一个用户数据库来注册或登录账号。
    • 数据库采用数据库管理系统(SQLite),以提供更高效的数据存储和查询能力。
  5. 异常处理和错误处理:

    • 服务器和客户端都需要处理异常情况和错误信息,例如连接中断、无法连接到服务器、无效的查询请求等。
    • 可以通过返回特定的错误码或消息来向客户端报告错误,并在客户端显示相应的错误信息。

2.2流程图

2.2.1.服务器

TCP云词典项目_第1张图片

2.2.2客户端

TCP云词典项目_第2张图片

3.效果展示 

TCP云词典项目_第3张图片

4.源码

4.1服务器源码: 

//srever_head.h 文件
#ifndef _HEAD_H_
#define _HEAD_H_

#include 
#include  /* See NOTES */
#include 
#include 
#include 
#include 

typedef struct mes
{
    int zero_one;
    int type;
    char name[32];
    char passwd[32];
    char text[128];
} MSG_t;

enum Type
{
    enroll,
    login,
    quit_login,
    search,
    history,
    quit_search
};

typedef struct node
{
    int fd;
    char word[32];
    struct node *next;
} snode;

MSG_t msg;

int enroll_server(int, MSG_t, sqlite3 *);//服务器处理注册
int login_server(int, MSG_t, sqlite3 *);//服务器处理登录
int search_server(int, MSG_t, sqlite3 *, char *);//服务器处理查找
int add_word(int, MSG_t, snode *);//添加单词到链表,用户客户查询历史记录

void enoll_feedback(int, sqlite3 *);//注册反馈
void login_feedback(int, sqlite3 *);//登录反馈
void search_feedback(int, sqlite3 *,snode *);//查找反馈
void history_feedback(int,snode *);//历史记录反馈

int show_history(snode *);
sqlite3 *mydtbase();//用户数据库
sqlite3 *dictionary();//词典数据库
snode *create_link();//存储客户客户查询单词的链表

#endif

//server_head.c 文件
#include 
#include "server_head.h"
#include 
#include  /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int enroll_server(int acceptfd, MSG_t msg, sqlite3 *db)//注册函数
{
    char buf[128] = {};
    char *errmsg = NULL;
    sprintf(buf, "insert into dic values(\"%s\",\"%s\");", msg.name, msg.passwd);
    if (sqlite3_exec(db, buf, NULL, NULL, &errmsg))
    {
        printf("sqlist3_exec insert is err: %s\n", errmsg);
        return -1;//注册失败返回值
    }
    return 0;//注册成功返回值
}

int login_server(int acceptfd, MSG_t msg, sqlite3 *db)//处理登录函数,返回值-1,代表失败;成功返回零
{
    char **result;
    int hang, lie;
    char *errmsg = NULL;
    char buf[128] = {};
    sprintf(buf, "select * from dic where name=\"%s\" and passwd=\"%s\";", msg.name, msg.passwd);//查找用户表,查找对应的账号密码
    if (sqlite3_get_table(db, buf, &result, &hang, &lie, &errmsg))
    {
        printf("sqlist3_get_table is err:%s\n", errmsg);
        return -1;//查找失败代表没有注册或者密码错误
    }
    if (hang == 0 & lie == 0)
        return -1;//查找行列值为零代表没有注册或者密码错误
    return 0;//查找成功
}

int search_server(int acceptfd, MSG_t msg, sqlite3 *dicdb, char *explain)//查找单词函数
{
    char **result;
    int hang, lie;
    char *errmsg = NULL;
    char buf[128] = {};
    sprintf(buf, "select * from dict where word=\"%s\";", msg.text);
    if (sqlite3_get_table(dicdb, buf, &result, &hang, &lie, &errmsg))
    {
        printf("sqlist3_get_table is err:%s\n", errmsg);
        return -1;
    }
    if (hang == 0 & lie == 0)
        return -1;
    if (hang != 1)
        return -1;
    strcpy(explain, result[3]);//拷贝到传入的数组里面。
    return 0;
}
void enoll_feedback(int acceptfd, sqlite3 *db)//注册反馈函数
{
    msg.zero_one = 0;//代表只能处理第一界面的输入
    msg.type = enroll;
    if (enroll_server(acceptfd, msg, db))//判断是否存在该用户
    {
        strcpy(msg.text, "Registration failed, user already exists");
        send(acceptfd, &msg, sizeof(msg), 0);//返回错误信息
    }
    else
    {
        strcpy(msg.text, "enroll is success");
        send(acceptfd, &msg, sizeof(msg), 0);//返回成功信息
    }
}

void login_feedback(int acceptfd, sqlite3 *db)//登录反馈函数
{
    if (login_server(acceptfd, msg, db))//判断账号密码合法性
    {
        strcpy(msg.text, "Login failed, please check your account or password");
        msg.zero_one = 0;//登录失败,依然只能处理第一界面的输入
        send(acceptfd, &msg, sizeof(msg), 0);
    }
    else
    {
        strcpy(msg.text, "login is success");
        msg.zero_one = 1;//登录成功,代表可以进入第二界面,也就是查询单词或查询历史记录界面
        msg.type = login;
        send(acceptfd, &msg, sizeof(msg), 0);
    }
}

void search_feedback(int acceptfd, sqlite3 *dicdb, snode *H)//查找反馈函数
{
    char explain[128];
    add_word(acceptfd, msg, H);
    msg.type = search;
    msg.zero_one = 1;
    if (search_server(acceptfd, msg, dicdb, explain))//判断词典数据库是否存在该单词
    {
        strcpy(msg.text, "Search failed, you can use abbreviations");
        send(acceptfd, &msg, sizeof(msg), 0);//返回错误信息
    }
    else
    {
        strcpy(msg.text, explain);//返回单词信息
        send(acceptfd, &msg, sizeof(msg), 0);
    }
}

void history_feedback(int acceptfd, snode *H)//反馈历史记录函数
{
    msg.type = history;
    msg.zero_one = 1;//代表只能处理第二界面的输入
    snode *p = H;
    if (H->next == NULL)
    {
        strcpy(msg.text, "You haven't searched for records yet!");//链表没节点,代表没有历史记录
        send(acceptfd, &msg, sizeof(msg), 0);
    }
    while (p->next)
    {
        p = p->next;
        strcpy(msg.text, p->word);
        if(p->fd==acceptfd)//只反馈给自己
            send(acceptfd, &msg, sizeof(msg), 0);
    }
    memset(msg.text, 0, sizeof(msg.text));//最后一次发送空,用于客户端判断是否发送结束,结束做相应的处理,打印帮助信息,否则一直打印查找记录
    send(acceptfd, &msg, sizeof(msg), 0);
}

sqlite3 *dictionary()//词典数据库函数
{
    sqlite3 *db = NULL;
    int ret = sqlite3_open("./dict.db", &db);
    if (ret != 0)
    {
        printf("sqlite3_open is err : %s\n", sqlite3_errmsg(db));
        return NULL;
    }
    printf("sqlite3_open is success\n");
    char *errmsg = NULL;
    if (sqlite3_exec(db, "create table if not exists dict (word char,explain char);", NULL, NULL, &errmsg))
    {
        printf("sqlite3_exec create table is err: %s\n", errmsg);
        return NULL;
    }
    printf("create word table is success\n");
    return db;
}

sqlite3 *mydtbase()//用户信息数据库函数
{
    sqlite3 *db = NULL;
    int ret = sqlite3_open("./mydata.db", &db);
    if (ret != 0)
    {
        printf("sqlite3_open is err : %s\n", sqlite3_errmsg(db));
        return NULL;
    }
    printf("sqlite3_open is success\n");
    char *errmsg = NULL;
    if (sqlite3_exec(db, "create table if not exists dic (name char primary key,passwd char);", NULL, NULL, &errmsg))//如果不存咋创建,存在就打开,并且设置key值
    {
        printf("sqlite3_exec create table is err: %s\n", errmsg);
        return NULL;
    }
    printf("create client table is success\n");
    return db;
}

snode *create_link()//头节点函数(存储查找记录的链表)
{
    snode *H;
    H = (snode *)malloc(sizeof(snode));
    if (H == NULL)
        return NULL;
    memset(H, 0, sizeof(snode));
    H->next = NULL;
    return H;
}

int add_word(int acceptdfd, MSG_t msg, snode *H)//加入单词记录
{
    if (H == NULL)
    {
        perror("H is NULL");
        return -1;
    }
    snode *p;
    p = (snode *)malloc(sizeof(snode));
    p->fd=acceptdfd;//保存文件描述符的值到节点
    strcpy(p->word, msg.text);//存储单词的信息到节点上
    p->next = H->next;
    H->next = p;
    return 0;
}

int show_history(snode *H)
{
    if (H == NULL)
        return -1;
}
//server.c 文件
#include 
#include  /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "server_head.h"
#include 
#include 
MSG_t msg;

int main(int argc, char *argv[])
{
	if (argc != 2)//参数判断
	{
		printf("Usage : %s \n", argv[0]);
		return -1;
	}
	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		perror("socket is err:");
		return -1;
	}
	struct sockaddr_in addr, clientaddr;//定义两个结构体存值
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = INADDR_ANY;
	addr.sin_port = htons(atoi(argv[1]));
	int addrlen = sizeof(addr);
	if (bind(sockfd, (struct sockaddr *)&addr, addrlen))
	{
		perror("bind err:");
		return -1;
	}
	if (listen(sockfd, 5))
	{
		perror("listen err:");
		return -1;
	}
	struct pollfd fds[20];//采取poll函数
	fds[0].fd = sockfd;
	fds[0].events = POLLIN;
	sqlite3 *db = mydtbase();//调用函数
	sqlite3 *dicdb = dictionary();//调用函数
	snode *H = create_link();//调用函数
	int sum = 1;
	while (1)
	{
		int ret = poll(fds, sum, -1);//循环监听
		if (ret < 0)
		{
			printf("poll err:");
			break;
		}
		else if (ret > 0)
		{
			for (int i = 0; i < sum; i++)
			{
				if (fds[i].revents == POLLIN)
				{
					if (fds[i].fd == sockfd)//加入新的客户端连接
					{
						int acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);
						if (acceptfd < 0)
						{
							perror("accept err:");
							break;
						}
						printf("client%d ip :%s port :%d\n", acceptfd, inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
						fds[sum].fd = acceptfd;//加入新的文件描述符
						fds[sum].events = POLLIN;//加入事件
						sum++;
						break;
					}
					else
					{
						int ret = recv(fds[i].fd, &msg, sizeof(msg), 0);
						if (ret < 0)
						{
							perror("recv err:");
							break;
						}
						else if (ret == 0)//断开连接,修改监听的对象
						{
							close(fds[i].fd);
							printf("client%d quit\n", fds[i].fd);
							fds[i] = fds[sum - 1];
							i--;
							sum--;
						}
						else//判断接收到的信息的类型,做对应的事情
						{
							if (msg.type == enroll)
								enoll_feedback(fds[i].fd, db);//注册反馈函数
							else if (msg.type == login)
								login_feedback(fds[i].fd, db);//登录反馈函数
							else if (msg.type == search)
								search_feedback(fds[i].fd, dicdb, H);//查找反馈函数
							else if (msg.type == history)
								history_feedback(fds[i].fd, H);//历史记录反馈函数
						}
					}
				}
			}
		}
	}
	return 0;
}

4.2客户端源码:

//client.h 文件
#ifndef _CLIENT_H_
#define _CLIENT_H_

#include 
#include  /* See NOTES */
#include 
#include 
#include 
#include 

typedef struct mess
{
    int zero_one;//代表当前处在的第几界面,第一界面是注册,登录等,第二界面是查找单词,历史记录等
    int type;//消息类型
    char name[32];
    char passwd[32];
    char text[128];
} MSG_tt;

enum Type
{
    enroll,//注册
    login,//登录
    search,//查找
    history,//历史记录
};

MSG_tt msg;
struct sockaddr_in addr;

void start_jm();//第一界面函数
void enroll_client(int);//注册函数
void login_client(int);//登录函数
void login_start();//第二界面函数
void search_client(int);//查找函数
void history_client(int);//历史记录函数
void login_feedback();//登录反馈函数
int if1(int);//三大判断
void if2(int);
void if3(int);

#endif
//client_head.c 文件
#include 
#include  /* See NOTES */
#include 
#include 
#include  /* superset of previous */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "client.h"

void enroll_client(int socketfd)//发送账号密码给服务器,用于服务器判断注册合法性
{
	printf("please input your name:");
	fgets(msg.name, sizeof(msg.name), stdin);
	if (msg.name[strlen(msg.name) - 1] == '\n')
		msg.name[strlen(msg.name) - 1] = '\0';
	printf("please input your password:");
	fgets(msg.passwd, sizeof(msg.passwd), stdin);
	if (msg.passwd[strlen(msg.passwd) - 1] == '\n')
		msg.passwd[strlen(msg.passwd) - 1] = '\0';
	msg.zero_one = 0;//只能在第一界面
	msg.type = enroll;
	if (send(socketfd, &msg, sizeof(msg), 0) < 0)
	{
		perror("send err:");
		return;
	}
}

void login_client(int socketfd)//发送账号密码给服务器,用于服务器判断登录合法性
{
	printf("please input your name:");
	fgets(msg.name, sizeof(msg.name), stdin);
	if (msg.name[strlen(msg.name) - 1] == '\n')
		msg.name[strlen(msg.name) - 1] = '\0';
	printf("please input your password:");
	fgets(msg.passwd, sizeof(msg.passwd), stdin);
	if (msg.passwd[strlen(msg.passwd) - 1] == '\n')
		msg.passwd[strlen(msg.passwd) - 1] = '\0';
	msg.zero_one = 0;//只能在第一界面
	msg.type = login;
	if (send(socketfd, &msg, sizeof(msg), 0) < 0)
	{
		perror("send err:");
		return;
	}
}

void search_client(int socketfd)//将输入的单词发送给客户端
{
	printf("please input word:");
	fflush(stdout);
	fgets(msg.text, sizeof(msg.text), stdin);
	if (msg.text[strlen(msg.text) - 1] == '\n')
		msg.text[strlen(msg.text) - 1] = '\0';
	msg.type = search;
	msg.zero_one = 1;
	if (send(socketfd, &msg, sizeof(msg), 0) < 0)
	{
		perror("send err:");
		return;
	}
}

void history_client(int socketfd)//发送查询历史记录请求给服务器
{
	if (msg.text[strlen(msg.text) - 1] == '\n')
		msg.text[strlen(msg.text) - 1] = '\0';
	msg.type = history;
	msg.zero_one = 1;
	if (send(socketfd, &msg, sizeof(msg), 0) < 0)
	{
		perror("send err:");
		return;
	}
}

void start_jm()//第一界面
{
	printf("\n");
	printf("*********************************\n");
	printf("***1.enroll   2.logon   3.quit***\n");
	printf("*********************************\n");
	printf("\n");
	printf("Please choose num:");
	fflush(stdout);//记得刷新缓冲区
}

void login_start()//第二界面
{
	printf("\n");
	printf("*********************************\n");
	printf("**A.search   B.history   C.quit**\n");
	printf("*********************************\n");
	printf("\n");
	printf("Please choose letter:");
	fflush(stdout);//记得刷新缓冲区
}
void login_feedback()//登录反馈函数
{
	if (msg.zero_one == 1)
	{
		printf("name:%s passwd:*** %s\n", msg.name, msg.text);
		login_start();//登录成功进入第二界面
	}
	else
	{
		printf("%s\n", msg.text);
		start_jm();//登陆失败依旧处于第一界面,并且打印错误信息
	}
}
int if1(int socketfd)//判断键盘输入
{
	if (strncmp(msg.text, "1", 1) == 0)
		enroll_client(socketfd);//输入1注册
	else if (strncmp(msg.text, "2", 1) == 0)
		login_client(socketfd);//输入2登录
	else if (strncmp(msg.text, "3", 1) == 0)
		return 1;//输入3返回不为零的值,用于调用者判断退出程序
	else
	{
		printf("Please enter according to the rules\n");//错误处理
		start_jm();//将处在第一界面
	}
	return 0;
}
void if2(int socketfd)//判断键盘输入
{
	if (strncmp(msg.text, "A", 1) == 0)
		search_client(socketfd);//查找单词
	else if (strncmp(msg.text, "B", 1) == 0)
		history_client(socketfd);//查找历史记录
	else if (strncmp(msg.text, "C", 1) == 0)
	{
		start_jm();//回到第一界面
		msg.zero_one = 0;//将值置为0,表示回到第一界面
	}
	else
	{
		printf("Please enter according to the rules\n");//错误处理
		login_start();//处在第二界面
	}
}
void if3(int socketfd)
{
	printf("Please enter according to the rules\n");
	if (msg.zero_one == 0)
		start_jm();//显示第一界面
	if (msg.zero_one == 1)
		login_start();//显示第二界面
}

//client.c 文件
#include 
#include  /* See NOTES */
#include 
#include 
#include  /* superset of previous */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "client.h"

void *pthread_read(void *arg)//线程处理收到的信息
{
    int socketfd = *(int *)arg;
    while (1)
    {
        int ret = recv(socketfd, &msg, sizeof(msg), 0);
        if (ret < 0)
            return NULL;
        //判断收到的消息类型是那种,分别处理对应的事情
        if (msg.type == enroll)//
        {
            printf("name:%s passwd:*** %s\n", msg.name, msg.text);
            start_jm();
        }
        else if (msg.type == login)
            login_feedback();//服务器反馈的登录信息
        else if (msg.type == search)
        {
            printf("%s\n", msg.text);//打印单词信息
            login_start();
        }
        else if (msg.type == history)
        {
            printf("%s\n", msg.text);//打印收到服务器的单词历史记录
            if(strlen(msg.text)==0)
            {
                login_start();//当收到空,代表没有数据了,
            }
        }
    }
    pthread_exit(NULL);//结束线程
}

int main(int argc, char const *argv[])
{
    int socketfd;
    if (argc != 3)
    {
        printf("Useage:%s  \n", argv[0]);
        return -1;
    }
    socketfd = socket(AF_INET, SOCK_STREAM, 0);
    if (socketfd < 0)
    {
        perror("socket err:");
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    addr.sin_port = htons(atoi(argv[2]));
    socklen_t addrlen = sizeof(addr);

    if (connect(socketfd, (struct sockaddr *)&addr, addrlen) < 0)
    {
        perror("connect err:");
        return -1;
    }
    pthread_t tid;
    if (pthread_create(&tid, NULL, pthread_read, &socketfd))
    {
        perror("pthread_create err:");
        return -1;
    }
    start_jm();//开始界面
    msg.zero_one = 0;//刚开始处于第一界面
    while (1)
    {
        fgets(msg.text, sizeof(msg), stdin);
        msg.text[strlen(msg.text) - 1] = '\0';
        if (msg.zero_one == 0)//处于第一界面,处理对应事件
        {
            if (if1(socketfd))
                break;
        }
        else if (msg.zero_one == 1)//处于第二界面,处理对应事件
            if2(socketfd);
        else
            if3(socketfd);//错误信息
    }
    printf("Thanks your use\n");
    close(socketfd);
    return 0;
}

5.项目亮点

  1. 客户端和服务器的通信协议:定义了明确的请求和响应格式,包括数据包的结构、字段含义、编码方式等。确保客户端和服务器能够正确解析和处理通信数据。

  2. 异常处理和错误提示:可以合理处理异常情况,例如网络连接失败、数据库访问失败等,给予用户友好的错误提示信息。

  3. 数据库设计和优化:设计了适当的数据库模式,确保存储和查询数据的高效性。使用索引、优化查询语句、避免重复数据等方法,提高查询性能。

  4. 并发处理和线程安全:采用线程栈存储变量,服务器同时处理多个客户端的请求,可以保证并发访问数据库的线程安全性。(个人采用的poll)

  5. 调试信息:测试阶段,编写详细的调试信息,助于快速定位和修复错误。

  6. 代码清晰和可维护性:编写清晰、模块化的代码,并且使用良好的注释和命名规范。

  7. 用户界面设计:考虑用户友好性和易用性,提供了清晰的指导和操作步骤。界面设计符合用户习惯和视觉美感,提供了良好的用户体验。

 6.遇到的问题

  1. 并发服务器的建立,没有严格区分对应客服端的文件描述符,导致监听失败。
  2. 发送数据中出现乱码,未将发送数据的容器初始化(最好所有数据全部发送)。
  3. 单词表加载到数据库遇到问题,应该将单词和解释做好风格。
  4. 客户端输入的容错判断遇到问题,出现应答不一致,检查消息类型以及服务器的权限

你可能感兴趣的:(tcp/ip,网络协议,网络,数据结构,c语言,linux)