nukeexplode / co_uring_web

使用c++20协程的web server,网络部分使用了io_uring和epoll两种实现

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

co_uring_web

这个项目是把C++20协程与io_uring/epoll结合的一个web server。 主要内容包括:

  • C++20协程调度器的实现
  • 使用io_uring--linux上的新型高性能异步io接口
  • 使用epoll,并封装成proactor的形式
  • 一个简易的单生产者环形无锁队列用来传递accept的连接
  • 定时器队列的实现,支持对数复杂度的添加,移除,和提取过期定时器
  • Json解析器的实现,能解析和导出Json
  • 多线程日志模块,支持日志滚动(使用dup2系统调用),格式化打印(支持函数名,行号等)
  • http报文解析和导出
  • 一个静态的http web server实现
  • 杂项
    • 尝试SFINAE来定义调度器接口
    • 使用编译期字符串哈希来进行字符串的switch

C++20协程

对于网络编程来说,逻辑如果能写成

co_await asyncWrite(xxx,timeout);
co_await asyncRead(xxx,timeout);
co_await asyncConnect(xxx,timeout);

那确实非常直观。 以往的写法都是写成io操作完成之后运行的回调函数。逻辑比较割裂,而且还要维护状态机。 C++20的协程内容比较复杂。基本上对一个协程的每一个生命周期变化的点都能进行控制。 但是总的来说,定义与网络编程相关的协程的运行逻辑大致是

//定义一个协程函数
Task coroutineFunc(args){
    auto result=co_await AsyncWrite{...}; //1

    ...
    co_return ;                   //2
}
//运行时就是
{
    Task coroutine1= coroutineFunc(args);//程序会自动调用new在堆上分配一个coroutine1,
    //然后直接进入coroutine1一直运行到1处,coroutine1挂起,
    //然后返回当前函数作用域运行do_something
    do_something...//
    ...
    //然后等到coroutine1 等待的事情完成了,
    //需要手动恢复运行coroutine1
    auto handle=xxx;//前面通过某种方式获取到的coroutine1的句柄
    handle.resume();//继续运行coroutine1,获取result,
    //然后运行到2处,运行完毕,退出协程,
    //此时coroutine1在堆上的内容会被自动回收。
    
}

就是定义这么一个函数, 首先,需要定义一个task类型,task类型里还需要定义promise类型。promise类型需要定义几个固定的成员函数,get_return_object,initial_suspend,final_suspend,unhandled_exception,比如说我写的HttpTask

struct HttpTask {
	struct promise_type {
		HttpTask get_return_object() {
			return std::experimental::coroutine_handle<promise_type>::from_promise(*this);
		}
        // initial_suspend函数,定义是否一进入协程就直接挂起
		auto initial_suspend() noexcept { 
            //返回suspend_never就是不要挂起
            return std::experimental::suspend_never {};
        }             
        // final_suspend函数,定义是否在协程结束之前挂起
		auto final_suspend() noexcept { 
            //返回suspend_never就是不要挂起
            return std::experimental::suspend_never {}; 
        }
        //协程里抛出了没有捕获的异常怎么处理
		void unhandled_exception() {};
        //定义了return_void就说明协程没有返回值
		void return_void() {};
	};
    //这个handle就是用来恢复协程运行的句柄
	std::experimental::coroutine_handle<promise_type> handle;
	HttpTask(std::experimental::coroutine_handle<promise_type> h) : handle(h) {};
};

以上就是定义协程的大致思路。 接下来是相关的其他的基础设施。awaitable可等待体。一元运算符 co_await 暂停协程并将控制返回给调用方。其操作数是一个表达式,其类型必须要么定义 operator co_await,要么能以当前协程的 Promise::await_transform 转换到这种类型。项目里的AsyncWrite就是后者。

struct AsyncWrite {
		
    /*await_ready如果返回true,那就意味着 
    co_await AsyncWrite{};表达式立即返回,不会让协程挂起,
    而是继续运行。
    当使用epoll时这里其实可以优化一下,非阻塞fd可以直接尝试写,如果恰好一次
    性就写完,就直接返回true,协程继续运行,就没必要再往epoll注册事件
    */
	inline bool await_ready() noexcept { return false; }
	inline void await_suspend(std::experimental::coroutine_handle<typename Task::promise_type> h) {
		//在这里可以往epoll注册监听事件,或者提交io_uring的读写请求
	}
        
    //co_await AsyncWrite{};异步操作完成,获取这个表达式的返回值,在调用 handle.resume()之后运行。
	inline IoRequest *await_resume() { return req; }
	};

About

使用c++20协程的web server,网络部分使用了io_uring和epoll两种实现


Languages

Language:C++ 99.0%Language:CMake 1.0%