我是靠谱客的博主 淡然歌曲,最近开发中收集的这篇文章主要介绍(c语言)即时通讯系统---epoll+线程池 版本注意一 .项目功能二.项目简介三.项目演示四.项目功能基本介绍四.代码部分,觉得挺不错的,现在分享给大家,希望可以做个参考。
概述
目录
注意
一 .项目功能
二.项目简介
三.项目演示
四.项目功能基本介绍
四.代码部分
server.c
server.h
mian.c
客户端
client.c
client.h
main.c
注意
相比较第一个版本 第二个版本将服务器的多线程并发服务器改成了epoll+线程池处理
客户端依旧运用的是上个版本的客户端
一 .项目功能
目前该项目实现基本功能有:
基本功能:
1.注册 2.登录 3.群聊 4快捷短语 5.保存聊天记录 6.查看聊天记录
7.文件传输 8.敏感词汇判断 9.私聊 10.离线消息发送
11.查看在线用户 12.管理员申请 13.管理员专属头像 14.禁言用户
15.解除禁言 16.管理员踢人 17.修改密码 18.注销用户 19.撤销管理员身份
二.项目简介
epoll
1.相比较: select内部使用数组实现,poll是链表。他们需要做内核区到用户区的转换,还需要做数据拷贝,因此效率低
2.epoll不需要做内核区到用户区的转换,因为数据存在共享内存中。epoll维护的树在共享内存中,内核区和用户区去操作共享内存,因此不需要区域转换,也不需要拷贝操作。
线程池
(1)降低销毁资源:重复利用线程池中已经存在的线程,减少了线程的创建和消亡造成的性能开销。
(2)提高了相应速率:当任务到达时,任务可以不需要等到线程创建就能够执行。
(3)防止服务器过载:形成内存溢出,或者cpu耗尽。
(4)提高线程的可管理性:线程时稀缺资源,若无限的创建线程,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以统一的分配,调优和监控
三.项目演示
即使通讯系统项目
四.项目功能基本介绍
## 三 .项目具体介绍
### 1.注册
```
注册有重复注册判断 在数据库 普通用户表中遍历 如果存在该用户则 停止注册
将注册的信息保存在通信协议中 服务器收到注册的功能标志 调用注册函数 将注册的信息插入数据库的普通用户表中;
```
#### 2.登录
```
用户登录
用一个链表保存在线用户 链表里包含用户的文件描述符 负责与用户之间通信
登录成功将会给所有在线用户发送自己已上线;
登录第一步在数据库中遍历该用户是否注册 只有注册了才能进行登录操作 第一步是遍历链表是否已经登录如果已经在别处登录 将不能进行登录操作.
```
### 3.群聊
```
因为登录过程中所有的用户的文件描述符 都会保存在在线链表中 群聊直接遍历在线用户链表
对每个文件描述符对应的用户发送消息
群聊的第一步 首先判断除了自己以外是否还有别的用户在线
第二步才是遍历链表 对除了自己的每个在线用户发送消息
```
### 4.快捷短语
```
在发送消息之前 判断 所要发送的消息 是否有对应的快捷短语 如果有快捷短语 则将快捷短语strcpy 至通信协议
聊天内容的字符串数组 进行发送
如果没有找对对应的快捷消息 则正常发送
```
### 5.聊天记录的保存以及查看
```
在服务器有一个recv函数循环的阻塞接受 客户端发来的结构体 也就是通信协议 每一次的信息传递都要经过这个函数
只用在recv函数下面 运用数据库相关的操作将 发送信息的用户 发送时间 以及消息内容存储至数据库中
查看聊天记录遍历数据库内容 将遍历的内容以固定格式保存在buf 发送给客户端;
```
### 6.文件传输
```
使用read函数 从打开的文件中循环读取内容 将内容与读取到的字节数
保存在通信协议中内容保存专用于文件传输的字符串数组中,将读取到的字节数一并传输 为了写入时 按读取的字节数进行写入
发送给服务器,再由服务器进行转发到对应的客户端 为了防止粘包现象采用传输结构体
```
### 7.敏感词判断
```
敏感词判断 数据中有一张表保存了各种敏感词汇 当客户端进行发送消息时 服务器遍历数据库判断消息是否为敏感词汇
一旦是敏感词汇 返回客户端 发送失败信号 并停止将敏感词汇转发给其他客户端
```
## 8.私聊
```
私聊第一步就是 服务器遍历在线用户链表 判断私聊对象是否在线 不在线返回给客户端 不在线消息
私聊和群聊相似 当客户端发送私聊信号时 服务器遍历链表 遍历出私聊对象 再将消息内容转发给对应的客户端
```
### 9.离线消息的发送
```
当私聊不在线是私聊信息会保存在数据库的 离线消息表中 当对方上线时 立即将消息发送给对方
```
### 10.查看在线用户
```
查看在线用户 和群聊相似 都是一个遍历链表的过程 当客户端发送查看在线用户的信号给 服务器
服务器在进行遍历在线链表将每遍历一个用户 将其用户名按固定格式 发送给 需要查看在线用户的用户
```
## 管理员用户的相关操作
### 11.申请称为管理员设置管理员头像
```
只有先成为普通用户以后 在能申请管理员 管理员有专属的管理员头像
申请成管理员后必需重新以管理员身份进行登录
在群聊过程中 管理员自带头像
客户端发送申请管理员信号以后 服务器 会将管理员用户信息 姓名密码头像等
保存在数据库的root表中
管理员用户 不可以重复申请
```
### 12.管理员禁言用户
### 13.解除禁言
```
管理员禁言 客户端给服务器发送一个 禁言信号 以及禁言对象 服务器首先判断发送禁言指令的是不是管理员 在对方是否在线 其次判断对方是不是管理员
管理员不能对管理员进行踢人 或者 禁言操作
如果对方是普通用则给对方发送禁言信号 这里采用的是全局变量flag 客户端收到禁言信号时flag至0 失去通信权限
收到解禁信号 flag至1 恢复通信权限
```
### 14.管理员踢人
```
管理员踢人 和禁言一样判断是否在线 在判断是否为管理员 如果为普通用户 则向对应的客户端发送退出聊天室指令
客户端收到退出指令以后自主结束整个进程 同时服务器将在线用户链表中的该用户踢出链表 并给所有在线用户发送消息
```
#### 15.修改密码
```
没有设置密保 直接根据名字进行修改密码 修改密码以后需要从新登录
客户端发送 修改密码信号 以及新密码 至服务器 服务器调用封装好的数据库函数 执行update更新数据库的语句 根据名字对密码进行更新
update user set password = '%s' where name = '%s'
```
### 16.注销用户
```
注销用户和修改密码类似
首先进行账户密码判断 账户密码正确才能进行注销用户
数据库找到关于注销名字的记录 然后删除这条记录 同时服务器将在线用户链表中的该用户踢出链表;
delete from user where name = '%s'
```
#### 17.撤销管理员身份
```
注销用户和注销用户类似
首先进行账户密码判断 账户密码正确才能进行撤销root
数据库找到关于撤销名字的记录 然后删除这条记录 同时服务器将在线用户链表中的该用户踢出链表;
delete from user where name = '%s'
```
退出聊天室 结束整个进程
四.代码部分
server.c
#include "server.h"
int flag2 = 1; //发送文件信号
extern struct pthreadpool *pool ;
extern online_userlist *head;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int socket_init()
{
unsigned char ip_str[] = "192.168.91.151";
unsigned char port_str[] = "8787";
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket error");
return -1;
}
//设置套接字端口属性为端口释放后可以重复使用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
//将套接字进行IP与端口的绑定
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); //清空内存
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(port_str)); //端口和ip要改成大端模式
addr.sin_addr.s_addr = inet_addr(ip_str);
int ret = bind(sockfd, (struct sockaddr *)&(addr), sizeof(addr));
if (ret == -1)
{
perror("bind error");
printf("绑定套接字失败.n");
return -1;
}
//监听
ret = listen(sockfd, 20);
if (ret == -1)
{
perror("listen error");
printf("监听套接字失败.n");
return -1;
}
printf("客户机已经开启,等待用户连接中.....n");
return sockfd;
}
// epoll操作
int mypoll(int sockfd)
{
int N = sizeof(clientlist);//MSg为通信协议 计算通信协议的大小
struct sockaddr_in clientaddr;//创建被填充的网络信息结构体
socklen_t addrlen = sizeof(clientaddr); //计算被网络信息结构体的大小
//第一步:创建epoll对象
int epfd = epoll_create(2000); //创建内核事件表 最大保存文件描述符个数为2000
//如果用epoll_create1一般参数为0 每添加一个自动增加结点保存文件描述符
if (-1 == epfd)//成功返回非负数的文件描述符 失败返回-1
{
ERRLOG("epoll");
}
//epoll树上挂的就是下面这个结构体
struct epoll_event ev, events[2000] = {0};//事件初始化 读 写 异常等
ev.events = EPOLLIN; //监听sockfd可读
ev.data.fd = sockfd;
//操作epoll内核时间表
// 参数一create生成的专用文件描述符 二操作类型 增 删 改 三 关联的文件描述符 四epoll_event结构体也就是上面的结构体
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
if (-1 == ret)
{//成功返回0 失败返回-1
printf("%sn", strerror(errno));
return -1;
}
//accept接受客户端连接
int i;
int acceptfd = 0;
while (1)
{
clientlist agreement;
//Msg msg;//通信协议
//参数分析 参数一 create返回值 参数二结点结构体(传出参数) 参数三事件表的最大存储个数 参数四 阻塞方式永久阻塞0是立即返回
int num = epoll_wait(epfd, events, 2000, -1);//等待IO事件发生
if (-1 == num)
{
printf("epoll_wait() failedn");
return -1;
}//成功:返回准备好的文件描述符的个数,如果没有准备号的,则返回0 错误:返回 -1
for (i = 0; i < num; i++)
{
if (events[i].data.fd == sockfd) //表示有客户端发起链接
{
if ((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
ERRLOG("accept error");
continue;
}
//为新的文件描述符注册事件
printf("accept fd %dn", acceptfd);
ev.events = EPOLLIN; //监听sockfd可读
ev.data.fd = acceptfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &ev);
if (-1 == ret)
{
ERRLOG("epoll_ctl");
}
printf("客户端%s:%d 连接了n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
else //有客户端发消息
{
//Myarg arg;
Myarg* arg = (Myarg *)malloc(sizeof(Myarg));
memset(arg, 0, sizeof(Myarg));
if (events[i].events & EPOLLIN) //如果事件是可读的
{
memset(&agreement, 0, sizeof(agreement));
ret = recv(events[i].data.fd, &agreement, N, 0);
if(agreement.flag == 3 || agreement.flag == 5)
{
insert_uprecord(&agreement);
}
if (ret == -1)
{
ERRLOG("recv");
}
else if (0 == ret)
{
ev.events = EPOLLIN; //监听sockfd可读
ev.data.fd = events[i].data.fd;
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &ev); //客户端退出,注销事件
if (-1 == ret)
{
ERRLOG("epoll_ctl");
}
printf("退出的客户端fd=%dn",events[i].data.fd);
Offlineprocessing(events[i].data.fd);//掉线处理函数
close(events[i].data.fd);
}
else
{
// //信息拷贝
arg->msg.flag=agreement.flag;
arg->msg.root=agreement.root;
arg->msg.num=agreement.num;
strcpy(arg->msg.name, agreement.name);
strcpy(arg->msg.to_name, agreement.to_name);
strcpy(arg->msg.msg, agreement.msg);
printf("agreement.msg = %sn",agreement.msg);
strcpy(arg->msg.emojio, agreement.emojio);
strcpy(arg->msg.password, agreement.password);
strcpy(arg->msg.buf, agreement.buf);
arg->fd = events[i].data.fd;
ThreadAddJob(pool, work, (void *)arg);//任務添加到任務隊列
}
}
}
}
}
}
//线程函数从任务队列去任务
void *ThreadRun(void *arg)
{
struct pthreadpool *pool = (struct pthreadpool *)arg;
struct job *pjob = NULL;//任务队列指针
while (1)
{
pthread_mutex_lock(&pool->mutex);
if (pool->m_QueueCurNum == 0)
{
printf("当前任务队列为空,线程%ld阻塞等待任务到来!n", pthread_self());
pthread_cond_wait(&pool->m_QueueNotEmpty, &pool->mutex);
}
pjob = pool->head;//指针指向任务队列的头部
pool->m_QueueCurNum--;//当前任务数量--
if (pool->m_QueueCurNum != pool->m_QueueMaxNum)//如果当前任务队列没有满
{
pthread_cond_signal(&pool->m_QueueNotFull); //每次线程取出一个任务之后,都要唤醒线程去添加任务
}
if (pool->m_QueueCurNum == 0)//如果当前任务对列没有任务
{
pool->head = pool->rear = NULL;
pthread_cond_broadcast(&pool->m_QueueEmpty); //当通知任务队列添加任务无果后,发送条件变量,通知其销毁线程
}
else
{
pool->head = pool->head->next;
}
pthread_mutex_unlock(&pool->mutex);
pjob->func(pjob->arg);//运行任务函数
free(pjob);
pjob = NULL;
}
}
//参数分析 参数1线程池线程数量 参数二 任务队列最大任务数
struct pthreadpool *InitPthreadPool(int ThreadNum, int QueueMaxNum) //初始化線程池 創建線程池
{
struct pthreadpool *pool = (struct pthreadpool *)malloc(sizeof(struct pthreadpool)); //创建一个线程池初始化的结构体
pool->m_QueueCurNum = 0;//当前任务队列的任务数
pool->m_QueueMaxNum = QueueMaxNum;//任务队列的最大最大任务数
pool->head = NULL;
pool->rear = NULL;
pool->m_threadNum = ThreadNum;//已经开启的线程数量也就是创建的线程数量
pool->m_pthreadIDs = (pthread_t *)malloc(sizeof(pthread_t) * ThreadNum);//保存已开启的线程IO
//动态数组名 保证后续指针的移动合法
pthread_mutex_init(&pool->mutex, NULL);//初始化一把锁
pthread_cond_init(&pool->m_QueueEmpty, NULL);//任务队列为空条件
pthread_cond_init(&pool->m_QueueNotEmpty, NULL);//任务队列不为空的条件
pthread_cond_init(&pool->m_QueueNotFull, NULL);//任务队列不满的条件
for (int i = 0; i < ThreadNum; i++)
{
pthread_create(&pool->m_pthreadIDs[i], NULL, ThreadRun, pool);
}
return pool;
}
//將任務函數添加到任務隊列
void ThreadAddJob(struct pthreadpool *pool, void *(*func)(void *arg), void *arg)//
{
pthread_mutex_lock(&pool->mutex);
if (pool->m_QueueCurNum == pool->m_QueueMaxNum)
{
printf("任务队列已满,挂起等待线程执行完毕。。。n");
pthread_cond_wait(&pool->m_QueueNotFull, &pool->mutex);
}
struct job *pjob = (struct job *)malloc(sizeof(struct job)); //创建任务队列
pjob->func = func;//任务函数通过任务传参传入任务函数
pjob->arg = arg;//任务函数的参数
// pjob->func(pjob->arg);
pjob->next = NULL;
if (pool->head == NULL)//如果任务队列 里面没有任务
{
pool->head = pool->rear = pjob;//那么将任务函数放置头结点处
pthread_cond_broadcast(&pool->m_QueueNotEmpty); //添加任务后,唤醒任意一个线程开始执行任务
}
else
{
pool->rear->next = pjob;//如果任务队列有任务 就将新的任务函数放在任务队的末尾
pool->rear = pjob;//尾指针指向最后一盒任务函数
}
pool->m_QueueCurNum++;//当前任务数量++
pthread_mutex_unlock(&pool->mutex);
}
//銷毀線程池
void ThreadDestroy(struct pthreadpool *pool)
{
pthread_mutex_lock(&pool->mutex);
while (pool->m_QueueCurNum != 0)
{
printf("阻塞等待销毁线程。。。n");
pthread_cond_wait(&pool->m_QueueEmpty, &pool->mutex);
}
printf("任务结束,线程%ld被销毁n", pthread_self());
pthread_mutex_unlock(&pool->mutex);
pthread_cond_broadcast(&pool->m_QueueNotEmpty);
pthread_cond_broadcast(&pool->m_QueueNotFull);
int i;
for (i = 0; i < pool->m_threadNum; i++)
{
pthread_join(pool->m_pthreadIDs[i], NULL);
}
pthread_mutex_destroy(&pool->mutex);
pthread_cond_destroy(&pool->m_QueueEmpty);
pthread_cond_destroy(&pool->m_QueueNotEmpty);
pthread_cond_destroy(&pool->m_QueueNotFull);
free(pool->m_pthreadIDs);
struct job *tmp;
while (pool->head != NULL)
{
tmp = pool->head;
pool->head = pool->head->next;
free(tmp);
}
free(pool);
}
//任务工作函数
void *work(void *arg)
{
Myarg *workarg = (Myarg *)arg;
printf("work: %dn", workarg->fd);
printf("msg: %sn",workarg->msg.msg);
printf("msg: %dn",workarg->msg.flag);
switch(workarg->msg.flag)
{
case REGISTER:
Register(workarg,&workarg->msg);//注册用户
printf(" 注册用户n");
break;
case LOGIN:
login(workarg,&workarg->msg);//登录
printf("用户登录功能n");
break;
case PRIVATE:
privatechat(workarg,&workarg->msg); //私聊
printf("私聊功能n");
break;
case CHPASSWORD:
changepassword(workarg,&workarg->msg); //修改密码
printf("修改密码n");
break;
case GROUPCHAT:
groupchat(workarg,&workarg->msg);//进行群聊
break;
case VIEWUSER: //查看所有在线用户
viewonline_user(workarg,&workarg->msg);
break;
case LOGUSER://注销用户
loguser(workarg,&workarg->msg);
break;
case CHECKRECORD:
Checkrecord(workarg);//查看聊天记录
break;
case FILETRAN:
filetransfer(workarg,&workarg->msg); //文件传输
break;
case APPLYROOT:
applyroot(workarg,&workarg->msg); //申请管理员用户
break;
case ROOTLOGIN:
rootlogin(workarg,&workarg->msg); //管理员登录
break;
case ROOTKICK:
rootkick(workarg,&workarg->msg); //管理员踢人
break;
case SILENT:
silent(workarg,&workarg->msg); //管理员禁言
break;
case REMSILENT:
remsilent(workarg,&workarg->msg); //管理员解除用户禁言
break;
case CANCELROOT:
cancelroot(workarg,&workarg->msg); //取消管理员身份
break;
default:
break;
}
free(arg);
}
//在线用户链表
online_userlist* online_userlistcreate()
{
online_userlist *head = (online_userlist *)malloc(sizeof(online_userlist));
head->next = NULL;
return head;
}
//注册部分
void Register(Myarg *node,clientlist *c)
{
printf("判断:%s 是否在数据库n",c->name);
int ret = searchtable(c->name); //在数据库中查找是否已经注册过
printf("%d",ret);
if(1 == ret)
{
char arr[128] = {"账号已经存在,请重新注册"};
if(send(node->fd,arr,sizeof(arr),0) == -1)
{
ERRLOG("send error");
}
return;
}
else
{
insert_updata(node,c); //向数据库中插入数据
}
}
//向数据库中插入注册数据
void insert_updata(Myarg *node,clientlist *c)
{
printf("注册的名字%sn",c->name);
char *errmsg;
sqlite3 *db =NULL;
char sql[200] = "