目录
前言
在本文中我将使用linux下的epoll技术替换上一篇文章中的select技术,以此来实现一个更优化的服务端,上一篇文章链接如下:
TCP/IP网络编程(5)——基于I/O复用(select)和多进程编写简易聊天室
相较于select函数,epoll有以下两个优点:第一,不需要对所有文件描述符进行遍历来找出发生事件的文件描述符。epoll函数会把发生事件的文件描述符单独保存在一个空间(数组)中;第二,epoll调用前不需要传递监视对象信息,而是直接由操作系统保存(就是select函数的fd_set参数,因为select调用完,fd_set会变化,所以每次调用前都要传递)。由于是向操作系统传递监视对象信息,所以这个开销很大。
epoll技术分为条件触发和边缘触发两种方式:条件触发即只要满足条件,就注册一次事件,比如事件为读取缓冲区中的值,只要缓冲区中有值,那么就会注册事件。边缘触发即只有事件发生时才注册事件,即使缓冲区还有值,也不会再注册事件。所以边缘触发的性能优于条件触发,但是需要保证在一次触发中处理完事件,条件触发则不用担心。
一、 实现epoll的相关函数和结构体
1. event结构体
该结构体用于声明监视对象和对应的事件
struct epoll_event{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data{
void* ptr;
int fd;
__unit32_t u32;
__unit64_t u64;
}epoll_data_t;
常用的事件类型如下:
EPOLLIN:需要读取数据
EPOLLOUT:可以立即发送数据
EPOLLERR:发生错误
EPOLLRDHUP:断开连接或半关闭
EPOLLET:以边缘触发方式得到事件通知
当同时绑定多个事件时,用或运算符“|”
2. epoll_create
该函数用来创建保存epoll文件描述符的空间,这个空间称为epoll例程
int epoll_create(int size);
成功时返回epoll文件描述符,失败返回-1
(1)size:epoll实例的大小。但是操作系统会完全忽略传入的size参数
3. epoll_ctl
该函数用于向空间注册或注销文件描述符。对应select方法中的FD_SET、FD_CLR
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
成功返回0,失败返回-1
(1)epfd:epoll例程的文件描述符
(2)op:选择对于监视对象的操作。常见的操作有:EPOLL_CTL_ADD(添加)、EPOLL_CTL_DEL(删除)、EPOLL_CTL_MOD(更改)
(3)fd:监视对象的文件描述符
(4)event:事件类型
4. epoll_wait
该函数用于等待文件描述符发生变化。对应select方法中的select函数
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
成功返回发生事件的文件描述符数,失败返回-1
(1)epfd:epoll例程的文件描述符
(2)events:保存发生事件文件描述符集合的地址(event结构体数组)
(3)maxevents:events可以保存的最大事件数
(4)timeout:等待时间,传递-1表示一直等待
二、 条件触发版本的聊天室服务端
完整代码如下:
#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/epoll.h>
#include<map>
using namespace std;
void addUser(int,char*,map<int,char*>&); //用户上线
void removeUser(int,map<int,char*>&); //用户下线
int findUser(const char*,const map<int,char*>&); //用户查找
char* findName(int,map<int,char*>&); //用户名查找
void showUser(char*,const map<int,char*>&); //展示在线用户
int main()
{
int epollfd; //epoll的例程文件描述符
int eventcount; //记录epoll_wait返回值
epollfd = epoll_create(50); //创建epoll例程空间
struct epoll_event event; //创建epoll事件结构体
struct epoll_event events[50]; //设置最大event的个数
char* ip = "127.0.0.1";
char* port = "9955";
int opt = 1;
map<int,char*> users; //记录客户端套接字和用户的对应关系
char choice; //记录客户端请求的服务,0:注册、1:通信、2:查看在线用户、3:下线
char name[20]; //要通信的用户名
char msg[1000]; //信息
char buf[1024]; //存储所有数据
int server_socket, client_socket, to;
struct sockaddr_in server_addr, client_addr;
socklen_t server_addr_len = sizeof(server_addr), client_addr_len = sizeof(client_addr);
server_socket = socket(PF_INET,SOCK_STREAM,0);
setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt)); //创建套接字并避免timeout
memset(&server_addr,0,sizeof(server_addr)); //设置地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(atoi(port));
if(bind(server_socket,(struct sockaddr*)&server_addr,server_addr_len)==-1) //分配套接字地址
cout << "bind error" << endl;
if(listen(server_socket,5)==-1) //开启监听
cout << "listen error" << endl;
event.events = EPOLLIN;
event.data.fd = server_socket;
epoll_ctl(epollfd,EPOLL_CTL_ADD,server_socket,&event); //监控输入方式注册server_socket
while(1)
{
eventcount = epoll_wait(epollfd,events,50,-1); //-1表示一直等待
if(eventcount == -1)
cout << "epoll_wait error" << endl;
else //有文件描述符有输入
{
for(int i=0;i<eventcount;i++)
{
if(events[i].data.fd == server_socket) //说明有连接请求
{
client_socket = accept(server_socket,
(struct sockaddr*)&client_addr,&client_addr_len);
event.events = EPOLLIN;
event.data.fd = client_socket;
epoll_ctl(epollfd,EPOLL_CTL_ADD,client_socket,&event);
cout << "connect..." << client_socket << endl;
}
else //客户端有输入
{
memset(buf,0,sizeof(buf));
recv(events[i].data.fd,buf,sizeof(buf),0);
choice = buf[0];
switch(choice)
{
case '0':
strncpy(name,&buf[1],20);
if(findUser(name,users)==-1) //检查是否已经登陆
{
addUser(events[i].data.fd,name,users); //添加用户
send(events[i].data.fd,"log in",7,0);
}
else
send(events[i].data.fd,"already log in",15,0);
continue;
case '1':
strncpy(name,&buf[1],20);
strncpy(msg,&buf[21],1000);
to = findUser(name,users);
if(to==-1)
send(events[i].data.fd,"not found user",15,0);
else
{
int len = 0; //构造消息
char* fromname = findName(events[i].data.fd,users);
strncpy(&buf[len],fromname,strlen(fromname));
len+=strlen(fromname);
buf[len++] = ':';
strncpy(&buf[len],msg,strlen(msg));
send(to,buf,sizeof(buf),0);
send(events[i].data.fd,"send successfully",18,0);
}
continue;
case '2':
showUser(buf,users); //展示用户
send(events[i].data.fd,buf,sizeof(buf),0);
continue;
case '3':
removeUser(events[i].data.fd,users); //删除用户
send(events[i].data.fd,"log out",8,0);
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
close(events[i].data.fd);
continue;
}
}
}
}
}
close(server_socket);
return 0;
}
void addUser(int client_socket,char* username,map<int,char*>& users)
{
char* name = new char(20); //创建一个新地址存放名字并存储到map字典中
strcpy(name,username);
if(users.count(client_socket))
return;
else
{
users.insert(pair<int,char*>(client_socket,name));
cout << "client " << username << " log in" << endl;
}
}
void removeUser(int client_socket,map<int,char*>& users)
{
if(users.count(client_socket))
{
delete[] users.find(client_socket)->second; //释放内存
users.erase(client_socket);
cout << "client " << client_socket << " log out" << endl;
}
else
return;
}
int findUser(const char* username,const map<int,char*>& users)
{
map<int,char*>::const_iterator it = users.begin();
for(;it!=users.end();it++)
{
if(!strcmp(it->second,username))
return it->first;
}
return -1;
}
void showUser(char* buf,const map<int,char*>& users)
{
int len = 0;
map<int,char*>::const_iterator it = users.begin();
for(;it!=users.end();it++)
{
strncpy(&buf[len],it->second,strlen(it->second));
len+=strlen(it->second);
buf[len++] = ' ';
}
}
char* findName(int client_socket,map<int,char*>& users)
{
map<int,char*>::iterator it;
it = users.find(client_socket);
return it->second;
}
三、 边缘触发版本的聊天室服务端
上面说过,边缘触发的性能优于条件触发。使用边缘触发需要额外注意以下三点:
1. 因为事件只会触发一次,所以数据必须一次性处理完,可以通过errno变量来判断数据处理情况(文件头errno.h)
2. 文件描述符注册事件需要添加EPOLLET
3. 必须更改套接字特性,实现非阻塞I/O。因为边缘触发下如果使用阻塞I/O,会导致数据处理不完整或者影响其他事件的处理。具体方法如下,使用fcntl函数设置文件描述符
int flag = fcntl(client_socket,F_GETFL,0);
fcntl(client_socket,F_SETFL,flag|O_NONBLOCK);
首先获取当前文件描述符属性记录在flag变量中,然后用或操作添加非阻塞属性
完整代码如下:
#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/epoll.h>
#include<map>
#include<fcntl.h>
#include<errno.h>
using namespace std;
void addUser(int,char*,map<int,char*>&); //用户上线
void removeUser(int,map<int,char*>&); //用户下线
int findUser(const char*,const map<int,char*>&); //用户查找
char* findName(int,map<int,char*>&); //用户名查找
void showUser(char*,const map<int,char*>&); //展示在线用户
int main()
{
int epollfd; //epoll的例程文件描述符
int eventcount; //记录epoll_wait返回值
epollfd = epoll_create(50); //创建epoll例程空间
struct epoll_event event; //创建epoll事件结构体
struct epoll_event events[50]; //设置最大event的个数
int flag; //记录文件描述符的模式信息
char* ip = "127.0.0.1";
char* port = "9955";
int opt = 1;
map<int,char*> users; //记录客户端套接字和用户的对应关系
char choice; //记录客户端请求的服务,0:注册、1:通信、2:查看在线用户、3:下线
char name[20]; //要通信的用户名
char msg[1000]; //信息
char buf[1024]; //存储所有数据
int server_socket, client_socket, to;
struct sockaddr_in server_addr, client_addr;
socklen_t server_addr_len = sizeof(server_addr), client_addr_len = sizeof(client_addr);
server_socket = socket(PF_INET,SOCK_STREAM,0);
setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt)); //创建套接字并避免timeout
memset(&server_addr,0,sizeof(server_addr)); //设置地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(atoi(port));
if(bind(server_socket,(struct sockaddr*)&server_addr,server_addr_len)==-1) //分配套接字地址
cout << "bind error" << endl;
if(listen(server_socket,5)==-1) //开启监听
cout << "listen error" << endl;
flag = fcntl(server_socket,F_GETFL,0);
fcntl(server_socket,F_SETFL,flag|O_NONBLOCK); //把文件描述符添加一个为非阻塞I/O状态
event.events = EPOLLIN;
event.data.fd = server_socket;
epoll_ctl(epollfd,EPOLL_CTL_ADD,server_socket,&event); //监控输入方式注册server_socket
while(1)
{
eventcount = epoll_wait(epollfd,events,50,-1); //-1表示一直等待
if(eventcount == -1)
cout << "epoll_wait error" << endl;
else //有文件描述符有输入
{
for(int i=0;i<eventcount;i++)
{
if(events[i].data.fd == server_socket) //说明有连接请求
{
client_socket = accept(server_socket,
(struct sockaddr*)&client_addr,&client_addr_len);
flag = fcntl(client_socket,F_GETFL,0);
fcntl(client_socket,F_SETFL,flag|O_NONBLOCK); //把文件描述符添加一个为非阻塞I/O状态
event.events = EPOLLIN|EPOLLET; //将事件注册为边缘触发
event.data.fd = client_socket;
epoll_ctl(epollfd,EPOLL_CTL_ADD,client_socket,&event);
cout << "connect..." << client_socket << endl;
}
else //客户端有输入
{
memset(buf,0,sizeof(buf));
int len = 0;
int readlen = 0;
while(1)
{
readlen = recv(events[i].data.fd,&buf[len],sizeof(buf)-len,0);
if(readlen < 0)
{
if(errno == EAGAIN)
break;
}
else if(readlen > 0)
len += readlen;
else
break;
}
choice = buf[0];
switch(choice)
{
case '0':
strncpy(name,&buf[1],20);
if(findUser(name,users)==-1) //检查是否已经登陆
{
addUser(events[i].data.fd,name,users); //添加用户
send(events[i].data.fd,"log in",7,0);
}
else
send(events[i].data.fd,"already log in",15,0);
continue;
case '1':
strncpy(name,&buf[1],20);
strncpy(msg,&buf[21],1000);
to = findUser(name,users);
if(to==-1)
send(events[i].data.fd,"not found user",15,0);
else
{
int len = 0; //构造消息
char* fromname = findName(events[i].data.fd,users);
strncpy(&buf[len],fromname,strlen(fromname));
len+=strlen(fromname);
buf[len++] = ':';
strncpy(&buf[len],msg,strlen(msg));
send(to,buf,sizeof(buf),0);
send(events[i].data.fd,"send successfully",18,0);
}
continue;
case '2':
showUser(buf,users); //展示用户
send(events[i].data.fd,buf,sizeof(buf),0);
continue;
case '3':
removeUser(events[i].data.fd,users); //删除用户
send(events[i].data.fd,"log out",8,0);
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
close(events[i].data.fd);
continue;
}
}
}
}
}
close(server_socket);
return 0;
}
void addUser(int client_socket,char* username,map<int,char*>& users)
{
char* name = new char(20); //创建一个新地址存放名字并存储到map字典中
strcpy(name,username);
if(users.count(client_socket))
return;
else
{
users.insert(pair<int,char*>(client_socket,name));
cout << "client " << username << " log in" << endl;
}
}
void removeUser(int client_socket,map<int,char*>& users)
{
if(users.count(client_socket))
{
delete[] users.find(client_socket)->second; //释放内存
users.erase(client_socket);
cout << "client " << client_socket << " log out" << endl;
}
else
return;
}
int findUser(const char* username,const map<int,char*>& users)
{
map<int,char*>::const_iterator it = users.begin();
for(;it!=users.end();it++)
{
if(!strcmp(it->second,username))
return it->first;
}
return -1;
}
void showUser(char* buf,const map<int,char*>& users)
{
int len = 0;
map<int,char*>::const_iterator it = users.begin();
for(;it!=users.end();it++)
{
strncpy(&buf[len],it->second,strlen(it->second));
len+=strlen(it->second);
buf[len++] = ' ';
}
}
char* findName(int client_socket,map<int,char*>& users)
{
map<int,char*>::iterator it;
it = users.find(client_socket);
return it->second;
}