指路:

B站:毛毛and西西 视频 : c++携程库,第2部分
协程框架已开源到github和gitee上
gitee地址:https://gitee.com/meiqizhang/xfiber
github地址:https://github.com/meiqizhang/xfiber.git

main.cpp


#include "xfiber.h"
#include <iostream>
#include <unistd.h>
#include "xsocket.h"

int main()
{
    XFiber xfiber;
    Fiber * fiber = new Fiber([](){},&xfiber);
    xfiber.CreateFiber([](){
        std::cout << "hellow World" << std::endl;
    });
    xfiber.CreateFiber([&xfiber](){
        std::cout << "how are you" << std::endl;
        // 让出CPU
        xfiber.Yiled();
        std::cout << "how are you" << std::endl;
    });
    xfiber.CreateFiber([&](){
       Listener listener;
       listener.ListenTCP(8888);
       while (true) {
           int client_fd = listener.Accept();
           if (client_fd < 0) {
               continue;
           }
           xfiber.CreateFiber(
               [&](){
                   char buf[32];
                   int n = read(client_fd,buf,32);
                   printf("recv : %s \n",buf);
                   write(client_fd,buf,n);
               }
           );
       }
    });
    xfiber.Dispatch();
    return 0;
}

xsocket.cpp


#include "xsocket.h"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <inttypes.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <error.h>

