【Linux网络编程】IO多种转接之Reactor

Reactor

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

基于上一篇epoll的学习,现在我们也知道epoll的工作模式有两种,一种默认LT工作模式,另一种是ET模式。关于epoll的LT工作模式我们已经写过了。接下来我们写一份基于ET模式下的Reator,处理所有的IO。

Reactor = 如何正确的处理IO+协议定制+业务逻辑处理

下面我们写一个简洁版的Reactor,它是一个半同步半异步IO,具体它什么原理,怎么做的,有什么特征。我们在代码层面上解开它的面纱。代码写完总结就理解了。其实Reactor是在Liunx网络中,最常用,最频繁的一种网络IO设计模式!

我们是这样打算的,对错误码,日志函数,套接字,epoll做封装然后在写服务器的时候用到的时候调用即可。错误码,日志函数,套接字以前我们封装过今天直接用就行了。

错误码封装

#pragma once

enum
{
   
    USAGG_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    EPOLL_CREATE_ERR
};

日志函数封装

#pragma once

#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>

#define DEBUG  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"

const char* level_to_string(int level)
{
   
    switch(level)
    {
   
        case DEBUG: return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
    }
}

//时间戳变成时间
char* timeChange()
{
   
    time_t now=time(nullptr);
    struct tm* local_time;
    local_time=localtime(&now);

    static char time_str[1024];

    snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\
                    local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \
                    local_time->tm_min, local_time->tm_sec);

    return time_str;
}



void logMessage(int level,const char* format,...)
{
   
    //[日志等级] [时间戳/时间] [pid] [message]
    //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024
    //获取时间
    char* nowtime=timeChange();
    char logprefix[NUM];
    snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());

    //
    char logconten[NUM];
    va_list arg;
    va_start(arg,format);
    vsnprintf(logconten,sizeof logconten,format,arg);

    
    std::cout<<logprefix<<logconten<<std::endl;  
};

今天这里我们把套接字封装改一下,以前把它相关接口都写成静态的了,不需要对象直接调用了。今天呢,都改成非静态的,未来这个类提供的方法都要以对象调用访问。

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Err.hpp"
#include "Log.hpp"

using namespace std;

class Sock
{
   
    const static int backlog = 32;
    const static int defaultsock = -1;

public:
    Sock(int sock = defaultsock) : _listensock(sock)
    {
   }

    ~Sock()
    {
   
        if (_listensock != defaultsock)
            close(_listensock);
    }

public:
    int sock()
    {
   
        // 1. 创建socket文件套接字对象
        _listensock= socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
   
            logMessage(FATAL, "create socket error");
            exit(SOCKET_ERR);
        }
        logMessage(NORMAL, "create socket success: %d", _listensock);


        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    }

	//方便外面获取_listensock
    int Fd()
    {
   
        return _listensock;
    }

    void Bind(int port)
    {
   
        // 2. bind绑定自己的网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
   
            logMessage(FATAL, "bind socket error");
            exit(BIND_ERR);
        }
        logMessage(NORMAL, "bind socket success");
    }

    void Listen()
    {
   
        // 3. 设置socket 为监听状态
        if (listen(_listensock, backlog) < 0)
        {
   
            logMessage(FATAL, "listen socket error");
            exit(LISTEN_ERR);
        }
        logMessage(NORMAL, "listen socket success");
    }

    int Accept(string *clientip, uint16_t *clientport,int* err)
    {
   
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
        *err=errno;
        if (sock < 0){
   }
            //logMessage(ERROR, "accept error, next");
        else
        {
   
            //logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }
        return sock;
    }

private:
    int _listensock;
};

对于一个epoll来说,首先要先给epoll创建出来,然后epoll的接口都要用到epoll创建成功的返回值。所以直接在这个epoll类中定义个成员变量直接就把创建epoll成功的返回值拿到。然后服务器不需要直接拿着这个返回值,而直接调用这个类对象使用里面的提供的接口就行了。epoll这里先写一个大体框架,后面需要什么了再加

#pragma once

#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>
#include "Err.hpp"
#include "Log.hpp"

using namespace std;

const int defaultepfd = -1;
const int size = 128;

class Epoller
{
   

public:
    Epoller(int epfd = defaultepfd) : _epfd(epfd)
    {
   
    }

    ~Epoller()
    {
   
        if (_epfd != defaultepfd)
            close(_epfd);
    }

public:
   
private:
    int _epfd;//创建epoll返回值
};

目前调用逻辑,后面在加内容

#include "TcpServer.hpp"
#include "Err.hpp"
#include <memory>

static void usage(std::string proc)
{
   
    std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}

int main(int argc, char *argv[])
{
   
    if (argc != 2)
    {
   
        usage(argv[0]);
        exit(USAGG_ERR);
    }

    uint16_t port=atoi(argv[1]);
    std::unique_ptr<TcpServer> uls(new TcpServer(port));
    uls->initServer();
    uls->Dispatch();

    return 0;
}

服务器大体框架

#pragma once

#include <iostream>
#include "Sock.hpp"
#include "Err.hpp"
#include "Log.hpp"
#include "Epoller.hpp"

const int defaultport = 8080;

class TcpServer
{
   

public:
    TcpServer(int port = defaultport) : _port(port)
    {
   
    }

    ~TcpServer()
    {
   
    }

