上次写到将Epoll,socket,address都封装了一下,实际上进度还是相当慢的。照着敲肯定快,主要是还是要自己理解,再加上智能指针的自动析构特性在部分场景有点反向优化的坑,bug一改就是好久。
这次新增了一个Channel类,该类与一个文件描述符相关,不同的Channel负责不同的文件描述符,可以对不同的服务或者不同的事件类型做出不同的处理。
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;
里面的data成员是一个联合类型,可以储存一个指针,通过指针理论上可以指向任何一个地址,所以可以指向任何一个类型,从而达到针对不同类型实现不同处理的效果。
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。
使用的话,也是用智能指针来管理,然后设置监听读事件,当然也可以设置监听其他事件
pChannel servChannel(new Channel(ep.get(), servSock->getFd()));
servChannel->enableReading();
现在server代码:
int main() {
pSocket servSock(new Socket());
pInetAddress servAddr(new InetAddress("127.0.0.1", 8888));
servSock->bind(servAddr.get());
servSock->listen();
pEpoll ep(new Epoll());
servSock->setNonBlocking();
pChannel servChannel(new Channel(ep.get(), servSock->getFd()));
servChannel->enableReading();
while (true) {
std::vector<pChannel> activeChannel = ep->poll(-1);
int nfds = activeChannel.size();
for (int i = 0; i < nfds; i++) {
int chfd = activeChannel[i]->getFd();
if (chfd == servSock->getFd()) { //新客户端连接
pInetAddress clntAddr(new InetAddress());
Socket *clntSock = new Socket(servSock->accept(clntAddr.get()));
printf("new client fd %d! IP: %s Port: %d\n", clntSock->getFd(),
inet_ntoa(clntAddr->getSocketaddr().sin_addr), ntohs(clntAddr->getSocketaddr().sin_port));
clntSock->setNonBlocking();
pChannel clntChannel(new Channel(ep.get(), clntSock->getFd()));
clntChannel->enableReading();
} else if (activeChannel[i]->getFd() & EPOLLIN) { //可读事件
handleReadEvent(activeChannel[i]->getFd());
} else {
printf("something else happened");
}
}
}
return 0;
}
void handleReadEvent(int sockfd) {
char buf[READ_BUFFER];
while (true) { //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
bzero(buf, sizeof(buf));
ssize_t bytesRead = read(sockfd, buf, sizeof(buf));
if (bytesRead > 0) {
printf("message from clinet fd %d: %s\n", sockfd, buf);
write(sockfd, buf, sizeof(buf));
} else if (bytesRead == -1 && errno == EINTR) { //客户端正常中断、继续读取
printf("continue reading\n");
continue;
} else if (bytesRead == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))) { //非阻塞IO,这个条件表示数据全部读取完毕
printf("reading finished, errno:%d\n", errno);
break;
} else if (bytesRead == 0) { //EOF,客户端断开连接
printf("client fd: %d disconnected\n", sockfd);
close(sockfd); //关闭socket会自动将文件描述符从epoll树上移除
break;
}
}
}
p开头的全为typedef定义的智能指针,这次遇到的主要问题是Epoll类里获取活动事件的函数poll
std::vector<pChannel> Epoll::poll(int timeout) const{
std::vector<pChannel> activeEvents;
int nfds = epoll_wait(epFd, events.get(), MAX_EVENTS, timeout);
errif(nfds == -1, "epoll wait error");
auto ptr = events.get();
for(int i = 0; i < nfds; ++i){
auto t = reinterpret_cast<Channel*>((ptr + i)->data.ptr);
pChannel ch(t, [](Channel *p){});
ch->setRevents((ptr + i)->events);
activeEvents.emplace_back(ch);
}
return activeEvents;
}
该函数通过epoll_wait等待io事件发生,如果发生比如客户端连接或者读事件时则唤醒该函数,并返回事件数量,上文说到epoll里data结构里可以储存一个void*指针,如果要使用的话需要转换,说实话auto确实省事很多,可以不用过多纠结类型问题,但不能产生太大依赖。
前不久才看到《Effective STL》里说到谨慎使用储存指针类型的vector,因为要自己释放指针或者直接使用储存智能指针的vector上,这里问题就出在pChannel类型上,智能指针会自动析构,从而导致返回的vector activeEvents里空无一物,结果就是接收到了请求却无法处理,客户端无法连接,解决方法也很简单,要么就用普通指针,要么在构造ch的时候传入自定义的析构函数,这里用的lambda表达式,函数体为空,也就是什么都不干,说实话感觉真不如用普通指针,也就当练练手了,看来任何一条建议都要结合实际情况来看,不能盲从。