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

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