8.3.1.6. IP address stickiness support
Note
Code related to this tutorial can be found under examples/custom-network/ip-stickiness in the Anjay source directory.
8.3.1.6.1. Introduction
This tutorial builds up on the previous one and adds support for IP address stickiness, i.e. makes it possible for Anjay to guarantee that the same IP address will be used for connecting to a server configured using a DNS hostname each time, regardless of the order of entries in DNS response.
Important
This tutorial expects Anjay to be configured differently than the previous
ones. WITHOUT_IP_STICKINESS
should be set to OFF
(default) this
time. Otherwise the added code will not be used.
For the IP stickiness feature to work, the preferred_endpoint
field of avs_net_socket_configuration_t
must be supported. Additionally,
avs_net_resolved_endpoint_get_host_port()
also has to be implemented.
8.3.1.6.2. Theory of operation
The avs_net_resolved_endpoint_t
type has been introduced so that resolved addresses (e.g. the
struct sockaddr
family can be shared outside of avs_commons
without the
need to depend on platform-specific types in public API.
Any data up to AVS_NET_SOCKET_RAW_RESOLVED_ENDPOINT_MAX_SIZE
(128 bytes) in
size can be stored in that structure. There is also the size
field that can
be used to preserve the size information.
In our example, we will always store an instance of the sockaddr_union_t, and we will always store
sizeof(sockaddr_union_t)
in the size
field. However, your implementation
is free to use these fields in whatever way you feel is appropriate.
This type is primarily used in the avs_net_addrinfo_resolve()
family of
functions, which is basically a portable version of the getaddrinfo()
API.
However, this API is not used by Anjay. However, the type may also be used for
storage of the preferred endpoint by the Connect
function.
The avs_net_resolved_endpoint_get_host_port()
function is used to convert a
resolved address into stringified form that is the primary form of passing host
addresses in avs_commons
. In Anjay, it is actually used only to determine
the family (IPv4 vs. IPv6) of the stored address.
8.3.1.6.3. Initialization
typedef struct {
const avs_net_socket_v_table_t *operations;
int socktype;
int fd;
avs_time_duration_t recv_timeout;
char remote_hostname[256];
bool shut_down;
size_t bytes_sent;
size_t bytes_received;
avs_net_resolved_endpoint_t *preferred_endpoint;
} net_socket_impl_t;
// ...
static avs_error_t
net_create_socket(avs_net_socket_t **socket_ptr,
const avs_net_socket_configuration_t *configuration,
int socktype) {
assert(socket_ptr);
assert(!*socket_ptr);
(void) configuration;
net_socket_impl_t *socket =
(net_socket_impl_t *) avs_calloc(1, sizeof(net_socket_impl_t));
if (!socket) {
return avs_errno(AVS_ENOMEM);
}
socket->operations = &NET_SOCKET_VTABLE;
socket->socktype = socktype;
socket->fd = -1;
socket->recv_timeout = avs_time_duration_from_scalar(30, AVS_TIME_S);
socket->preferred_endpoint = configuration->preferred_endpoint;
*socket_ptr = (avs_net_socket_t *) socket;
return AVS_OK;
}
The preferred_endpoint
field is intended as a pointer into user-allocated
storage, so we just store that pointer at creation time.
8.3.1.6.4. Changes to the connect function
Note
In addition to the highlighted changes, the original addr
variable has
been renamed to addrs
. This change has not been highlighted for clarity.
static avs_error_t
net_connect(avs_net_socket_t *sock_, const char *host, const char *port) {
net_socket_impl_t *sock = (net_socket_impl_t *) sock_;
struct addrinfo hints = {
.ai_socktype = sock->socktype
};
if (sock->fd >= 0) {
getsockopt(sock->fd, SOL_SOCKET, SO_DOMAIN, &hints.ai_family,
&(socklen_t) { sizeof(hints.ai_family) });
}
struct addrinfo *addrs = NULL;
avs_error_t err = AVS_OK;
if (getaddrinfo(host, port, &hints, &addrs) || !addrs) {
err = avs_errno(AVS_EADDRNOTAVAIL);
} else if (sock->fd < 0
&& (sock->fd = socket(addrs->ai_family, addrs->ai_socktype,
addrs->ai_protocol))
< 0) {
err = avs_errno(AVS_UNKNOWN_ERROR);
} else {
const struct addrinfo *addr = addrs;
if (sock->preferred_endpoint
&& sock->preferred_endpoint->size == sizeof(sockaddr_union_t)) {
while (addr) {
if (addr->ai_addrlen <= sizeof(sockaddr_union_t)
&& memcmp(addr->ai_addr,
sock->preferred_endpoint->data.buf,
addr->ai_addrlen)
== 0) {
break;
}
addr = addr->ai_next;
}
}
if (!addr) {
// Preferred endpoint not found, use the first one
addr = addrs;
}
if (connect(sock->fd, addr->ai_addr, addr->ai_addrlen)) {
err = avs_errno(AVS_ECONNREFUSED);
}
if (sock->preferred_endpoint && avs_is_ok(err)) {
assert(addr->ai_addrlen <= sizeof(sockaddr_union_t));
memcpy(sock->preferred_endpoint->data.buf, addr->ai_addr,
addr->ai_addrlen);
sock->preferred_endpoint->size = sizeof(sockaddr_union_t);
}
}
if (avs_is_ok(err)) {
sock->shut_down = false;
snprintf(sock->remote_hostname, sizeof(sock->remote_hostname), "%s",
host);
}
freeaddrinfo(addrs);
return err;
}
In the code before the connect()
call, if the preferred_endpoint
pointer
is set and filled with valid data, we iterate over all the entries in the list
returned by getaddrinfo()
, and check if any of them matches. If so, that
entry will be passed to the connect()
function. If not, the first entry will
be used.
After a successful connect()
call, the selected address is stored into the
preferred_endpoint
structure.
8.3.1.6.5. avs_net_resolved_endpoint_get_host_port()
avs_error_t
avs_net_resolved_endpoint_get_host_port(const avs_net_resolved_endpoint_t *endp,
char *host,
size_t hostlen,
char *serv,
size_t servlen) {
AVS_STATIC_ASSERT(sizeof(endp->data.buf) >= sizeof(sockaddr_union_t),
data_buffer_big_enough);
if (endp->size != sizeof(sockaddr_union_t)) {
return avs_errno(AVS_EINVAL);
}
const sockaddr_union_t *addr = (const sockaddr_union_t *) &endp->data.buf;
avs_error_t err = AVS_OK;
(void) ((host
&& avs_is_err(
(err = stringify_sockaddr_host(addr, host, hostlen))))
|| (serv
&& avs_is_err((err = stringify_sockaddr_port(addr, serv,
servlen)))));
return err;
}
Since in our implementation avs_net_resolved_endpoint_t
is just a wrapper
around sockaddr_union_t
, we can use the previously introduced stringify_sockaddr_host()
and
stringify_sockaddr_port()
functions.
Please note however, that either of the host
and serv
arguments may be
NULL
, in which case this function shall only fill the non-NULL arguments.