Network sockets
Introduction
Network devices like the esp8266 are awesome, providing communication capabilities and access to the internet. However, it would be less than ideal if applications had to "know" how to talk to every single different network device.
To make things easier, most operating systems provide a "generic network stack" to applications, allowing them to create sockets (communication endpoints between applications) regardless of the underlying physical network device, be it WiFi, Ethernet or anything else.
In Dooba, we also have a generic network stack, though much simplified (when compared to a PC operating system). This network stack defines a common "standard" interface for network device drivers, so that applications can create sockets easily without having to care about what kind of network device is used.
For now, the Dooba socket system allows only stream-based sockets (TCP, not UDP).
This is all contained in the 'socket' library, which by itself doesn't do much but:
- allows applications to create sockets easily
- defines the common interfaces that any network device driver should follow
Sockets
The socket library of Dooba provides a few simple functions to help us create sockets and exchange data through them.
Creating sockets
To create either a client (connecting) or a server (accepting connections) socket, we can use the following:
// Connect (Create Client Socket through Interface) extern uint8_t socket_cnct(struct socket_iface *iface, struct socket **s, char *host, uint16_t port, void *user, socket_recv_handler_t recv_handler, socket_dcnt_handler_t dcnt_handler); // Connect (Create Client Socket through Interface) - Fixed Length Host extern uint8_t socket_cnct_n(struct socket_iface *iface, struct socket **s, char *host, uint8_t host_l, uint16_t port, void *user, socket_recv_handler_t recv_handler, socket_dcnt_handler_t dcnt_handler); // Connect (Create Client Socket through Interface) using URL extern uint8_t socket_cnct_u(struct socket_iface *iface, struct socket **s, struct url *u, void *user, socket_recv_handler_t recv_handler, socket_dcnt_handler_t dcnt_handler); // Listen (Create Server Socket through Interface) extern uint8_t socket_lstn(struct socket_iface *iface, struct socket **s, uint16_t port, void *user, socket_cnct_handler_t cnct_handler, socket_recv_handler_t recv_handler, socket_dcnt_handler_t dcnt_handler);
Notice how these functions expect some callbacks as part of their arguments. Dooba is based on asynchronous reactive sockets. What this means is simply that your application can 'send' whenever it wants, but cannot decide to 'receive'. Instead, the socket system calls back the application whenever some inbound event occurs - like some data that was received or a client that connected to us (hence the callbacks).
-
cnct_handler
will be called whenever a new client has connected to us. It will be passed the new socket as arguments. -
data_handler
will be called whenever some data was received on a socket. -
dcnt_handler
will be called whenever an existing socket is dropped by the peer.
Also note how these take a "pointer to pointer to socket" argument (struct socket **s
). This is because sockets are not meant to be managed by the user (application), but rather by network devices themselves. This is due to the particular nature of embedded network devices. Basically, this means that the user only needs to allocate a pointer and pass the address of that pointer to these functions, which will either set the pointer to something valid (a socket that is managed internally by the network device driver) or nothing at all in case of error.
Let us assume we have a socket interface available called 'wifi0'. The examples below shows how to create sockets using the functions presented above.
To keep things simple, we are not setting the callbacks in these short examples.
Connect example
struct socket *sock; // ... // Create client socket - Connect to 'example.dooba.io' on port 80 if(socket_cnct(wifi0, &sock, "example.dooba.io", 80, 0, 0, 0)) { // Something went wrong :( }
Listen example
1 struct socket *sock; 2 3 // ... 4 5 // Create server socket - listen on port 4444 6 if(socket_lstn(wifi0, &sock, 4444, 0, 0, 0, 0)) 7 { 8 // Something went wrong :( 9 }
Exchanging data
Once we have a socket, we can send data through it. For that we use the 'socket_send' function which takes a socket, a buffer, and a size as its parameters:
// Send data over our socket if(socket_send(sock, "Hello world!", 12)) { // Something went wrong :( }
Getting Peer Info
For any active socket, we can determine the IP address and TCP port of the remote peer through socket_get_peer, which takes two optional parameters: an ip buffer and port.
char peer_addr[NET_IP_ADDR_LEN_TXT]; uint16_t peer_port; // Get Remote Peer Address & Port if(socket_get_peer(sock, peer_addr, &peer_port)) { // Something went wrong :( }
The first argument should be either 0 or a pointer to a buffer of at least NET_IP_ADDR_LEN_TXT. This buffer will be filled with a 0-terminated IP address string.
Closing sockets
After we are done using a socket, we can close it simply by calling 'socket_close':
// Close our socket socket_close(sock);
Usage examples
Let's have a look at some complete usage examples. For these we will assume an esp8266 is used and provided as a 'wl0' socket interface. Also, we will assume SCLI is included. We will not present the substrate here.
Client example
Let's connect to port 4444 on 'example.dooba.io', say hello, and display anything we get back.
1 #include <socket/socket.h> 2 3 #include "substrate.h" 4 5 // Our Socket 6 struct socket *sock; 7 8 // "Data Received" callback for our socket 9 void on_data_received(void *user, struct socket *s, uint8_t *data, uint16_t size) 10 { 11 // Display received data & Close socket 12 scli_printf("Got response! Message data: [%t]\n", data, size); 13 socket_close(sock); 14 } 15 16 // "Disconnected" callback for our socket 17 void on_disconnected(void *user, struct socket *s) 18 { 19 // Inform 20 scli_printf("Peer disconnected socket!\n"); 21 } 22 23 // Main Initialization 24 void init() 25 { 26 // Init Substrate 27 substrate_init(); 28 29 // Connect socket 30 scli_printf("Connecting socket to example.dooba.io:4444...\n"); 31 if(socket_cnct(wl0, &sock, "example.dooba.io", 4444, 0, on_data_received, on_disconnected)) 32 { 33 // Something went wrong :( 34 scli_printf("ERROR: Failed to connect socket :(\n"); 35 return; 36 } 37 38 // Send Hello 39 scli_printf("Sending hello message...\n"); 40 if(socket_send(sock, "Hello world!\n", 6)) 41 { 42 // Something went wrong :( 43 scli_printf("ERROR: Failed to send data :(\n"); 44 socket_close(sock); 45 return; 46 } 47 } 48 49 // Main Loop 50 void loop() 51 { 52 // Update Substrate 53 substrate_loop(); 54 }
Server example
Let's create a very basic server. It will accept incoming connections, then for each connected socket simply echo back whatever the client sends.
1 #include <socket/socket.h> 2 3 #include "substrate.h" 4 5 // Our Server Socket 6 struct socket *serv; 7 8 // "Connected" callback 9 void on_connected(void *user, struct socket *s) 10 { 11 char peer_a[NET_IP_ADDR_LEN_TXT]; 12 uint16_t peer_p; 13 14 // Get Peer Addr / Port 15 if(socket_get_peer(s, peer_a, &peer_p)) 16 { 17 // Something went wrong.... 18 scli_printf("ERROR: Socket connected but failed to get peer info!\n"); 19 socket_close(s); 20 } 21 22 // Inform 23 scli_printf("Client connected from %s:%i!\n", peer_a, peer_p); 24 } 25 26 // "Data Received" callback 27 void on_data_received(void *user, struct socket *s, uint8_t *data, uint16_t size) 28 { 29 // Echo back & display received data 30 socket_send(s, data, size); 31 scli_printf("Got message from client: [%t]\n", data, size); 32 } 33 34 // "Disconnected" callback 35 void on_disconnected(void *user, struct socket *s) 36 { 37 // Inform 38 scli_printf("Client disconnected!\n"); 39 } 40 41 // Main Initialization 42 void init() 43 { 44 // Init Substrate 45 substrate_init(); 46 47 // Create listening socket 48 scli_printf("Opening listenting socket on port 4444...\n"); 49 if(socket_lstn(wl0, &sock, 4444, 0, on_connected, on_data_received, on_disconnected)) 50 { 51 // Something went wrong :( 52 scli_printf("ERROR: Failed to open listening socket :(\n"); 53 return; 54 } 55 } 56 57 // Main Loop 58 void loop() 59 { 60 // Update Substrate 61 substrate_loop(); 62 }
Socket Interfaces
To serve as a 'socket interface', a network device driver needs to populate a structure of type struct socket_iface *
. This structure is rather simple:
// Socket Interface Structure struct socket_iface { // Network Interface Data void *ifdata; // Connect Socket (Client) socket_iface_cnct_t cnct; // Bind Socket (Server) socket_iface_lstn_t lstn; // Send Data socket_iface_send_t send; // Get Peer socket_iface_get_peer_t get_peer; // Close Socket socket_iface_close_t close; };
The 'ifdata' member can be used to reference some internal driver-specific data for this device.
Then, the function pointers represent the interface to the implementation of the driver:
uint8_t (*socket_iface_cnct_t)(void *ifdata, struct socket **s, char *host, uint8_t host_l, uint16_t port)
-
- Request an outbound socket from the interface and attempt to establish a TCP connection to some host:port combination.
uint8_t (*socket_iface_lstn_t)(void *ifdata, struct socket **s, uint16_t port)
-
- Request an inbound socket from the interface and listen for incoming TCP connections on some local port.
uint8_t (*socket_iface_send_t)(void *ifdata, struct socket *s, uint8_t *data, uint16_t size)
-
- Send some data through a previously opened socket.
uint8_t (*socket_iface_get_peer_t)(void *ifdata, struct socket *s, char *ip, uint16_t *port)
-
- Get the IP address and TCP port from the remote peer on an active socket (both arguments are optional).
void (*socket_iface_close_t)(void *ifdata, struct socket *s)
-
- Close a previously opened socket.
Socket management by the driver
As you may have already understood from the definitions above, socket objects are actually managed and 'provided' by network device drivers.
Because some embedded network devices (such as the esp8266 WiFi chip) can sometimes impose some very special socket management constraints, we want our network drivers to handle everything relating to socket management.
Want to know more?
If you would like to create a new network device driver or simply understand in more detail how this all works, we recommend you check out the source code for both the esp8266 driver and the socket library.