Experimental C++ network library based on io_uring and coroutine. This project is developed for understanding and practice C++20 new features(ranges, coroutine) and new Linux I/O API io_uring.
- Header only
- Based on io_uring, the new Linux asynchronous I/O API
- Based on C++20 coroutine
file_descriptoris designed in the CRTP Mixin pattern, all operations(recv, send, listen...) are modularized and thus optional.buffer_sequenceis designed to be compatible with C++20std::span- like C++17
<filesystem>, support error handling in bothstd::error_codeand exception - coroutine part is based on Lewis Baker's cppcoro, modified so that it could compile using gcc10.
- Efficient:
- Few dynamic allocation: most of the allocations are for the coroutine stack itself.
- Few system calls compared with epoll based libraries: All asynchronous operations are submitted and reaped through io_uring by a single system call.
- Compiler: GCC10.0.1 (or later)
- Linux Kernel: 5.2 (or later)
- Liburing: 0.5(or later)
- OpenSSL: for the SHA-1 in websocket(will be removed later)
- RFC864 Character Generator Protocol: chargen.cpp
- RFC867 Daytime Protocol: daytime.cpp
- RFC863 Discard Protocol: discard.cpp
- RFC862 Echo Protocol: echo.cpp
- RFC868 Time Protocol: time.cpp
- boost::asio ping-pong performance test: pingpong
- ttcp: ttcp
- boost::asio chatroom: chat
- websocket discard: websocket_discard.cpp
- websocket echo: websocket_echo.cpp
- boost::beast websocket chatroom: websocket_chat.cpp
-
file_descriptortemplate <template <typename...> typename ... Modules> class file_descriptor<detail::module_list<Modules ...>> : public file_descriptor_base , public Modules<file_descriptor<detail::module_list<Modules ...>>>... {};
file_descriptoris a handler that has unique ownership of file descriptor, likestd::unique_ptr.- move-only,
- RAII.
::close()is called in its destructor if the underlying file descriptor is valid. - Modularized:
Modulesare interfaces that are mixin'ed into the base class.Modulesaccessfile_descriptorby static polymorphism, i.e. castthisinto the base class type. - default constructor will initialize the underlying file descriptor to be an invalid one.
- Mixin'ed modules use
set()andget()to work with the file_descriptor.
-
socket_tusing socket_t = file_descriptor < detail::module_list < socket_init, address, operation_shutdown, operation_set_options, operation_bind, operation_listen, operation_accept, operation_connect, operation_send, operation_recv, operation_close > >;
convenient type alias that has all modules for socket. You can define you own type alias. For example, an acceptor does not need to do send or recv.
using acceptor_t = file_descriptor < detail::module_list < socket_init, operation_set_options, operation_bind, operation_listen, operation_accept > >;
-
buffer_sequenceandconst_buffer_sequencebuffer_sequencetakes a sequences of Containers which satisfy the conceptstd::ranges::contiguous_rangethen transform them into iovec.buffer_sequencecould be constructed using:template<typename... Containers> buffer_sequence(Containers&&... containers)
where
Containerscould bestd::span<std::byte, size or std::dynamic_extent>std::span<Ts, size or std::dynamic_extent>- contiguous ranges that are writable
or
template<typename BufferRange> requires std::ranges::viewable_range<BufferRange&> && std::ranges::contiguous_range<std::ranges::range_value_t<BufferRange>> buffer_sequence(BufferRange& buffer_range)
where
BufferRangeis a range of contiguous ranges. Notice that, different from the former one,iovecs are stored in astd::vectorwhere dynamic allocation is inevitable. -
Modules
All modules support error handling by
-
std::error_code: by passing an lvalue reference ofstd::error_code(like C++17<filesystem>), should there be an error, the lvalue reference passed will assign with a new error_code, otherwise, it will beclear(). -
exception. Anstd::system_errorconstructed by the reason encapsulated in astd::error_codewill be throwed if there is an error. -
init(),init(std::erro_code& error)create a IPv4/TCP socket and set thefile_descriptorwith the file_descriptor returned form::socket().
-
the size of
file_descriptorwill increase if this module is used.Getters and setters are provided:
set_local_address(const socket_address& address),set_peer_address(const socket_address& address),get_local_address(),get_peer_address(). Moudles likeoperation_accept,operation_connect, will set theaddresson success.
synchronous operation modules
-
shutdown(int how = SHUTWR),shutdown(int how, std::error_code& error),shutdown(std::error_code& error) -
bind the socket to a given address. If module
addressis used, set the local address on success. Ifbindwithsocket_address{0}, then a random port will be assigned and the local address will also be set on success.bind(const socket_address& address),bind(const socket_address& address, std::error_code& error) -
call
::setsockopt.void setsockopt(int level, int optname, const void* optval, socklen_t optlen, std::error_code& error),void setsockopt(int level, int optname, const void* optval, socklen_t optlen),void reuse_address(std::error_code& error),void reuse_address() -
put the socket into listen state
listen(int backlog = SOMAXCONN),listen(int backlog, std::error_code& error),listen(std::error_code& error).
asynchronous operation modules
all asynchronous operations provides an optional argument
durationto impose a timeout.template<typename F2, typename... Args> [[nodiscard]] decltype(auto) accept(F2& peer_socket, Args&&... args) noexcept
-
peer_socketThe socket into which the new connection will be accpeted.-
Args is void: The awaiter returned by the function will throw exception to report the error. A std::system_error constructed with the corresponding std::error_code will be throwed if the accept operation is failed after being co_await'ed.
-
Args is a Duration, i.e. std::chrono::duration<Rep, Period>. This duration will be treated as the timeout for the operation. If the operation does not finish within the given duration, the operation will be canneled and an error_code (std::errc::operation_canceled) will be returned.
-
Args is an lvalue reference of a std::error_code The awaiter returned by the function will use std::error_code to report the error. Should there be an error in the operation, the std::error_code passed by lvalue reference will be reset. Otherwise, it will be clear.
-
Args are first a Duration, second an lvalue reference of a std::error_code The operation will have the features described in 2 and 3.
-
template<typename... Args> [[nodiscard]] decltype(auto) connect(const socket_address& address, Args&&... args) noexcept
address: the address with which the connection will be established.args: same asargsdesribed inoperation_acceptThis operation will read from the socket until it reads a 0(eof). Then it will call close(2) on the socket.
template<typename... Args> [[nodiscard]] decltype(auto) close(Args&&... args) noexcept
args: same asargsdesribed inoperation_accept-
recv([std::error_code& error], [Duration&& duration], Args&&... args)whereargswill be forwarded to the constructor ofbuffer_sequence.- The buffers will be filled with the same order as the order they were in the lvalue reference of a viewable range or the order they were in args.
- The operation will finish if there is an error(including the socket reads EOF) or all the buffers are filled. That is, it may use recvmsg(2) more than once.
- After the operation is co_await'ed and then finishes, it will return the bytes transferred.
recv_some([std::error_code& error], [Duration&& duration], Args&&... args)whereargswill be forwarded to the constructor ofbuffer_sequence.this awaiter will resume the coroutine after the first recv operation finished, regardless of whether the buffers are filled up or not.
-
recv([std::error_code& error], [Duration&& duration], Args&&... args)whereargswill be forwarded to the constructor ofconst_buffer_sequence
-