void Listener::ListenTCP(uint16_t port)
{
    //创建TCP套接字
    //函数说明: int socket(int domain, int type, int protocol)
    /*    1. domain:协议版本:
                    1. AF_INET :IPV4
                    2. AF_INET6: IPV6
                    3. AF_UNIX | AF_LOCAL  :本地套接字使用
        2.type:协议类型
                1.SOCK_STREAM 流式套接字 : 默认是TCP
                2.SOCK_DGRAM  报式套接字 : 默认是UDP
        3. protocal:一般填0, 表示使用对应类型的默认协议
        4. 返回值 :
                1. 成功:  返回一个大于0的文件描述符
                2. 失败: 返回-1, 并设置errno
        5. 当调用socket函数以后, 返回一个文件描述符,
             内核会提供 与 该文件描述符相对应的
             读和写缓冲区, 
             同时还有两个队列, 分别是请求连接队列和已连接队列
    
    */
    this->fd_ = socket(AF_INET,SOCK_STREAM,0);
    if (this->fd_ < 0) {
        perror("socket");
        exit(0);
    }
    /*
        fcntl系统调用 : 可以用来对已打开的文件描述符进行各种控制操作
                        来改变已打开文件的的各种属性
        原型: int fcntl(int fd, int cmd ,struct flock* lock);
        参数: 
                1. fd: 被打开的文件的文件描述符
                2. cmd:
                        ⚫ 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC );
                        ⚫ 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD );
                        ⚫ 获取/设置文件状态标志( cmd=F_GETFL 或 cmd=F_SETFL );
                        ⚫ 获取/设置异步 IO 所有权( cmd=F_GETOWN 或 cmd=F_SETOWN );
                        ⚫ 获取/设置记录锁( cmd=F_GETLK 或 cmd=F_SETLK );
                3. 设置为阻塞属性
    */

    // F_SETFL  设置给arg描述符状态标志,可以更改的几个标志是: O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。
    // 暨 设置该文件描述符为阻塞数学
    if (fcntl(fd_,F_SETFL,O_NONBLOCK) < 0) {
        perror("fcntl");
        exit(0);
    }
    /*
        //一半不用这个
        struct sockaddr {
            unsigned  short  sa_family;     address family, AF_xxx 
            char  sa_data[14];  14 bytes of protocol address 
        }; 
//sockaddr_in : 可以和sockaddr相互转换
//他俩大小一样,这个是ipv4版本
struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};
//ipv6版本
struct sockaddr_in6 {
sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
in_port_t sin6_port;  //(2)16位端口号
uint32_t sin6_flowinfo;  //(4)IPv6流信息
struct in6_addr sin6_addr;  //(4)具体的IPv6地址
uint32_t sin6_scope_id;  //(4)接口范围ID
};

    */
   //初始化一个套接字
    struct sockaddr_in addr;

    //套接字监听 端口
    /*  
uint32_t htonl(uint32_t hostlong); 本机 字节序 转换为 网络字节序 大端
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
    */
    addr.sin_port = htons(port);
    //
//INADDR_ANY: 表示使用本机任意有效的可用IP
    /*
    函数说明: 将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    网络IP转换为字符串形式的点分十进制的IP
    int inet_pton(int af, const char *src, void *dst);
    */
    addr.sin_addr.s_addr = INADDR_ANY;
    //表示使用 ipv4协议
    addr.sin_family = AF_INET;

/*
函数描述: 将socket文件描述符和IP,PORT绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
    socket: socket套接字的描述符
    addr: socketaddr结构,包含了 IP地址 + 端口
    addrlen : addr结构的长度 
返回值: 
    成功: 返回0
    失败: 返回-1, 并设置errno
*/
    if (bind(fd_,(const struct sockaddr *)(&addr),sizeof(addr))) {
        perror("bind");
        exit(0);
    }
/*
函数描述: 将套接字由主动态变为被动态, 暨 转换文侦听套接字
int listen(int sockfd, int backlog);
参数说明:
    sockfd: 调用socket函数返回的文件描述符,
    backlog: 未连接队列 的队列长度 
返回值:
    成功: 返回0
    失败: 返回-1, 并设置errno      
*/
    if (listen(fd_,32) < 0) {
            perror("listen");
            exit(0);
    }
    printf("listen %d success \n",port);
}
int Listener:: Accept()
{
    while (true) {
/*
函数说明:获得一个连接, 若当前没有连接则会阻塞等待.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);    
函数参数:
    sockfd: 调用socket函数返回的文件描述符,一般也就是从侦听套接字哪里接受
    addr: 传出参数, 保存客户端的地址信息
    addrlen: 传入传出参数,  addr变量所占内存空间大小
返回值:
    成功: 返回一个新的文件描述符,用于和客户端通信
    失败: 返回-1, 并设置errno值.


accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接,
并获得一个新的文件描述符, 
该文件描述符用于和客户端通信.  
(内核会负责将请求队列中的连接拿到已连接队列中)
*/
        int client_fd = accept(fd_,nullptr,nullptr);
        if (client_fd > 0) {
            printf("accept  %d success \n",fd_);
            return client_fd;
        } else if (errno == EAGAIN) {
            // YIELD
        } else if (errno == EINTR) {
            continue;   
        } else {
            perror("accept");
            return -1;
        }
    }
    return -1;
/*
EAGAIN (Try again ) : 一句话: 没有数据可以接受
    在读数据的时候,没有数据在底层缓冲的时候会遇到.比如我们epoll使用边缘
触发的时候,是需要在每次事件轮回中处理所有的消息,因为不然就得等下一次事件轮回才能处理了,因此
我们基本用边缘跟非阻塞fd一起使用,然后通过判断返回值-1时,errno为EAGAIN来判断已经读完了

EWOULDBLOCK (Operation would block) : 有的系统是EWOULDBLOCK,而不是EAGAIN

EPIPE(Broken pipe) : 一句话: 接收端关闭,但是发送方还在发送导致破裂
    接收端关闭(缓冲中没有多余的数据),但是发送端还在write.

EINTR(Interrupted system call) : 一句话: 陷入了系统调用
    被其它的系统调用中断了, 对于fd进行操作比较容易出现,一般
裸用recv都是需要判断的, 处理也很简单, 再进行一次操作就可以了

ECONNRESET(Connection reset by peer) : 收到RST包可能是接收到数据后不进行读取或者没读取
完毕直接close,另一端再调用write或者read操作,另外使用了SO_LINGER设置发送RST直接断开连接后
close连接,另一端也会收到这个错误. 另外在epoll中一般也是可能返回EPOLLHUP事件。连接的时候也
可能出现这样的错误

ETIMEDOUT (Connection timed out) : 连接超时
ECONNREFUSED (Connection refused) : 拒绝连接, 一般在机器存在但是相应的端口上没有数据的
时候出现

ENETUNREACH (Network is unreachable) : 网络不可达,可能是由于路器的限制不能访问,需要
检查网络

EADDRNOTAVAIL (Cannot assign requested address) : 不能分配本地地址,一般在端口不够用
的时候会出现,很可能是短连接的TIME_WAIT问题造成

EADDRINUSE (Address already in use) : 地址已经被使用, 已经有相应的服务程序占用了这个
端口, 或者占用端口的程序退出了但没有设置端口复用

*/
}

