include/boost/corosio/tcp_socket.hpp

88.9% Lines (40/45) 100.0% Functions (22/22)
include/boost/corosio/tcp_socket.hpp
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 // Copyright (c) 2026 Steve Gerbino
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_TCP_SOCKET_HPP
12 #define BOOST_COROSIO_TCP_SOCKET_HPP
13
14 #include <boost/corosio/detail/config.hpp>
15 #include <boost/corosio/detail/platform.hpp>
16 #include <boost/corosio/detail/except.hpp>
17 #include <boost/corosio/io/io_stream.hpp>
18 #include <boost/capy/io_result.hpp>
19 #include <boost/corosio/detail/buffer_param.hpp>
20 #include <boost/corosio/endpoint.hpp>
21 #include <boost/corosio/tcp.hpp>
22 #include <boost/capy/ex/executor_ref.hpp>
23 #include <boost/capy/ex/execution_context.hpp>
24 #include <boost/capy/ex/io_env.hpp>
25 #include <boost/capy/concept/executor.hpp>
26
27 #include <system_error>
28
29 #include <concepts>
30 #include <coroutine>
31 #include <cstddef>
32 #include <stop_token>
33 #include <type_traits>
34
35 namespace boost::corosio {
36
37 #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
38 using native_handle_type = std::uintptr_t; // SOCKET
39 #else
40 using native_handle_type = int;
41 #endif
42
43 /** An asynchronous TCP socket for coroutine I/O.
44
45 This class provides asynchronous TCP socket operations that return
46 awaitable types. Each operation participates in the affine awaitable
47 protocol, ensuring coroutines resume on the correct executor.
48
49 The socket must be opened before performing I/O operations. Operations
50 support cancellation through `std::stop_token` via the affine protocol,
51 or explicitly through the `cancel()` member function.
52
53 @par Thread Safety
54 Distinct objects: Safe.@n
55 Shared objects: Unsafe. A socket must not have concurrent operations
56 of the same type (e.g., two simultaneous reads). One read and one
57 write may be in flight simultaneously.
58
59 @par Semantics
60 Wraps the platform TCP/IP stack. Operations dispatch to
61 OS socket APIs via the io_context reactor (epoll, IOCP,
62 kqueue). Satisfies @ref capy::Stream.
63
64 @par Example
65 @code
66 io_context ioc;
67 tcp_socket s(ioc);
68 s.open();
69
70 // Using structured bindings
71 auto [ec] = co_await s.connect(
72 endpoint(ipv4_address::loopback(), 8080));
73 if (ec)
74 co_return;
75
76 char buf[1024];
77 auto [read_ec, n] = co_await s.read_some(
78 capy::mutable_buffer(buf, sizeof(buf)));
79 @endcode
80 */
81 class BOOST_COROSIO_DECL tcp_socket : public io_stream
82 {
83 public:
84 /** Different ways a socket may be shutdown. */
85 enum shutdown_type
86 {
87 shutdown_receive,
88 shutdown_send,
89 shutdown_both
90 };
91
92 struct implementation : io_stream::implementation
93 {
94 virtual std::coroutine_handle<> connect(
95 std::coroutine_handle<>,
96 capy::executor_ref,
97 endpoint,
98 std::stop_token,
99 std::error_code*) = 0;
100
101 virtual std::error_code shutdown(shutdown_type) noexcept = 0;
102
103 virtual native_handle_type native_handle() const noexcept = 0;
104
105 /** Request cancellation of pending asynchronous operations.
106
107 All outstanding operations complete with operation_canceled error.
108 Check `ec == cond::canceled` for portable comparison.
109 */
110 virtual void cancel() noexcept = 0;
111
112 /** Set a socket option.
113
114 @param level The protocol level (e.g. `SOL_SOCKET`).
115 @param optname The option name (e.g. `SO_KEEPALIVE`).
116 @param data Pointer to the option value.
117 @param size Size of the option value in bytes.
118 @return Error code on failure, empty on success.
119 */
120 virtual std::error_code set_option(
121 int level,
122 int optname,
123 void const* data,
124 std::size_t size) noexcept = 0;
125
126 /** Get a socket option.
127
128 @param level The protocol level (e.g. `SOL_SOCKET`).
129 @param optname The option name (e.g. `SO_KEEPALIVE`).
130 @param data Pointer to receive the option value.
131 @param size On entry, the size of the buffer. On exit,
132 the size of the option value.
133 @return Error code on failure, empty on success.
134 */
135 virtual std::error_code
136 get_option(int level, int optname, void* data, std::size_t* size)
137 const noexcept = 0;
138
139 /// Returns the cached local endpoint.
140 virtual endpoint local_endpoint() const noexcept = 0;
141
142 /// Returns the cached remote endpoint.
143 virtual endpoint remote_endpoint() const noexcept = 0;
144 };
145
146 struct connect_awaitable
147 {
148 tcp_socket& s_;
149 endpoint endpoint_;
150 std::stop_token token_;
151 mutable std::error_code ec_;
152
153 8229 connect_awaitable(tcp_socket& s, endpoint ep) noexcept
154 8229 : s_(s)
155 8229 , endpoint_(ep)
156 {
157 8229 }
158
159 8229 bool await_ready() const noexcept
160 {
161 8229 return token_.stop_requested();
162 }
163
164 8229 capy::io_result<> await_resume() const noexcept
165 {
166 8229 if (token_.stop_requested())
167 return {make_error_code(std::errc::operation_canceled)};
168 8229 return {ec_};
169 }
170
171 8229 auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
172 -> std::coroutine_handle<>
173 {
174 8229 token_ = env->stop_token;
175 8229 return s_.get().connect(h, env->executor, endpoint_, token_, &ec_);
176 }
177 };
178
179 public:
180 /** Destructor.
181
182 Closes the socket if open, cancelling any pending operations.
183 */
184 ~tcp_socket() override;
185
186 /** Construct a socket from an execution context.
187
188 @param ctx The execution context that will own this socket.
189 */
190 explicit tcp_socket(capy::execution_context& ctx);
191
192 /** Construct a socket from an executor.
193
194 The socket is associated with the executor's context.
195
196 @param ex The executor whose context will own the socket.
197 */
198 template<class Ex>
199 requires(!std::same_as<std::remove_cvref_t<Ex>, tcp_socket>) &&
200 capy::Executor<Ex>
201 explicit tcp_socket(Ex const& ex) : tcp_socket(ex.context())
202 {
203 }
204
205 /** Move constructor.
206
207 Transfers ownership of the socket resources.
208
209 @param other The socket to move from.
210
211 @pre No awaitables returned by @p other's methods exist.
212 @pre @p other is not referenced as a peer in any outstanding
213 accept awaitable.
214 @pre The execution context associated with @p other must
215 outlive this socket.
216 */
217 176 tcp_socket(tcp_socket&& other) noexcept : io_object(std::move(other)) {}
218
219 /** Move assignment operator.
220
221 Closes any existing socket and transfers ownership.
222
223 @param other The socket to move from.
224
225 @pre No awaitables returned by either `*this` or @p other's
226 methods exist.
227 @pre Neither `*this` nor @p other is referenced as a peer in
228 any outstanding accept awaitable.
229 @pre The execution context associated with @p other must
230 outlive this socket.
231
232 @return Reference to this socket.
233 */
234 10 tcp_socket& operator=(tcp_socket&& other) noexcept
235 {
236 10 if (this != &other)
237 {
238 10 close();
239 10 h_ = std::move(other.h_);
240 }
241 10 return *this;
242 }
243
244 tcp_socket(tcp_socket const&) = delete;
245 tcp_socket& operator=(tcp_socket const&) = delete;
246
247 /** Open the socket.
248
249 Creates a TCP socket and associates it with the platform
250 reactor (IOCP on Windows). Calling @ref connect on a closed
251 socket opens it automatically with the endpoint's address family,
252 so explicit `open()` is only needed when socket options must be
253 set before connecting.
254
255 @param proto The protocol (IPv4 or IPv6). Defaults to
256 `tcp::v4()`.
257
258 @throws std::system_error on failure.
259 */
260 void open(tcp proto = tcp::v4());
261
262 /** Close the socket.
263
264 Releases socket resources. Any pending operations complete
265 with `errc::operation_canceled`.
266 */
267 void close();
268
269 /** Check if the socket is open.
270
271 @return `true` if the socket is open and ready for operations.
272 */
273 50367 bool is_open() const noexcept
274 {
275 #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
276 return h_ && get().native_handle() != ~native_handle_type(0);
277 #else
278 50367 return h_ && get().native_handle() >= 0;
279 #endif
280 }
281
282 /** Initiate an asynchronous connect operation.
283
284 If the socket is not already open, it is opened automatically
285 using the address family of @p ep (IPv4 or IPv6). If the socket
286 is already open, the existing file descriptor is used as-is.
287
288 The operation supports cancellation via `std::stop_token` through
289 the affine awaitable protocol. If the associated stop token is
290 triggered, the operation completes immediately with
291 `errc::operation_canceled`.
292
293 @param ep The remote endpoint to connect to.
294
295 @return An awaitable that completes with `io_result<>`.
296 Returns success (default error_code) on successful connection,
297 or an error code on failure including:
298 - connection_refused: No server listening at endpoint
299 - timed_out: Connection attempt timed out
300 - network_unreachable: No route to host
301 - operation_canceled: Cancelled via stop_token or cancel().
302 Check `ec == cond::canceled` for portable comparison.
303
304 @throws std::system_error if the socket needs to be opened
305 and the open fails.
306
307 @par Preconditions
308 This socket must outlive the returned awaitable.
309
310 @par Example
311 @code
312 // Socket opened automatically with correct address family:
313 auto [ec] = co_await s.connect(endpoint);
314 if (ec) { ... }
315 @endcode
316 */
317 8229 auto connect(endpoint ep)
318 {
319 8229 if (!is_open())
320 16 open(ep.is_v6() ? tcp::v6() : tcp::v4());
321 8229 return connect_awaitable(*this, ep);
322 }
323
324 /** Cancel any pending asynchronous operations.
325
326 All outstanding operations complete with `errc::operation_canceled`.
327 Check `ec == cond::canceled` for portable comparison.
328 */
329 void cancel();
330
331 /** Get the native socket handle.
332
333 Returns the underlying platform-specific socket descriptor.
334 On POSIX systems this is an `int` file descriptor.
335 On Windows this is a `SOCKET` handle.
336
337 @return The native socket handle, or -1/INVALID_SOCKET if not open.
338
339 @par Preconditions
340 None. May be called on closed sockets.
341 */
342 native_handle_type native_handle() const noexcept;
343
344 /** Disable sends or receives on the socket.
345
346 TCP connections are full-duplex: each direction (send and receive)
347 operates independently. This function allows you to close one or
348 both directions without destroying the socket.
349
350 @li @ref shutdown_send sends a TCP FIN packet to the peer,
351 signaling that you have no more data to send. You can still
352 receive data until the peer also closes their send direction.
353 This is the most common use case, typically called before
354 close() to ensure graceful connection termination.
355
356 @li @ref shutdown_receive disables reading on the socket. This
357 does NOT send anything to the peer - they are not informed
358 and may continue sending data. Subsequent reads will fail
359 or return end-of-file. Incoming data may be discarded or
360 buffered depending on the operating system.
361
362 @li @ref shutdown_both combines both effects: sends a FIN and
363 disables reading.
364
365 When the peer shuts down their send direction (sends a FIN),
366 subsequent read operations will complete with `capy::cond::eof`.
367 Use the portable condition test rather than comparing error
368 codes directly:
369
370 @code
371 auto [ec, n] = co_await sock.read_some(buffer);
372 if (ec == capy::cond::eof)
373 {
374 // Peer closed their send direction
375 }
376 @endcode
377
378 Any error from the underlying system call is silently discarded
379 because it is unlikely to be helpful.
380
381 @param what Determines what operations will no longer be allowed.
382 */
383 void shutdown(shutdown_type what);
384
385 /** Set a socket option.
386
387 Applies a type-safe socket option to the underlying socket.
388 The option type encodes the protocol level and option name.
389
390 @par Example
391 @code
392 sock.set_option( socket_option::no_delay( true ) );
393 sock.set_option( socket_option::receive_buffer_size( 65536 ) );
394 @endcode
395
396 @param opt The option to set.
397
398 @throws std::logic_error if the socket is not open.
399 @throws std::system_error on failure.
400 */
401 template<class Option>
402 60 void set_option(Option const& opt)
403 {
404 60 if (!is_open())
405 detail::throw_logic_error("set_option: socket not open");
406 60 std::error_code ec = get().set_option(
407 Option::level(), Option::name(), opt.data(), opt.size());
408 60 if (ec)
409 detail::throw_system_error(ec, "tcp_socket::set_option");
410 60 }
411
412 /** Get a socket option.
413
414 Retrieves the current value of a type-safe socket option.
415
416 @par Example
417 @code
418 auto nd = sock.get_option<socket_option::no_delay>();
419 if ( nd.value() )
420 // Nagle's algorithm is disabled
421 @endcode
422
423 @return The current option value.
424
425 @throws std::logic_error if the socket is not open.
426 @throws std::system_error on failure.
427 */
428 template<class Option>
429 62 Option get_option() const
430 {
431 62 if (!is_open())
432 detail::throw_logic_error("get_option: socket not open");
433 62 Option opt{};
434 62 std::size_t sz = opt.size();
435 std::error_code ec =
436 62 get().get_option(Option::level(), Option::name(), opt.data(), &sz);
437 62 if (ec)
438 detail::throw_system_error(ec, "tcp_socket::get_option");
439 62 opt.resize(sz);
440 62 return opt;
441 }
442
443 /** Get the local endpoint of the socket.
444
445 Returns the local address and port to which the socket is bound.
446 For a connected socket, this is the local side of the connection.
447 The endpoint is cached when the connection is established.
448
449 @return The local endpoint, or a default endpoint (0.0.0.0:0) if
450 the socket is not connected.
451
452 @par Thread Safety
453 The cached endpoint value is set during connect/accept completion
454 and cleared during close(). This function may be called concurrently
455 with I/O operations, but must not be called concurrently with
456 connect(), accept(), or close().
457 */
458 endpoint local_endpoint() const noexcept;
459
460 /** Get the remote endpoint of the socket.
461
462 Returns the remote address and port to which the socket is connected.
463 The endpoint is cached when the connection is established.
464
465 @return The remote endpoint, or a default endpoint (0.0.0.0:0) if
466 the socket is not connected.
467
468 @par Thread Safety
469 The cached endpoint value is set during connect/accept completion
470 and cleared during close(). This function may be called concurrently
471 with I/O operations, but must not be called concurrently with
472 connect(), accept(), or close().
473 */
474 endpoint remote_endpoint() const noexcept;
475
476 protected:
477 10 tcp_socket() noexcept = default;
478
479 explicit tcp_socket(handle h) noexcept : io_object(std::move(h)) {}
480
481 private:
482 friend class tcp_acceptor;
483
484 /// Open the socket for the given protocol triple.
485 void open_for_family(int family, int type, int protocol);
486
487 58962 inline implementation& get() const noexcept
488 {
489 58962 return *static_cast<implementation*>(h_.get());
490 }
491 };
492
493 } // namespace boost::corosio
494
495 #endif
496