一.要求
1.搭建的框架环境中实现并发,实现多个用户同时查询的功能。
2.服务器分别保存每个用户的使用记录,客户端可以查询日志的功能。
3.基本的查询单词的功能。
4.密码验证的功能,实现登录验证账号和密码是否正确。
二.流程和框架
框架
客户端
服务器
三.思路
1.首先你要准备好单词文件,用于英语单词的查询。
dict.txt2.该项目涉及多并发问题,可以使用多进程,多线程,IO多路复用中的一种,我这里采用IO多路复用的select实现。
3.实现日志功能,需要建立一个数据库,为每一个用户建立一个表,表中存储用户的各种记录。
4.密码验证,同样使用数据库完成,每次登录时,将和数据库中所有的用户信息比较,若账号密码正确,即可登录。
Linux的IO多路复用是一种高效的IO处理机制,通过允许一个线程同时监控多个文件描述符(包括套接字、管道等)的IO事件,从而避免了传统的多线程或多进程方式中的频繁的上下文切换和资源消耗。在Linux系统中,IO多路复用主要基于以下三种机制:select、poll和epoll。
-
select:select是最早引入的IO多路复用机制之一。它通过select系统调用来监控多个文件描述符上的IO事件,一旦有IO事件发生,就会通知应用程序进行处理。然而,select的一个缺点是每次调用都需要将所有的文件描述符从应用程序空间复制到内核空间,造成资源浪费。
-
poll:poll是对select的改进,它也能够监控多个文件描述符上的IO事件,并将有IO事件发生的文件描述符返回给应用程序。与select不同的是,poll使用了链表数据结构来存储文件描述符,减少了在内核空间和应用程序空间之间的数据复制。
-
epoll:epoll是Linux特有的高性能IO多路复用机制。它通过epoll系统调用来注册、注销和监控文件描述符上的IO事件。epoll采用事件驱动的方式,只会返回有IO事件发生的文件描述符,避免了无效的遍历和资源浪费。此外,epoll还提供了三种工作模式:EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符)和EPOLL_CTL_DEL(删除文件描述符),更加灵活和高效。
IO多路复用在网络编程中特别有用,可以用于实现高并发的服务器。通过IO多路复用,可以在单线程或少量线程的情况下同时处理多个连接的IO事件,提高服务器的并发性能和效率。
总结而言,Linux的IO多路复用是一种有效的IO处理机制,通过select、poll和epoll等机制,可以实现高效的监控和处理多个IO事件,提高系统的并发性能和效率。
四.具体实现代码
head.h
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>/* 消息对应的结构体(同一个协议) */
typedef struct msg_t
{int type; char name[32]; //用户名char text[256]; //消息正文char password[32]; //密码
} MSG_t;enum type_t
{my__register=1, //注册login, //连接 word, //查询单词history, //查询日志 quit,
};
#endif
client.c
#include "head.h"
struct sockaddr_in saddr;
int socked;
MSG_t msg;
//注册帐号函数
void my_register()
{MSG_t msg;msg.type = my__register;printf("%d\n", msg.type);printf("请输入帐号昵称:\n");scanf("%s", msg.name);printf("请输入帐号密码:\n");scanf("%s", msg.password);send(socked, &msg, sizeof(msg), 0);printf("sendis ok\n");int flage = recv(socked, &msg, sizeof(msg), 0);if (flage < 0){perror("recv is err");return;}else{printf("%s\n", msg.text);}
}//连接帐号函数,判断是否连接成功
int my_login()
{msg.type = login;printf("请输入帐号昵称:\n");scanf("%s", msg.name);printf("请输入帐号密码:\n");scanf("%s", msg.password);send(socked, &msg, sizeof(msg), 0);printf("send id ok\n");int flage = recv(socked, &msg, sizeof(msg), 0);if (flage < 0){perror("recv is err");return -1;}else{printf("%s\n",msg.text);if (strncmp(msg.text, "ok", 2) == 0){printf("登录成功\n");return 1;}else{printf("登录失败\n");return 0;}}
}//查询单词函数
void my_query_word()
{msg.type = word;printf("输入quit退出\n");getchar();while (1){msg.type = word;fgets(msg.text, sizeof(msg.text), stdin);if (msg.text[strlen((msg.text)) - 1] == '\n')msg.text[strlen((msg.text)) - 1] = '\0';if (strcmp(msg.text, "quit") == 0)return;printf("要查询的单词:%s\n",msg.text);send(socked, &msg, sizeof(msg), 0);int flage = recv(socked, &msg, sizeof(msg), 0);if (flage < 0){perror("recv is err");return;}else{printf("%s", msg.text);printf("\n");}}
}//查询日志函数
void my_history_record()
{msg.type = history;printf("\n请再次帐号密码:");scanf("%s", msg.password);send(socked, &msg, sizeof(msg), 0);while (1){int flage = recv(socked, &msg, sizeof(msg), 0);if (flage < 0){perror("recv is err");return;}else{if (strcmp(msg.text, "quit") == 0){break;}printf("%s", msg.text);}}printf("************************************\n");
}//查询函数,查询单词或者日志
void my_send()
{int choose;while (1){printf("************************************\n");printf("* 1: query_word 2: history_record 3: quit *\n");printf("************************************\n");printf("请输入你的选择:");scanf("%d", &choose);switch (choose){case 1:my_query_word();break;case 2:my_history_record();break;case 3:return;}}
}int main(int argc, char const *argv[])
{//1.创建套接字,IPv4socked = socket(AF_INET, SOCK_STREAM, 0);if (socked < 0){perror("socket is err\n");return -1;}//2.connect服务器saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));saddr.sin_addr.s_addr = inet_addr(argv[1]);if ((connect(socked, (struct sockaddr *)&saddr, sizeof(saddr))) < 0){perror("connect is err");return -1;}//3.选择注册,登录,退出int choose;while (1){printf("************************************\n");printf("* 1: register 2: login 3: quit *\n");printf("************************************\n");printf("请输入你的选择:");scanf("%d", &choose);switch (choose){case 1:my_register();break;case 2:if (my_login())my_send();break;case 3:close(socked);exit(0);}}close(socked);return 0;
}
server.c
#include "head.h"
#include <sqlite3.h>
#include <sys/select.h>
#include <time.h>
int sockfp, acceptfp, flage, sockfd, flages;
MSG_t msg, msg1;
sqlite3 *db = NULL;//读日志函数,发送给客户端
void my_history()
{char **result;int hang, lie;char *errmasg;char buf[1024];sprintf(buf, "select * from %s_%s;", msg.name, msg.password);sqlite3_get_table(db, buf, &result, &hang, &lie, &errmasg);printf("开始发送日志\n");for (int i = 0; i <= hang; i++){for (int j = 0; j < lie; j++){sprintf(msg.text, "%s\t", result[i * lie + j]);send(sockfd, &msg, sizeof(msg), 0);}strcpy(msg.text, "\n");send(sockfd, &msg, sizeof(msg), 0);}strcpy(msg.text, "quit");send(sockfd, &msg, sizeof(msg), 0);
}//写日志
void write_history()
{time_t current_time;struct tm *local_time;char time_string[100];char perate[100];char buf[1024];char *errmsg;// 获取当前时间戳current_time = time(NULL);// 将时间戳转换为本地时间local_time = localtime(¤t_time);// 格式化本地时间字符串strftime(time_string, sizeof(time_string), "%Y-%m-%d %H:%M:%S", local_time);switch (msg1.type){case my__register:strcpy(perate, "register");break;case login:strcpy(perate, "login");break;case word:strcpy(perate, "word");break;default:strcpy(perate, "quit");break;}sprintf(buf, "insert into %s_%s values(\"%s\",\"%s\");", msg1.name, msg1.password, time_string, perate);if ((sqlite3_exec(db, buf, NULL, NULL, &errmsg)) != SQLITE_OK){fprintf(stderr, "sqlite3_exec is err %s", errmsg);return;}
}int callback(void *data, int argc, char **argv, char **azColName)
{printf("调用callback函数\n");printf("%d\n", argc);char *knownTableName = (char *)data;for (int i = 0; i < argc; i++){printf("%s\n", argv[i]);if (strcmp(argv[i], knownTableName) == 0){strcpy(msg.text, "ok");send(sockfd, &msg, sizeof(msg), 0);printf("send is ok ok\n");printf("%s已登录成功\n", argv[i]);flages = 1;return 0;}}// strcpy(msg.text, "no");// send(sockfd, &msg, sizeof(msg), 0);// printf("send is ok\n");return 0;
}//注册函数
void my_register()
{char buf[512];char *errmsg;sprintf(buf, "create table %s_%s(time char,perate char);", msg.name, msg.password);printf("%s\n", buf);if ((sqlite3_exec(db, buf, NULL, NULL, &errmsg)) != SQLITE_OK){fprintf(stderr, "sqlite3_exec is err %s", errmsg);strcpy(msg.text, "注册失败\n");send(sockfd, &msg, sizeof(msg), 0);return;}elsestrcpy(msg.text, "注册成功\n");send(sockfd, &msg, sizeof(msg), 0);printf("send is ok\n");
}//连接函数只有昵称和密码都匹配才能连接
void my_login()
{printf("调用连接函数\n");char buf[512];char *errmsg;char knownTableName[512];int rc;sprintf(knownTableName, "%s_%s", msg.name, msg.password);strcpy(buf, "SELECT name FROM sqlite_master WHERE type='table';");printf("%s\n", buf);rc = sqlite3_exec(db, buf, callback, knownTableName, &errmsg);if (rc != SQLITE_OK){fprintf(stderr, "SQL error: %s\n", errmsg);}if (!flages){strcpy(msg.text, "no");send(sockfd, &msg, sizeof(msg), 0);}//sleep(1);
}//查询单词函数
void my_word()
{char buf[2048] = {0};int len = strlen(msg.text), flage = 0;FILE *fp = fopen("dict.txt", "r");if (fp == NULL){perror("fopen is err");strcpy(msg.text, "fopen is err");send(sockfd, &msg, sizeof(msg), 0);return;}while (fgets(buf, sizeof(buf), fp)){if ((strncmp(msg.text, buf, len)) == 0){flage = 1;break;}}int i;printf("len=%d\n", len);for (i = len; i < 2048; ++i){if (buf[i] != ' ')break;}//printf("%s\n",buf);printf("%s\n", msg.text);if (flage){strcpy(msg.text, buf + i);}else{strcpy(msg.text, "没有这个单词");}send(sockfd, &msg, sizeof(msg), 0);
}int main(int argc, char const *argv[])
{//1.创建或打开数据库if ((sqlite3_open("./dict.db", &db)) < 0){fprintf(stderr, "sqlite3_open id err %s\n", sqlite3_errmsg(db));return -1;}//2.创建socket套接字sockfp = socket(AF_INET, SOCK_STREAM, 0);if (sockfp < 0){perror("socket is err");return -1;}//3.绑定服务器ip和端口号struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(struct sockaddr_in);if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind is err");return -1;}//4.listen监听if (listen(sockfp, 90)){perror("liste err");return -1;}//4. select多路复用//4.1 创建表fd_set readfds, tempfds;//4.2 清空表FD_ZERO(&readfds);FD_ZERO(&tempfds);//4.3 将关心的文件描述符添加表FD_SET(0, &readfds);FD_SET(sockfp, &readfds);int maxfd = sockfp;while (1){tempfds = readfds;//4.4 select检测 阻塞select(maxfd + 1, &tempfds, NULL, NULL, NULL);//4.5 进行相应的逻辑处理//sockfp,监听套接字响应证明,有客户端要链接if (FD_ISSET(sockfp, &tempfds)){acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);if (acceptfp < 0){perror("acceptfp");exit(0);}printf("连接到%d端口号\n", acceptfp);printf("port:%d ip: %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));FD_SET(acceptfp, &readfds);if (acceptfp > maxfd)maxfd = acceptfp;}//检测客户端,检查是哪一个客户端发送的消息for (int i = 5; i <= maxfd; ++i){if (FD_ISSET(i, &tempfds)){sockfd = i;printf("端口号%d相应\n", i);flage = i;int recvbyte = recv(i, &msg, sizeof(msg), 0);if (recvbyte < 0){perror("recv err");return -1;}else if (recvbyte == 0){msg.type = quit;write_history();close(i);FD_CLR(i, &readfds);if (i == maxfd)--maxfd;}else{printf("recvbyte is ok\n");msg1 = msg;printf("******************\n");switch (msg.type){case my__register:my_register();write_history();break;case login:my_login();write_history();break;case word:my_word();write_history();break;case history:my_history();write_history();break;}}}}}return 0;
}