xsocket.h


#pragma once
#include <inttypes.h>
class Listener 
{
    public:
        //是有SOCKET变成
        // TCP ,UDP ,UNIXT本地套接字
        void ListenTCP(uint16_t port);
        int Accept();
    private:
        int fd_;
};

xfiber.cpp


#include "xfiber.h" //先扫描当前路径下寻找,然后再到系统库下面
#include <iostream>
XFiber::XFiber()
{
    //XFIBER调度器新建时,没有任何fiber类被调度
    this->cur_dispatch_fib_ = nullptr;//当前被调度的fiber对象(协程))
    this->ready_fibers_ = std::list<Fiber*>();//两个协程运行队列
    this->run_fibers_ = std:: list<Fiber*>();
    //XFIBER上下文信息爆出只能在这里
    //this->XFiber_ctx_ = 
}
XFiber::~XFiber()
{
}
void XFiber::CreateFiber(std::function<void()> run)
{
    Fiber * fiber = new Fiber(run,this);
    this->ready_fibers_.push_back(fiber);
    return ;
}
//获取 XFIBER调度器的当前上下文
ucontext_t * XFiber::Get_X_FiberCtx()
{
    return &this->XFiber_ctx_;
}
//XFIBER调度器的调度逻辑
void XFiber::Dispatch()
{
    //一个死循环,一直调用
    while(true) {
        if (this->ready_fibers_.empty()) {
            //如果就绪队列为空, 就没有协程可以调度
            //在这里等着吧
            continue;
        } 
        //就绪队列不为空
        //std::move :利用拷贝构造
        //知识点: 完美转发, 万能引用 + 应用折叠
        this->run_fibers_ = std::move(this->ready_fibers_);
        this->ready_fibers_.clear();//记得要清理
        
        //现在需要对运行队列里面的协程进行调度
        //先来先服务
        for (auto iter = this->run_fibers_.begin();iter != this->run_fibers_.end();iter ++) {
            //遍历 运行队列,运行队列里面的每一个FIBER对象都是XFIBER调度器的调度对象
            //获取下一个需要调度的对象
            Fiber * fiber = * iter;
            // 该对象即将要上 xfiber调度器了,
            //提前保存该对象,考虑到调度途中 万一需要让出cpu(暨YILED)
            this->cur_dispatch_fib_ = fiber;
            //XFIBER调度器调度Fiber对象(暨协程)
                //如何调度? swapcontext来进行CPU上下文切换
            //解释:
            /*
                1.当前cpu上运行的是XFIBER调度类,
                    ==> 所以当前cpu上下文也是 XFIBER的上下文
                2.XFIBER即将调度fiber对象上cpu运行,
                    ==> 所以就需要保存 当前 XFIBER得 上下文信息
                    ==> 并且 使用 cur_distance_fiber 的上下文信息来进行运行
                3. 由于 cur_dispacth_fib_.fib_ctx.uc_link == xfiber.xfiber_ctx
                    ==> 所以 在 FIBER对象(被调度协程)运行完毕后,会回归到 xfiber(调度协程)的上下文
                    ==> 所以 ,会回之后 的下一条命令 就是 cur_dispatch_fib_ = nullptr
            */
            swapcontext(this->Get_X_FiberCtx(),cur_dispatch_fib_->Get_FiberCtx());
            //该调度对象(暨协程)已经彻底啊调度完成
                // ===> 为什么说调度完成了?
                //      因为他已经 从被调度的fiber对象的上下文中,再一次的返回到了 XFIBER的上下文
                //      浏览 FIBER中 ctx的初始化
                //      fiber->fiber_ctx_.uc_link = xfiber_ctx_
                //      任何一个 fiber对象(暨协程),在调度后,都必须回到XFIBER调度器,从而继续调度运行队列里面的后续协程
            // 该fiber对象调度完成,就就没啥用了,所以就不需要保存了
            this->cur_dispatch_fib_ = nullptr;
            if (fiber->IsFinished()) {
                //如果 被调用的fiber对象(暨被调用协程)已经运行完毕,则删除掉这个fiber
                delete fiber;
            }
            
        }
        //运行队列中所有的被调度FIBER(暨被调度协程)已经调度完成
        this->run_fibers_.clear();
    }
    return ;
}
//调用该函数的协程: 主动让出cpu
// 让出cpu的结果就是,会回到XFIBER调度器,有调度器,去选择下一个调度对象
void XFiber::Yiled()
{
    //主动调用 YILED函数是,说明 当前被调度的fiber类一定没有 运行完毕
    //所以 保存的cur_dispatch_fib_ 还不是null
    // 把没有运行完毕的 fiber对象,放入 就绪队列里面去
    this->ready_fibers_.push_back(this->cur_dispatch_fib_);
    //现在 被调用fiber对象(暨被调用的协程),需要主动返回到XFIBER调度器(暨:xfiber协程)
    //  解释:
    /*
        1. 由于当前 fiber并没有彻底完成 isfinished is no 
            ==> 所以需要保存,当前fiber的当前上下文信息
        2. 由于让出cpu之后,需要 让XFIBER继续调度
            ==> 所以需要返回到 xFIBER协程
                ==> 暨,切换到 XFIBER调度类的上下文信息
                    ==> XFIBER,在第一次进入高fiber调度类时, 上下文信息是保存在XFIBER.xifber_ctx_中的
                        所以我们在切换回来就可以了
        
    */
    swapcontext(cur_dispatch_fib_->Get_FiberCtx(),this->Get_X_FiberCtx());
    return ;
}
void Fiber::Fiber_Entryfunc_Start(Fiber * fiber)
{
    // 每一个fiber对象,第一次被调度时
    //都会进入该函数
    // ===> 因为 在swapcontext(xfiber.ctx,fiber.ctx)前
    //  ==> 都早早的  makecontext(fiber.ctx,func,1,fiber)了
    fiber->entry_func_();
    //入口函数调用完成,说明fiber调度完成
    fiber->status_ = -1;
    return ;
}
Fiber::Fiber(std::function<void()> entry_func_,XFiber * xfiber)
{
    //fiber对象(暨背调度的协程)的入口函数地址
    //  该入口函数地址是保存在 uc_mcontext中的:
    // uc_mcontext:保存所有上下文信息,寄存器信息,入口函数地址

    this->entry_func_ = entry_func_;
    // isFinished的判断条件
    this->status_ = 0;
    getcontext(&this->Fiber_ctx_);
    //有栈协程: 栈大小为 128kb
    this->stack_size_ = 1024 * 128;
    this->Fiber_ctx_.uc_stack.ss_size = this->stack_size_;
    //协程的栈起始位置
    this->stack_sp_ = new char[this->stack_size_];
    this->Fiber_ctx_.uc_stack.ss_sp = this->stack_sp_;
    //最最最最最最最最最最最最重要的一点
    // =*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    //      任何一个 fiber对象的 fiber_ctx_.uc_link 一定要是
    //          xfiber.xfiber_ctx
    //          一定要保成,每一个被调用协程的后续上下文是
    //          xfiber调度器的上下文
    //          这样 才能保证 每一次调用完毕,都会回到
    //          xfiber
    // =*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
    this->Fiber_ctx_.uc_link = xfiber->Get_X_FiberCtx();

    //需要注意,参数个数
    //makecontext(&this->Fiber_ctx_,(void(*)())Fiber::Fiber_Entryfunc_Start,1,this);
    makecontext(&this->Fiber_ctx_,(void(*)())Fiber::Fiber_Entryfunc_Start,1,this);
    return ;
}
Fiber:: ~Fiber()
{
    delete this->stack_sp_;
    stack_sp_ = nullptr;
    stack_size_ = 0;
}

