include/boost/corosio/tcp_socket.hpp

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