Skip to content

File tcpsocket.hpp

File List > illixr > network > tcpsocket.hpp

Go to the documentation of this file

#pragma once
#if defined(_WIN32) || defined(_WIN64)
#    ifndef _WINSOCKAPI_
#        define _WINSOCKAPI_
#    endif
#    ifndef WIN32_LEAN_AND_MEAN
#        define WIN32_LEAN_AND_MEAN
#    endif
#endif

#include 

#if defined(_WIN32) || defined(_WIN64)
// clang-format off
#    include   // Must come FIRST
#    include 
#    include 
#    include 
#    include 
#    include 
#    include 
#    include 
#    pragma comment(lib, "Ws2_32.lib")
#    pragma comment(lib, "Iphlpapi.lib")
#    define BYTE_TYPE   int
#    define SOCKET_TYPE SOCKET
// clang-format on
#else
#    include 
#    include 
#    include 
#    include 
#    include 
#    include 
#    include 
#    define BYTE_TYPE   ssize_t
#    define SOCKET_TYPE int
#endif

#include "illixr/export.hpp"

#include 

namespace ILLIXR::network {

class MY_EXPORT_API TCPSocket {
public:
    TCPSocket() {
#if defined(_WIN32) || defined(_WIN64)
        static bool initialized = false;
        if (!initialized) {
            WSAData wsa_data;
            if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0) {
                throw std::runtime_error("WSAStartup failed.");
            }
            initialized = true;
        }
#endif
        fd_ = socket(AF_INET, SOCK_STREAM, 0);
#if defined(_WIN32) || defined(_WIN64)
        if (fd_ == INVALID_SOCKET)
            throw std::runtime_error("startup failed");
#endif
    }

    [[maybe_unused]] explicit TCPSocket(int fd) {
        fd_ = fd;
    }

    // Destructor
    // Close the file descriptor
    ~TCPSocket() {
#if defined(_WIN32) || defined(_WIN64)
        closesocket(fd_);
#else
        close(fd_);
#endif
    }

    // Bind socket to a specified local ip and port.
    // Always sets SO_REUSEADDR + SO_REUSEPORT and SO_LINGER=0 before binding
    // so that the port is immediately reusable after a crash or force-quit,
    // regardless of whether the caller remembered to set these options first.
    void socket_bind(const std::string& ip, int port) const {
        socket_set_reuseaddr();
        socket_set_linger_zero();
        sockaddr_in local_addr;
        local_addr.sin_family      = AF_INET;
        local_addr.sin_port        = htons(port);
        local_addr.sin_addr.s_addr = inet_addr(ip.c_str());
        if (bind(fd_, (struct sockaddr*) &local_addr, sizeof(local_addr)) < 0) {
            throw std::runtime_error("Bind failed");
        }
    }

    // Listen for a connection. It is typically called from the server socket.
    void socket_listen(const int backlog = 16) const {
        if (listen(fd_, backlog) < 0) {
            throw std::runtime_error("Listen failed");
        }
    }

    // Connect the socket to a specified peer addr
    void socket_connect(const std::string& ip, int port) const {
        sockaddr_in peer_addr;
        peer_addr.sin_family      = AF_INET;
        peer_addr.sin_port        = htons(port);
        peer_addr.sin_addr.s_addr = inet_addr(ip.c_str());
        if (connect(fd_, (struct sockaddr*) &peer_addr, sizeof(peer_addr)) < 0) {
#if defined(_WIN32) || defined(_WIN64)
            throw std::runtime_error("Connect failed");
#else
            throw std::runtime_error("Connect failed to " + ip + ":" + std::to_string(port) + ": " + std::strerror(errno));
#endif
        }
    }

    // Accept connect from the client. It is typically called from the server socket.
#if defined(_WIN32) || defined(_WIN64)
    SOCKET
#else
    int
#endif
    socket_accept() const {
#if defined(_WIN32) || defined(_WIN64)
        SOCKET
#else
        int
#endif
        fd = accept(fd_, nullptr, nullptr);
#if defined(_WIN32) || defined(_WIN64)
        if (fd == INVALID_SOCKET)
            throw std::runtime_error("Accept failed");
#endif
        return fd;
    }

