16天自制CppServer-day05

day05-epoll高级用法-Channel登场

在上一天,我们已经完整地开发了一个echo服务器,并且引入面向对象编程的思想,初步封装了SocketInetAddressEpoll,大大精简了主程序,隐藏了底层语言实现细节、增加了可读性。

让我们来回顾一下我们是如何使用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与哪个epollfd绑定:

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红黑树中更新该ChannelupdateChannel函数的实现如下:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值