Está en la página 1de 45

Sistemas Operativos II

Hugo Carrer

2021
Introducción a Sockets

I Socket es una forma de IPC (InterProcess


Communication) introducida por la Universidad de
Berkeley en su versión de Unix (BSD)
I El concepto de socket nos evita tener que aprender una
interfaz de programación diferente para cada protocolo
I La comunicación por sockets sigue el modelo
cliente-servidor
I El cliente debe conocer la dirección del servidor para la
comunicación
I El servidor no conoce la existencia del cliente
I Una vez establecido el contacto ambas partes pueden
enviar y recibir datos, no se reconoce un maestro o
esclavo
Introducción a Sockets

I Los procesos generan sockets que son la abstracción


utilizada para la comunicación
I A los sockets que van a prestar un servicio se les debe
asociar una dirección
I El cliente utilizando su socket, contacta al servidor (del
que debe conocer la dirección) para transferir datos.
Clases de Sockets

I La comunicación por sockets puede ser local o remota,


para esto disponemos de varias alternativas
I Tipos de sockets:
I Unix sockets
I Internet sockets
I X25 sockets
I Raw sockets
I Nosotros nos vamos a concentrar en Unix e Internet
sockets
Clases de Protocolos

I En forma genérica, tenemos protocolos basados en


caracteres y protocolos basados en paquetes.
I Los protocolos basados en paquetes generan un
envoltorio, que sirve como "sobre" al envío de datos, los
datos viajan encapsulados
I Prácticamente a casi todos los protocolos existentes
podemos ubicarlos en una categoría u otra
I La interfaz de sockets contempla ambas formas de
comunicación
Estilos de Comunicación

I Disponemos de varios tipos:


I SOCK_STREAM: es orientado a conexión full duplex, con
control de errores
I SOCK_DGRAM: No tiene orden secuencial, acepta
mensajes sin conexión.
I SOCK_RAW: Es de bajo nivel con protocolo propio de la
aplicación
Operaciones con Sockets

I Por medio de estas operaciones controlamos la vida


completa de un socket
I Creación
I Apertura
I Lectura
I Escritura
I Cierre
I Para lectura y escritura podemos utilizar las mismas
funciones que con archivos
I Pero también se dispone de funciones específicas que
otorgan ciertas ventajas y facilitan las operaciones para el
programador
Creación de un Socket

I Creamos un socket con:


int socket( int domain, int type, int protocol );

I domain: corresponde a los tipos de socket vistos


(AF_UNIX, AF_INET, etc.)
I type: Establece si es secuencia de caracteres o
datagrama (SOCK_DGRAM, SOCK STREAM)
I protocol: protocolo a utilizar, generalmente se usa 0 para
que adopte el valor por defecto basado en domain y type
I Cada extremo de la comunicación debe generar una
llamada similar.
I socket() devuelve un file descriptor, como sucede en la
creación de archivos.
Creación de un Socket

I Ejemplo:
int sock_serv;
sock_serv =
socket(int domain, int type, int protocol);
if( sock_serv < 0 ) {
perror( "socket" );
exit(1);
}

I socket() devuelve un número positivo si la llamada fue


exitosa, o un valor negativo en caso contrario.
Estructura sockaddr

I La estructura fundamental del API de sockets de Berkeley


se denomina sockaddr (dirección de socket)
I Es capaz de almacenar una dirección, requisito
fundamental para la comunicación
I Esta dirección tiene las características del entorno en que
se aplica
I Generalmente es importante conocer y controlar la
dirección del socket del servidor
I No así el caso de los clientes, que podemos dejar que el
sistema les asigne una dirección automáticamente al
utilizarlos
Estructura sockaddr

struct sockaddr {
unsigned short sa_family;
char sa_data[14];
}

I sa_family: es el tipo de dirección almacenada


I sa_data: es el valor de la dirección, el 14 es arbitrario
puede ser mayor o menor
I Valores de sa_family pueden ser: AF_UNIX AF_INET
AF_X25 AF_IPX AF_APPLETALK AF_XNS AF_ISO
AF_NS
Familias de Direcciones

I Para conexiones Unix elegimos: AF_UNIX


I Para conexiones Internet elegimos: AF_INET
I sa_data contiene la dirección del socket, en el caso
AF_UNIX, es un archivo especial (de dispositivo) que tiene
las siguientes características (Ej.)
srwxr-xr-x 1 owner group 0 2010-03-27 10:29 nombre
Asociar una Dirección al Socket

I Interviene la estructura sockaddr


I Como sockaddr es genérica, cada familia de protocolos
admite una propia que después ocupa el lugar de
sockaddr con casting.
I Para el caso Unix tenemos:
struct sockaddr_un {
short sun_family; /* AF_UNIX */
char sun_path[108]; /* Path name */
};

