Skip to content

Commit 3eee7dc

Browse files
committed
feat: add acceptor class for TCP accept operations
- Add acceptor.hpp with affine awaitable accept() returning io_result - Implement win_acceptor_impl for Windows IOCP-based accept - Add listen(), close(), cancel() and is_open() methods - Support cancellation via stop_token through affine awaitable protocol - Refactor accept_op to use impl_out instead of transfer callback - Add friend declaration in socket for impl_ access during accept - Separate socket_list_ and acceptor_list_ in win_iocp_sockets - Add unit tests for construction, listen, and move semantics
1 parent 40ca463 commit 3eee7dc

6 files changed

Lines changed: 830 additions & 15 deletions

File tree

include/boost/corosio/acceptor.hpp

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
//
2+
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/corosio
8+
//
9+
10+
#ifndef BOOST_COROSIO_ACCEPTOR_HPP
11+
#define BOOST_COROSIO_ACCEPTOR_HPP
12+
13+
#include <boost/corosio/detail/config.hpp>
14+
#include <boost/corosio/detail/except.hpp>
15+
#include <boost/corosio/io_object.hpp>
16+
#include <boost/corosio/io_result.hpp>
17+
#include <boost/corosio/endpoint.hpp>
18+
#include <boost/corosio/socket.hpp>
19+
#include <boost/capy/any_dispatcher.hpp>
20+
#include <boost/capy/concept/affine_awaitable.hpp>
21+
#include <boost/capy/execution_context.hpp>
22+
#include <boost/capy/concept/executor.hpp>
23+
24+
#include <boost/system/error_code.hpp>
25+
26+
#include <cassert>
27+
#include <concepts>
28+
#include <coroutine>
29+
#include <cstddef>
30+
#include <stop_token>
31+
#include <type_traits>
32+
33+
namespace boost {
34+
namespace corosio {
35+
36+
/** An asynchronous TCP acceptor for coroutine I/O.
37+
38+
This class provides asynchronous TCP accept operations that return
39+
awaitable types. The acceptor binds to a local endpoint and listens
40+
for incoming connections.
41+
42+
Each accept operation participates in the affine awaitable protocol,
43+
ensuring coroutines resume on the correct executor.
44+
45+
@par Thread Safety
46+
Distinct objects: Safe.@n
47+
Shared objects: Unsafe. An acceptor must not have concurrent accept
48+
operations.
49+
50+
@par Example
51+
@code
52+
io_context ioc;
53+
acceptor acc(ioc);
54+
acc.listen(endpoint(8080)); // Bind to port 8080
55+
56+
socket peer(ioc);
57+
auto [ec] = co_await acc.accept(peer);
58+
if (!ec) {
59+
// peer is now a connected socket
60+
auto [ec2, n] = co_await peer.read_some(buf);
61+
}
62+
@endcode
63+
*/
64+
class acceptor : public io_object
65+
{
66+
struct accept_awaitable
67+
{
68+
acceptor& acc_;
69+
socket& peer_;
70+
std::stop_token token_;
71+
mutable system::error_code ec_;
72+
mutable io_object::io_object_impl* peer_impl_ = nullptr;
73+
74+
accept_awaitable(acceptor& acc, socket& peer) noexcept
75+
: acc_(acc)
76+
, peer_(peer)
77+
{
78+
}
79+
80+
bool await_ready() const noexcept
81+
{
82+
return token_.stop_requested();
83+
}
84+
85+
io_result<> await_resume() const noexcept
86+
{
87+
if (token_.stop_requested())
88+
return {make_error_code(system::errc::operation_canceled)};
89+
90+
// Transfer the accepted impl to the peer socket
91+
// (acceptor is a friend of socket, so we can access impl_)
92+
if (!ec_ && peer_impl_)
93+
{
94+
peer_.close();
95+
peer_.impl_ = peer_impl_;
96+
}
97+
return {ec_};
98+
}
99+
100+
template<capy::dispatcher Dispatcher>
101+
auto await_suspend(
102+
std::coroutine_handle<> h,
103+
Dispatcher const& d) -> std::coroutine_handle<>
104+
{
105+
acc_.get().accept(h, d, token_, &ec_, &peer_impl_);
106+
return std::noop_coroutine();
107+
}
108+
109+
template<capy::dispatcher Dispatcher>
110+
auto await_suspend(
111+
std::coroutine_handle<> h,
112+
Dispatcher const& d,
113+
std::stop_token token) -> std::coroutine_handle<>
114+
{
115+
token_ = std::move(token);
116+
acc_.get().accept(h, d, token_, &ec_, &peer_impl_);
117+
return std::noop_coroutine();
118+
}
119+
};
120+
121+
public:
122+
/** Destructor.
123+
124+
Closes the acceptor if open, cancelling any pending operations.
125+
*/
126+
BOOST_COROSIO_DECL
127+
~acceptor();
128+
129+
/** Construct an acceptor from an execution context.
130+
131+
@param ctx The execution context that will own this acceptor.
132+
*/
133+
BOOST_COROSIO_DECL
134+
explicit acceptor(capy::execution_context& ctx);
135+
136+
/** Construct an acceptor from an executor.
137+
138+
The acceptor is associated with the executor's context.
139+
140+
@param ex The executor whose context will own the acceptor.
141+
*/
142+
template<class Executor>
143+
requires (!std::same_as<std::remove_cvref_t<Executor>, acceptor>) &&
144+
capy::executor<Executor>
145+
explicit acceptor(Executor const& ex)
146+
: acceptor(ex.context())
147+
{
148+
}
149+
150+
/** Move constructor.
151+
152+
Transfers ownership of the acceptor resources.
153+
154+
@param other The acceptor to move from.
155+
*/
156+
acceptor(acceptor&& other) noexcept
157+
: ctx_(other.ctx_)
158+
{
159+
impl_ = other.impl_;
160+
other.impl_ = nullptr;
161+
}
162+
163+
/** Move assignment operator.
164+
165+
Closes any existing acceptor and transfers ownership.
166+
The source and destination must share the same execution context.
167+
168+
@param other The acceptor to move from.
169+
170+
@return Reference to this acceptor.
171+
172+
@throws std::logic_error if the acceptors have different execution contexts.
173+
*/
174+
acceptor& operator=(acceptor&& other)
175+
{
176+
if (this != &other)
177+
{
178+
if (ctx_ != other.ctx_)
179+
detail::throw_logic_error(
180+
"cannot move acceptor across execution contexts");
181+
close();
182+
impl_ = other.impl_;
183+
other.impl_ = nullptr;
184+
}
185+
return *this;
186+
}
187+
188+
acceptor(acceptor const&) = delete;
189+
acceptor& operator=(acceptor const&) = delete;
190+
191+
/** Open, bind, and listen on an endpoint.
192+
193+
Creates an IPv4 TCP socket, binds it to the specified endpoint,
194+
and begins listening for incoming connections. This must be
195+
called before initiating accept operations.
196+
197+
@param ep The local endpoint to bind to. Use `endpoint(port)` to
198+
bind to all interfaces on a specific port.
199+
200+
@param backlog The maximum length of the queue of pending
201+
connections. Defaults to a reasonable system value.
202+
203+
@throws std::system_error on failure.
204+
*/
205+
BOOST_COROSIO_DECL
206+
void listen(endpoint ep, int backlog = 128);
207+
208+
/** Close the acceptor.
209+
210+
Releases acceptor resources. Any pending operations complete
211+
with `errc::operation_canceled`.
212+
*/
213+
BOOST_COROSIO_DECL
214+
void close();
215+
216+
/** Check if the acceptor is listening.
217+
218+
@return `true` if the acceptor is open and listening.
219+
*/
220+
bool is_open() const noexcept
221+
{
222+
return impl_ != nullptr;
223+
}
224+
225+
/** Initiate an asynchronous accept operation.
226+
227+
Accepts an incoming connection and initializes the provided
228+
socket with the new connection. The acceptor must be listening
229+
before calling this function.
230+
231+
The operation supports cancellation via `std::stop_token` through
232+
the affine awaitable protocol. If the associated stop token is
233+
triggered, the operation completes immediately with
234+
`errc::operation_canceled`.
235+
236+
@param peer The socket to receive the accepted connection. Any
237+
existing connection on this socket will be closed.
238+
239+
@return An awaitable that completes with `io_result<>`.
240+
Returns success on successful accept, or an error code on
241+
failure including:
242+
- operation_canceled: Cancelled via stop_token or cancel()
243+
244+
@par Preconditions
245+
The acceptor must be listening (`is_open() == true`).
246+
The peer socket must be associated with the same execution context.
247+
248+
@par Example
249+
@code
250+
socket peer(ioc);
251+
auto [ec] = co_await acc.accept(peer);
252+
if (!ec) {
253+
// Use peer socket
254+
}
255+
@endcode
256+
*/
257+
auto accept(socket& peer)
258+
{
259+
assert(impl_ != nullptr);
260+
return accept_awaitable(*this, peer);
261+
}
262+
263+
/** Cancel any pending asynchronous operations.
264+
265+
All outstanding operations complete with `errc::operation_canceled`.
266+
*/
267+
BOOST_COROSIO_DECL
268+
void cancel();
269+
270+
/** Return the execution context.
271+
272+
@return Reference to the execution context that owns this acceptor.
273+
*/
274+
auto
275+
context() const noexcept ->
276+
capy::execution_context&
277+
{
278+
return *ctx_;
279+
}
280+
281+
struct acceptor_impl : io_object_impl
282+
{
283+
virtual void accept(
284+
std::coroutine_handle<>,
285+
capy::any_dispatcher,
286+
std::stop_token,
287+
system::error_code*,
288+
io_object_impl**) = 0;
289+
};
290+
291+
private:
292+
293+
inline acceptor_impl& get() const noexcept
294+
{
295+
return *static_cast<acceptor_impl*>(impl_);
296+
}
297+
298+
capy::execution_context* ctx_;
299+
};
300+
301+
} // namespace corosio
302+
} // namespace boost
303+
304+
#endif

