include/boost/corosio/tcp_socket.hpp

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