    // Allow the reuse of local addresses.
    // On Linux/Android, also sets SO_REUSEPORT so that a new socket can bind
    // to a port still in TIME_WAIT after a crash, without waiting ~60 seconds.
    void socket_set_reuseaddr() const {
#if defined(_WIN32) || defined(_WIN64)
        int enable = 1;
        if (setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, (char*) &enable, sizeof(enable)) < 0) {
            int err = WSAGetLastError();
            throw std::runtime_error("Reuseaddr failed");
        }
#else
        const int enable = 1;
        setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
        setsockopt(fd_, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(int));
#endif
    }

    // Set SO_LINGER with a zero timeout so that close() sends a TCP RST
    // rather than going through the graceful FIN/FIN-ACK sequence.
    // This prevents the socket from entering TIME_WAIT after a crash,
    // allowing the port to be reused immediately on restart.
    void socket_set_linger_zero() const {
#if defined(_WIN32) || defined(_WIN64)
        LINGER lg{};
        lg.l_onoff  = 1;
        lg.l_linger = 0;
        setsockopt(fd_, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char*>(&lg), sizeof(lg));
#else
        struct linger lg{};
        lg.l_onoff  = 1;
        lg.l_linger = 0;
        setsockopt(fd_, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
#endif
    }

    // Read data from the socket
    [[nodiscard]] std::string read_data(const size_t limit = BUFFER_SIZE) {
#ifndef __ANDROID__
        char buffer_[BUFFER_SIZE];
#endif
        BYTE_TYPE bytes_read =
#if defined(_WIN32) || defined(_WIN64)
            recv(fd_, buffer_, static_cast<int>(min(BUFFER_SIZE, limit)), 0);
#elif defined(__ANDROID__)
            read(fd_, static_cast<void* const>(buffer_), std::min(BUFFER_SIZE, limit));
#else
            read(fd_, buffer_, static_cast<int>(std::min(BUFFER_SIZE, limit)));
#endif
        if (bytes_read <= 0)
            return {};
        return std::string(buffer_, bytes_read);
    }

    // Write data to the socket
    void write_data(const std::string& buffer) {
        auto it = buffer.begin();
        do {
            it = write_helper(it, buffer.end());
        } while (it != buffer.end());
    }

    // Disable naggle algorithm. This allows socket to flush data immediately after calling write().
    void enable_no_delay() const {
#if defined(_WIN32) || defined(_WIN64)
        int enable = 1;
        if (setsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, (char*) &enable, sizeof(enable)) < 0)
            throw std::runtime_error("No delay failed");
#else
        const int enable = 1;
        if (setsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)) < 0)
            throw std::runtime_error("TCPSocket] TCP_NODELAY setsockopt failed");
#endif
    }

    void socket_shutdown() const noexcept {
#if defined(_WIN32) || defined(_WIN64)
        shutdown(fd_, SD_BOTH);
#else
        shutdown(fd_, SHUT_RDWR);
#endif
    }

    /* accessors */
    [[maybe_unused]] [[nodiscard]] std::string local_address() const {
        sockaddr_in local_address;
        socklen_t   size = sizeof(local_address);
        getsockname(fd_, (struct sockaddr*) &local_address, &size);

#if defined(_WIN32) || defined(_WIN64)
        char local_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &local_address.sin_addr, local_ip, sizeof(local_ip));
#else
        char* local_ip = inet_ntoa(local_address.sin_addr);
#endif
        int local_port = ntohs(local_address.sin_port);
        return std::string(local_ip) + ":" + std::to_string(local_port);
    }

    [[nodiscard]] std::string peer_address() const {
        sockaddr_in peer_address;
        socklen_t   size = sizeof(peer_address);
        getpeername(fd_, (struct sockaddr*) &peer_address, &size);
#if defined(_WIN32) || defined(_WIN64)
        char peer_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &peer_address.sin_addr, peer_ip, sizeof(peer_ip));
#else
        char* peer_ip = inet_ntoa(peer_address.sin_addr);
#endif
        int peer_port = ntohs(peer_address.sin_port);
        return std::string(peer_ip) + ":" + std::to_string(peer_port);
    }

    [[nodiscard]] bool is_no_delay() const {
        int val = 0;
#if defined(_WIN32) || defined(_WIN64)
        int len = sizeof(val);
        getsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, (char*) &val, &len);
#else
        socklen_t len = sizeof(val);
        getsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, &val, &len);
#endif
        return val != 0;
    }

private:
    [[nodiscard]] std::string::const_iterator write_helper(const std::string::const_iterator& begin,
                                                           const std::string::const_iterator& end) const {
        BYTE_TYPE bytes_written =
#if defined(_WIN32) || defined(_WIN64)
            send(fd_, &*begin, static_cast<int>(end - begin), 0);
        if (bytes_written < 0)
            throw std::runtime_error("Write failed");
#else
            send(fd_, &*begin, end - begin, MSG_NOSIGNAL);
        if (bytes_written < 0) {
            if (errno == EINTR) {
                return begin;
            }
            throw std::runtime_error(std::string{"Write failed: "} + std::strerror(errno));
        }
#endif
        return begin + bytes_written;
    }

    SOCKET_TYPE fd_;
    /* maximum size of a read */
#if defined(_WIN32) || defined(_WIN64)
    static constexpr size_t BUFFER_SIZE = 1024 * 256;
#else
    static constexpr size_t BUFFER_SIZE = 1024 * 1024;
#endif

#ifdef __ANDROID__
    char buffer_[BUFFER_SIZE];
#endif
};

} // namespace ILLIXR::network