Sockets
Socket communication covers a wide range of network topics, from various protocols like TCP and UDP to the various idioms within how to communicate, like data streams, broadcast messages and much more.

Please note that due to the scope of networking concepts and the plethora of information available regarding sockets and networking, this example will <i>not</i> cover what a socket <b>is</b> and expects you to have a basic understanding of some of the underlying concepts of network communications. A few great resources include Beejs socket programming guide, IBMs how sockets work, and the Wikipedia pages on sockets; these cover low level APIs as well as going into details about client/server communication and more.

The Socket

The omni::net namespace contains the various definitions for socket creation and communication. To create a basic IPv4 socket for communication, you can simply do the following:
// Create a TCP socket
omni::net::socket tcp_sock(omni::net::address_family::INET,
                           omni::net::socket_type::STREAM,
                           omni::net::protocol_type::TCP);

// Create a UDP socket
omni::net::socket udp_sock(omni::net::address_family::INET,
                           omni::net::socket_type::DGRAM,
                           omni::net::protocol_type::UDP);

// Create a RAW socket
omni::net::socket raw_sock(omni::net::address_family::INET,
                           omni::net::socket_type::RAW,
                           omni::net::protocol_type::RAW);
The socket class does not have a default constructor since the address family, socket and protocol type must be specified before the socket can connect, in this way, you must create a omni::net::socket object with the omni::net::address_family, omni::net::socket_type, and omni::net::protocol_type specified. If you create a socket with out an address_family the address_family will be that of omni::net::address_family::UNSPECIFIED.

Since the default constructor is not accessible, this also means that you cannot have a list of reference socket types (i.e. std::vector<omni::net::socket> would have to be std::vector<omni::net::socket*>). This is by design since it does not make since to have two objects that contain a reference to the same socket channel, instead if you wanted two sockets of communication connected to the same end point, you would simply create two socket objects of the same time and connect both of them. This allows the OS to handle the underlying I/O in a more efficient manner.

If there was an error creating the socket, the omni::net::socket::last_error member function of the socket class will alert you to the specific problem on creation (possible errors include too many sockets already opened). Note that the last_error function returns the last socket error that happened on the socket object.

After the socket has been created, you can now communicate through it by connecting to a listening host, or by having the socket object listen and accept connections.

Note that you cannot have a client socket also be a server socket. That is to say that if a socket has been created and then connect or connect_host has been called, you cannot have the same socket object then call bind or any of the other "server" like functions, as well, if you create a socket object and then bind it, listen or accept, you cannot then call connect or connect_host as an error will be returned.

If you have closed a socket and shut it down, you can reuse a socket and reset its family type as well as other socket functionality, however, this is only recommended if memory is a concern.

Address Family, Protocol and Socket Type

Note that the last_error function will be set to one of the address or protocol error flags if you try to specify a combination of address family, protocol and socket type that do not work. For instance, you cannot have a omni::net::socket_type::RAW socket type specified with a a protocol type of omni::net::protocol_type::TCP.

If you create a socket with invalid parameters specified or if you wish to change one of them after having closed the socket, you can use the omni::net::socket::set_address_family, omni::net::socket::set_protocol_type, or omni::net::socket::set_socket_type function. Additionally, you can call one of the omni::net::socket::open functions specifying the socket, protocol and address family.

Endian-ness

No conversions are done to the data being sent or received, so if a server expects the client to send data in a certain endian-ness, you will need to ensure to do the conversions in your code as no socket objects preform any conversions to the data being sent or received.

Client Sockets

After you have created your socket object, you can connect to a host with the omni::net::socket::connect function and specify a dotted-notation IP4 address and port to connect to. If you want to connect the socket to a specific host name and port, you can use the omni::net::socket::connect_host function which will resolve the address to connect to.

Once a client socket has been connected, it can then be used to send and receive data. The example at the bottom shows how to connect to a website (like zeriph.com) and send a simple HTTP GET request then receiving the data; in the example below the request sent receives the main pages HTML (i.e. zeriph.com/index.html).

Server Sockets

Server sockets typically need to be bound and then set to listen in order to accept connections or data. Since server sockets can vary based on needs, this section will only cover basic TCP server socket.

Just like a client socket, you specify the address family, protocol and socket type, like so:
omni::net::socket server_sock(omni::net::address_family::INET,
                              omni::net::socket_type::STREAM,
                              omni::net::protocol_type::TCP);
