找工作小项目:day4-将socket、地址、epoll进行类封装,走向面对对象编程

day4-将socket、地址、epoll进行类封装,走向面对对象编程

今天的内容很简单,将面向过程转向面向对象,即将程序封装成类。
今天被问了个问题,什么情况下使用socket懵了,细想下应该是在进行进程间通信的过程中需要用到socket,不要被限制到服务器和客户端。

1、地址创建的封装

首先考虑前文中创建地址的方法。先看一下之前怎么创建的吧。

//服务器
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);
//客户端
struct sockaddr_in clnt_addr;
bzero(&clnt_addr, sizeof(clnt_addr));
socklen_t clnt_addr_len = sizeof(clnt_addr);

那么要构建关于地址创建的类需要sockaddr_in、socklen_t、两种构造函数及析构函数。
先看声明:

class InetAddress
{
public:
    struct sockaddr_in addr;
    socklen_t addr_len;
    InetAddress();
    InetAddress(const char* ip, uint16_t port);
    ~InetAddress();
};

再看实现,无参构造函数用以实现客户端地址初始化,有参构造函数用以实现服务端地址初始化:

InetAddress::InetAddress() : addr_len(sizeof(addr)){
    bzero(&addr, sizeof(addr));
}
InetAddress::InetAddress(const char* ip, uint16_t port) : addr_len(sizeof(addr)){
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);
    addr_len = sizeof(addr);
}

InetAddress::~InetAddress(){
}

2、socket的封装

之后对socket进行封装,首先看一下之前是如何创建scoket并应用scoket的

//创建
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//连接
connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr))
//绑定
bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr))
//监听
listen(sockfd, SOMAXCONN)
//无阻塞
setnonblocking(sockfd)
//接收客户端,并为客户端创建socket
accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len)

上述是对于socket的几种必要方法,那么明确socket类需要哪些变量和函数
首先是socket变量即一个int值,两个构造函数(一个创建服务器,一个创建客户端),析构函数,绑定方法(需要地址类)、监听方法、无阻塞方法、接收客户端方法以及一个获取socket值的方法。
来看声明:

class InetAddress;
class Socket
{
private:
    int fd;
public:
    Socket();
    Socket(int);
    ~Socket();

    void bind(InetAddress*);
    void listen();
    void setnonblocking();

    int accept(InetAddress*);

    int getFd();
};

之后是定义,无参构造函数和有参构造函数一眼看出来都是为什么服务的,析构函数顺手看一眼

Socket::Socket() : fd(-1){
    fd = socket(AF_INET, SOCK_STREAM, 0);
    errif(fd == -1, "socket create error");
}
Socket::Socket(int _fd) : fd(_fd){
    errif(fd == -1, "socket create error");
}
Socket::~Socket(){
    if(fd != -1){
        close(fd);
        fd = -1;
    }
}

之后是绑定方法的定义,主要是bind的使用,加::的原因是在使用全局变量和函数

void Socket::bind(InetAddress *addr){
    errif(::bind(fd, (sockaddr*)&addr->addr, addr->addr_len) == -1, "socket bind error");
}

无阻塞方法,没啥新东西

void Socket::setnonblocking(){
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

接收客户端,并为客户端创建socket,创建客户端的scoket了,一定要加返回值接住,然后获取个目前类对象的scoket。

int Socket::accept(InetAddress *addr){
    int clnt_sockfd = ::accept(fd, (sockaddr*)&addr->addr, &addr->addr_len);
    errif(clnt_sockfd == -1, "socket accept error");
    return clnt_sockfd;
}
int Socket::getFd(){
    return fd;
}

3、并发epoll

回忆一下创建epoll我们都做了什么事情

//创建红黑树
int epfd = epoll_create1(0);
//创建事件表,创建当前要加入树的端口
struct epoll_event events[MAX_EVENTS], ev;
//添加ev到epoll
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
//等待事件
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);

请开始思考,怎么构建这个类,要哪些变量及方法
需要epoll的scoket、事件表,构造方法、析构方法、将端口加入红黑树的方法以及从红黑树上获取事件表的方法。
那么接下来看看都是如何实现的
构造函数,注意这里一定要先初始化再进行赋值,不然会导致正常运行异常输出。析构函数看一下吧。

Epoll::Epoll() : epfd(-1), events(nullptr){
    epfd = epoll_create1(0);
    errif(epfd == -1, "epoll create error");
    events = new epoll_event[MAX_EVENTS];
    bzero(events, sizeof(*events) * MAX_EVENTS);
}
Epoll::~Epoll(){
    if(epfd != -1){
        close(epfd);
        epfd = -1;
    }
    delete [] events;
}

将端口加入到红黑树上,这里就能很好地理解加入的实际是一个文件描述。

void Epoll::addFd(int fd, uint32_t op){
    struct epoll_event ev;
    bzero(&ev, sizeof(ev));
    ev.data.fd = fd;
    ev.events = op;
    errif(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1, "epoll add event error");
}

这里其实很繁琐的,在这里进行了一遍轮询,之后在服务器中还需要再一遍轮询,体量大的话并不好,之后会优化。

std::vector<epoll_event> Epoll::poll(int timeout){
    std::vector<epoll_event> activeEvents;
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
    errif(nfds == -1, "epoll wait error");
    for(int i = 0; i < nfds; ++i){
        activeEvents.push_back(events[i]);
    }
    return activeEvents;
}

4、服务器

创建socket、创建地址、绑定服务器、监听socket、创建红黑树、无阻塞及添加事件到红黑树一气呵成。

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();
ep->addFd(serv_sock->getFd(), EPOLLIN | EPOLLET);

之后到了服务器对红黑树上的socket进行监听获取的过程了

while(true){
	std::vector<epoll_event> events = ep->poll();
	int nfds = events.size();
	for(int i = 0; i < nfds; ++i){
		if(events[i].data.fd == 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();
			ep->addFd(clnt_sock->getFd(), EPOLLIN | EPOLLET);
		} else if(events[i].events & EPOLLIN){      //可读事件
			handleReadEvent(events[i].data.fd);
		} else{         //其他事件,之后的版本实现
			printf("something else happened\n");
		}
	}
}

这里首先利用pool获取事件容器,之后轮询事件,如果遇到新客户端接入的事件,则创建地址创建socket、无阻塞并加入红黑树,如果是可读事件,则利用handleReadEvent进行处理。
到这里再加一个很熟悉的handleReadEvent就算结束了

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;
        }
    }
}
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值