Network sockets

From Dooba Wiki
Jump to navigation Jump to search

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.