一、项目名称
基于TCP的在线词典
二、功能
1.新用户使用时需要注册,用户名不可以和已有用户重复。
2.登录时需要校验用户名及密码。
3.输入要查询的单词后返回查询结果,可循环查询,在输入#后退出查询界面。
4.查询历史记录时需要返回历史查询成功的所有单词及查询时的日期时间。
5. 服务器支持并发,支持多用户同时在线使用。
整体功能需求流程图:
三、初步构思
1.用户登录时服务器需要保存用户名和密码,用户查询单词时需要保存查询记录,所以需要用数据库建两个表,分别保存用户名密码和查询成功的单词及对应时间。’
2.消息类型有四种,注册、登录、查询、历史,需要定义消息结构体,结构体中需要包含信息操作码、用户名、消息数据(密码)。
四、代码构思流程图
五、代码实现
服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <sqlite3.h>
//定义消息结构体
typedef struct _MSG {
char code; //操作码:'R'表示注册'L'表示登录 'Q'表示查询 'H'表示查询历史记录
char usrname[32];
char txt[256];//将登录时的密码和查询时的单词或者解释都放在TXT中
} msg_t;
//初始化数据库
sqlite3* init_process()
{
sqlite3* my_db;
int ret = 0;
char sqlbuf[256] = { 0 };
if (SQLITE_OK != (ret = sqlite3_open("dic.db", &my_db))) {
printf("%s:%d:errno[%d]:errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
return NULL;
}
//组装seq语句 创建usr表 用户名设置为KEY值,用来判断用户名是否重复
sprintf(sqlbuf, "CREATE TABLE IF NOT EXISTS usr(name TEXT PRIMARY KEY, passwd TEXT)");
//执行sql语句
if (SQLITE_OK != (ret = sqlite3_exec(my_db, sqlbuf, NULL, NULL, NULL))) {
printf("%s:%d:errno[%d]:errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
return NULL;
}
memset(sqlbuf, 0, sizeof(sqlbuf));
//组装seq语句 创建record表,用来保存查询记录 设置用户名、时间、单词 三个字段
sprintf(sqlbuf, "CREATE TABLE IF NOT EXISTS record (name TEXT, date TEXT, word TEXT)");
//执行sql语句
if (SQLITE_OK != (ret = sqlite3_exec(my_db, sqlbuf, NULL, NULL, NULL))) {
printf("%s:%d:errno[%d]:errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
return NULL;
}
return my_db;
}
//注册操作的函数
void do_register(int acceptfd, msg_t* msg, sqlite3* my_db)
{
msg_t msg_tmp;
memset(&msg_tmp, 0, sizeof(msg_t));
msg_tmp = *msg;
char sqlbuf[500] = { 0 };
int ret = 0;
//将接收到的用户名及密码存入数据库中
sprintf(sqlbuf, "INSERT INTO usr VALUES ('%s','%s')", msg_tmp.usrname, msg_tmp.txt);
if (SQLITE_OK != (ret = sqlite3_exec(my_db, sqlbuf, NULL, NULL, NULL))) {
printf("%s:%d:errno[%d]:errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
//此分支表示存入数据库失败,组装失败消息返回给客户端
strcpy(msg_tmp.txt, "The username has been used..");
if (-1 == send(acceptfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
} else {
//此分支表示存入数据库成功,组装成功消息返回给客户端
memset(&msg_tmp, 0, sizeof(msg_tmp));
strcpy(msg_tmp.txt, "OK!");
if (-1 == send(acceptfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
}
return;
}
//登录操作函数
void do_login(int acceptfd, msg_t* msg, sqlite3* my_db)
{
msg_t msg_tmp = *msg;
char sqlbuf[500] = { 0 };
int ret = 0;
int rows = 0;
int columns = 0;
char** result = NULL;
//组装sql语句,在数据库中查询此用户名和密码
sprintf(sqlbuf, "SELECT * FROM usr WHERE name = '%s' AND passwd = '%s'", msg_tmp.usrname, msg_tmp.txt);
if (SQLITE_OK != (ret = sqlite3_get_table(my_db, sqlbuf, &result, &rows, &columns, NULL))) {
printf("%s:%d -- errcode[%d] errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
return;
}
//如果返回1行信息,说明数据库中有对应的信息,说明用户名密码均正确
if (rows == 1) {
memset(&msg_tmp, 0, sizeof(msg_tmp));
strcpy(msg_tmp.txt, "OK!");//组装登录成功消息并返回给客户端
if (-1 == send(acceptfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
} else {//如果返回非1行信息,说明数据库中没有查到对应的用户信息
memset(&msg_tmp, 0, sizeof(msg_tmp));
//组装用户名或者密码错误信息并返回给客户端
strcpy(msg_tmp.txt, "Wrong username or password..");
if (-1 == send(acceptfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
}
sqlite3_free_table(result);//需要释放由sqlite3_get_table函数产生的结果集
return;
}
//查询单词函数
void do_query(int acceptfd, msg_t* msg, sqlite3* my_db)
{
FILE* fp;//本地词典的文件描述符
char* p;
int ret;//用来debug
time_t tm;
struct tm* my_time;//获取当前系统时间
char sqlbuf[500] = { 0 };
char timebuf[128] = { 0 };
char line_buf[300] = { 0 };
msg_t msg_tmp = *msg;
if (NULL == (fp = fopen("dictionary.txt", "r"))) {//打开本地词典
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("fopen error");
return;
}
while (NULL != fgets(line_buf, sizeof(line_buf), fp)) {//循环读取本地词典内容
//在本地词典文件中查找客户端输入的单词
if (!strncmp(msg->txt, line_buf, strlen(msg->txt)) && line_buf[strlen(msg->txt)] == ' ') {
line_buf[strlen(line_buf) - 1] = '\0';
p = line_buf;
while (*p != ' ') {
p++;
}
while (*p == ' ') { //把指针定位到解释的第一个字符
p++;
}
strcpy(msg->txt, p);//截取单词的解释
if (-1 == send(acceptfd, msg, sizeof(msg_t), 0)) {//将对应单词的解释返回给客户端
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
//每次查询到结果后记录下时间,连同用户名及查询的单词存入数据库record表中
time(&tm); //方便后续查询历史记录时使用
my_time = localtime(&tm);
sprintf(timebuf, "%d.%02d.%02d %02d:%02d:%02d", my_time->tm_year + 1900, my_time->tm_mon, my_time->tm_mday, my_time->tm_hour, my_time->tm_min, my_time->tm_sec);
sprintf(sqlbuf, "INSERT INTO record VALUES ('%s','%s','%s')", msg_tmp.usrname, timebuf, msg_tmp.txt);
if (SQLITE_OK != (ret = sqlite3_exec(my_db, sqlbuf, NULL, NULL, NULL))) {
printf("%s:%d:errno[%d]:errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
return;
}
return;
}
}
//while循环结束后说明在本地词典文件中没有找到查询的单词
sprintf(msg->txt, "No explanation found!");//组装没有查到的信息返回给客户端
if (-1 == send(acceptfd, msg, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
return;
}
//sqlite3_exec函数查询时用到的回调函数
int callback(void* arg, int ncolumn, char** f_value, char** f_name)
{
int acceptfd = *(int*)arg;//接收由sqlite3_exec函数传递来的acceptfd
msg_t msg_tmp;
//f_value[0]保存的是时间 f_value[1]保存的是查询的单词
//每查询到一个结果,callback函数就被调用一次,给客户端返回一次数据
sprintf(msg_tmp.txt, " %s : %s ", f_value[0], f_value[1]);
if (-1 == send(acceptfd, &msg_tmp, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return -1;
}
return 0;
}
//查询历史记录的函数
void do_history(int acceptfd, msg_t* msg, sqlite3* my_db)
{
char sqlbuf[500] = { 0 };
int ret;
int i = 0;
int j = 0;
msg_t msg_tmp = { 0 };
//按照用户名在数据库中查询数据
sprintf(sqlbuf, "SELECT date,word FROM record WHERE name = '%s'", msg->usrname);
if (SQLITE_OK != (ret = sqlite3_exec(my_db, sqlbuf, callback, (void*)&acceptfd, NULL))) {
printf("%s:%d -- errcode[%d] errstr[%s]\n", __FILE__, __LINE__, ret, sqlite3_errmsg(my_db));
return;
}
//当callback函数调用完毕后,说明查询到的数据已发送完毕,但是客户端不知道什么时候停止接收数据,
//所以定义一个标识语句,发送此语句,标识已经传输结束
strcpy(msg_tmp.txt, "**over**");
if (-1 == send(acceptfd, &msg_tmp, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
return;
}
}
int main(int argc, const char* argv[])
{
if (3 != argc) {//考虑用命令行传参方式输入ip地址及端口号,先进行参数判断
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(-1);
}
int sockfd;
int acceptfd;
pid_t pid;
msg_t msg = { 0 };
//初始化数据库;
sqlite3* my_db = init_process();
//创建套接字
if (-1 == (sockfd = socket(AF_INET, SOCK_STREAM, 0))) {
perror("socket error");
exit(-1);
}
//填充网络信息结构体
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//绑定网络信息结构体
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr))) {
perror("bind error");
exit(-1);
}
//设置套接字为被动接听状态
if (-1 == listen(sockfd, 5)) {
perror("listen error");
exit(-1);
}
//创建文件描述符集合,存放需要监视的文件描述符
int max_fd = 0;//记录最大的文件描述符,select函数使用
int ret;//select函数的返回值,用来减少每次循环遍历文件描述符的个数
int rvalue;//recv函数的返回值,用来判断某个客户端退出或断开连接
int i;
fd_set fds;
FD_ZERO(&fds);
fd_set fds_tmp;
FD_ZERO(&fds_tmp);
FD_SET(sockfd, &fds);
max_fd = max_fd > sockfd ? max_fd : sockfd;
while (1) {
//使用select实现IO多路复用,实现服务器并发
fds_tmp = fds;
if (-1 == (ret = select(max_fd + 1, &fds_tmp, NULL, NULL, NULL))) {
perror("select error");
exit(-1);
}
//遍历所有的文件描述符
for (i = 3; i < max_fd + 1 && ret != 0; i++) {
//判断文件描述符i是否就绪
if (FD_ISSET(i, &fds_tmp)) {
//判断是否是sockfd就绪
if (i == sockfd) {
if (-1 == (acceptfd = accept(sockfd, NULL, NULL))) {
perror("accept error");
exit(-1);
}
//将生成的acceptfd放入到要监视的文件描述符集合中
FD_SET(acceptfd, &fds);
//判断新的文件描述符是否是集合中的最大值
max_fd = max_fd > acceptfd ? max_fd : acceptfd;
} else {
//进入此分支说明是有客户端发来消息
if (-1 == (rvalue = recv(i, &msg, sizeof(msg), 0))) {
perror("recv error");
exit(-1);
}
//判断recv的返回值是否为0,为0说明有客户端退出或者断开连接
if(rvalue == 0){//客户端断开连接后需要将文件描述符在监视集合中删除并关闭
FD_CLR(i,&fds);
close(i);
}
//判断客户端发来的消息是走哪个分支
switch (msg.code) {
case 'R': //注册
do_register(i, &msg, my_db);
break;
case 'L': //登录
do_login(i, &msg, my_db);
break;
case 'Q': //查询单词
do_query(i, &msg, my_db);
break;
case 'H': //查询历史
do_history(i, &msg, my_db);
break;
}
}
ret--;//就绪的文件描述符集合里判断完一个减去一个,
//如果ret为0,则for循环就可以直接停止了
}
}
}
close(sockfd);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <sqlite3.h>
typedef struct _MSG {
char code;
char usrname[32];
char txt[256];
} msg_t;
//1级菜单打印函数
void _1menu_print()
{
printf("----------------------------------------------\n");
printf("| 1: registr 2:login 3: quit |\n");
printf("----------------------------------------------\n");
}
//2级菜单打印函数
void _2menu_print()
{
printf("----------------------------------------------\n");
printf("| 1: quary 2:history 3: return |\n");
printf("----------------------------------------------\n");
}
//注册操作函数
void do_register(int sockfd, msg_t* msg)
{
msg_t msg_tmp;
msg->code = 'R';
printf("input your name:>>");
scanf("%s", msg->usrname);
printf("input your passwd:>>");
scanf("%s", msg->txt);
if (-1 == send(sockfd, msg, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
exit(-1);
}
if (-1 == recv(sockfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("recv error");
exit(-1);
}
printf("%s\n", msg_tmp.txt);
return;
}
//登录操作的函数
int do_login(int sockfd, msg_t* msg)
{
msg_t msg_tmp;
msg->code = 'L';
printf("input your name:>>");
scanf("%s", msg->usrname);
printf("input your passwd:>>");
scanf("%s", msg->txt);
if (-1 == send(sockfd, msg, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
exit(-1);
}
if (-1 == recv(sockfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("recv error");
exit(-1);
}
if (!strcmp(msg_tmp.txt, "Wrong username or password..")) {
printf("%s\n", msg_tmp.txt);
return -1;//如果用户名密码错误,返回-1,主函数需要根据结果判断下一步操作
} else if (!strcmp(msg_tmp.txt, "OK!")) {
printf("%s\n", msg_tmp.txt);
return 0;//如果登陆成功,返回0,主函数需要根据结果判断下一步操作
}
}
//查询单词函数
void do_query(int sockfd, msg_t* msg)
{
msg_t msg_tmp;
msg->code = 'Q';
puts("-------------------------------");
while (1) {//循环接收终端输入的单词
printf("input the word to query(quit #):>>");
scanf("%s", msg->txt);
if (!strcmp(msg->txt, "#")) {//如果输入#号退出查询
break;
}
if (-1 == send(sockfd, msg, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
exit(-1);
}
if (-1 == recv(sockfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("recv error");
exit(-1);
}
printf("%s: %s\n", msg->txt,msg_tmp.txt);//打印接收到的单词解释
}
}
//查询历史记录的函数
void do_history(int sockfd, msg_t* msg)
{
msg_t msg_tmp;
msg->code = 'H';
puts("--------------------------------");
if (-1 == send(sockfd, msg, sizeof(msg_t), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("send error");
exit(-1);
}
while (1) {//循环打印接收到的数据
if (-1 == recv(sockfd, &msg_tmp, sizeof(msg_tmp), 0)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("recv error");
exit(-1);
}
if(!strncmp(msg_tmp.txt,"**over**",8)){//判断接收到的数据是不是特殊标识
break; //如果是表示服务器已传输完毕,直接退出循环
}
printf("%s\n",msg_tmp.txt);
}
return;
}
int main(int argc, const char* argv[])
{
if (3 != argc) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(-1);
}
//创建套接字 流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("socket error");
exit(-1);
}
//填充网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
socklen_t serveraddr_len = sizeof(serveraddr);
//与服务器建立连接
if (-1 == connect(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
printf("[%s:%d]\n",__FILE__, __LINE__);
perror("connect error");
exit(-1);
}
int choose = 0;
msg_t msg;
_1menu:
while (1) {
_1menu_print();//打印1级菜单
int ret;
printf("please choose:>>");
scanf("%d", &choose);
switch (choose) {
case 1: //注册
do_register(sockfd, &msg);
break;
case 2: //登录
if (-1 == (ret = do_login(sockfd, &msg))) {
break;
} else if (0 == ret) {//如果登录成功了,进入2级菜单
goto _2menu;
}
case 3: //退出
close(sockfd);
exit(0);
}
}
_2menu:
while (1) {
_2menu_print();//打印2级菜单
printf("please choose:>>");
scanf("%d", &choose);
switch (choose) {
case 1: //查询
do_query(sockfd, &msg);
break;
case 2: //查询历史
do_history(sockfd, &msg);
break;
case 3: //返回
goto _1menu; //返回1级菜单
}
}
return 0;
}
六、最终效果
需要用到的本地词典文件无法上传,故未上传。