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
The
Since the default constructor is not accessible, this also means that you cannot have a list of reference socket types (i.e.
If there was an error creating the socket, the
After the socket has been created, you can now communicate through it by connecting to a listening host, or by having the socket object
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
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
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
Endian-ness
No conversions are done to the data being sent or received, so if a server expects the client to
Client Sockets
After you have created your socket object, you can connect to a host with the
Once a client socket has been connected, it can then be used to
Server Sockets
Server sockets typically need to be bound and then set to
Just like a client socket, you specify the address family, protocol and socket type, like so:
After it is created, you then need to
To have a client connect, you will need to have an
The
Send and Receiving
Since socket communication can have both string and binary data, the socket can
The
Care must be taken not to cause a buffer under/over flow error by passing a buffer with an inappropriate size, for example:
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
Example of getting the IPs of a host:
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
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
Example
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);
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);
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 } }
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; } } }
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);
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; } }
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; }