I sun_family: es justamente AF_UNIX


I sun_path: es el nombre de archivo especial.
Llenado de sockaddr_un

I Los argumentos se llenan así:


#include <sys/socket.h>
struct sockaddr_un st_servidor;
/* Limpieza de la estructura */
memset( &st_servidor, 0, sizeof( st_servidor ) );
/* Carga de la familia de direcciones */
st_servidor.sun_family = AF_UNIX;
/* Carga de nombre de archivo, asumiendo que
fue pasado como primer argumento al programa */
strncpy( st_servidor.sun_path, argv[1],
sizeof( st_servidor.sun_path ) );
Asociar una Dirección al Socket

I Se hace con la función bind()


int bind(int sd,
struct sockaddr *my_addr,int addrlen);

I sd: file descriptor devuelto por socket()


I my_addr: estructura que contiene la dirección,
sockaddr_un para Unix, sockaddr_in para Internet que es
convertida a sockaddr mediante casting.
I addrlen: Longitud del campo dirección.
I bind() devuelve cero si la llamada fue exitosa, o un valor
negativo en caso contrario.
Asociar una Dirección al Socket

int resultado;
resultado = bind( sock_serv,
(struct sockaddr *) &st_servidor,
SUN_LEN( &st_servidor ) );
if( resultado < 0 ) {
perror( "bind" );
exit(1);
}

I A partir de aquí, se ha relacionado el proceso en ejecución


con la dirección correspondiente y se está en condiciones
de recibir datos.
Asociar una Dirección al Socket

I El cliente puede optar por no realizar una asociación de


dirección a su socket
I Para el caso de un servidor es esencial porque publicando
esa dirección va a poder ser contactado
I El cliente remite sus datos con el primer datagrama que
envía a destino para que el servidor lo pueda ubicar
Modo Unix sin Conexión

I Muchas de las funciones usadas en sockets son comunes


a todos los modos de comunicación
I A continuación se ve el formato específico para el modo
Unix (Local) sin conexión
I Esto obliga a especificar en cada envío el destino a que va
dirigida la comunicación
Modo Unix sin Conexión
Envío sin Conexion

I Estas funciones son exclusivas para sockets. Envío:


ssize_t sendto( int sockfd, const void *buf,
size_t len, int flags,
const struct sockaddr *dest_addr,
socklen_t addrlen );

I sockfd: Socket descriptor.


I buf: Buffer a transmitir (puntero a la primera posición).
I len: Longitud del buffer de transmisión, se obtiene
generalmente con sizeof( buf ).
I flags: Banderas que se agrupan haciendo un OR por bits,
ej.: MSG_DONTWAIT, utilizada para envío no bloqueante.
I dest_addr: Estructura sockaddr con datos del destino.
I addrlen: Longitud de la estructura sockaddr.
Recepción sin Conexión

ssize_t recvfrom( int sockfd, void *buf,


size_t len, int flags,
struct sockaddr *src_addr,
socklen_t *addrlen );

I sockfd: Socket descriptor.


I buf: Buffer de recepción (puntero a la primera posición).
I len: Longitud del buffer de recepción, se obtiene
generalmente con sizeof( buf ).
I flags: Banderas que se agrupan haciendo un OR por bits,
ej.: MSG_DONTWAIT, utilizada para envío no bloqueante.
I src_addr: Estructura sockaddr con datos del emisor.
I addrlen: Longitud de la estructura sockaddr.
Envío y Recepción

I sendto() y recvfrom() devuelven la cantidad de caracteres


transmitidos o recibidos si fueron exitosas, -1 en caso
contrario.
I El error queda almacenado en errno y es posible conocer
su contenido utilizando perror().
Ejemplos de Programas

I Se muestra en código aparte un programa servidor en


modo caracteres para la familia de direcciones AF_UNIX y
un cliente que envía un mensaje.
I El servidor itera permanentemente recibiendo datos de los
clientes, e imprimiendo el contenido en la salida estándar.
sock_srv_u_sc.c y sock_cli_u_sc.c
Modo Unix con Conexión

I En este caso el servidor debe ligarse al socket y esperar


por una conexión
I En este caso aparecen funciones adicionales
I El manejo de los sockets es diferente porque la conexión
una vez establecida permite funciones más simples para el
envío y recepción ya que las características de origen y
destino son conocidas y no hay que especificarlas en cada
envío
Gestionando una Conexión

I Función listen():
int listen( int sockfd, int backlog );

I sockfd: Socket descriptor.


I backlog: Cantidad de conexiones simultáneas admitidas.
I listen() devuelve 0 si fue exitoso, -1 en caso contrario.
I Función accept():
int accept( int sockfd, struct sockaddr *addr,
socklen_t *addrlen );

