libs/corosio/include/boost/corosio/tcp_socket.hpp

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