//功能: 执行某一个 fiber的入口函数
//执行完毕,就需要改变状态
//该函数只是 一层包装,
// 调用该函数,在该函数内部 进行真正的 入口函数调用


bool Fiber::IsFinished()
{
    return status_ == -1;
}
ucontext_t * Fiber::Get_FiberCtx()
{
    return &Fiber_ctx_;
}

xfiber.h

#pragma once
#include <functional>
#include <list>
#include <ucontext.h>
#include <iostream>
class Fiber;
class XFiber {
    public:
        XFiber();
        ~XFiber();
        //Xfiber调度类,新建一个 fiber对象
        // 每一个fiber对象都是一个协程
        // run就是协程的 入口函数
        void CreateFiber(std::function<void()> run);
        //Xfiber调度类,通过该函数,
        //对 Fiber对象进行调度
        void Dispatch();
        //XFiber调度类,通过使用该函数
        //来是 正在调度的 Fiber对象,让出CPU
        //然后 回到 XFiber的调度上下文,继续进行调度
        void Yiled();
        //获取当前XFIBER调度类的调度上下文
        ucontext_t * Get_X_FiberCtx();
    private:
        //XFIBER调度器 正在调度的fiber对象(一个协程)
        Fiber * cur_dispatch_fib_;
        //XFIBER的调度上下文:
        //Xfiber也是一个协程,
        //在调度完成任何一个fiber对象后,都应该继续执行该XFIBER协程
        ucontext_t  XFiber_ctx_; 
        // 两个队列: 模仿 CPU调度
        std::list<Fiber *> ready_fibers_; //就绪队列 
        std::list<Fiber *> run_fibers_;  // 运行队列 
};
class Fiber {
    public:
        //构造函数: 参数1: 协程的入口函数,参数2:当前的XFIBER调度器
        Fiber( std::function<void()> run,XFiber * xfiber);
        ~Fiber();
        