I sockfd: Socket descriptor.


I addr: Estructura sockaddr usada para la comunicación
I addrlen: Tamaño de la estructura sockaddr
Gestionando una Conexión

I accept() devuelve un file descriptor positivo si fue exitoso,


-1 en caso contrario.
I El file descriptor devuelto por accept() se usará para las
comunicaciones siguientes con ese cliente hasta el cierre
de conexión.
I El servidor sigue listo para recibir y gestionar nuevas
conexiones, hasta que se supere el valor: backlog.
Solicitando una Conexión

I Función connect(), ejecutada por el cliente:


int connect( int sockfd,
const struct sockaddr *addr, socklen_t addrlen );

I sockfd: Socket descriptor.


I addr: Estructura sockaddr usada para la comunicación.
I addrlen: Tamaño de la estructura sockaddr.
I connect() devuelve 0 si fue exitoso, -1 en caso contrario.
Ejemplos de Programas

I Se muestra en código aparte un programa servidor en


modo streaming para la familia de direcciones AF_UNIX y
un cliente con el que dialoga.
I El servidor sigue escuchando por nuevas conexiones,
después de generar una tarea que atiende a un cliente en
particular.
sock_srv_u_cc.c y sock_cli_u_cc.c
Estructura sockaddr_in

I En el caso TCP/IP AF_INET es la dirección y puerto de


destino.
I Esto no se refleja claramente en sockaddr y para
solucionarlo se generó:
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
};

I sin_family: Familia de direcciones (AF_INET)


I sin_port: Número de puerto
I sin_addr: Dirección Internet (estructura)
Estructura sockaddr_in

I sockaddr_in tiene entre sus componentes una dirección IP


expresada como una estructura:
struct in_addr sin_addr; // Dirección Internet

I Que tiene el formato:


struct in_addr {
uint32_t s_addr;
};

I Se puede llenar con esta función:


int inet_aton( const char *name,
struct in_addr *addr);
Llenado de sockaddr_in

I Los argumentos se llenan así:


#include <sys/socket.h>
struct sockaddr_in st_servidor;
long int test;
/* Limpieza de la estructura */
memset(&st_servidor, 0, sizeof( st_servidor ) );
/* Carga de la familia de direccioens */
st_servidor.sin_family = AF_INET;
/* Carga del número de puerto */
st_servidor.sin_port = htons( num_puerto );
/* Carga de dirección IPv4 del socket, asumiendo que
fue pasado como primer argumento al programa */
test = aton( argv[1], st_servidor.sin_addr );

I aton() retorna 0 si falló, valor positivo si tuvo éxito


Estructura sockaddr_in

I La estructura sockaddr_in se asocia a sockaddr con un


casting (al final debe quedar sockaddr)
(struct sockaddr *)&nombre_struct_sockaddr_in

I Ahora el bind() se realiza en la misma forma que para


Unix.
Estructura hostent

I La estructura hostent contiene datos del host remoto, su


contenido es:
struct hostent {
char *h_name; // nombre oficial del host
char **h_aliases; // conjunto de punteros
int h_addrtype; // AF_INET para IPv4
int h_length; // 4 para IPv4
char **h_addr_list; // conjunto de punteros
char *h_addr; // sinónimo de h_addr_list[0]
}

I Sirve para sostener los datos del host remoto que utiliza
en cada envío, usándose en la práctica el campo h_addr.
Estructura hostent

I hostent se maneja con funciones específicas.


I Entre estas tenemos:
struct hostent *gethostbyname(
__in const char *name );

name: nombre del host que resuelve DNS o /etc/hosts


struct hostent *gethostbyaddr( const char *addr,
size_t length, int format );

I addr: dirección IP del host.


I format: AF_INET para IPv4, AF_INET6 para IPv6
I length: longitud de la dirección en bytes.
Orden de Bytes
I Los fabricantes de computadoras no se pusieron de
acuerdo sobre la forma de almacenar cantidades y esto
originó la existencia de dos formatos opuestos.
I Estos son:
I Big Endian
I Little Endian
I Big Endian almacena el byte más significativo de un
número en la posición más baja de memoria.
I Little Endian almacena el byte menos significativo de un
número en la posición más baja de memoria.
I Procesadores de las series Motorola, SPARC, etc. son Big
Endian
I Procesadores de las series Intel y similares son Little
Endian.
I La Internet transmite en formato Big Endian, la máquina
virtual de Java también es Big Endian.
Orden de Bytes

I Algunas arquitecturas de procesador pueden trabajar con


ambos formatos.
I Ej.: ARM, PowerPC, Alpha de DEC, PA-RISC, MIPS.
I Se los conoce como sistemas Middle Endian.
I Un byte se almacena siempre en el mismo orden, la
diferencia está en el orden de los bytes.
Orden de Bytes