include/boost/corosio/socket.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ class socket : public io_stream
284284
};
285285

286286
private:
287+
friend class acceptor;
287288

288289
inline socket_impl& get() const noexcept
289290
{

src/src/acceptor.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/corosio
8+
//
9+
10+
#include <boost/corosio/acceptor.hpp>
11+
12+
#ifdef _WIN32
13+
#include "src/detail/win_iocp_sockets.hpp"
14+
#endif
15+
16+
#include <boost/corosio/detail/except.hpp>
17+
18+
#include <cassert>
19+
20+
namespace boost {
21+
namespace corosio {
22+
namespace {
23+
24+
#ifdef _WIN32
25+
using acceptor_service = detail::win_iocp_sockets;
26+
using acceptor_impl_type = detail::win_acceptor_impl;
27+
#else
28+
#error "Unsupported platform"
29+
#endif
30+
31+
} // namespace
32+
33+
acceptor::
34+
~acceptor()
35+
{
36+
close();
37+
}
38+
39+
acceptor::
40+
acceptor(
41+
capy::execution_context& ctx)
42+
: ctx_(&ctx)
43+
{
44+
}
45+
46+
void
47+
acceptor::
48+
listen(endpoint ep, int backlog)
49+
{
50+
if (impl_)
51+
close();
52+
53+
auto& svc = ctx_->use_service<acceptor_service>();
54+
auto& impl = svc.create_acceptor_impl();
55+
impl_ = &impl;
56+
57+
system::error_code ec = svc.open_acceptor(impl, ep, backlog);
58+
if (ec)
59+
{
60+
impl.release();
61+
impl_ = nullptr;
62+
detail::throw_system_error(ec, "acceptor::listen");
63+
}
64+
}
65+
66+
void
67+
acceptor::
68+
close()
69+
{
70+
if (!impl_)
71+
return;
72+
73+
impl_->release();
74+
impl_ = nullptr;
75+
}
76+
77+
void
78+
acceptor::
79+
cancel()
80+
{
81+
assert(impl_ != nullptr);
82+
static_cast<acceptor_impl_type*>(impl_)->cancel();
83+
}
84+
85+
} // namespace corosio
86+
} // namespace boost

0 commit comments

Comments
 (0)