ObzLib networking
obz::transport
Small RAII wrappers for moving bytes between IPv4 TCP, UDP, and multicast endpoints.
Packets, Not A Networking Framework
The interesting thing about transport is what it does not try to become. It does not
define messages, replication, sessions, matchmaking, feed recovery, or an event loop. It gives the
program a small way to move bytes between IPv4 endpoints.
That makes it a good partner for small binary packets. A game prototype might build a compact packet elsewhere, then use UDP to send it to a local server, editor tool, test harness, or multicast group.
obz::transport::udp_socket receiver;
receiver.bind({"127.0.0.1", 0});
obz::transport::udp_socket sender;
sender.open();
std::array<std::byte, 4> payload{
std::byte{1},
std::byte{2},
std::byte{3},
std::byte{4},
};
sender.send_to(receiver.local_endpoint(), payload);
std::array<std::byte, 1024> buffer{};
const auto datagram = receiver.receive_from(buffer);
Multicast Is A Membership, Not A Message Format
The latest transport work adds IPv4 multicast group membership. That is useful for projects such as ObzMarketLab, where one publisher can send feed packets to a group and one or more listeners can join that group to receive them.
obz::transport::udp_socket receiver;
receiver.bind({"0.0.0.0", 30001});
receiver.join_multicast_group("239.255.0.1", "0.0.0.0");
std::array<std::byte, 4096> buffer{};
const auto packet = receiver.receive_from(buffer);
Binding still selects the local UDP port. Joining selects the multicast group and receiving interface. The library keeps that socket-level work separate from packet decoding, sequence numbers, and application recovery policy.
Two Different Ways To Move Bytes
UDP and TCP are kept as different types because they behave differently. UDP sends one datagram to one endpoint. The receive result reports the sender, the number of bytes retained, and whether the datagram was complete or truncated.
struct udp_receive_result {
endpoint sender;
std::size_t bytes_received;
datagram_status status;
};
TCP is stream-oriented. It has a listener that accepts connections, and a socket that sends and receives bytes once that connection exists.
obz::transport::tcp_listener listener;
listener.listen({"127.0.0.1", 9000});
auto client = listener.accept();
client.send_all(response_bytes);
The Handle Has One Owner
Native sockets are operating-system resources, so ownership matters. The transport types are move-only: one object owns one native socket handle, and that handle is closed automatically when the object is destroyed.
tcp_socket(const tcp_socket&) = delete;
tcp_socket& operator=(const tcp_socket&) = delete;
tcp_socket(tcp_socket&& other) noexcept;
tcp_socket& operator=(tcp_socket&& other) noexcept;
~tcp_socket();
The clean-code point is the boundary. Game code, tools, or tests can talk in terms of endpoints, byte spans, and datagrams, while the platform-specific file descriptors, Winsock handles, close calls, and error conversion stay behind the wrapper.
struct endpoint {
std::string host;
std::uint16_t port;
};
One API, Two Platform Backends
The other neat part of the design is the platform split. The public classes do not expose POSIX calls or Winsock calls. They call a small internal socket layer instead.
native_socket_handle create_tcp_socket();
native_socket_handle create_udp_socket();
void connect_socket(native_socket_handle handle, const endpoint& remote_endpoint);
void bind_socket(native_socket_handle handle, const endpoint& local_endpoint);
native_socket_handle accept_socket(native_socket_handle handle);
std::size_t send_tcp(native_socket_handle handle, std::span<const std::byte> data);
std::vector<std::byte> receive_tcp(native_socket_handle handle, std::size_t max_bytes);
The shared code owns the shape of the library. The platform files own the awkward details: address conversion, error codes, close semantics, and Winsock startup on Windows.
if (WIN32)
target_sources(obz_transport
PRIVATE
src/platform/win32_socket_platform.cpp
)
target_link_libraries(obz_transport
PRIVATE
ws2_32
)
else()
target_sources(obz_transport
PRIVATE
src/platform/posix_socket_platform.cpp
)
endif()
That is a useful pattern in a small library: keep the user-facing API stable, then let the build choose the implementation that matches the operating system.