    void initServer()
    {
     
    }

    void Dispatch()//事件派发
    {
   
    }
public:


private:
    uint16_t _port;
    Sock _sock;
    Epoller _epoll;
};

接下来就是编写服务器了

初始化服务器

void initServer()
{
     
    // 1.创建套接字
    _sock.sock();
    _sock.Bind(_port);
    _sock.Listen();

    // 2.创建epoll模型
}

创建套接字之后,我们要在创建一个epoll模型,因此我们在Epoller写一个创建epoll模型的接口,然后服务器直接调用就行了

```cpp
void Create()
{
   
    _epfd = epoll_create(size);
    if (_epfd < 0)
    {
   
        logMessage(FATAL, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));
        exit(EPOLL_CREATE_ERR);
    }
    logMessage(NORMAL, "epoll create success, epfd: %d", _epfd);
}
void initServer()
{
     
    // 1.创建套接字
    _sock.sock();
    _sock.Bind(_port);
    _sock.Listen();

    // 2.创建epoll模型
    _epoll.Create();
    
	// 3.将目前唯一的一个sock,添加到epoll中
}

创建好套接字,epoll模型接下来我们先把_listensock套接字添加到epoll里,看看这样写有没有什么问题

不过先在Epoller类中补充一个用户告诉内核你要帮我关心对应fd的什么事件。

// user -> kernel
bool AddEvents(int sock, uint32_t event)
{
   
    struct epoll_event ev;
    ev.events = event;
    ev.data.fd = sock;
    int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
    if (n < 0)
    {
   
        logMessage(ERROR, "sock join epoll fail");
        return false;
    }
    return true;
}

写好后我们直接调用就好了

void initServer()
{
     
    // 1.创建套接字
    _sock.sock();
    _sock.Bind(_port);
    _sock.Listen();

    // 2.创建epoll模型
    _epoll.Create();

    // 3.将目前唯一的一个sock,添加到epoll中
    _epoll.AddEvents(_sock.Fd(),EPOLLIN | EPOLLET);
}

但是现在有小一个问题,前面说过的,如果未来你的套接字工作模式是ET模式,那么该套接字必须处于非阻塞,_listensock套接字创建处理默认就是阻塞的,因此我们需要将文件描述符设为非阻塞

我们在高级IO的非阻塞IO哪里就已经写过一个将一个fd变成非阻塞了,现在拿过来用就行了,不过我们也还是把它封装起来。然后调用。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

class Util
{
   
public:
    static bool SetNonBlock(int fd)
    {
   
        int fl = fcntl(fd, F_GETFL);
        if (fl < 0)
            return false;
        fcntl(fd, F_SETFL, fl | O_NONBLOCK);
        return true;
    }
};

到目前为止好像貌似没问题了,但是真的吗?

void initServer()
{
     
    // 1.创建套接字
    _sock.sock();
    _sock.Bind(_port);
    _sock.Listen();

    // 2.创建epoll模型
    _epoll.Create();

    // 3. 将目前唯一的一个sock,添加到epoller中, 之前需要先将对应的fd设置成为非阻塞
    Util::SetNonBlock(_sock.Fd());
    _epoll.AddEvents(_sock.Fd(),EPOLLIN | EPOLLET);
}

我们回过头看看之前写过的epoll服务器的代码,这里处理普通sock就绪事件的问题,你怎么保证你把本轮收到的数据都读完?即使把本轮数据都读完了,就一定能够读到一个完整的请求吗?即使未来在这里循环读取,可以反正我们非阻塞也都写过。那就能保证读到一个完整请求吗?
不一定!

那没读到完整的请求我们是不是只能把本轮读到的数据暂时保存到buffer里,可是暂时保存到buffer里,你保存了,那别人怎么办? 你这个sock和其他sock说你们别着急先别覆盖我的数据,buffer里面保存的是我的数据,你们先不要写。你想多了!

你读完之后整个这个代码区间就全部释放掉了,因为这是栈上面的空间。所以即便你没读完,或者你把这轮读完了。那么下轮在读,buffer早就释放了。你可能本轮读到后半部分但前半部分已经没有了,可能不到下次,下一个循环进来就给你清空了。

在这里插入图片描述
所以怎么样进行正确读写,光有一个套接字和定义一个栈上的缓存区远远不够!

所以我们需要给每一个文件描述符,都要把输入,输出用户层缓存区都要给它带上!

现在我们清楚知道历史代码的问题,我们要正确写整个服务,现在已经不够了。我们需要将每一个套接字进行封装,每一个套接字都要包含自己对应的输入,输出缓存区空间,只有每一个套接字都有自己对应的缓存空间,读取的时候把数据读取赞存在自己对应的缓存区中,没读完下次在读,这时候在添加到我自己的缓存区里,不就行了吗,这个时候谁和谁都不揉在一起。所以我们在封装一个类! 也就是说我认为未来每一个套接字都看类对象。

class Connection
{
   
public:
    Connection(int sock = defaultport) : sock_(sock)
    {
   
    }

    ~Connection()
    {
   
        if (sock_ == defaultsock)
            close(sock_);
    }

public:
    int sock_;//这个类对应的套接字是谁
    string inbuffer_;//输入缓存区,这里我们暂时没有考虑处理图片,视频等二进制信息,不然stirng就不太合适了                  
评论 59
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值