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_descriptor
is designed in the CRTP Mixin pattern, all operations(recv, send, listen...) are modularized and thus optional.buffer_sequence
is designed to be compatible with C++20std::span
- like C++17
<filesystem>
, support error handling in bothstd::error_code
and 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_descriptor
template <template <typename...> typename ... Modules> class file_descriptor<detail::module_list<Modules ...>> : public file_descriptor_base , public Modules<file_descriptor<detail::module_list<Modules ...>>>... {};
file_descriptor
is 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:
Modules
are interfaces that are mixin'ed into the base class.Modules
accessfile_descriptor
by static polymorphism, i.e. castthis
into 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_t
using 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_sequence
andconst_buffer_sequence
buffer_sequence
takes a sequences of Containers which satisfy the conceptstd::ranges::contiguous_range
then transform them into iovec.buffer_sequence
could be constructed using:template<typename... Containers> buffer_sequence(Containers&&... containers)
where
Containers
could 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
BufferRange
is a range of contiguous ranges. Notice that, different from the former one,iovec
s are stored in astd::vector
where 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_error
constructed by the reason encapsulated in astd::error_code
will be throwed if there is an error. -
init()
,init(std::erro_code& error)
create a IPv4/TCP socket and set thefile_descriptor
with the file_descriptor returned form::socket()
.
-
the size of
file_descriptor
will 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 theaddress
on 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
address
is used, set the local address on success. Ifbind
withsocket_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
duration
to impose a timeout.template<typename F2, typename... Args> [[nodiscard]] decltype(auto) accept(F2& peer_socket, Args&&... args) noexcept
-
peer_socket
The 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 asargs
desribed inoperation_accept
This 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 asargs
desribed inoperation_accept
-
recv([std::error_code& error], [Duration&& duration], Args&&... args)
whereargs
will 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)
whereargs
will 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)
whereargs
will be forwarded to the constructor ofconst_buffer_sequence
-