员工管理系统
- 前言
- 需求分析
- 系统设计
- 系统框图
- 所需技术
- 系统实现
- 编写代码
- 测试
前言
这是一个使用epoll实现TCP并发服务器,并让客户端登录服务器可以进行员工的管理,员工的信息存储在sqlite数据库中,对数据库进行增删改查实现对员工的添加,删除,修改,查询等功能;
需求分析
1)服务器负责管理所有员工表单(以数据库形式),其他客户端可通过网络连接服务器来查询员工表单。
2)需要账号密码登陆,其中需要区分管理员账号还是普通用户账号。
3)管理员账号可以查看、修改、添加、删除员工信息,同时具有查询历史记录功能,管理员要负责管理所有的普通用户。
4)普通用户只能查询修改与本人有关的相关信息,其他员工信息不得查看修改。
5)服务器能同时相应多台客户端的请求功能。实现并发
系统设计
系统框图
server端:
client端:
所需技术
一、信息存储:
使用sqlite数据库对员工信息的存储,其中包括管理员信息和普通员工信息;同时也要对历史记录进行存储;
二、TCP通信:
使用TCP服务器,实现服务器和客户端之间的接发数据,处理客户端发来的请求,实现对员工的管理;
三、并发服务器:
并发服务器的实现方法很多,可以使用多进程多线程,也可以使用IO多路复用,这里我使用了epoll的方法来实现并发服务器,可以同时处理多个客户端发来的请求;
系统实现
编写代码
一、函数和结构体的封装
#ifndef __COMMON_H__
#define __COMMON_H__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sqlite3.h>
#include <time.h>
//定义大小
#define NAMELEN 20
#define DATALEN 50
#define MSGLEN 500
#define MAX_EVENTS 10
//定义IP地址和端口号
#define IP "192.168.250.100"
#define PORT 8888
// 定义消息类型
#define LOGIN 1
#define ADD 2
#define DELETE 3
#define MODIFY 4
#define SEARCH 5
#define HISTORY 6
//用户类型
#define ADMIN 0
#define USER 1
//定义员工信息结构体
typedef struct staff_info
{
int id; // 员工编号
int usertype; // ADMIN 0 USER 1
char name[NAMELEN]; // 姓名
char passwd[8]; // 密码
int age; // 年龄
char phone[NAMELEN]; // 电话
char addr[DATALEN]; // 地址
char work[DATALEN]; // 职位
char date[DATALEN]; // 入职年月
int level; // 等级
double salary; // 工资
} staff_info_t;
/*定义双方通信的结构体信息*/
typedef struct
{
int msgtype; // 请求的消息类型
char recvmsg[MSGLEN]; // 通信的消息
int flags; // 标志位
staff_info_t info; // 员工信息
} MSG;
//服务器用到的函数
int create_socket(const char *address, int port);
void init_sql(sqlite3 *db);
int handle_client(int clientfd, sqlite3 *db);
void getdata(char *date); //获取时间
int do_login(int clientfd, MSG *msg, sqlite3 *db);
int do_add(int clientfd, MSG *msg, sqlite3 *db);
int do_delete(int clientfd, MSG *msg, sqlite3 *db);
int do_change(int clientfd, MSG *msg, sqlite3 *db);
int do_search(int clientfd, MSG *msg, sqlite3 *db);
int do_history(int clientfd, MSG *msg, sqlite3 *db);
//客户端用到的函数
int create_socket(const char *address, int port);
int login(int socket, MSG *msg, int flag);
int add(int sockfd, MSG *msg);
int delete(int sockfd, MSG *msg);
int change(int sockfd, MSG *msg);
int search(int sockfd, MSG *msg);
int history(int sockfd, MSG *msg);
#endif
二、epoll并发服务器模型
epfd = epoll_create(1);
if (epfd == -1)
{
perror("epoll_create1() error");
exit(1);
}
// 将服务器套接字加入epoll实例的监听列表
event.data.fd = sockfd;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1)
{
perror("epoll_ctl() error");
exit(1);
}
while (1)
{
epct = epoll_wait(epfd, events, MAX_EVENTS, -1); // 等待事件到来,阻塞模式
if (epct == -1)
{
perror("epoll_wait() error");
exit(1);
}
// 处理准备就绪的套接字
for (i = 0; i < epct; i++)
{
if (events[i].data.fd == sockfd)
{
// 新的客户端连接请求
clientfd = accept(sockfd, (struct sockaddr *)&cin, &cin_len);
if (clientfd < 0)
{
perror("accept() error");
exit(1);
}
printf("[%s:%d]连接到服务器..\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
// 将客户端套接字加入epoll实例的监听列表
event.data.fd = clientfd;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &event) == -1)
{
perror("epoll_ctl() error");
exit(1);
}
}
else
{
// 客户端数据可读
handle_client(events[i].data.fd, db);
}
}
}
三、初始化数据库
void init_sql(sqlite3 *db)
{
printf("正在初始化...\n");
// 创建表
char sql[256] = "";
char *errmsg = NULL;
strcpy(sql, "create table if not exists usr (id int,name char PRIMARY KEY,passwd char,age int,phone char,addr char,work char,data char,level int,salary double);");
if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK)
{
printf("sqlite3_exec error:%s\n", errmsg);
return;
}
strcpy(sql, "create table if not exists log (name char,operations char,time char);");
if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK)
{
printf("sqlite3_exec error:%s\n", errmsg);
return;
}
// 判断管理员的信息是否在数据库中
char **result = NULL;
int rows = -1;
int columns = 0;
strcpy(sql, "select * from usr where name='admin'");
if (sqlite3_get_table(db, sql, &result, &rows, &columns, &errmsg) != SQLITE_OK)
{
printf("sqlite3_get_table error:%s line=%d\n", errmsg, __LINE__);
return;
}
if (rows == 0)
{
// 插入管理员信息
sprintf(sql, "insert into usr values('1000','admin','admin','20','19156058040','上海','嵌入式','2022.09','4','20000');");
}
sqlite3_free_table(result);
}
这里直接初始化了一个管理员账户信息,管理员信息后续无法修改,有且只有一个管理员,后续对员工的管理只可以通过此管理员进行管理。
四、员工登录界面无法使用管理员账号登录
在客户端记录了登录身份信息,当处于普通员工登录界面时,无法使用管理员账户登录,服务器端会对客户端发来的数据进行判断,如果处于员工登录界面时,且用户信息是管理员,则发送登录失败。
int do_login(int clientfd, MSG *msg, sqlite3 *db)
{
char data[128];
int row;
int cloumn;
char sql[128];
char *errmsg = NULL;
char **result;
// 匹配用户信息是否与密码表中相同
sprintf(sql, "select * from usr where name = '%s' and passwd = '%s';", msg->info.name, msg->info.passwd);
if (sqlite3_get_table(db, sql, &result, &row, &cloumn, &errmsg) != SQLITE_OK)
{
printf("sqlite3_get_table error:%s line=%d\n", errmsg, __LINE__);
return -1;
}
// 密码表中存在改用户
if (row == 1)
{
strcpy(msg->recvmsg, "登录成功!!");
msg->flags = 1; // 代表操作成功
// 插入记录(用户登陆成功)
getdata(data);
sprintf(sql, "insert into log values('%s', 'login', '%s')", msg->info.name, data);
if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK)
{
printf("sqlite3_exec error:%s line=%d\n", errmsg, __LINE__);
return -1;
}
}
// 密码表中不存在改用户或者用户登录管理员账号
if (row == 0 || (msg->info.usertype == 1 && strcmp(msg->info.name, "admin") == 0))
{
printf("登录失败\n");
strcpy(msg->recvmsg, "登录失败!!");
msg->flags = 0; // 代表操作失败
}
// 返回用户登录信息
if (send(clientfd, msg, sizeof(MSG), 0) < 0)
{
perror("send err");
}
return 0;
}
五、查找用户信息(添加,修改,删除 功能类似)
查找分为普通员工查找和管理员查找,管理员查找又可以根据姓名查找和查找全部,由于普通员工查找信息也是根据自己的用户名查找,所以这里只需要分两种情况编写代码,一种是根据用户名,一种是查找全部,这里是通过判断客户端传来的flag来进行判断,如果是员工查找或者是管理员通过用户名查找,flag=1,查找全部flag=0。
在使用sqlite3_get_table时,循环向客户端发送信息,一行一行的发送,客户端循环接收,当循环发送结束时,向客户端发送结束标志。
客户端
int search(int sockfd, MSG *msg)
{
int n;
msg->msgtype = SEARCH;
if (msg->info.usertype == ADMIN)
{
system("clear");
printf("======================可查找选项==========================\n");
printf("------------------------菜单-----------------------------\n");
printf("\t\t\t1.根据用户名查找\n");
printf("\t\t\t2.查找全部\n");
printf("-----------------------------------------------------------\n");
printf("请输入选项:\n");
scanf("%d", &n);
getchar();
if (n == 1)
{
msg->flags = 1;
printf("请输入要查找的用户名:\n");
scanf("%s", msg->info.name);
getchar();
if (send(sockfd, msg, sizeof(MSG), 0) < 0)
{
printf("send err\n");
return -1;
}
printf("id\t\tname\t\tpasswd\t\tage\tphone\t\taddr\twork\tdate\t\tlevel\tsalary\t\t\n");
// 接收成功与否
while (1)
{
if (recv(sockfd, msg, sizeof(MSG), 0) < 0)
{
printf("recv err\n");
return -1;
}
printf("%s\n", msg->recvmsg);
if (0 == strcmp(msg->recvmsg, "query end"))
{
break;
}
}
// 本次操作如果失败直接返回
if (msg->flags == 0)
{
printf("查找失败%s\n2秒刷新页面\n", msg->recvmsg);
sleep(2);
return -1;
}
// 操作成功打印查找之后的信息
printf("按任意键退出查询界面:\n");
getchar();
return 0;
}
else if (n == 2)
{
msg->flags = 0;
if (send(sockfd, msg, sizeof(MSG), 0) < 0)
{
printf("send err\n");
return -1;
}
// 接收成功与否
printf("id\t\tname\t\tpasswd\t\tage\tphone\t\taddr\twork\tdate\t\tlevel\tsalary\t\t\n");
while (1)
{
if (recv(sockfd, msg, sizeof(MSG), 0) < 0)
{
printf("recv err\n");
return -1;
}
printf("%s\n", msg->recvmsg);
if (0 == strcmp(msg->recvmsg, "query end"))
{
break;
}
}
// 本次操作如果失败直接返回
if (msg->flags == 0)
{
printf("查找失败%s\n2秒刷新页面\n", msg->recvmsg);
sleep(2);
return -1;
}
// 操作成功打印查找之后的信息
printf("按任意键退出查询界面:\n");
getchar();
return 0;
}
return 0;
}
else
{
// 员工查询自己
msg->flags = 1;
strcpy(msg->info.name, name);
if (send(sockfd, msg, sizeof(MSG), 0) < 0)
{
printf("send err\n");
return -1;
}
printf("id\t\tname\t\tpasswd\t\tage\tphone\t\taddr\twork\tdate\t\tlevel\tsalary\t\t\n");
// 接收成功与否
while (1)
{
if (recv(sockfd, msg, sizeof(MSG), 0) < 0)
{
printf("recv err\n");
return -1;
}
printf("%s\n", msg->recvmsg);
if (0 == strcmp(msg->recvmsg, "query end"))
{
break;
}
}
// 本次操作如果失败直接返回
if (msg->flags == 0)
{
printf("查找失败%s\n2秒刷新页面\n", msg->recvmsg);
sleep(2);
return -1;
}
printf("按任意键退出查询界面:\n");
getchar();
return 0;
}
}
服务器
int do_search(int clientfd, MSG *msg, sqlite3 *db)
{
char sql[128];
char *errmsg;
char **result;
int row;
int cloum;
int i, j;
if (msg->flags == 1) // 根据姓名查找
{
sprintf(sql, "select *from usr where name = '%s' ", msg->info.name);
}
else if (msg->flags == 0) // 查找全部
{
sprintf(sql, "select *from usr ");
}
if (sqlite3_get_table(db, sql, &result, &row, &cloum, &errmsg) != SQLITE_OK)
{
printf("%s\n", errmsg);
msg->flags = 0; // 失败标志
}
else
{
msg->flags = 1; // 成功标志
}
for (int i = 1; i <= row; i++)
{
sprintf(msg->recvmsg, "%-8s\t%-8s\t%-8s\t%-5s\t%-15s\t%-8s\t%-8s\t%-10s\t%-5s\t%-10s\n", result[i * cloum], result[i * cloum + 1], result[i * cloum + 2], result[i * cloum + 3], result[i * cloum + 4], result[i * cloum + 5], result[i * cloum + 6], result[i * cloum + 7], result[i * cloum + 8], result[i * cloum + 9]);
if (send(clientfd, msg, sizeof(MSG), 0) < 0)
{
perror("send err");
}
}
strcpy(msg->recvmsg, "query end");
if (send(clientfd, msg, sizeof(MSG), 0) < 0)
{
perror("send err");
}
sqlite3_free_table(result);
return 0;
}
六、查询历史记录功能
管理员可以查询历史记录,在执行登录、添加员工、删除员工、修改员工信息时,都会将记录插入到记录表中,管理员可以查询记录表中的内容。查询记录和查询员工信息相似。
int do_history(int clientfd, MSG *msg, sqlite3 *db)
{
char sql[128];
char *errmsg;
char **result;
int row;
int cloum;
int i, j;
sprintf(sql, "select *from log ");
if (sqlite3_get_table(db, sql, &result, &row, &cloum, &errmsg) != SQLITE_OK)
{
printf("%s\n", errmsg);
msg->flags = 0; // 失败标志
}
else
{
msg->flags = 1; // 成功标志
}
for (int i = 1; i <= row; i++)
{
sprintf(msg->recvmsg, "%-8s\t%-8s\t%-8s\t\n", result[i * cloum], result[i * cloum + 1], result[i * cloum + 2]);
if (send(clientfd, msg, sizeof(MSG), 0) < 0)
{
perror("send err");
}
}
strcpy(msg->recvmsg, "query end");
if (send(clientfd, msg, sizeof(MSG), 0) < 0)
{
perror("send err");
}
sqlite3_free_table(result);
return 0;
}
注:完整代码见:员工管理系统
测试
对所有功能进行测试,是否可以实现多个客户端同时登录,是否可以添加、删除、修改、查看员工信息,以及管理员查看历史记录等功能:
员工管理系统