I Programa C para saber si un procesador es Big o Little


Endian.
#include <stdio.h>
int main(void)
{
int i = 1;
char *p = (char *) &i;
if ( p[0] == 1 )
printf("Little Endian\n");
else
printf("Big Endian\n");
return 0;
}
Valores Especiales

I Elegir un número concreto de puerto es algo que


generalmente hacen los procesos servidores.
I El sistema operativo lo otorga si está disponible, si no lo
está da un error.
I Para el caso de un cliente, es posible que simplemente
pida un puerto disponible al SO, sin importar el número
(que será superior a 1023).
st_cliente.sin_port = htons( 0 );

I También es posible asociar direcciones IP sin tener que


averiguar su valor.
st_cliente.sin_addr.s_addr = htonl( INADDR_ANY );

I La dirección que se toma corresponde a cualquiera que


tenga disponible el host.
Envío y Recepción

I Se pueden usar las funciones clásicas de bajo nivel:


ssize_t write( int fd, const void *buf,
size_t count );

I fd: File descriptor, en este caso, socket descriptor.


I buf: buffer a transmitir (puntero a la primera posición).
I count: Longitud del buffer a transmitir, se obtiene
generalmente con sizeof( buf ).
I write() devuelve la cantidad de caracteres transmitidos si
fue exitoso, -1 en caso contrario.
Envío y Recepción

I Para lectura tenemos:


ssize_t read( int fd, void *buf, size_t count );

I fd: File descriptor, en este caso: socket descriptor.


I buf: buffer de recepción (puntero a la primera posición).
I count: Longitud del buffer de recepción, se obtiene
generalmente con sizeof( buf ).
I read() devuelve la cantidad de caracteres leidos si fue
exitoso, -1 en caso contrario.
I Como en todos los casos, el error queda almacenado en
errno.
Envío con Flags

I Para el envío con flags se usa:


int send( int fd, const void *buf, size_t count,
int flags );

I fd: File descriptor, en este caso, socket descriptor.


I buf: buffer a transmitir (puntero a la primera posición).
I count: Longitud del buffer a transmitir, se obtiene
generalmente con sizeof( buf ).
I flags: Banderas que se agrupan haciendo un OR por bits,
ej.: MSG_DONTWAIT, utilizada para envío no bloqueante,
suele ir 0.
I send() devuelve la cantidad de caracteres transmitidos si
fue exitoso, -1 en caso contrario.
Recepción con Flags

I Para lectura tenemos:


int recv( int fd, void *buf, size_t count
unsigned int flags );

I fd: File descriptor, en este caso: socket descriptor.


I buf: buffer de recepción (puntero a la primera posición).
I count: Longitud del buffer de recepción, se obtiene
generalmente con sizeof( buf ).
I flags: Banderas que se agrupan haciendo un OR por bits,
ej.: MSG_DONTWAIT, utilizada para envío no bloqueante,
suele ir 0.
I read() devuelve la cantidad de caracteres leidos si fue
exitoso, -1 en caso contrario.
Conexión y sin Conexión
I Para las comunicaciones en la familia de protocolos
TCP/IP se dispone de sockets orientados y no orientados
a conexión
I Recordar que esto se selecciona con la llamada a
socket(), que es previa a la asociación bind(), para la que
se usa la estructura sockaddr, esta es quien indica la
familia de direcciones (AF_UNIX, AF_INET)
I Si se ha seleccionado sin conexión en TCP/IP el protocolo
que se usará es UDP
I Si se ha seleccionado con conexión, el protocolo entonces
será TCP
I El sistema solo se encarga de realizar el Three Way
Handshake de TCP al convocar connect()
I Se sugiere verificar esto mediante sniffing trabajando en
modo debugging
Ejemplos de Programas

I Se muestra en código aparte un programa servidor sin


conexión para la familia de direcciones AF_INET y un
cliente que envía un mensaje.
sock_srv_i_sc.c y sock_cli_i_sc.c
I El servidor itera permanentemente recibiendo datos de los
clientes y respondiendo.
I El cliente envía un mensaje y espera solo una respuesta.
Ejemplos de Programas

I Se muestra en código aparte un programa servidor


orientado a conexión para la familia de direcciones
AF_INET y un cliente que dialoga con el servidor.
sock_srv_i_cc.c y sock_cli_i_cc.c
I El servidor itera permanentemente recibiendo conexiones
en forma concurrente.
I Cuando acepta una conexión, genera un proceso hijo con
fork(), para que atienda a ese cliente en particular.
I El servidor sigue a la espera de nuevas conexiones.
I Mientras, los procesos hijos quedan en diálogo con cada
cliente, hasta el momento que se cierra la conexión.

También podría gustarte