day05-epoll高级用法-Channel登场
在上一天,我们已经完整地开发了一个echo服务器,并且引入面向对象编程的思想,初步封装了Socket
、InetAddress
和Epoll
,大大精简了主程序,隐藏了底层语言实现细节、增加了可读性。
让我们来回顾一下我们是如何使用epoll
:将一个文件描述符添加到epoll
红黑树,当该文件描述符上有事件发生时,拿到它、处理事件,这样我们每次只能拿到一个文件描述符,也就是一个int
类型的整型值。试想,如果一个服务器同时提供不同的服务,如HTTP、FTP等,那么就算文件描述符上发生的事件都是可读事件,不同的连接类型也将决定不同的处理逻辑,仅仅通过一个文件描述符来区分显然会很麻烦,我们更加希望拿到关于这个文件描述符更多的信息。
在day03介绍epoll
时,曾讲过epoll_event
结构体:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
可以看到,epoll中的data
其实是一个union类型,可以储存一个指针。而通过指针,理论上我们可以指向任何一个地址块的内容,可以是一个类的对象,这样就可以将一个文件描述符封装成一个Channel
类,一个Channel类自始至终只负责一个文件描述符,对不同的服务、不同的事件类型,都可以在类中进行不同的处理,而不是仅仅拿到一个int
类型的文件描述符。
这里读者务必先了解C++中的union类型,在《C++ Primer(第五版)》第十九章第六节有详细说明。
Channel
类的核心成员如下:
class Channel{
private:
Epoll *ep;
int fd;
uint32_t events;
uint32_t revents;
bool inEpoll;
};
显然每个文件描述符会被分发到一个Epoll
类,用一个ep
指针来指向。类中还有这个Channel
负责的文件描述符。另外是两个事件变量,events
表示希望监听这个文件描述符的哪些事件,因为不同事件的处理方式不一样。revents
表示在epoll
返回该Channel
时文件描述符正在发生的事件。inEpoll
表示当前Channel
是否已经在epoll
红黑树中,为了注册Channel
的时候方便区分使用EPOLL_CTL_ADD
还是EPOLL_CTL_MOD
。
接下来以Channel
的方式使用epoll: 新建一个Channel
时,必须说明该Channel
与哪个epoll
和fd
绑定:
Channel *servChannel = new Channel(ep, serv_sock->getFd());
这时该Channel
还没有被添加到epoll红黑树,因为events
没有被设置,不会监听该Channel
上的任何事件发生。如果我们希望监听该Channel
上发生的读事件,需要调用一个enableReading
函数:
servChannel->enableReading();
调用这个函数后,如Channel
不在epoll红黑树中,则添加,否则直接更新Channel
、打开允许读事件。enableReading
函数如下:
void Channel::enableReading(){ events = EPOLLIN | EPOLLET; ep->updateChannel(this); }
可以看到该函数做了两件事,将要监听的事件events
设置为读事件并采用ET模式,然后在ep指针指向的Epoll红黑树中更新该Channel
,updateChannel
函数的实现如下:
void Epoll::updateChannel(Channel *channel){ int fd = channel->getFd(); //拿到Channel的文件描述符 struct epoll_event ev; bzero(&ev, sizeof(ev)); ev.data.ptr = channel; ev.events = channel->getEvents(); //拿到Channel希望监听的事件 if(!channel->getInEpoll()){ errif(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1, "epoll add error");//添加Channel中的fd到epoll channel->setInEpoll(); } else{ errif(epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1, "epoll modify error");//已存在,则修改 } }
在使用时,我们可以通过Epoll
类中的poll()
函数获取当前有事件发生的Channel
:
while(true){ vector<Channel*> activeChannels = ep->poll(); // activeChannels是所有有事件发生的Channel }
新客户端连接时,还可以用clntChannel监听新客户端发生的事件。
Channel *clntChannel = new Channel(ep, clnt_sock->getFd());
for(int i = 0; i < nfds; ++i){
int chfd = activeChannels[i]->getFd();
if(chfd == serv_sock->getFd()){ //新客户端连接
InetAddress *clnt_addr = new InetAddress(); //会发生内存泄露!没有delete
Socket *clnt_sock = new Socket(serv_sock->accept(clnt_addr)); //会发生内存泄露!没有delete
printf("new client fd %d! IP: %s Port: %d\n", clnt_sock->getFd(), inet_ntoa(clnt_addr->addr.sin_addr), ntohs(clnt_addr->addr.sin_port));
clnt_sock->setnonblocking();
Channel *clntChannel = new Channel(ep, clnt_sock->getFd());
clntChannel->enableReading();
} else if(activeChannels[i]->getRevents() & EPOLLIN){ //可读事件
handleReadEvent(activeChannels[i]->getFd());
} else{ //其他事件,之后的版本实现
printf("something else happened\n");
}
Channel可以分别用于监听serv_sock(服务器等待客户端连接的socket)和clnt_sock(已连接的客户端的socket)。每个channel对应一个文件描述符。
注:在今天教程的源代码中,并没有将事件处理改为使用Channel
回调函数的方式,仍然使用了之前对文件描述符进行处理的方法,这是错误的,将在明天的教程中进行改写。
至此,day05的主要教程已经结束了。服务器的功能和昨天一样,添加了Channel
类,可以让我们更加方便简单、多样化地处理epoll中发生的事件。同时脱离了底层,将epoll、文件描述符和事件进行了抽象,形成了事件分发的模型,这也是Reactor模式的核心,将在明天的教程进行讲解。
Channel.cpp的源码:
#include "Channel.h"
#include "Epoll.h"
Channel::Channel(Epoll *_ep, int _fd) : ep(_ep), fd(_fd), events(0), revents(0), inEpoll(false){
}
Channel::~Channel()
{
}
void Channel::enableReading(){
events = EPOLLIN | EPOLLET;
ep->updateChannel(this);
}
int Channel::getFd(){
return fd;
}
uint32_t Channel::getEvents(){
return events;
}
uint32_t Channel::getRevents(){
return revents;
}
bool Channel::getInEpoll(){
return inEpoll;
}
void Channel::setInEpoll(){
inEpoll = true;
}
// void Channel::setEvents(uint32_t _ev){
// events = _ev;
// }
void Channel::setRevents(uint32_t _ev){
revents = _ev;
}
Channel.h的源码:
#pragma once
#include <sys/epoll.h>
class Epoll;
class Channel
{
private:
Epoll *ep;
int fd;
uint32_t events;
uint32_t revents;
bool inEpoll;
public:
Channel(Epoll *_ep, int _fd);
~Channel();
void enableReading();
int getFd();
uint32_t getEvents();
uint32_t getRevents();
bool getInEpoll();
void setInEpoll();
// void setEvents(uint32_t);
void setRevents(uint32_t);
};
Server.cpp的代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <vector>
#include "util.h"
#include "Epoll.h"
#include "InetAddress.h"
#include "Socket.h"
#include "Channel.h"
#define MAX_EVENTS 1024
#define READ_BUFFER 1024
void setnonblocking(int fd){
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}
void handleReadEvent(int);
int main() {
Socket *serv_sock = new Socket();
InetAddress *serv_addr = new InetAddress("127.0.0.1", 8888);
serv_sock->bind(serv_addr);
serv_sock->listen();
Epoll *ep = new Epoll();
serv_sock->setnonblocking();
Channel *servChannel = new Channel(ep, serv_sock->getFd());
servChannel->enableReading();
while(true){
std::vector<Channel*> activeChannels = ep->poll();
int nfds = activeChannels.size();
for(int i = 0; i < nfds; ++i){
int chfd = activeChannels[i]->getFd();
if(chfd == serv_sock->getFd()){ //新客户端连接
InetAddress *clnt_addr = new InetAddress(); //会发生内存泄露!没有delete
Socket *clnt_sock = new Socket(serv_sock->accept(clnt_addr)); //会发生内存泄露!没有delete
printf("new client fd %d! IP: %s Port: %d\n", clnt_sock->getFd(), inet_ntoa(clnt_addr->addr.sin_addr), ntohs(clnt_addr->addr.sin_port));
clnt_sock->setnonblocking();
Channel *clntChannel = new Channel(ep, clnt_sock->getFd());
clntChannel->enableReading();
} else if(activeChannels[i]->getRevents() & EPOLLIN){ //可读事件
handleReadEvent(activeChannels[i]->getFd());
} else{ //其他事件,之后的版本实现
printf("something else happened\n");
}
}
}
delete serv_sock;
delete serv_addr;
return 0;
}
void handleReadEvent(int sockfd){
char buf[READ_BUFFER];
while(true){ //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
bzero(&buf, sizeof(buf));
ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
if(bytes_read > 0){
printf("message from client fd %d: %s\n", sockfd, buf);
write(sockfd, buf, sizeof(buf));
} else if(bytes_read == -1 && errno == EINTR){ //客户端正常中断、继续读取
printf("continue reading");
continue;
} else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,这个条件表示数据全部读取完毕
printf("finish reading once, errno: %d\n", errno);
break;
} else if(bytes_read == 0){ //EOF,客户端断开连接
printf("EOF, client fd %d disconnected\n", sockfd);
close(sockfd); //关闭socket会自动将文件描述符从epoll树上移除
break;
}
}
}
Client.cpp的源码:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include "util.h"
#define BUFFER_SIZE 1024
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
errif(sockfd == -1, "socket create error");
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);
errif(connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1, "socket connect error");
while(true){
char buf[BUFFER_SIZE]; //在这个版本,buf大小必须大于或等于服务器端buf大小,不然会出错,想想为什么?
bzero(&buf, sizeof(buf));
scanf("%s", buf);
ssize_t write_bytes = write(sockfd, buf, sizeof(buf));
if(write_bytes == -1){
printf("socket already disconnected, can't write any more!\n");
break;
}
bzero(&buf, sizeof(buf));
ssize_t read_bytes = read(sockfd, buf, sizeof(buf));
if(read_bytes > 0){
printf("message from server: %s\n", buf);
}else if(read_bytes == 0){
printf("server socket disconnected!\n");
break;
}else if(read_bytes == -1){
close(sockfd);
errif(true, "socket read error");
}
}
close(sockfd);
return 0;
}
Makefile:
server:
g++ util.cpp client.cpp -o client && \
g++ util.cpp server.cpp Epoll.cpp InetAddress.cpp Socket.cpp Channel.cpp -o server
clean:
rm server && rm client