        //为什么这个是静态函数?
        // 参数有什么意义?
        //解答: 1.参数,FIBER对象
        //      2.如果是成员函数,那么 makecontext时 并不是很好绑定(如果刚开始类没有实例化时,start函数为null)
        // 这样并不是很好,静态函数,可以保证任何时候这个函数都是存在的
        static void Fiber_Entryfunc_Start(Fiber * fiber);
        //获取当前FIBER对象的上下文信息
        ucontext_t * Get_FiberCtx();
        //判断 当前FIBER是否调度完成
        bool IsFinished();
    private:

        int status_;
        char * stack_sp_;
        size_t stack_size_;
        //该协程的入口函数 (makecontext会绑定协程的入口函数地址并且,进行上下文切换)
        std::function<void()> entry_func_;
        ucontext_t Fiber_ctx_;// 保存当前上下文信息
        // uc_link : ucp结构可以形成一个链表
        // uc_flage:
        // uc_stack: ucp结构堆栈信息:
            //uc_stack.ss_sp :栈顶指针
            //uc_stack.ss_size : 栈空间大小
            //uc_stack.ss_flage :
            //协程分类,有栈协程和无栈协程
        //uc_mcontext :存储当前上下文 ===>各种各样的寄存器信息
            //注意: get/set_context修改的都是 这里面的
            //      makecontext: 将后续上下文入口函数地址,也是保存在 这些寄存器信息里面的
        //uc_sigmask:喜好屏蔽掩码
};
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