After it is created, you then need to listen on the socket, but you cannot listen on an unbound socket, so you must call bind, using either a port or IP/port combo to bind to, then listen to have a client then connect, like so:
if (server_sock.bind(12345) != omni::net::socket_error::SUCCESS) {
    std::cout << "Error binding on socket: " << server_sock.last_error() << std::endl;
} else {
    std::cout << "Bound to port " << server_sock.bound_port() << std::endl;
    if (server_sock.listen() != omni::net::socket_error::SUCCESS) {
        std::cout << "Error listening on socket: " << server_sock.last_error() << std::endl;
    } else {
        std::cout << "Ready to accept connections" << std::endl;
        // accept connections here
    }
}
To have a client connect, you will need to have an endpoint_descriptor that you communicate over, like so:
omni::net::endpoint_descriptor remote_ep;
if (server_sock.accept(remote_ep) != omni::net::socket_error::SUCCESS) {
    std::cout << "Error accepting on socket: " << server_sock.last_error() << std::endl;
} else {
    uint32_t xfr = 0; // how much actually sent/received
    char buff[1024] = {0}; // data buffer to send/receive

    std::cout << "Client connected: " << remote_ep << std::endl;
    // omni::net::endpoint_descriptor::to_string shows the IP:port of the client

    // call send/receive on the endpoint_descriptor and _NOT_ the server_sock object
    if (remote_ep.receive(buff, 1024, xfr) != omni::net::socket_error::SUCCESS) {
        std::cout << "Error receiving on socket: " << remote_ep.last_error() << std::endl;
    } else {
        std::cout << "Received " << xfr << " bytes: [" << buff << "]" << std::endl;
        std::cout << "Sending: WELCOME!\\r\\n" << std::endl;
        if (remote_ep.send("WELCOME!\r\n", 11, xfr) != omni::net::socket_error::SUCCESS) {
            std::cout << "Error sending on socket: " << remote_ep.last_error() << std::endl;    
        } else {
            std::cout << "Sent " << xfr << " bytes to client" << std::endl;
        }
    }
}
The omni::net::endpoint_descriptor <i>does</i> have a default and copy constructors to allow having a list of reference types so that you can have a server socket maintain a list of endpoints that can be communicated with, so if OMNI_SAFE_SOCKET_EP is <i>not</i> defined, care must be taken not to cause a race condition on the endpoint_descriptor object. The endpoint_descriptor has some more simplistic socket communication interfaces as it is meant to be only a remote endpoint that has been accepted.

Send and Receiving

Since socket communication can have both string and binary data, the socket can send and receive simplistic types of data, like the int8_t* the uint8_t* along with char* types.

The send and receive functions only accept raw pointer types due to how the underlying data is to be actually sent and received, as well as to allow the greatest efficiency and flexibility across platforms.

Care must be taken not to cause a buffer under/over flow error by passing a buffer with an inappropriate size, for example:
// WRONG
uint32_t xfr = 0;
const char buff[] = "1234";
sock.send(buff, 6, xfr); // buffer overflow

// CORRECT
uint32_t xfr = 0;
const char buff[] = "1234";
sock.send(buff, sizeof(buff), xfr);
Omni follows a "trust the programmer" idiom in this context, so <b>please</b> take care with handling the data sizes properly.

Helper Functions

In addition to the socket class, there are is other functionality in the omni::net::util namespace that allows you to handle other aspects of network programming, like getting a list of IPs associated with a host name or getting the host name of an IP, or validating an IP address or port.

Example of getting the IPs of a host:
std::deque<std::string> ips;
omni::net::socket_error err = omni::net::util::get_ip("zeriph.com", ips);
if (err != omni::net::socket_error::SUCCESS) {
    std::cout << "Error getting IPs: " << err << std::endl;
} else {
    std::cout << "IPs for zeriph.com:" << std::endl;
    for (std::deque<std::string>::iterator it = ips.begin(); it != ips.end(); ++it) {
        std::cout << *it << std::endl;
    }
}
Socket Errors

Exceptions in the library are kept for areas of code that were not validated by the user of the code (like invalid ranges or division by zero).

Sockets are a special area of the library that only throw exceptions in a very few functions and instead, the design choice was made to return an omni::net::socket_error for most of the functions defined in the socket class. This is due to the simple fact that sockets can fail in numerous and unexpected ways and throwing exceptions is not always the desired course of action, for various reasons.

You will need to read the documentation for the individual functions in the socket class to verify which socket errors are possible since a call to bind could result in different socket errors than a call to send or the others.

Example
#include <omnilib>

#define BUFFSZ 65535

int main(int argc, char* argv[])
{
    omni::net::socket sock(omni::net::address_family::INET, omni::net::socket_type::STREAM, omni::net::protocol_type::TCP);
    if (sock.connect_host("zeriph.com", 80) != omni::net::socket_error::SUCCESS) {
        std::cout << "Could not connect to host: " << sock.last_error() << std::endl;
        return -1;
    }

    uint32_t xfr = 0;
    const std::string req = "GET / HTTP/1.0\r\nHost: zeriph.com\r\n\r\n";
    std::cout << "Connected to " << sock << std::endl;
    if (sock.send(req.c_str(), req.size(), xfr) != omni::net::socket_error::SUCCESS) {
        std::cout << "Could not send data: " << sock.last_error() << std::endl;
        return -1;
    }

    std::cout << "Sent " << xfr << " bytes" << std::endl;
    char* data = new char[BUFFSZ];
    while (sock.receive(data, BUFFSZ, xfr) == omni::net::socket_error::SUCCESS) {
        if (xfr > 0) { std::cout << data; }
        std::memset(data, 0, BUFFSZ);
    }
    delete[] data;
    
    if (sock.last_error() != omni::net::socket_error::SUCCESS) {
        std::cout << std::endl << "Last socket error: " << sock.last_error() << std::endl;
    }

    // Strictly speaking this is not necessary here since `sock` is a local
    // object, so when it goes out of scope it will call close on destruction.
    sock.close();

    return 0;
}
call close on destruction.     sock.close();     return 0; }