Comunicaciones en red
Tanto los modelos B+ como la 2B y la 3B incluyen interfaz Ethernet. La Raspberry Pi 3 modelo B que utilizamos en este taller incluye WiFi pero cualquiera de las otras puede tener también WiFi por un precio de unos 4€ empleando una interfaz WiFi USB. Por tanto cualquier proyecto de Raspberry Pi debe plantearse la posibilidad de comunicar datos a través de una red TCP/IP.
Para programar en red en GNU/Linux, como en la mayoría de los sistemas operativos modernos, se utiliza una interfaz de programación denominada socket API. Se trata de un conjunto de funciones diseñadas para que la programación de redes se parezca mucho a la entrada/salida con archivos.
En el capítulo 2 ya hemos introducido la interfaz socket. En C se
incluye esta interfaz en la biblioteca del sistema libc
que se
incorpora automáticamente. La forma de usar esta biblioteca es
prácticamente lo que hemos visto en el capítulo 2.
Comunicaciones UDP
El siguiente ejemplo muestra el servidor UDP más simple posible.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
static int udp_server_socket(const char* service);
int main() {
int fd = udp_server_socket("9999");
for(;;) {
char buf[1024];
int n = read(fd, buf, sizeof(buf));
assert(n >= 0);
if (n == 0) break;
buf[n]='\0';
printf("%s", buf);
}
close(fd);
return 0;
}
La función udp_server_socket
simplemente crea un nuevo socket UDP
con los parámetros indicados y llama a bind para fijar el puerto
donde escucha mensajes.
static struct sockaddr_in ip_address(const char* host,
const char* service,
const char* proto);
static int udp_server_socket(const char* service)
{
struct sockaddr_in sin = ip_address("0.0.0.0", service, "udp");
struct protoent* pe = getprotobyname("udp");
assert(pe != NULL);
int fd = socket(PF_INET, SOCK_DGRAM, pe->p_proto);
assert (fd >= 0);
assert (bind(fd, (struct sockaddr*)&sin, sizeof(sin)) >= 0);
return fd;
}
La función ip_address
construye una estructura que representa una
dirección completa de un servicio IP (host, puerto y protocolo).
static struct sockaddr_in ip_address(const char* host,
const char* service,
const char* proto)
{
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
struct hostent* he = gethostbyname(host);
if (he != NULL)
memcpy(&sin.sin_addr, he->h_addr_list[0], he->h_length);
else
sin.sin_addr.s_addr = inet_addr(host);
struct servent* se = getservbyname(service, proto);
sin.sin_port = (se != NULL? se->s_port : htons(atoi(service)));
return sin;
}
Mete estas tres funciones en un archivo y compílalo. Prueba que funciona usando un cliente netcat.
El cliente correspondiente en C es similar.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
static int udp_client_socket(const char* host, const char* service);
int main() {
int fd = udp_client_socket("localhost", "9999");
for(;;) {
char buf[1024];
fgets(buf, sizeof(buf), stdin);
int n = write(fd, buf, strlen(buf));
assert(n >= 0);
if (n == 0) break;
}
close(fd);
return 0;
}
La función udp_client_socket
es similar a udp_server_socket
pero
en lugar de llamar a bind llama a connect para fijar la dirección
destino de los mensajes.
static struct sockaddr_in ip_address(const char* host,
const char* service,
const char* proto);
static int udp_client_socket(const char* host, const char* service)
{
struct sockaddr_in sin = ip_address(host, service, "udp");
struct protoent* pe = getprotobyname("udp");
assert(pe != NULL);
int fd = socket(PF_INET, SOCK_DGRAM, pe->p_proto);
assert (fd >= 0);
assert (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) >= 0);
return fd;
}
La función ip_address
ya la comentamos en el servidor. Construye el
cliente y prueba que funciona correctamente con netcat. Después haz
la prueba de integración con servidor y cliente.
Comunicaciones TCP
Desde el punto de vista de la programación la diferencia fundamental con el caso anterior radica en la necesidad de establecer conexiones para realizar una comunicación. Esto complica algo el servidor.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
static int tcp_master_socket(const char* service);
int main() {
int master = tcp_master_socket("9999");
for(;;) {
int fd = accept(master, (struct sockaddr*) NULL, NULL);
assert (fd >= 0);
for(;;) {
char buf[1024];
int n = read(fd, buf, sizeof(buf));
assert(n >= 0);
if (n == 0) break;
buf[n]='\0';
printf("%s", buf);
}
close(fd);
}
close(master);
return 0;
}
Ahora el socket maestro simplemente se usa para crear nuevos sockets esclavos cuando ocurre una conexión con accept. El esclavo se utiliza para manejar los datos relativos a esa conexión.
La función tcp_master_socket
simplemente crea el socket TCP y llama
a bind y a listen.
static struct sockaddr_in ip_address(const char* host,
const char* service,
const char* proto);
static int tcp_master_socket(const char* service)
{
struct sockaddr_in sin = ip_address("0.0.0.0", service, "tcp");
struct protoent* pe = getprotobyname("tcp");
assert(pe != NULL);
int fd = socket(PF_INET, SOCK_STREAM, pe->p_proto);
assert (fd >= 0);
assert(bind(fd, (struct sockaddr*)&sin, sizeof(sin)) >= 0);
assert(listen(fd, 10) >= 0);
return fd;
}
Al igual que antes bind asigna la dirección y puerto en los que escucha, pero ahora tenemos que configurar el backlog con listen (cuántas conexiones se retienen sin aceptar) y aceptar nuevas conexiones con accept.
En realidad esto es una sobre-simplificación. Una vez aceptada la conexión se puede atender en un hilo independiente y aceptar nuevas conexiones en el hilo principal inmediatamente.
El cliente es prácticamente idéntico al de UDP.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
static int tcp_client_socket(const char* host, const char* service);
int main() {
int fd = tcp_client_socket("localhost", "9999");
for(;;) {
char buf[1024];
fgets(buf, sizeof(buf), stdin);
int n = write(fd, buf, strlen(buf));
assert(n >= 0);
if (n == 0) break;
}
close(fd);
return 0;
}
Y la función tcp_client_socket
es prácticamente igual que su
homóloga en UDP.
static struct sockaddr_in ip_address(const char* host,
const char* service,
const char* proto);
static int tcp_client_socket(const char* host, const char* service)
{
struct sockaddr_in sin = ip_address(host, service, "tcp");
struct protoent* pe = getprotobyname("tcp");
assert(pe != NULL);
int fd = socket(PF_INET, SOCK_STREAM, pe->p_proto);
assert (fd >= 0);
assert (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) >= 0);
return fd;
}