Programacion de Socket Linux
Programacion de Socket Linux
de Socket Linux
Sean Walton
Traducción
Clave Informática l+D, S.A.
1/512
Índice de contenido
Introducción
PARTE I
Programación de red desde la perspectiva del cliente
2/512
Resumen: aplicación de herramientas y numeración IP
Capítulo 3. Tipos de paquetes de Internet
El paquete de red fundamental
Campo versión
Campo header len
Campo serve_type
Campo ID
Campo frag_offset y flags dont_frag y more_frags
Campo time_to_live (TTL)
Campo protocol
Campo options
Campo data
Análisis de varios paquetes
Cuestiones relacionadas con los paquetes
Tipos de paquete
Cómo encajan los protocolos IP
Cómo escudriñar la red con Tcpdump
Escritura de un escudriñador de red a medida
Resumen: elección de los mejores paquetes para el envío de mensajes
3/512
Transaction TCP (T/TCP): un TCP sin conexión
Envío de un mensaje directo
Asociación del puerto al socket
Cómo enviar el mensaje
Cómo coger el mensaje
Garantía de llegada de un mensaje UDP
Cómo fortalecer la fiabilidad de UDP
Secuencia de los paquetes
Redundancia de paquete
Verificación de la integridad de los datos
Fallos imprevistos del flujo de datos
Tareas enrevesadas: una introducción a la multitarea
Resumen: modelos conectados frente a modelos sin conexión
4/512
Capa 2: extensiones de la gestión de mensajes de error-control (ICMP)
Capa 3; de host a host (UDP)
Capa 3: flujos de host (TCP)
Capa 4: capa de aplicación
Diferencias fundamentales entre OSI e IP
¿Qué da servicio a qué?
Resumen: de la teoría a la práctica
PARTE II
La perspectiva del servidor y el control de carga
5/512
Capítulo 7. División de la carga: multitarea
Definición de multitarea: procesos frente a threads
Cuándo se debe utilizar la multitarea
Características de la multitarea
Diferencias en las tareas
¿Cómo creo un proceso?
¿Cómo creo un thread?
La llamada del sistema _clone(): la llamada de los valientes
Comunicación entre tareas
Venciendo al reloj: condiciones de carrera y exclusiones mutuas (mutex)
Condiciones de carrera
Exclusión mutua (mutex)
Problemas de exclusión mutua de Pthread
Prevención del interbloqueo
Control de hijos y eliminación de procesos zombis
Preste más atención a los hijos: prioridad y planificación
Entierro de los procesos zombis: limpieza tras la finalización
Ampliación de los clientes y servidores actuales
Llamada de programas externos con el exec del servidor
Resumen: distribución de la carga del proceso
6/512
Lectura bajo demanda
Escrituras asíncronas
Conexiones bajo demanda
Resolución de bloqueos de la E/S no deseados con poll() y select()
Implementación de los tiempos de espera
Resumen: elección de las estrategias de E/S
7/512
Resumen: discusión de las características de rendimiento
8/512
PARTE III
Examen objetivo de los sockets
9/512
Interfaces
Eventos y excepciones
Formatos especiales
Registro-Estructura
Colección de funciones
Soporte del lenguaje
Soporte activado frente a soporte orientado
Cómo incluir objetos en los lenguajes no orientados a objetos
Resumen: mentalidad orientada a objetos
10/512
Cómo simplificar la conexión con los sockets
Cómo ocultar los detalles de implementación
Implantación de componentes reutilizables que faciliten el uso de las
interfaces
Demostración de los procesos del diseño del marco de trabajo
Disposición del marco de trabajo
Definición de las características generales
Agrupamiento en componentes principales
Creación de la jerarquía del marco de trabajo
Definición de las capacidades de cada clase
Prueba del marco de trabajo basado en sockets
El cliente/servidor de eco
Multitarea de igual a igual
Limitaciones de implementación
Envío de mensajes desconocidos o indefinidos
Incorporación de la multitarea
Resumen: un marco de trabajo basado en sockets de C++ simplifica la
programación
11/512
Código inalcanzable
Complejidad dar y tomar
Simplificación de programas con interfaces establecidas
El enigma de la herencia múltiple
Incremento del tamaño del código
El dilema de la administración de proyectos
Obtención del personal apropiado en el momento oportuno
Fenómeno WISCY ("Whisky")
Prueba de (des-)integración
Resumen: tenga cuidado con las pendientes
PARTE IV
Sockets avanzados: más prestaciones
12/512
Recuperación desde un estado erróneo
Resumen: creación de una caja de herramientas para RPC
13/512
Limitaciones de la difusión
Multidifusión de mensajes a un grupo
Unión a grupos de multidifusión
Envío de mensajes de multidifusión
Cómo la red proporciona la multidifusión
Salida del mensaje de multidifusión
Limitaciones de la multidifusión
Resumen: compartición eficiente de los mensajes
14/512
¿A qué se parece !Pv6?
¿Cómo funcionan juntos IPv4 e IPv6?
Cómo poner a prueba IPv6
Configuración del núcleo
Configuración de las herramientas
Transformación de las llamadas IPv4 a IPv6
Transformación de sockets raw a IPv6
Transformación de sockets ICMPv6 a IPv6
El nuevo protocolo multidifusión
Pros y contras de lPv6
Incorporación esperada de Linux
Resumen: traslado del código hacia el futuro
PARTE V
Apéndices
15/512
Apéndice B. API de red
Conexión a la red
Comunicación por un canal
Terminación de conexiones
Conversiones de datos de red
Herramientas de direccionamiento de red
Controles de socket
16/512
Datagram (Clase)
Broadcast (Clase)
MessageGroup (Clase)
Excepciones de Java
java.io.IOException (Clase)
java.net.SocketException (Clase)
Clases de soporte Java
java.net.DatagramPacket (Clase)
java.net.InetAddress (Clase)
Clases Java de E/S
java.io.InputStream (Abstract Class)
java.io.ByteArraylnputStream (Clase)
java.io.ObjectlnputStream (Clase)
java.io. OutputStream (Clase abstracta)
java.io.ByteArrayOutputStream (Clase)
ava.io.ObjectOutpurStream (Clase)
java.io.BufferedReader (Clase)
java.io.PrintWriter (Clase)
Clases de sockets Java
java.net.Socket (Clase)
java.net.ServerSocket (Clase)
java.net.DatagramSocket (Clase)
javanet.MuIticastSocket (Clase)
índice alfabético
17/512
Acerca del autor
Sean Walton obtuvo su titulación en Ciencias de la computación en 1990 en la
Brigham Young University, en la especialidad de teoría de la multitarea y fusiones
de lenguajes. En 1988 fue contratado por el departamento de ciencias de la
computación de la BYU como ayudante para el desarrollo de teorías y métodos
para la administración de procesos transputer, migración y comunicación. Comenzó
a trabajar con sockets BSD cuando trabajó como administrador en el departamento
de ciencias de la computación. Durante su trabajo en Hewlett-Packard, desarrolló
el método de detección automática de lenguaje (entre PostScript y PCL), que
implementan hoy día las impresoras LaserJet 4 y superiores. Asimismo, desarrolló
un sistema operativo de micro en tiempo real para el microcontrolador 8052 que
permite emular motores de impresión.
Sean posee años de experiencia profesional en varios tipos de programación y
administración UNIX, incluido Linux, HPUX, Ultrix, SunOS y System V. Como
consecuencia de su trabajo en diferentes sistemas, ha centrado su atención en
estilos de programación independientes del sistema, que permiten una fácil
portabilidad.
En los últimos años, Sean ha trabajado como asistente profesional, autor-
diseñador de cursos, y profesor de conceptos básicos de computadoras, obtención
de requisitos, OOA/D, Java y C++. A principios de 1998 comenzó a trabajar con
sockets Java, integrando esta información en su curso de Java. Sus servicios como
profesor están muy solicitados. Durante su trabajo en el equipo Nationwide
Financial Process Improvement, definió los procesos de análisis y diseño para
integrar los nuevos desarrollos con los estándares existentes. Ha dirigido y
diseñado el producto Nationwide Insurance Authentication, que incluye políticas y
autenticación divididas.
Dedicatoria
Al dador de todo bien y príncipe de la paz perfecta.
Agradecimientos
Soy consciente de que este libro no habría sido posible sin la colaboración
esencial de varias personas. En primer lugar, a mi amada esposa, Susan, que me
ha animado constantemente, dándome todo el tiempo que he necesitado para
llevar a término este proyecto. En segundo lugar a Wendel, mi padre, que me
enseñó la importancia de una buena organización y presentación. En tercer lugar,
al movimiento Linux que, de forma desinteresada, me ha proporcionado un sistema
operativo efectivo y fiable sobre el que poder trabajar. En cuarto lugar, a Beverly
Scherf, quien me abrió los ojos y me mostró una forma efectiva de comunicarme.
Por último, no fui consciente de lo importante que era este trabajo para mí
profesionalmente hasta recibir el entusiasmo y el apoyo de mis amigos Charles
Knutson y Mike Holstein.
18/512
Introducción
Expansión del desafío en la programación
La mayoría de los objetivos en programación tiene que ver de forma inmediata
con funciones o tareas que se llevan a cabo en una computadora de sobremesa o
un portátil. En contadas ocasiones, estas tareas involucran la comunicación con
algo más que un ratón, un teclado, un monitor y un sistema de archivos. Un paso
más allá en la programación nos llevaría a considerar programas en varios equipos
conectados a través de un canal de red. La programación en red constituye una
expansión de este concepto, ya que se precisa coordinar tareas y enviar
asignaciones.
La unidad fundamental de toda la programación de red en Linux (y en la mayoría
de los sistemas operativos) es el socket. De la misma forma que la E/S de archivos
permite la conexión con el sistema de archivos, el socket permite establecer
conexión con la red. El socket es un medio que el programa utiliza para direccionar,
enviar y recibir mensajes.
La programación de socket o de red va más allá que la programación de tareas
individuales o incluso de multitarea, debido a la mayor potencia y elementos
introducidos por el multiprocesamiento. La potencia es algo obvio: Parallel Virtual
Machines (PMV), o máquinas virtuales en paralelo, como Beowolf pueden llevar a
cabo un volumen de procesamiento mucho mayor mediante la organización de
bloques de tareas y la distribución de éstas entre las computadoras de la red. Los
temas a dilucidar aquí tienen que ver con la consecución de un rendimiento óptimo,
la coordinación de las transferencias y la administración de E/S.
En este libro se describen y se ofrecen varias soluciones para dar respuesta a
estos temas. Está pensado para dar respuesta a las necesidades primarias y a
medio plazo del programador de red profesional.
19/512
La Parte II amplía la programación de socket con servidores, técnicas de
multitarea, control E/S y opciones de socket.
• Parte V: Apéndices.
Los apéndices vienen a consolidar muchos de los recursos que son de
importancia para los sockets. El primer apéndice incluye tablas y listados que
son demasiado largos para incluirlos en los capítulos. El segundo y tercer
apéndices describen las API Socket y Kernel.
El sitio web que sirve de apoyo al libro y que contiene todo el código fuente de
los ejemplos incluidos en el mismo, lo puede obtener de nuestra página Web
(www.pearson-neducacion.com), en el "Área de descarga" de la sección
"Informática".
Puede obtener más información de la propia página web del autor, www.linux-
socket.org.
20/512
información que anda buscando.
3. Ejecutar el programa.
21/512
Por último, el libro no trata de otros sistemas operativos (Microsoft Windows o
Macintosh). Algunos capítulos ponen de manifiesto algunas comparaciones, pero
todos los algoritmos soportan Linux/UNIX.
22/512
Parte I
En esta parte
1 Recetario del cliente de red
2 Elocuencia del lenguaje de red TCP/IP
3 Tipos de paquetes de Internet
4 Envío de mensajes entre peers
5 Explicación del modelo de capas de red
23/512
Capitulo I
En este capítulo
Un mundo conectado mediante sockets
Generalidades del direccionamiento TCP/IP
Escucha del servidor: el algoritmo básico del cliente
Resumen: ¿qué ocurre entre bastidores?
¡Esa maldita batería RAM CMOS! Bien, ¿que hora es? No se ve el reloj, llamaré
a la información horaria. 1-614-281-8211. Ring."...son las ocho y veintitrés y
cuarenta segundos". Clic. ¡Humm! ¿a.m. o p.m.? ¿Esperan ellos que lo mire
afuera?
La computadora que utiliza probablemente se encuentra conectada a algún tipo
de red. Podría ser una intranet corporativa completa con firewalls dentro de
Internet; o quizá un par de computadoras que conecta en su tiempo libre. La red
conecta estaciones de trabajo, servidores, impresoras, arrays de discos, faxes,
módems, etc. Y cada conexión de red utiliza o suministra un servicio. Algunos
servicios ofrecen información sin ninguna interacción. Igual que en una llamada a la
información horaria, un cliente de red básico se conecta con un servidor y lo
escucha.
¿Qué tipos de servicios ofrecen los servidores? Muchos. Todos los servicios se
ajustan a cuatro categorías de recursos: común, restringida o valiosa, compartida y
delegada. He aquí algunos ejemplos de cada una:
Común Espacio de disco (normalmente respaldado),
Restringida Impresoras, módems, arrays de discos.
Compartida Bases de datos, control de proyectos, documentación.
Delegada Programas remotos, consultas distribuidas.
En este capítulo se describe el código de un cliente básico que se conecta a un
servidor. Este proceso ayuda a entender todo lo que envuelve a la escritura de
programas de red. El cliente inicialmente se conecta al servicio de información
24/512
horaria del servidor (o a cualquier otro servicio que no necesite entrada de datos al
inicio). Junto con el procedimiento, en este capítulo se explican las diferentes
llamadas, sus parámetros y errores más comunes.
El programa cliente necesita una interfaz para enviar-recibir y una dirección para
conectarse al servidor. Los clientes y los servidores necesitan utilizar sockets para
conectarse y enviar mensajes independientemente de la situación. Considere el
ejemplo telefónico otra vez: el auricular tiene dos partes, un micrófono (transmisión)
y un altavoz (recepción). Los sockets tienen también estos dos canales.
Adicionalmente, el número telefónico es en esencia la dirección única para el
teléfono.
De igual modo, el socket tiene dos partes o canales: uno para la escucha y otro
para el envío (como el modo de lectura-escritura para un archivo de E/S). El cliente
(o la llamada) se conecta con el servidor (o contestador) para comenzar una
conversación de red. Cada host ofrece varios servicios estándar (véase
/etc/services en el sistema de archivos), como el número telefónico de información
horaria correcta.
EJECUCIÓN DE LOS EJEMPLOS DEL LIBRO
Se pueden ejecutar la mayor parte de los ejemplos del libro sin estar conectado a la red,
si se tiene habilitada la red en el núcleo y el demonio servidor de red, inetd, está
ejecutándose. De hecho, muchos ejemplos utilizan la dirección local (o loopback) de
127.0.0.1. Si no se tienen los controladores de red configurados y funcionando, la
mayoría de las distribuciones Linux incluyen como mínimo todo lo que se necesita para el
funcionamiento en red del loopback.
El programa cliente debe seguir varios pasos para comunicarse con un equipo
homólogo o servidor. Estos pasos tienen que seguir una secuencia particular. Por
supuesto, se puede preguntar: "¿por qué no se reemplazan todos estos pasos con
menos llamadas?" En medio de cada paso, el programa puede seleccionar de
muchas opciones. No obstante, algunos pasos son opcionales. Sí el cliente omite
algunos pasos, normalmente el sistema operativo rellena esas opciones con los
valores predeterminados. Puede seguir algunos de estos pasos básicos para crear
un socket, configurar el host de destino, establecer el canal a otro programa de red y
cerrarlo. La Figura 1.1 muestra gráficamente los pasos que el cliente toma para
conectarse a un servidor.
25/512
FIGURA 1.1 Cada cliente interactúa con el sistema operativo realizando
varias llamadas en sucesión.
En la siguiente lista se describe cada paso:
1. Crear un socket. Se selecciona de diversos dominios de red (por ejemplo,
Internet) y clases de socket (como flujo).
2. Configurar las opciones del socket (opcional). Se dispone de muchas
opciones que afectan al comportamiento del socket Una vez abierto el socket, el
programa puede cambiar estas opciones en cualquier momento. (Para obtener
más detalle véase el Capítulo 9, "Cómo romper las barreras del rendimiento".)
3. Asociar con una dirección-puerto (opcional). Se aceptan conexiones de
todos o de una sola dirección IP, y se establece un puerto de servicio. Si se
omite, el sistema operativo asume cualquier dirección IP y asigna un número de
puerto aleatorio. (En el Capítulo 2, "Elocuencia del lenguaje de red TCP/IP", se
tratan las direcciones y puertos en mayor detalle.)
4. Conectar a un equipo homólogo-servidor (opcional). Se extiende y establece
un canal bidireccional entre el programa local y otro programa de red. Si se omite,
26/512
el programa utiliza una comunicación dirigida o sin conexión.
5. Cerrar la conexión parcialmente (opcional). Se restringe el canal de envío o
de recepción. Se puede utilizar este paso después de la duplicación del canal.
6. Enviar-recibir mensajes (opcional). Una razón para prescindir de cualquier
E/S podría incluir comprobación de disponibilidad de host.
7. Cerrar la conexión. Por supuesto este paso es importante: si los programas
no cierran las conexiones terminadas, los programas que consumen bastante
tiempo de CPU pueden agotar casualmente los descriptores de archivo.
En los siguientes apartados se describen algunos de estos pasos, definiendo las
llamadas del sistema y suministrando ejemplos.
27/512
paquetes necesitan tener toda la información necesaria para alcanzar el destino.
Como una carta, un paquete debe incluir la dirección de destino y de origen. El
paquete conmuta de una computadora a la próxima a través de las conexiones (o
enlaces). Si la red pierde un enlace mientras se transmite un mensaje, el paquete
encuentra otra ruta (conmutación de paquetes), y si no se alcaliza el host, el router
envía un error hacia el origen. Esto asegura la habilidad de los datos. Los caminos
rotos en la red son pausas de red. Probablemente encontró alguna vez alguna
pausa de red.
28/512
Como los 3 o 5 dígitos de extensiones telefónicas, cada dirección de la
computadora tiene varios puertos a través de los cuales la computadora se
comunica. Estos no son físicos; más bien, son abstracciones del sistema. Toda la
información se dirige a través de la dirección de red como el número telefónico
principal.
El formato de escritura estándar para la dirección IP es [0-255].[0-255].[0-255].
[0-255]—por ejemplo, 123.45.6.78. Se debe observar que el cero y el 255 son
números especiales utilizados en máscaras de red y multidifusión, así que hay que
tener cuidado con su uso (en el Capítulo 2 se trata la numeración IP con mayor
detalle). Los puertos Internet separan normalmente estos números con otro punto o
dos puntos:
[0-255].[0-255].[0-255].[0-255]:[0-65535]
Por ejemplo, 128.34.26.101:9090 (IP= 128.34.26.101, puerto-9090).
[0-255].[0-255].[0-255].[0-255]:[0-65535]
Por ejemplo, 64.3.24.24.9999 (IP=64.3.24.24, puerto=9999).
PUERTOS CON DOS PUNTOS CONTRA PUNTO
La notación con dos puntos es más común para los puertos que la notación punto
decimal.
Cada dirección IP ofrece efectivamente sobre 65.000 números de puerto al que
un socket se puede conectar. Véase el Capítulo 2 para más información.
29/512
Listado 1.1 Un algoritmo básico de cliente TCP
/*******************************************/
/*** Un algoritmo básico de cliente. ***/
/*******************************************/
Crear un socket.
Crear una dirección de destino para el servidor.
Conectar con el servidor.
Leer y visualizar cualquier mensaje.
Cerrar la conexión.
El algoritmo del Listado 1.1 puede parecer demasiado simplificado, y a lo mejor
lo es. Sin embargo, la conexión y la comunicación con un servidor es realmente
fácil. Los siguientes apartados describen cada uno de estos pasos. Puede
encontrar el programa fuente completo al final de este libro y en el CD-ROM
adjunto.
30/512
PF_IPX Protocolos de Novell.
PF_INET6 Protocolos de Internet IPv6; pila TCP/IP.
type SOCK_STREAM Fiable, flujo de datos secuencial (flujo de bytes) [Protocolo
para el control de la transmisión (TCP)].
SOCK_RDM Fiable, datos en paquetes (no implementado ya en muchos
sistemas).
SOCK_DGRAM No fiable, datos en paquetes (datagrama) [Protocolo de
datagrama del usuario (UDP)].
SOCK_RAW No fiable, datos en paquetes de bajo nivel.
protocol Éste es un entero de 32 bits en el orden de bytes de red
(véase el apartado sobre la ordenación de bytes de red en el
Capítulo 2). Muchos tipos de conexión soportan sólo el
protocolo = 0 (cero). SOCK_RAW necesita especificar un
valor de protocolo entre 0 y 255.
Por ahora, los únicos parámetros que el ejemplo utiliza son domain=PF_INET,
type=SOCK_STREAM y protocol=0 (cero).
VALORES PF FRENTE AF
En este libro se utilizan los dominios PF_* (familia de protocolos) en la llamada de socket,
porque la forma correcta es el uso de las constantes de dominio FP_*. Sin embargo,
muchos programas utilizan las constantes AF_* (familia de direcciones) de forma
intercambiable. Tenga cuidado y no se confunda cuando observe el código fuente que
utiliza el estilo AF. (Los archivos de cabecera en C definen las constantes AF_* como
PF_*.) Si desea, puede utilizar la notación AF_* perfectamente, pero puede aparecer
alguna incompatibilidad en un futuro.
A continuación aparece un ejemplo para una llamada TCP/IP de generación de
flujo:
int sd;
sd = socket(PF_INET, SOCK_STREAM, 0);
sd es el descriptor de socket. Funciona de la misma forma que un descriptor de
archivo fd:
int fd;
fd = open(...);
La llamada devuelve un valor negativo cuando ocurre un error y coloca el código
de error en errno (la variable global estándar para los errores de biblioteca). He
aquí algunos de los errores más comunes que puede obtener:
31/512
para crear un socket. SOCK_RAW y PF_PACKET necesitan los privilegios de
root.
32/512
• La conexión ofrece el canal para los mensajes. Una vez que se descuelga el
auricular, existe un canal a través de cual dos o más personas pueden
conversar. Su número de teléfono no es importante a menos que la persona
tenga que llamarle.
33/512
trabaje correctamente.
He aquí el registro genérico y el registro INET para que pueda compararlos (con
los archivos de cabecera):
struct sockaddr { struct sockaddr_in {
unsigned short int sa_family; sa_family_t sin_family;
unsigned char sa_data[14]; unsigned short int sin_port;
}; struct in_addr sin_addr;
unsigned char _pad[];
};
Se debe observar que sa_family y sin_family son comunes a las dos
estructuras. La tarea que cada procedimiento de llamada ejecuta cuando recibe
este registro es comprobar el primer campo. Se observa que éste es el único
campo que está en el orden de bytes del host (véase el Capítulo 2). El campo de
relleno (denominado sa_data y _pad) puede ser común para todas las familias de
sockaddr. Por convención, la estructura genérica sockaddr y la estructura INET
sockaddr_in tienen que ser de 16 bytes de tamaño (la estructura IPv6,
sockaddr_in6, es de 24 bytes). Así, el campo de relleno completa la estructura con
los bytes sin usar.
Puede observar que la longitud del campo de relleno en sockaddr_in está
ausente. Esto es meramente una convención. Como el relleno se tiene que
establecer a cero, el tamaño real no es importante. (En esta definición de
sockaddr_in, es 8 bytes de tamaño.) Algunas implementaciones pueden definir
campos adicionales para cálculos internos. No se preocupe de ellos —y no los use,
porque no se puede garantizar la disponibilidad de estos campos de un sistema a
otro y de una versión a otra. Cualquier cambio en los campos no estandarizados
puede paralizar al programa. En todos los casos, es mejor una iniciación a cero de
la instancia entera de la estructura.
En la siguiente tabla se describe cada campo. También se ofrecen ejemplos de
posibles valores.
Nombre de campo Descripción Orden de bytes Ejemplo
sin_family La familia de protocolos Host, nativo AF_
34/512
TRANSFORMACION DE LOS TIPOS DE SOCKADDR
Con otros SO compatibles con UNIX, puede trasformar cualquiera de los miembros de la
familia sockaddr_* a sockaddr para evitar advertencias de compilación. Los ejemplos aquí
mostrados no utilizan ninguna transformación simplemente para ahorrar espacio (y
porque Linux lo permite).
El código puede parecerse al del Listado 1.2.
Listado 1.2 Ejemplo de connect()
/*************************************************************/
/*** Trozo de código mostrando la iniciación e ***/
/*** invocación de la llamada del sistema connect(). ***/
/*************************************************************/
#define PORT_TIME 13
struct sockaddr_in dest;
char *host = "127.0.0.1";
int sd;
/**** Crear el socket y hacer otro trabajo. ****/
bzero(&dest, sizeof(dest)); /* comenzar con una pizarra limpia. */
dest.sin_family = AF_INET; /* Seleccionar la red deseada. */
dest.sin_port = htons(PORT_TIME); /* Seleccionar el puerto. */
inet_aton(host, &dest.sin_addr); /* Dirección remota. */
if ( connect(sd, &dest, sizeof(dest)) == 0 ) /* ¡Conectar! */
{
perror("Socket connection");
abort();
}
...
En este código, la llamada del sistema connect() requiere de varios pasos
preparatorios antes de que se conecte el socket al servidor. Primero, crear una
estructura sockaddr_in. Utilizar la dirección del servidor para la segunda línea. Si
se desea conectar a un servidor diferente, coloque la dirección IP apropiada en
esta cadena. El programa prosigue con otras iniciaciones, incluyendo la llamada
del sistema socket(). Cuando comienza el funcionamiento con la estructura, se
pone a cero con la función bzero(). El programa establece la familia a AF_INET.
Luego, el programa establece el puerto y la dirección IP. Las herramientas de
transformación htons() y inet_aton() utilizadas aquí se tratarán en el Capítulo 2.
La siguiente llamada es la conexión al servidor. Se debe observar que el
fragmento de código comprueba los valores devueltos para cada llamada de
35/512
procedimiento. Esta política es una de las muchas claves para la realización de
programas de red robustos.
Después de que el programa establezca la conexión, el descriptor del socket,
sd, se convierte en un canal de lectura/escritura entre los dos programas. Muchos
servidores a los que estamos acostumbrados ofrecen transacciones individuales y
luego cortan la comunicación (por ejemplo, un servidor HTTP 1.0 envía el archivo
pedido y luego cierra la conexión). Para interactuar con estos tipos de servidor, los
programas tienen que enviar la consulta, obtener la respuesta y cerrar la conexión.
36/512
perror("FILE* conversión failed");
else if ( fscanf(sp, "%*s, %*s, %*s\n",/* Utilizar como siempre. */
NAME, Name, ADDRESS, Address, PHONE, Phone) < 0 )
{
perror("FScanf");
...
Únicamente los sockets de flujo se pueden transformar de forma fiable a un flujo
FILE*. La razón es simple: los sockets de datagramas son normalmente sin
conexión— un mensaje se envía y ya está. También, los sockets de flujo ofrecen
integridad de datos y fiabilidad del mensaje, mientras que los datagramas son poco
fiables. Los datagramas son similares a la colocación de un mensaje en un sobre
con destino y sello que se envía a través del sistema postal: el mensaje puede no
llegar en absoluto. Una conexión FILE* debe ser un canal abierto. Si intenta
transformar un datagrama, puede perder datos críticos. En el Capítulo 3 "Distintos
tipos de paquetes de Internet", se puede obtener más información de flujos contra
datagramas.
Las conexiones de socket FILE* ofrecen recursos excelentes de búsqueda y de
análisis gramatical para el programador de red. Sin embargo, cuando se utilizan, se
deben comprobar todos los valores devueltos, incluyendo *printf() y *scanf(). Se
debe observar en el ejemplo anterior: si el valor devuelto de fscanf() es menor que
cero, había un error.
SEGURIDAD Y FIABILIDAD DE LA RED
La seguridad y fiabilidad son de extrema importancia cuando se crean los programas de
red. En el momento de la escritura de este libro, los sistemas operativos Microsoft han
afrontado varios problemas de seguridad fundamentales, algunos involucrados con las
conexiones de red. Cuando se escriben programas, hay que asegurarse que los buffers
no se puedan desbordar y que todos los valores devueltos se comprueben. En un
software de configuración de bazar, se puede solicitar la entrada de otros en el código
fuente. La revisión del punto de este software bazar es un recurso inmenso de
conocimiento y experiencia—utilícelo.
Con la llamada del sistema read(), se pueden obtener cualquiera de los
siguientes errores:
37/512
información mientras ofrece más control:
#include <sys/socket.h>
#include <resolv.h>
int recv(int sd, void *buf, int len, unsigned int flags);
La llamada recv() sólo se diferencia de la llamada read() en las señalizaciones.
Las señalizaciones dan un mayor control sobre el qué obtener, e incluso ofrece
control de flujo. Estos valores pueden agruparse con el operador O (FLAG1 I
FLAG2 I ...). Dentro de circunstancias normales, el parámetro se establece a cero.
Podría preguntarse por qué no se utiliza la llamada del sistema read() cuando las
señalizaciones son cero, ya que en este caso no se diferencian en nada. Como
sugerencia, utilice recv() en lugar de read()—esto puede ayudarle más adelante
cuando el programa se hace más complejo. También, generalmente hablando, es
mejor utilizar un conjunto de herramientas que herramientas mezcladas.
Finalizando, read() comprueba que tipo de descriptor de E/S envió y ejecuta la
llamada del sistema más apropiada.
He aquí las señalizaciones más usuales que se pueden utilizar para controlar la
llamada del sistema recv(). Se puede encontrar un listado más completo en el
Apéndice B, "API de red".
38/512
recv(). Se puede utilizar O_NOBLOCK con la llamada del sistema fcntl(). Esto
hace que el socket no se bloquee indefinidamente.)
OBTENCIÓN DE PAQUETES FRAGMENTADOS
Los programas se ejecutan mucho más rápido que las redes. Algunas veces un paquete
llega a la computadora en trozos, porque los routers de red lo fragmentan para adaptarlo
a las limitaciones de la red. Si se llama a recv() cuando esto ocurre, se puede obtener un
mensaje incompleto. Ésta es la razón de por qué MSG_PEEK puede dar diferentes
resultados en llamadas consecutivas: la primera llamada puede dar 500 bytes y la
segunda puede ser de 750 bytes. Asimismo, ésta es la razón de por qué existe una
señalización MSG_WAITALL.
La llamada del sistema recv() es más flexible que read(), permitiendo el uso de
distintas señalizaciones para modificar su comportamiento. Para realizar una
lectura normal desde un canal de socket (equivalente a read()), haga lo que se
indica a continuación:
...
int bytes_read;
bytes_read = recv(sd, buffer, MAXBUF, 0);
...
Para leer de forma no destructiva desde el canal de socket, haga lo siguiente:
int bytes_read;
bytes_read = recv(sd, buffer, MAXBUF, MSG_PEEK);
...
Para leer de forma no destructiva los datos fuera de banda del socket, haga lo
siguiente:
int bytes_read;
bytes_read = recv(sd, buffer, MAXBUF, MSG_OOB | MSG_PEEK);
...
El primer ejemplo obtiene la información del servidor proporcionando un buffer,
un tamaño y ninguna señalización. El segundo ejemplo muestra la información.
Existe un flujo intencionado aquí: ¿qué ocurre si el servidor envía más información
que la que pueda aceptar el buffer? No es un error crítico; nada fallará. El algoritmo
puede simplemente perder los datos que no se lean.
La llamada del sistema recv() produce códigos de error similares a read(), y
adicionalmente los códigos siguientes:
39/512
descriptor de socket se pueden obtener aún estos códigos de error, debido a que
read() ejecuta internamente a la llamada recv().
Cierre de la conexión
Una vez que se obtuvo la información que necesitaba del servidor y que todo se
realizó bien, se debe cerrar la conexión. De nuevo, existen dos formas para poder
cerrar la conexión. Muchos programas utilizan la llamada del sistema close() de la
E/S estándar:
#include <unistd.h>
int close(int fd);
Una vez más, el descriptor de socket (sd) se puede substituir por el descriptor
de archivo (fd), funciona igual. Si se ejecuta con éxito, el valor devuelto es cero.
Esta llamada produce un sólo error:
Valor Función
40/512
1 (uno) Sólo lectura (pensar en "I" de "input").
41/512
puede cerrar el canal después del envío
de los datos, si ofrece un mensaje de
boletín simple (como la hora actual).
8. Empieza la correspondencia de los
datos.
La red requiere un lenguaje y algoritmo muy particulares con objeto de
establecer una conexión. Ante todo, con la llamada del sistema socket() se inicia el
rodamiento del balón con la creación del auricular del teléfono, Este auricular
permite a un programa enviar y recibir mensajes si está conectado. El programa
puede utilizar las funciones normales read() y write() como se usan en los canales
y descriptores de archivo. Alternativamente, se pueden utilizar las llamadas del
sistema más especializadas como recv().
La red IP tiene varias características que necesitan de una mayor explicación.
En el próximo capítulo se trata la numeración IP, los puertos y la ordenación de
bytes. También se explican herramientas muy útiles para transformar la
información, simplificando los esfuerzos de programación.
42/512
Capitulo II
En este capítulo
Generalidades de la numeración IP
Números de puertos de host IP
Ordenación de bytes de red
Diferentes clases de sockaddr
Canales con nombre de UNIX
Resumen: aplicación de herramientas y numeración IP
Cuando se trabaja con Internet se necesita conocer cómo dirigir los mensajes
con el fin de que lleguen correctamente. La dirección es una parte importante de la
creación de un mensaje. Como en el teléfono, una computadora debe tener una
dirección o ID para que pueda obtener los mensajes correctos y otras
computadoras puedan dirigir el tráfico como corresponda.
En el capítulo anterior se introdujo el concepto el socket En este capítulo se
amplía el tema con la API de Socket. Primero se trata el sistema de numeración IP,
de cómo el encaminamiento funciona en un nivel abstracto, los formatos binarios
correctos y diferentes tipos de socket
Generalidades de la numeración IP
La numeración IP utiliza una dirección de longitud fija. Esta restricción requirió
una gran planificación cuando se diseñó el protocolo. El protocolo ofreció
soluciones a varias cuestiones durante su época de vida: identificación de
computadoras, organización de red, encaminamiento y resolución de direcciones.
Ahora afronta cuestiones nuevas como el crecimiento explosivo y la pérdida de
direcciones.
43/512
Identificación de la computadora
Las redes implican la compartición de un único recurso (el cable de red) con
varias computadoras. Debido a que toda esa información podría fluir en un medio
de red, cada computadora debe aceptar la información. Sin embargo, si cada
computadora de nuestra red se llamara Bert o Ernie, otros hosts no serían capaces
de determinar el destino real. Cada computadora de red necesita una única
identificación (ID). Pero eso no es todo.
CÓMO AFRONTAR LA COLISIÓN DE DIRECCIÓN
Una estación de trabajo no trabaja bien con la red cuando comparte una dirección
(colisión de red). Alguien que haya intentado localizar una colisión de red puede confirmar
que es bastante duro arreglarlo. Es aún peor cuando una de las computadoras está
utilizando alguna asignación de dirección dinámica (como DHCP). La forma más obvia (y
laboriosa) de resolver este problema es mirar cada computadora del lugar.
Las redes son dinámicas y tienden a aumentar en complejidad. La computadora
tiene que ser localizable fácilmente por su ID tanto como se amplíe la red. Toda
información colocada en la red consume una porción de ese valioso recurso, así
que cualquier información innecesaria no se debería incluir en el mensaje.
Algunos sistemas en red incluyen un mapa de rutas en el mensaje. Cada
servidor de la lista coge el mensaje y lo pasa al siguiente servidor. Este método
reduce el rendimiento de la red al descender el porcentaje de los datos reales con
la cabecera. Si se codifica la dirección del destino en la propia dirección, el mapa
de rutas es innecesario.
Las computadoras de red conectadas tienen todavía un único ID llamado
Control de acceso al medio (MAC); un ejemplo es el ID ethernet. La computadora
utiliza este ID para arrancar en red (sistemas sin disco que sólo tienen memoria
RAM y todo el almacenamiento está en el servidor). El ID ethernet tiene seis bytes
de tamaño, y usualmente lo veremos escrito en hexadecimal: 00:20:45:FE:A9:0B.
Cada tarjeta ethernet tiene uno y es único.
Desafortunadamente, no se puede utilizar ese ID para identificar a la
computadora exclusivamente. Tiene dos problemas fundamentales. Primero, cada
servidor de encaminamiento debe obtener cada ID de la red. La base de datos de
los ID podría ser muy grande y reducir significativamente el tiempo de
encaminamiento para cada mensaje. Segundo, todas las interfaces no tienen una
MAC (PPP, por ejemplo).
El ID de la computadora podría tener un mecanismo incorporado para el
encaminamiento del mensaje. Este mecanismo es como una dirección de una
carta: de lo más general a lo más específico. Los ID Internet resuelven los
problemas de unicidad mientras ofrecen indicadores para las cuestiones de
encaminamiento.
Una dirección IP tiene más ventajas que una MAC. El número puede cambiar
(no es fijo), así la implantación de clusters consistentes de direcciones es más fácil,
y los equipos portátiles no se encuentran con problemas. La red puede asignar la
IP a la MAC utilizando el Procolo de resolución de direcciones (ARP). Véase la
44/512
RFC 826 en el CD-ROM adjunto.
PROTOCOLO DE RESOLUCION DE DIRECCIONES (ARP)
ARP es una tabla de traducción simple que asigna a una IP su MAC asociada. Todos los
mensajes de la red deben tener una MAC para que el adaptador (o los controladores)
puedan recoger el mensaje. El origen puede desconocer la MAC del host destinatario, el
cual se puede ocultar detrás de varios routers. El router más bien envía el mensaje a los
routers que controlan las subredes definidas en la dirección IP del destinatario. Cuando el
mensaje alcanza un router con ARP, éste comprueba las tablas. Si el router encuentra la
dirección IP, la MAC del paquete consigue el destino correcto. En otro caso, el router
envía un mensaje de difusión a la subred para resolver la dirección IP.
45/512
Clase Dirección Rango de direcciones, descripción
0.0.0.0 a (Reservadas)
0.255.255.255
A 1.0.0.0 a 224-2 ó 16.777.214 de nodos por cada asignación de segmento.
Esta clase tiene 126 segmentos.
126.255.255.255
Utilizadas en organizaciones grandísimas que dividen la red en
subredes como se necesite, como por ejemplo un proveedor de
servicio de Internet (ISP).
127.0.0.0 a (Reservadas para la interfaz de loopback).
127.255.255.255
B 128.XXX.0.0 a 216-2 ó 65.534 nodos para cada asignación de segmento. Esta
clase tiene 64*256 segmentos disponibles.
191.XXX.255.255
Utilizadas en organizaciones grandes como empresas y
universidades. Se pueden crear subredes. La XXX es la subred
asignada (por ejemplo, 129.5.0.0 es una red asignada). Nota:
Este espacio de direcciones se encuentra poco utilizado.
C 192.XXX.XXX.0 a 28-2 ó 254 nodos por cada asignación de segmento. Esta clase
tiene 32*65.536 segmentos disponibles.
223.XXX.XXX.255
Utilizadas en empresas pequeñas o por particulares. La
dirección tiene varios espacios disponibles.
46/512
nuevas (localizadas en las RFC 1517-1519) identifican la localización y el camino
de un host utilizando los bits superiores de la dirección.
Con el protocolo IP se introdujo este esquema de numeración para la
identificación de la computadora y sus agrupaciones de forma eficiente. Con esto,
un router puede determinar rápidamente si intercepta un paquete o lo mueve a la
interfaz que conduce a la subred de destino. La propagación es muy importante:
simple recepción de un paquete, se comprueba y se pasa si no se acepta. Cada bit
que el router acepta significa un retardo en la red. El routerno deber retardar más
bits que los necesarios para determinar la relevancia del mensaje. En otras
palabras, no debe haber retardos. Por ejemplo, la conmutación de teléfono normal
en los Estados Unidos de América no intenta resolver el número telefónico
completo; los primeros tres dígitos indican un área o región, y los siguientes tres
dígitos indican la estación.
Máscaras de subredes
Algunas de las clases necesitan más filtrado que otras. Una red con 16 millones
de nodos es excesiva si todos se encuentran agrupados en un único espacio.
Como uno mismo se configura la computadora Linux y la red, puede haber
observado el término máscara de subred. La llegada de CIDR ha simplificado la
complejidad de grandes subredes [RFC950].
La máscara de subred se especifica para identificar el grupo de direcciones
contiguas que una interfaz puede alcanzar en una red. Con esto se obtiene un filtro
adicional que permite a ciertos mensajes específicos pasar de un lado a otro.
Cuando un mensaje llega, el router utiliza la máscara de subred en la dirección de
destino del mensaje. Si coincide, el router deja pasar el mensaje. Puede construir
máscaras de la forma que desee, pero el conjunto de bits menos significativo
especifica la subred.
Por ejemplo, si tenía una red pequeña de ocho máquinas, y previo que el grupo
creciera no más de cinco máquinas, podría establecer la máscara de subred del
router a 187.35.209.176. El último bit en la dirección está en 176 (1001,0000). El rango
de direcciones está en la parte 'X' (subred activa) de 1101,XXXX. A partir de aquí,
puede repartir las direcciones: 187.35.209.176 (router), 187.35.209.177 (host numero
1), hasta la 187.35.209.222. Puede tener cualquier combinación, pero el bit del
conjunto menos significativo es el marcador.
MASCARAS DE SUBRED Y DIRECCIONES POR OMISION
Al usar la máscara de red como una dirección host puede causar conflicto y pérdida de
paquetes. Esto es debido a la dirección especial 0 (cero). El ejemplo, 187.35.209.0, tiene
los últimos ocho bits a cero. Si la dirección se corresponde con la máscara de red, esa es
la dirección cero. Debería siempre reservar la dirección cero para la dirección de red de la
máscara. Asimismo, puede haber observado que el ejemplo omite 187.35.209.223. Si
fuera a usar ese ID, ese host nunca podrá ver un mensaje. El número 223 tiene la subred
activa todos a unos: 1101,1111. Ésta es una dirección de difusión. No debería usar ésa
como una dirección de host.
En muchos casos no necesita preocuparse de cómo configurar routers, la
configuración está más allá del ámbito de este libro. Normalmente, cuando observa
47/512
la máscara de subred en la configuración de red, puede aceptar la máscara que el
script le ofrece.
48/512
127.255.255.255. El primer grupo de direcciones significa "esta red" en nomenclatura
IP (véase la nota sombreada a continuación). Si tiene una dirección de la red
128.187.0.0, usando el valor 0.0.25.31 en el mensaje resulta 128.187.25.31,
implícitamente.
En 1992, el Comité de arquitectura de Internet (IAB—véase la RFC 1160 para el
diseño, ingeniería y administración de Internet), llegó a estar muy preocupado con
el crecimiento explosivo de Internet—incluso con todas las técnicas de
direccionamiento y planificación esmeradas. La adjudicación de direcciones
sobrepasó los 450 millones de nodos. El comité vio que el incremento en las
asignaciones acabaría con el espacio de direcciones disponibles. Sin embargo,
sólo el 2% de las asignaciones fueron realmente utilizadas. ¿Dónde estaban el
resto de todas las direcciones?
IAB asigna rangos de direcciones a compañías en bloques enteros. Las
compañías, anticipándose a usos adicionales y con el fin de mantener sus
direcciones lo más contiguas posible, compraban rangos muy superiores a sus
necesidades reales.
INFLACION DE ASIGNACIONES
En un lugar pequeño donde trabajé me mostraron su asignación de 128 direcciones; sólo
30 estaban en uso a la vez. Me asignaron 10 unidades. Nunca utilicé más de 2. A día de
hoy, creo que aquellas direcciones pueden seguir asignadas a mí, aunque dejé la
compañía hace cuatro años.
En todo, las excepciones de dirección y las asignaciones desperdiciadas han
comido la mayor parte del espacio de direcciones. Estimaciones recientes indican
que las redes de Clase B y C están completas. Los ISP están utilizando más de
clase A. Dentro de poco tiempo, la demanda puede utilizar todas las direcciones.
¿Qué va hacer Internet con el espacio de direcciones perdido? ¿Cómo puede el
direccionamiento IP actual utilizarse mejor? IAB, ahora llamado Internet
Corporation for assigned names and numbers (ICANN), no puede reclamar
fácilmente las unidades inutilizadas vendidas a las compañías, porque los
departamentos IT de las compañías han ya asignado esas direcciones dentro de la
organización.
Muchas compañías ahora utilizan Dynamic host configuration protocol (DHCP)
[RFC2131], lo cual asigna una dirección IP una vez iniciado. El host no posee una
dirección IP de forma fija. DHCP ayuda también con la seguridad: los crackersde
las computadoras adoran las direcciones IP fijas. Si recuerda, bootp envía un
mensaje de difusión para encontrar un servidor bootp. Cuando se encuentra, el
servidor emite una dirección IP. ÍX' forma similar, un host normal carga su
generador do peticiones durante el inicio. El generador de peticiones envía un
mensaje de difusión para encontrar un servidor DHCP activo. Cuando lo encuentra,
el servidor asigna una dirección de su grupo de direcciones disponibles y se la
pasa (junto con la máscara de red apropiada) al generador de peticiones.
El generador de peticiones DHCP acepta la información y configura los
protocolos de red del host local. En algunos casos esto puede llevar varios
segundos, debido a la congestión de la red. Hasta que el host local es configurado
49/512
correctamente, puede sufrir de amnesia IP(el host no conoce su propia dirección).
AMNESIA DE HOST O IP
La amnesia de host o IP también sucede cuando el nombre de la red y el host no están
correctamente configurados. Puede ser un problema serio. Hasta ejecutar un ping
127.0.0.1 puede no funcionar. Si el host tiene amnesia, compruebe la distribución. La
distribución RedHat (y derivados) utiliza /etc/sysconfig/network y /etc/sysconfig/network-
scripts/ifcfg-* para definir y establecer los valores.
Otra solución es prolongar el tamaño de la dirección IP. Introduciendo: ;IPv6
IRFC2460]! IPv6 en resumidas cuentas es cuatro veces el tamaño de una dirección
IP: 128 bits contra 32 bits. IPv6 también cambió la apariencia de forma dramática:
8008:4523:F0E1:23:830:CF09:1:385
El resultado es una dirección difícil de recordar.
La ventaja principal de IPv6 es que tiene el espacio de direcciones más grande
para trabajar. Con más de 3x1038 direcciones elimina probablemente cualquier
problema de limitaciones de direcciones por muchísimo tiempo. Para obtener más
información véase el Capítulo 17, "Cómo compartir mensajes con multidifusión,
difusión y Mbone".
50/512
CÓMO COMPARTIR PUERTOS EN SMP
La regla de que dos sockets no pueden compartir el mismo puerto se aplica también a
los procesadores simétricos. La razón es simple: los procesadores, como recursos,
comparten la memoria y el sistema operativo. Si se tienen dos sistemas operativos
funcionando, se pueden tener teóricamente dos sockets en programas separados en
ejecución con el mismo número de puerto, con tal de que estos residan en diferentes
espacios del sistema operativo.
Todos los servicios estándar tienen asignados números de puerto. Se puede ver
la lista completa en /etc/services, en el Apéndice A, "Tablas de datos", y en la
estación de trabajo Linux. A continuación se muestran unos pocos puertos
estándar:
Puerto Nombre del servicio, alias Descripción
1 tcpmux Multiplexor del servicio de puertos TCP.
7 echo Servidor eco.
51/512
el sistema operativo asigna un puerto local disponible al socket.
PUERTOS PRIVILEGIADOS
Como parte de la seguridad del núcleo, los puertos con valor menor a 1024 requiere
acceso de root o privilegiado. Por ejemplo, si desea crear un servidor de hora (puerto 13),
el usuario root debe de ejecutar el programa (o con SUID de root). SUID es un acrónimo
de "Set user ID" (establece el ID del usuario). Cada programa en el sistema de archivos
puede permitir a los usuarios que lo ejecuten como si el propietario del programa lo
hiciera. Normalmente, cuando un usuario ejecuta un programa, este programa tiene los
mismos permisos del usuario. Algunas veces el programa necesita permisos diferentes o
adicionales. Al cambiar el identificador del usuario, el programa se puede ejecutar como si
lo hiciera el propietario. Por ejemplo, /usr/bin/at necesita acceder a las tablas cron
(propiedad del root). Para que esto ocurra, /usr/bin/at cambia automáticamente el acceso
a root. Para obtener más información, se puede consultar la documentación del UNIX
estándar. Precaución: el SUID de root es una fuente potencial de riesgos de seguridad.
Nunca se debe crear o establecer el SUID mientras se es root, a menos que se conozca
el programa muy bien y se conozcan todos los riesgos.
Como se ha advertido previamente, si no se realiza la asignación de un puerto a
un socket, el sistema operativo asigna uno automáticamente. Estos puertos
asignados dinámicamente y automáticamente se llaman puertos efímeros. Linux
parece seguir el estilo BSD en la asignación de puertos: asigna los puertos
efímeros a partir del puerto 1024.
DEPURACIÓN SEGURA
Al experimentar con diferentes puertos, se pueden seguir estas reglas simples: Ejecutar y
depurar un programa como un usuario corriente (no como root). Usar puertos y
direcciones que no afecten a otros o causen riesgos de seguridad. Registrar el origen de
todos los mensajes entrantes.
52/512
La utilidad de los distintos endianness es un asunto largo de debate. Aquí no se
resucitan estas discusiones, sólo se apuntan lus usos importantes y las diferencias.
Por supuesto, ¿por qué no se puede utilizar sencillamente ASCII hexadecimal? La
representación es ineficaz, doblando el número de bytes que se necesitan. Para
que las computadoras de una red heterogénea se comuniquen eficientemente,
tienen que usar binario y deben establecer un endianness. El endianness de una
computadora o host es el orden de bytes de host. De la misma forma, el
endianness de una red se denomina orden de bytes de red. El orden de bytes de
red es siempre big-endian.
53/512
Llamada Significado Descripción
54/512
incluye varias páginas de manual aplicables en los apéndices.)
• EPIPE. fd está conectado a un canal o socket cuyo fin de lectura está cerrado.
Cuando esto ocurre el proceso de escritura recibe una señal SIGPIPE; si se
atrapa, bloquea o ignora esto, se devuelve el error EPIPE. Este error ocurre
sólo en la segunda llamada write(). Un escenario puede ser:
1. Un cliente se conecta a un servidor y envía algunos datos.
2. El servidor lee parte del mensaje, luego cierra la conexión (o falla).
3. No se conoce ningún problema, el cliente envía el siguiente bloque de datos.
De igual modo que en la llamada write(), puede substituir el descriptor de
archivo (fd) con un descriptor de socket (sd). Como ejemplo, a continuación puede
ver parte de una salida de socket típica con write():
int sd, bytes_written=0, retval;
55/512
if ( retval >= 0 )
bytes_written += retval;
else
/*--Informar de error de conexión.--*/*/
}
Por otro lado, a continuación se muestra parte de la salida de un socket típico
con fprintf() a través de la transformación del descriptor de socket a FILE*:
FILE *sp;
int sd;
sd = socket (AF_INET, SOCK_STREAM, 0);
Todos los parámetros son los mismos que en write() excepto el último—flags.
La llamada del sistema send() tiene varias opciones que ayudan a revisar el
funcionamiento de la llamada:
• MSG_OOB. Envía datos "fuera de banda" (OOB). Como se utilizó antes, eso
permite enviar un byte a un equipo homólogo, cliente o servidor para indicar
una condición urgente. El receptor tiene un sólo byte dedicado para datos OOB;
subsiguientes mensajes OOB sobreescriben el último mensaje. Cuando un
mensaje OOB llega, el sistema operativo emite una SIGURG (señal urgente) al
programa responsable.
56/512
ocasiona que el paquete omita las tablas de rutas, obligando a la red a intentar
contactar directamente con el receptor. Si el destino es inaccesible
directamente, la llamada produce un error ENETUNREACH (red inalcanzable).
Solamente utilizan esta opciónprogramas de diagnóstico o de encaminamiento.
int bytes_sent;
bytes_sent = send(sd, buffer, MAXBUF, 0);
...
/"Enviar cualquier dato fuera de banda desde el canal de socket.*/
int bytes_sent;
bytes_sent = send(sd, buffer, MAXBUF, MSG_OOB | MSG_NOSIGNAL);
...
El segundo ejemplo selecciona MSG_OOB y MSG_NOSIGNAL. Se puede
recordar que estos ejemplos se diferencian en la llamada del sistema write() al
añadir un parámetro nuevo para el control del socket. Además, el socket se debe
conectar al servidor o host con objeto de usar write(). Aquí están algunos errores
con los que se puede encontrar:
57/512
• EAGAIN. El socket se marca como no bloqueante y la operación solicitada
bloquearía. Éste no es un error, simplemente es un "no preparado todavía".
Intentar el envío otra vez más tarde .
58/512
recv(sockfd, buffer, MAXBUF-1 , 0);
printf("%s", buffer);
close(sockfd);
return 0;
}
Si quiere utilizar este algoritmo para obtener una respuesta larga, necesita
cambiar la última sección a lo siguiente:
/ *****************************************************************/
/*** Revisión del código para obtener respuestas largas. ***/
/ ****************************************************************/
/ * - - Vaciar el buffer y leer la respuesta CORTA. — - * / * /
do
{
bzero(buffer, MAXBUF);
bytes = recv(sockfd, buffer, MAXBUF, 0);
printf("%s", buffer);
}
while ( bytes > 0 );
close(sockfd) ;
Este cambio funciona correctamente si el servidor cierra la conexión después
del envío de los datos. De otro modo, el programa espera indefinidamente los
datos.
La espera de la información es un problema particular con la programación de
sockets. Algunas veces puede confiar en que el servidor cierre la conexión cuando
el envío está hecho. Sin embargo, algunos servidores dejan el canal abierto hasta
que el cliente lo cierra. Hay que ser consciente de que si el programa espera por
mucho tiempo, puede estar afrontando este tipo de problema.
59/512
Sockets con PF_LOCAL Actualmente no se conecta a la red Este tipo se
nombre usa estrictamente para colas de procesamiento
en el sistema de archivos.
Protocolo de PF_INET (Demostrado ya).
Internet
Protocolo de PF_IPX Para la comunicación con redes Novell.
Novell
AppleTalk PF_APPLETALK Para la comunicación con redes AppleTalk.
Puede encontrar más protocolos soportados y definidos en el Apéndice A, Cada
tipo utiliza su propio sistema de nombrado y convenciones, y todos ellos usan la
API de Socket. Esto debería realizar la programación más directa.
Lamentablemente, hay demasiado contenido para incluir aquí, así que este libro fija
su atención principalmente en el protocolo de Internet.
bzero(&addr, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy (addr.sun_path, "/tmp/mysocket"); /* Asignar nombre. */
60/512
Todo debería parecer relativamente familiar. El campo sun_path permite un
camino de hasta 104 bytes (incluyendo la terminación NULL). Desde aquí, puede
usar todas las llamadas API normales. Es lo mismo para todos los protocolos
soportados.
Después de ejecutar este fragmento, puede mirar en /tmp para observar el
archivo nuevo. Asegúrese de borrar este archivo antes de la ejecución de nuevo
del programa.
61/512
Capitulo III
Tipos de paquetes de
Internet
En este capítulo
La red física soporta distintos tipos de redes lógicas como Novell (IPX),
Microsoft (NetBEUI), AppleTalk y por supuesto, TCP/IP. Cada red lógica utiliza
mensajes de datos distintos llamados paquetes, como se definió en el capítulo
anterior. Los paquetes pueden ser mensajes reales en la línea de transmisión (los
cuales tienen mucha más información incluida) o solamente el mensaje que se está
enviando.
El paquete de red lógico en un nivel genérico consta de información sobre el
origen, destino y datos de carga útil. Cada red lógica ofrece diversos grados de
características e interfaces (protocolos). Con la programación de red, están
disponibles todos los tipos de paquetes y protocolos. Cada tipo tiene puntos fuertes
y débiles significativos. Como en la compra de herramientas, la elección del tipo de
paquete depende de cómo se use.
Se puede elegir de entre cuatro protocolos de paquetes de Internet: IP raw,
ICMP, UDP (generación de mensajes no fiables), y TCP (generación de flujo),
basados todos ellos en capas ubicadas encima de la red física (véase la Figura
3.1). En este capítulo se describe cada tipo y se presenta sus ventajas,
desventajas y usos típicos.
62/512
FIGURA 3.1 La API de Sockets ofrece distintos niveles de mensajes fiables.
63/512
Listado 3.1 Definición de la estructura IP
/************************************/
/*** Definición del paquete IP. ***/
/************************************/
#typedef unsigned int uint;
#typedef unsigned char uchar;
struct ip_packet {
uint version:4; /* [Nota] versión de 4 bits. */
uint header_len:4; /* (Nota] tamaño de cabecera en words. */
uint serve_type:8; /* [Nota] cómo servir el paquete. */
uint packet_len:16; /* Tamaño total del paquete en bytes. */
uint ID:16; /* [Nota] ID del paquete. */
uint _reserved: 1 ; /* Siempre cero. */
uint dont_frag: 1 ; /* Flag para permitir la fragmentación. */
uint more_frags:1 ; /* Flag para "continúan más fragmentos". */
uint frag_offset:13; /* Ayudar a la recomposición. */
uint time_to_live:8; /* [Nota] número de saltos de router permitidos. */
uint protocol:8; /* [Nota] ICMP, UDP, TCP. */
uint hdr_chksum:16; /* Suma de comprobación de la cabecera. */
uint Ipv4_source:32; /* Dirección IP del origen. */
uint IPv4_dest:32; /* Dirección IP del destino. */
uchar options[]; /* [Nota] hasta 40 bytes. */
uchar data[]; /* [Nota] datos del mensaje hasta 64KB. */
};
64/512
FIGURA 3.2 Esquema de la cabecera IP.
Se observa que la estructura del paquete incluye muchos más campos de los
cuatro campos básicos vistos anteriormente en este capítulo. El subsistema IP
utiliza estos campos adicionales para controlar el paquete. Por ejemplo, el campo
dont_frag especifica a la red que, en lugar de desmenuzar el mensaje en trozos
pequeños, se debería aceptar el mensaje completamente o rechazarlo.
Los comentarios al lado de los campos ofrecen una descripción suficiente. Los
siguientes apartados definen los campos IP que se pueden modificar o usar. Este
libro no es exhaustivo, si desea aprender más sobre cada campo, puede consultar
una buena documentación en los RFC de los protocolos TCP/IP.
Campo versión
Este primer campo IP es el número de versión del protocolo IP. Muchos de estos
valores están reservados o sin asignar; por ejemplo, IPv4 coloca un 4 en este
campo. Los pocos valores definidos están incluidos en la Tabla 3.1.
Tabla 3.1 Valores del campo versión
Valor Descripción/Uso
4 IPv4.
5 Modo de datagrama IP de flujo (IP experimental).
6 IPv6.
7 TP/IX (el "próximo" protocolo de Internet).
8 El protocolo de Internet "P".
9 TUBA.
65/512
El único cambio que tiene que hacer en este campo es cuando crea un socket
raw y decide rellenar ta cabecera (utilizando la opción de socket IP_HDRINCL).
Incluso entonces, debe establecer el campo a 0. El cero indica al núcleo que
complete este campo con el valor apropiado.
Campo header_len
Este campo indica al receptor la longitud de la cabecera utilizando words de 32
bits. Desde el valor 0, que está reservado (y que no tiene significado), al tamaño
mayor de 15 words o 60 bytes. De nuevo, la única situación en la que debe rellenar
este campo es cuando utiliza un paquete socket raw y IP_HDRINCL. Como todas
las cabeceras IP tienen al menos 20 bytes, el valor mínimo de este campo es de 5
(20/4) bytes.
Campo serve_type
El campo serve_type indica cómo administrar el paquete. Tiene dos
subcampos: un subcampo precedence (ignorado en muchos sistemas) y un
subcampo de tipo de servicio (TOS). Normalmente establecerá TOS con la llamada
del sistema setsockpt(). TOS tiene cuatro opciones: retardo mínimo, rendimiento
máximo, fiabilidad máxima y coste mínimo (monetario). Si no selecciona ningún
servicio especial significa una administración normal. (Para obtener un mayor
detalle de setsockopt() y sus valores véase el Capítulo Sí, "Cómo romper las
barreras del rendimiento".)
Campo ID
El subsistema IP le da a cada paquete un ID único. Con un campo de sólo 16
bits, puede uno imaginarse que se alcanzan rápidamente los números usados con
anterioridad. Sin embargo, el subsistema IP reutiliza un ID por medio de la hora del
sistema, el paquete que se envío previamente del mismo valor probablemente haya
caducado ya.
El ID ayuda a recomponer paquetes fragmentados. Si decide administrar la
cabecera (IP_HDRINCL), debe administrar los ID también.
USO DE ID
Si decide manipular la cabecera recuerde que su programa no es el único que puede
enviar mensajes. El subsistema IP sigue la pista de los ID. Debe tener precaución (y
utilizar programación adicional) para reducir la probabilidad de seleccionar un ID que el
subsistema pueda usar o haya usado.
66/512
rendimiento de la red.
GESTOR DE RECOMPOSICION DE PAQUETES DEL NUCLEO
Cuando el host es un router se puede elegir que el núcleo de Linux recomponga los
paquetes fragmentados. Esta opción es parte de la sección firewall/router en la
configuración del núcleo. Se debe observar que la recomposición de los paquetes
requiere tiempo, especialmente si éstos están dispersos y llegan a distinto tiempo. Sin
embargo, como el destino tiene que recomponer los paquetes de cualquier forma,
seleccionando esta opción se reduce el tráfico de red dentro del firewall (en la intranet del
destino).
El bit dont_frag indica al router o host que no divida el paquete. Si establece
este bit y el paquete es demasiado grande para un segmento de red reducido, el
router descarta el paquete y devuelve un paquete de error (ICMP).
El bit more_frags indica al destino que existen más trozos del paquete
fragmentado. El último fragmento establece este bit a 0. (Un paquete no
fragmentado tiene este bit a 0.) Si configura la cabecera manualmente, deberá
establecer siempre este bit a 0.
El campo frag_offset indica a qué zona del paquete pertenece el fragmento.
Puesto que los fragmentos de los paquetes pueden viajar a través de diferentes
rutas en la red, pueden llegar a su destino en momentos diferentes. El destino tiene
que recomponer el paquete, y utiliza el offset para colocar el fragmento en su
localización correcta.
El campo frag_offset es de sólo 13 bits de largo—demasiado pequeño para un
paquete que puede llegar a ser de 64KB. El offset se multiplica por 8 para colocar
la posición del byte real en el paquete. Esto significa que cada fragmento (excepto
el último) debe ser un múltiplo de 8. El subsistema IP administra completamente la
fragmentación y recomposición del paquete, no se debe preocupar de ello.
Con estos campos y el ID del paquete, el subsistema IP puede fragmentar y
recomponer el paquete. Si el subsistema no puede conseguir todos los trozos
dentro de un tiempo específico, descarta el paquete y devuelve un error al origen.
67/512
Campo protocol
Cada paquete en Internet tiene un valor de protocolo asignado, e ICMP (IPPRO-
TO_ICMP o 1), UDP (IPPROTO_UDP o 17) y TCP (IPPROTO_TCP o 6) poseen
cada uno un código. El protocolo indica al sistema cómo tratar el paquete entrante.
Puede establecer este valor con la opción SOCK_RAW en la llamada del sistema
socket(). El valor de protocolo es el último parámetro de la llamada. El archivo de
cabecera netinet/in.h del núcleo contiene muchos más valores. (Se recuerda que
aunque el núcleo incluya una definición de protocolo, éste puede que no lo
soporte.)
Campo options
El subsistema IP puede pasar varias opciones con cada paquete. Estas
opciones incluyen información de encaminamiento, marcas de tiempo, medidas de
seguridad, registro de encaminamiento y alarmas de caminos. Este campo puede
llegar a ser de hasta 40 bytes de longitud. Puesto que algunas de estas opciones
dependen del sistema, nunca debe tocar estas opciones directamente.
Campo data
El mensaje se incluye aquí y puede alcanzar hasta 65.535 bytes (menos 60
bytes, del tamaño máximo de la cabecera). Esta sección de datos incluye cualquier
información de cabecera que los protocolos de las capas superiores necesitan. Por
ejemplo, ICMP necesita 4 bytes, UDP necesita 8 bytes y TCP necesita de 20 a 60
bytes.
El sistema de paquetes de Internet basa todos sus paquetes IPv4 en esta
estructura. Cada capa superior añade características y habilidad.
68/512
Raw ICMP UDP TCP
Sobrecarga fija (bytes) 20-60 20-60+[4] 20-60+[8] 20-60+[20-60]
Fragmentación Sí Sí Sí Baja
Fiabilidad de protocolo
Parte del problema con las redes es la posibilidad de perder mensajes. Un
mensaje se puede corromper o desechar cuando se traslada de un host o router a
otro, o si falla o se rompe el propio host o router. En cada caso, un mensaje se
69/512
puede simplemente perder, y el programa tendrá que repetirlo.
Es probable también, que quiera asegurarse que el destino procesa los
paquetes en el orden correcto. Por ejemplo, puede componer un mensaje que no
cabe bien en un paquete. Si el segundo paquete llega antes que el primero, el
receptor debe conocer cómo identificar y corregir el problema. Sin embargo, el
orden no es importante cuando cada mensaje es independiente y autocontrolado.
La fiabilidad del paquete indica la certeza de seguridad de la llegada de los
mensajes y su orden. Baja fiabilidad significa que el protocolo no puede garantizar
que el paquete alcance el destino o que los paquetes estén en orden.
Rendimiento de protocolo
El aspecto más notable de la transmisión de datos es el rendimiento de la red. Al
conseguir el valor máximo los usuarios se muestran felices. Para obtener el mejor
funcionamiento, necesita conocer el rendimiento. A menudo, los bits por segundo
es una pequeña parte de la ecuación entera; indicando cómo la red puede rendir
bajo circunstancias idóneas.
El rendimiento de protocolo mide cuántos datos reales puede enviar el origen al
destino dentro de un periodo de tiempo. Si las cabeceras son grandes y los datos
pequeños, el resultado es un rendimiento bajo. La demanda de un acuse de recibo
para cada mensaje reduce drásticamente el rendimiento. Por defecto, alta fiabilidad
e integridad implican un bajo rendimiento y viceversa.
70/512
decir, algunos datos requieren un seguimiento con mucho cuidado, mientras que
datos menos importantes son menos críticos. A continuación se describen algunos
tipos de datos:
• Fallo intolerable. Datos críticos de vida. Cualquier cosa que pueda afectar a la
salud o vida privada o publica. Por ejemplo, señales de vida o señales vitales
del equipo médico y comandos de lanzamiento de misiles.
Fragmentación de protocolo
Mensajes grandes en redes lentas pueden frustrar a los usuarios. Todas las
redes definen un tamaño de trama máximo para que esos mensajes grandes no
avasallen la red. Se recuerda que los host de encaminamiento pueden dividir, o
fragmentar, los mensajes grandes que atraviesan una red estrecha.
Cada protocolo tiene una probabilidad distinta de fragmentación. Puesto que la
recomposición de mensajes fragmentados es función de IP, la reagrupación se
puede realizar de forma transparente a los protocolos de la capa superior. Sin
embargo, existen ocasiones en las que se requiere que el mensaje esté completo.
Esto es particularmente importante para el rendimiento de la red. Cuando los
routers dividen el paquete en trozos pequeños, el router pierde tiempo en dividir el
mensaje, y los paquetes resultantes incrementan la sobrecarga fija, Al bloquear la
71/512
fragmentación, la red descarta el paquete y devuelve un mensaje de error (paquete
demasiado grande) al programa.
Tipos de paquete
En los apartados siguientes se describen cada tipo de paquete, se muestran sus
estadísticas y se define la cabecera (si existe). En cada apartado se utiliza una
tabla que ayuda a visualizar rápidamente las características de cada protocolo. El
uso de esta tabla ayuda a elegir el paquete correcto para la aplicación.
El paquete raw
Un paquete raw tiene acceso directo al paquete y la cabecera IF. Se utiliza en la
programación de protocolos especiales o a medida. Sus atributos están listados en
la Tabla 3.3.
Tabla 3.3 Atributos del paquete raw
Tamaño del mensaje (bytes) 65.535 (65.515 máxima carga útil de datos).
Fragmentación Sí.
72/512
Gestión de mensajes de error y control IP (ICMP)
El Protocolo de mensajes de control en Internet (ICMP) es una de las capas
construidas encima del paquete básico IP. Todas las computadoras conectadas a
Internet (hosts, clientes, servidores y routers) utilizan ICMP para el control o los
mensajes de error. Se utiliza para et envío de mensajes de error o control. Algunos
programas de usuario también implantan este protocolo, como por ejemplo
traceroute y ping. Los atributos de ICMP están incluidos en la Tabla 3.4.
Tabla 3.4 Atributos de ICMP
Tamaño del mensaje (bytes) 65.535 (65.511 máxima carga útil de datos).
Sobrecarga fija (bytes) De 24 a 64.
73/512
Listado 3.2 Definición de la estructura ICMP
/***********************************************************/
/*** Definición de la estructura ICMP. ***/
/*** Definición formal en netinet/ip_icmp.h ***/
/***********************************************************/
typedef unsigned char u i 8 ;
typedef unsigned short int ui16;
struct ICMP_header {
ui8 type; /* Tipo de error. */
ui8 code; /* Código de error. */
ui16 checksum; /* Suma de comprobación del mensaje. */
uchar msg[]; /* Descripción de datos adicionales. */
};
74/512
Tabla 3.5 Atributos UDP
Tamaño del mensaje (bytes) 65,535 (65.507 máxima carga útil de datos).
Fiabilidad Baja.
Rendimiento Medio.
Fragmentación Sí.
Cada capa superior de la pila IP fija más su atención en los datos y menos en la
red. UDP oculta algunos detalles de los mensajes de error y de cómo el núcleo
transmite los mensajes. También, recompone un mensaje fragmentado.
Un mensaje que se envía a través de UDP es como un mensaje e-mail: el
destino, origen y datos es toda la información que se necesita. El núcleo toma el
mensaje y lo coloca en la red pero no verifica su llegada. Al igual que el paquete
ICMP, puede enviar a múltiples destinos desde un socket individual, utilizando
distintas llamadas del sistema send(). No obstante, sin la verificación, se pueden
alcanzar rendimientos máximos.
Sin verificar la llegada, la red puede perder la fiabilidad de los datos. La red
puede perder paquetes o fragmentos, o corromper el mensaje. Los programas que
utilizan UDP, o bien siguen la pista de los mensajes, o no les importa si se pierden
o quedan corrompidos. (Se debe observar que, aunque los datagramas son poco
fiables, esto no significa que algo salga mal. Sólo significa que el protocolo se
efectúa sin garantías.)
De los distintos tipos de datos (definidos anteriormente), Informativo, Temporal y
Desechable se adaptan mejor a los servicios UDP. La razón principal es su
tolerancia a la pérdida de paquetes. Si la cámara web falla para actualizar todos los
navegadores, el usuario final es improbable que lo advierta o le importe. Otro
posible uso es un servicio horario exacto. Debido a que la hora exacta es Temporal,
un host puede descartar un par de instantes de reloj sin la pérdida de integridad.
UDP ofrece la ventaja de mayor velocidad. Además, se puede incrementar su
fiabilidad de las siguientes formas:
75/512
último número de mensaje o envía un mensaje de reanudación.
76/512
FIGURA 3.4 Esquema UDP.
UDP crea un receptáculo de red virtual para cada mensaje en forma de puertos.
Con el puerto, IP puede repartir rápidamente los mensajes al propietario correcto.
Aunque no se defina un puerto con bind(), el subsistema IP crea uno temporal para
el programa de la lista de puertos efímeros (véase el Capítulo 2).
Protocolo para el control de la transmisión (TCP)
Protocol para el control de la transmisión (TCP) es el protocolo de socket
utilizado de forma más habitual en Internet. Se pueden utilizar read() y wr¡te(), y se
requiere volver a crear un socket para cada conexión. Los atributos TCP están
incluidos en la Tabla 3.6.
Tabla 3.6 Atributos TCP
Tamaño del mensaje (bytes) (ilimitado).
Fragmentación Improbable.
77/512
Conexiones dinámicas
Un host envía un mensaje a otro host. Ese mensaje viaja a través de las redes,
atravesando varios routers y gateways. Cada mensaje enviado puede utilizar un
camino distinto. Los segmentos de red (conexiones entre computadoras) a menudo
aparecen y desaparecen cuando los servidores se inician y apagan. La potencia de
Internet es su capacidad para adaptarse a estos cambios y encaminar la
información de forma consecuente.
La adaptabilidad es una de las fuerzas impulsivas detrás de Internet. La
computadora puede realizar una consulta, y la red intenta posibles vías para
completar la orden. Desafortunadamente, esta ventaja implica que el camino entre
la computadora y el servidor o equipo homólogo puede cambiar, alargando o
acortando la distancia.
Si el camino se alarga, el tiempo de propagación se incrementa. Esto significa
que el programa puede enviar mensajes sucesivos y muchos llegarán a distinto
tiempo, muchas veces desordenados.
TCP garantiza que el destino ha recibido correctamente el último mensaje antes
de enviar el siguiente. Se puede comparar esto a una serie de mensajes
numerados
(así es como funciona realmente TCP). El programa puede enviar 10 mensajes
sucesivos. TCP toma cada mensaje, le adjunta un número único, y lo envía. El
destino acepta el mensaje y responde con un acuse de recibo. Una vez recibido el
acuse de recibo, TCP permite al programa enviar el siguiente mensaje.
PROTOCOLO DE VENTANA DIVIDIDA
TCP utiliza una técnica mejor que el protocolo enviar-esperar (o ACK/NACK), el cual es
demasiado lento para la paciencia de cualquiera. En lugar de eso, utiliza una ventana
dividida: mide cuándo y con qué frecuencia responder con un ACK (acuse de recibo). Las
conexiones lentas o malas pueden incrementar los mensajes de acuse de recibo.
Conexiones rápidas y con menos pérdidas permiten que se envíen más mensajes antes
de la recepción de un acuse de recibo. Es parte del algoritmo de Nagle. Se puede
deshabilitar utilizando opciones de sockets (véase el Capítulo 9).
Pérdida de datos
Cuando el destino obtiene el mensaje, determina la integridad de los datos. Los
datos pueden viajar a lo largo de caminos de comunicación poco óptimos, que
pueden desechar o corromper los bits del mensaje. Se debe recordar que la red
envía cada mensaje de bit en bit. TCP envía con el mensaje una suma de
comprobación para verificar los datos. TCP es la última capa que puede detectar y
remediar datos erróneos.
Si el destino detecta cualquier error, envía al emisor un error, solicitando una
retransmisión al programa. Asimismo, si la computadora no obtiene un acuse de
recibo dentro de un tiempo determinado, el subsistema TCP reenvía
automáticamente el mensaje sin la intervención del programa.
78/512
Caminos reducidos
Regresando al mensaje individual enviado a un host particular, suponga que el
mensaje es demasiado grande para los segmentos que intervienen en el camino.
Los problemas que el paquete encuentra cuando pasa a través de la red son las
distintas tecnologías y las portadoras de transmisión. Algunas computadoras en red
permiten paquetes extensos; otras colocan límites en el tamaño.
UDP intenta enviar el mensaje tan grande como se pueda. Esto puede ser un
problema con los caminos reducidos de datos. Los algoritmos IP se adelantan a
que los routers puedan fragmentar los datos. De igual modo, IP espera que
recompongan el mensaje entrante.
TCP, por otra parte, limita cada paquete a trozos pequeños. TCP divide
mensajes grandes, antes de que la red tenga la oportunidad de cogerlos. TCP elige
el tamaño en uno que las mayorías de las redes pueden dejarlo intacto. Por
omisión, TCP utiliza 536 bytes y negocia normalmente hasta 1.500. Para
incrementar ese tamaño manualmente, establezca la opción de socket TCP MSS
(tamaño de segmento máximo) (véase el Capítulo y).
El receptor se puede encontrar que los paquetes del mensaje están
desordenados. TCP los ordena antes de pasar el mensaje al programa.
La solución a todos estos problemas de red es añadir sobrecarga fija de
cabecera y protocolo al algoritmo TCP. Por supuesto, la sobrecarga fija añadida de
todas las técnicas TCP reducen el rendimiento notablemente.
/*************************************************/
typedef unsigned char ui8;
typedef unsigned short int ui16;
typedef unsigned int ui32;
typedef unsigned int uint;
struct TCP_header {
ui16 src_port; / * Número de puerto del origen. * /
79/512
ui16 dst_port; /* Número de puerto del destino. * /
ui32 seq_num; / * Número de secuencia. * /
ui32 ack_num; / * Número de acuse de recibo. * /
80/512
hacia el principio de los datos. Para guardar el espacio de cabecera, este campo
actúa como el campo headerjen de IP: asigna el número de words de 32 bits que
físicamente precede los datos.
TCP utiliza algunos de los campos exclusivamente para la apertura de la
conexión, control de flujo y cierre de la conexión. Durante una sesión de
comunicación, algunas de las cabeceras están vacías. Los siguientes apartados
describen algunos campos interesantes.
La cabecera TCP utiliza el mismo número de puerto encontrado en UDP. Pero
seq_num y ack_num ofrecen seguimiento al flujo. Cuando envía un mensaje, el
subsistema ÍP adjunta un número de secuencia (seq_num). El receptor responde
que obtuvo el mensaje con un número de acuse de recibo (ack_num) que es
superior en 1 al número de secuencia. Esta característica permite que los paquetes
de acuse de recibo transporten también datos.
SYN=0 (syn_flag)
81/512
Tabla 3.8 Cierre de una conexión TCP
Cliente Servidor Descripción
El cierre de la conexión TCP hace imposible la reutilización del socket para otras
conexiones. Por ejemplo, si se conecta a un servidor, la única forma de cortar la
conexión es cerrando el canal, lo cual cierra también el socket. Si entonces quiere
conectarse a otro servidor, debe crear un socket nuevo. Los otros protocolos no
tienen esta limitación.
82/512
EL EQUILIBRIO DE LA ÉTICAS DE RED
El conocimiento de cómo hacer muchas cosas es muy poderoso y soporta bastante
responsabilidad. Con privilegios de root, puede hacer mucho bien y mucho daño a la red.
Cuando instala Linux en la computadora, la distribución asume que actúa con las mismas
buenas intenciones como aquellas a quien le ofreció la habilidad de fisgonear otros
paquetes. Una forma efectiva de destruir el movimiento Free Software es abusar de la
potencia y confianza que la gente buena y bien intencionada dan.
Normalmente, el adaptador de interfaz hardware recoge solamente aquellos
mensajes de la dirección ethernet que es capaz de reconocer. Se puede recordar
del Capítulo 2 que cada adaptador de hardware ethernet tiene un único ID de 6
bytes. El adaptador utiliza este ID para ignorar todos los paquetes excepto los que
coinciden con el ID.
ID ETHERNET PROGRAMABLES
Algunos OEM (fabricantes de equipos originales) ofrecen sus tarjetas de interfaz de red
(PCI o PCMACIA) las cuales soportan dirección MAC programable (o ID ethernet). Esto
hace posible la producción en serie para algunos fabricantes de tarjetas mientras se
sirven a varios cientos de marcas de empresa. Desafortunadamente, se puede obtener
una tarjeta que tiene un ID falso, porque no se ha programado correctamente la marca de
la empresa. Este error puede hacer que la tarjeta no sea única en la red.
Si no desea modo promiscuo, puede desconectarlo con una de las opciones.
Tcpdump tiene muchas opciones que le ayudan a filtrar mensajes no deseados y
seleccionar redirecciones de datos y datos visualizados. A continuación están
algunas opciones interesantes de la línea de comandos:
• -a Intenta asignar nombres de red y direcciones de difusión.
Esto requiere acceder al servidor de nombres.
• -c <cuenta> Finaliza después de alcanzar la cuenta específica de mensajes.
• -n No convierte las direcciones de los nodos a sus nombres
(esto es útil cuando no se tiene un servidor de nombres).
• -p No coloca la interfaz en modo promiscuo. Si se tiene una red
pequeña o cluster, viendo todos los paquetes que pueden ser
interesantes. De otra manera, con el modo promiscuo habilitado,
la red puede fácilmente abrumar a la computadora.
• -v Visualiza una captura con alguna información. Incluye el campo
de tiempo de vida (TTL).
• -vv Visualiza una captura con bastante información.
• -w <archivo> Escribe el paquete raival archivo.
Tcpdump puede ejecutarlo sin ninguna opción, y visualizará más información de
la que necesita. Puede también ver interacciones interesantes, cómo ARP
(Protocolo de resolución de direcciones) pregunta por el ID ethernet y lo adquiere
de la dirección IP. A continuación se muestra un ejemplo de captura de 100
paquetes con información y sin marca de tiempo:
83/512
tcpdump -v -t -c 100
La opción -t suprime la marca de tiempo. Ya que a menudo los mensajes se
desplazan fuera de la pantalla muy rápidamente, puede desear redireccionar el
resultado a un archivo.
Tcpdump tiene unas pocas anomalías; por ejemplo, no recoge los mensajes de
él mismo. No observa los paquetes de ping 127.0.0.1, porque el subsistema de red
no envía estos mensajes a las capas inferiores donde tcpdump trabaja.
Después de que la llamada haya terminado con éxito, cada llamada del sistema
recvfrom() que realice devuelve una trama de red (un mensaje de red físico). La
trama de red incluye la dirección hardware (por ejemplo, la dirección ethernet) y la
cabecera.
84/512
El SOCK_PACKET ofrece acceso a las tramas de nivel hardware y a todos los
datos asociados de cada transmisión. Con ello puede observar cómo el subsistema
de red construye las tramas.
Puede utilizar la estructura IP definida al principio de este capítulo. Sin embargo,
recuerde que el almacenamiento está condicionado al hardware, así que los bits de
los campos pueden estar en el orden equivocado. La estructura asume que el bit
número 0 es el primer bit en el flujo de la trama.
El programa ejemplo del sitio web, snooper.c, reconfigura los campos para
hacerlos coincidir con la trama hardware real para un procesador little-endian
(compatible con Intel) y un compilador GNU. Si tiene un procesador diferente
(incluso un compilador diferente), debe de modificar levemente la estructura.
85/512
Capitulo IV
86/512
niveles distintos de comunicación para los programas. Todos los niveles requieren
la utilización de la llamada del sistema socket(). Estos niveles le dan a conocer las
dinámicas de la programación de red y pueden realizar una programación
desafiante.
Los tres protocolos básicos (IP raw, UDP y TCP que se encuentran definidos en
el Capítulo 3, "Distintos tipos de paquetes de red") abstraen la red y añaden
fiabilidad mientras se reduce necesariamente el rendimiento. El nivel más alto,
TCP, tiene la fiabilidad más grande de todos los niveles de protocolo. Lo cual
garantiza que los datos alcancen el destino correctamente y lleguen ordenados. Es
tan fiable que se puede considerar como un archivo o un canal de interproceso.
Esa es la razón de que los socketsTCP son sockets basados en la conexión.
Comunicaciones fiables
TCP es una E/S de flujo, de alta fiabilidad y acceso a la E/S de alto nivel,
implicando que el camino de los datos está limpio y sin trabas. TCP garantiza
comunicaciones fiables: el equipo homólogo recibe todo lo que el programa
necesita. El subsistema de red que incluye los dispositivos y las pilas de protocolos
(en el servidor o cliente) aceptan un mensaje, lo comprueban y lo pasan al
programa.
Si la comprobación falla y el subsistema detecta un error, el subsistema se
encarga de reclamar una retransmisión. Ambos extremos del camino de
comunicación ejecutan este chequeo de validación, Como se describió en el
capítulo anterior, algunos mensajes tienen un ID de paquete único y secuencia]. El
ID es importante para asegurar la fiabilidad y la ordenación. Obtiene esta
característica cuando selecciona el protocolo TCP.
Los dos problemas más grandes con la comunicación de red son la pérdida y la
87/512
reordenación de paquetes. Suponga que el programa envía un mensaje. Los
programas no pueden detectar absolutamente que el destino consigue el mensaje
a menos que reciba una respuesta del destino. Por ejemplo, el programa envía el
mensaje, y el destino espera la llegada del paquete. Si el destino no recibe nada en
un rato, envía un mensaje al programa indicando el último número de secuencia
que recibió bien. Esta colaboración estrecha acopla el programa con el destino.
Por otra parte, el destino puede recibir algunas partes del mensaje
desordenadas con el resto del mensaje. Cuando esto ocurre, el destino aguanta el
fragmento del mensaje hasta que obtiene los segmentos intermedios. El destino
entonces recompone el mensaje utilizando el número de secuencia como clave de
ordenación.
Como se debió observar antes, las llamadas de biblioteca de alto nivel cuentan
con un canal abierto y también requieren que el camino de comunicación sea
fiable. El protocolo TCP fija la atención en el canal del mensaje con objeto de que
la información aparezca como un flujo sin paquetes. Al terna tivamente, los
protocolos de bajo nivel fijan la atención en los paquetes, con lo que no pueden
soportar llamadas de biblioteca de E/S de alto nivel como printf().
Los protocolos menos fiables ofrecen enviar mensajes al programa con un canal
muy rápido. UDP, en particular, intenta obtener el mensaje rápidamente sin tener
en cuenta la ordenación. Similarmente, el destino acepta cada mensaje
independiente, sin esperar ninguna ordenación.
La ordenación es un asunto importante para el diseño. Puede esperar que los
datos no estén ordenados cuando, en realidad, pueden estarlo. A continuación se
muestran algunas consultas que le pueden ayudar a decidir si los mensajes
requieren flujos o no (con rapidez, resúmenes explicativos).
88/512
varios bultos en cualquier orden y puede sólo responder a un par. El flujo del
canal no puede soportar esto sin perder información. (FedEx=UDP;
canal=TCP.)
• ¿Debe el programa seguir la pista de quién ha dicho y qué mientras se sirven varias
conexiones? Algunas veces necesita tener diálogos (con o sin flujos de datos)
en la cual el servidor debe seguir la pista del cliente. Por ejemplo, la
personalización de Internet permite que el usuario construya a su medida en
entorno operativo. Algunos programas pueden hacerlo de forma similar.
(Sí=UDP; no=TCP.)
89/512
Listado 4.1 Un datagrama simple conectado
/***************************************************************/
/*** Muestra de ejemplo de datagrama con conexión. ***/
/**" Extracto de connected-peer.c ***/
/********************************************************/
int sd;
struct sockaddr_in addr;
sd * socket(PF_INET, SOCK_DGRAM, 0); /* socket datagrama * /
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sinjsort = htons(DEST_PORT);
inet_aton(DEST_IPADDR, &addr.sin_addr);
if ( connect(sd, &addr, sizeof(addr)) != 0 ) /* ¡conectado! */
perrorf"connect");
I * —Nota: esto no es un send de flujo---*//*
send(sd, buffer, msg_len); /* send como en TCP */
Normalmente, con UDP se utiliza la llamada del sistema sendto() o recvfrom()
(las cuales se describen más adelante, en este mismo capítulo). La llamada del
sistema send() asume por omisión que el programa ha registrado ya el destino con
un connect().
El equipo homólogo (o servidor) que espera la conexión puede utilizar la misma
interfaz de conexión, o puede utilizar sendto() y recvfromO. Sin embargo, para que
el programa se conecte al equipo homólogo, se necesita que el equipo homólogo
publique su número de puerto con la llamada del sistema bind(). Para una
descripción completa de la llamada del sistema bindf) se puede consultar el
apartado "Envío de un mensaje directo", más adelante en este capítulo.
El fragmento de código de ejemplo en el Listado 4.1 hace referencia a
DEST_PORT. Este número es una interfaz de puerto acordado entre los dos
programas. El equipo homólogo que escucha activa el puerto a través de la
llamada bind() para solicitar DEST_PORT. Cuando el mensaje llega, el destino
puede emitir una llamada del sistema connect() él mismo.
De distinta forma a la llamada del sistema connect() en TCP, el programa puede
volver a conectarse tantas veces como desee a otros equipos homólogos o
servidores sin cerrar el socket. Con TCP, no puede conectarse a otros equipos
homólogos o servidores a menos que cierre y luego vuelva abrir el socket. Una de
las características más importantes que UDP ofrece es la capacidad de enviar
mensajes a destinos distintos sin cerrar el socket. El uso de la llamada del sistema
connect() con el protocolo UDP le ofrece esta flexibilidad a pesar de todo.
De nuevo, si utiliza UDP de cualquier forma, elige la poca fiabilidad que conlleva.
90/512
Puede perder paquetes o los paquetes pueden llegar desordenados. La llamada
del sistema connect() para UDP sólo registra el destino y no incrementa la
fiabilidad del canal entre los programas.
MENSAJES ENTREGADOS DE FORMA FIABLE (RDM)
El Protocolo de mensajes entregados de forma fiable (RDM) [RFC908, RFC1151] ofrece
la garantía de reparto que ofrece TCP pero le permite tener la velocidad basada en el
mensaje (no de flujo) de UDP. RDM puede recoger los mensajes desordenados, pero no
ofrece un buen compromiso entre UDP y TCP. Desafortunadamente, aunque este
protocolo ha estado en los libros durante muchos años, Linux (y otros sistemas operativos
UNIX) no lo soportan todavía.
91/512
Obtención de una página HTTP
La composición de la consulta es la parte más fácil de la conexión. La consulta
le da también mayor flexibilidad en añadir lo que necesita. El único requisito es
asegurase de que el servidor puede entender el mensaje. El Listado 4.2 presenta
una forma de obtención de una página web. Este programa abre una conexión
especificada en la línea de comandos y envía una consulta HTTP. El programa
visualiza el resultado en stdout.
Listado 4.2 Obtención de una página web de un servidor HTTP
/********************************************************/
/*** Extracto del archivo http-client.c (en el sitio web).***/
/********************************************************/
int sd;
struct servent *serv;
if ( (serv = getservbyname("http", "tcp")) == NULL )
PANIC{"HTTP servent");
if ( (sd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
PANIC{"Socket");
92/512
printf("%s", buffer);
}
while ( bytes_read > 0 );
93/512
Los cuatro primeros parámetros son los mismos que en recv() y send(). Incluso
las opciones y condiciones de error posibles son las mismas. La llamada del
sistema sendto() añade la dirección del socket del destino. Al enviar un mensaje al
destino, rellena la estructura addr y llama a sendto(). El Listado 4.3 muestra un
ejemplo:
Listado 4.3 Ejemplo de sendtof()
/************************/
/*** Ejemplo sendto(). ***/
/************************/
int sd;
struct sockaddr_in addr;
Sd = socket(PF_INET, SOCK_DGRAM, 0);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(DEST_PORT);
inet_aton(DEST_ADDR, &addr.sin_addr);
sendto(sd, "This is a test", 15, 0, &addr, sizeof(addr));
Este ejemplo envía un mensaje directamente a DEST_ADDR:DEST_PORT. Por
supuesto, puede utilizar datos raw o ASCII en el cuerpo del mensaje. No tiene
importancia.
La llamada del sistema recvfrom() es similar a recv(), y cuando se ejecuta
espera hasta que un mensaje llegue de algún emisor.
El último parámetro tiene un tipo distinto. Es un puntero a un entero. Los dos
últimos parámetros de sendto() son el destino. Los dos últimos parámetros de
recvfrom() son la dirección del origen. Ya que la familia de estructura sockaddr
pueden ser de distintos tamaños, puede recoger posiblemente un mensaje del
origen que es distinto del tipo de síKket (por omisión es AF_INET).
CÓMO PASAR LA LONGITUD DE LA DIRECCIÓN
La llamada recvfrom() (como recv()) pasa el puntero de la longitud de la estructura. Esto
es objeto de familias de protocolos distintas, PF_LOCAL. Cuando la llamada finaliza, se
necesita conocer cuántos datos addr utiliza la llamada. También se necesita indicar en la
llamada cuánto espacio está disponible para usar. De esa manera, el parámetro se pasa
por referencia con el fin de que la llamada del sistema pueda devolver el número de bytes
que utiliza actualmente.
Puesto que recvfrom() puede cambiar el parámetro addr_len, necesita
establecer el valor cada vez que realice la llamada del sistema. De otra manera, el
valor en addr_len puede disminuir potencialmente con cada llamada del sistema. A
continuación se muestra un ejemplo del uso de recvfrom().
94/512
/*****************************/
/*** Ejemplo recvfrom().***/
/*****************************/
int sd;
struct sockaddr_in addr;
sd = socket(PF_INET, SOCK_DGRAM, 0);
95/512
paquetes de configuración en total. Bastante bueno para comunicaciones de alta
velocidad.
96/512
/***********************************/
/*** Algoritmo básico T/TCP. ***/
/**********************************/
int flag=1;
int sd;
sd = socket (PF_INET, SOCK_STREAM, 0);
FIGURA 4.1 Las señales de intercambio de T/TCP abren, envían y cierran un diálogo
en tres paquetes.
Con T/TCP se tiene que deshabilitar TCP y vaciar rápidamente sus buffers. Eso
es lo que la llamada del sistema setsockopt() hace en el ejemplo. También, se
puede enviar tantos mensajes como se desee, pero el último mensaje debe tener
97/512
MSG_FIN (véase el listado anterior) en la llamada del sistema sendto().
T/TCP Y LINUX
Linux, desafortunadamente, no soporta actualmente T/TCP. En un futuro será, pero por
ahora, estas técnicas se pueden utilizar en otros sistemas operativos UNIX. Puede
observar algunos flags que indican un trabajo en proceso en el núcleo del Linux (por
ejemplo, MSG_EOF y MSG_FIN se definen en linux/socket.h). Todavía, puede poner a
prueba los programas T/TCP encontrados en el sitio web.
TCP, como se mencionó antes, requiere volver a crear un socket TCP para cada
conexión nueva, porque se puede cerrar una conexión sólo con cerrar el socket
T/TCP puede tener un lado positivo al no requerir volver a crear un socket como
TCP, puesto que la conexión y el cierre están implícitos.
T/TCP le da la ventaja de tener despliegues cortos de interacción con cualquier
servidor que lo soporte, mientras se minimizan los tiempos de inicio y de cierre. Las
ventajas de T/TCP ayudan al programa a responder más rápido a los servidores.
98/512
receptor. Por ejemplo el archivo /etc/services incluye los números de puerto
publicados que ofrecen los servicios estándar.
99/512
Si se compara este ejemplo de la llamada del sistema bind() con la llamada deí
sistema connect(), se debe observar dos diferencias principales: aquí, el código
recortado está consultando MY_PORT, y la dirección es INADDR_ANY. El programa
necesita un número de puerto específico para que el equipo se conecte.
INADDR_ANY es un flag especial (esencialmente 0.0.0.0) que indica que cualquier
equipo puede conectarse desde cualquier interfaz. Algunas computadoras tienen
más de una interfaz física (por ejemplo, dos tarjetas LAN, un módem con una
tarjeta LAN, direcciones IP con alias). Cada interfaz lógica o hardware tiene su
propia dirección IP. Al usar la llamada del sistema bind(), puede especificar si servir
una o todas esas interfaces. Puede utilizar esta característica para atravesar
conversiones o filtros por un firewall, y se aplica a los protocolos TCP o UDP. Para
indicar una interfaz específica, puede utilizar lo siguiente:
if ( inet_aton("l28.48.5.l6l", &addr.sin_addr) == 0 )
perror("address error");
...
Esto realiza la petición de que el puerto de escucha esté sobre 128.48.5.161.
Puede utilizar las transformaciones de ordenación de bytes de red como htonl():
addr.sin_addr.s_addr = htonl(0x803005Al); /* 128.48.5.161 */
Funciona lo mismo de cualquier forma. Se debe observar que INADDR_ANY no
utiliza la llamada de transformación htonl(). No se necesita, puesto que está todo a
cero, y se garantiza que está en el orden de bytes de red.
100/512
char *request= "select * from TableA where fieldl - 'test';";
char buffer[1324];
/*---Activar el socket.---*/*/
sd = socket(PF_INET, SOCK_DGRAM, 0);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9999); /* Petición de un puerto específico. */
if ( inet_aton{DEST_ADDR, Saddr.sin_addr) == 0 ) /* Dest IP */
perror('Network IP bad");
/*---Enviar mensaje.---*/*/
if ( sendto(sd, request, strlen(request), 0, Saddr, addr_len) < 0 )
perror("Tried to reply with sendto");
/*---Obtener respuesta.---*/*/
bytes = recvfrom(sd, buffer, sizeof(buffer), 0, Saddr, Saddr_len);
if { bytes > 0 >
perrorf"Reply problem");
else
printf("%s", buffer);
...
Dado que el puerto del receptor es 9999, el emisor transmite una consulta SQL
en este ejemplo.
El emisor no necesita consultar un puerto específico, porque el receptor escoge
esa información de los datos que recvfrom() coloca en addr. (De nuevo, se
recuerda que todos los sockets necesitan un puerto. Si no se pide uno, el núcleo
elige uno para el programa.)
101/512
Listado 4.5 Ejemplo de un receptor de datagramas
/***********************************************/
/*** Ejemplo de receptor. ***/
/*** {Extracto de connectionless-receiver.c.) ***/
/***********************************************/
struct sockaddr addr;
int sd;
/*---Activar el socket y pedir una asignación de puerto.---*/*/
sd = socket (PF_INET, SOCK_DGRAM, 0);
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9999); /* Petición de puerto especifico. */
addr.sin_addr_s_addr = INADDR_ANY; /* Cualquier interfaz IP. */
if ( bind(sd, &addr, sizeof(addr)) != 0 )
perror("bind");
/*---Obtener y procesar todas las peticiones.---*/*/
do
{ int bytes, reply_len, addr_len=sizeof(addr);
char buffer[1024];
/*---Esperar mensaje, si correcto procesar petición y responder.---*/*/
bytes = recvfrom(sd, buffer, sizeof(buffer), 0, Saddr, &addr_len);
if ( bytes > 0 ) /* Si valido devolver, */
{ /* ...declarar, conectar y responder. */
printf("Caught message from %s:%d (%d bytes)\n",
inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), bytes);
/****Procesar mensaje.****/
if { sendtofsd, buffer, reply_len, 0, &addr,
addr_len) < 0 )
perror("Tried to reply with sendto");
}
else
perror("Awaiting message with RecvFrom");
102/512
}
while ( ¡quit! );
...
En este extracto, el receptor solicita un puerto y luego espera la llegada de los
mensajes. Si obtiene un mensaje, procesa la petición y responde con el resultado.
El emisor transmite solamente un mensaje. El receptor, por otra parte, actúa
más como un servidor, recibiendo, procesando y respondiendo todos los mensajes
que llegan. Esto presenta un problema fundamental entre estos dos algoritmos: la
asignación de espacio suficiente para el mensaje entero.
Si el programa requiere un mensaje de longitud variable, puede que necesite
crear un buffer grande. El mensaje UDP más grande está sobre 64 KB; éste es
finalmente su tamaño más grande.
MENSAJES UDP GRANDES
Si el mensaje es más grande que el buffer, la cola de mensajes descarta cualquier resto
de bytes del mensaje entrante. Esto se aplica a la mayoría de familias de protocolos que
soportan datagramas.
103/512
La resolución del primer problema ayuda a preparar al receptor a seguir la pista
de cada paquete de un mensaje. Si aparece un problema de secuencia, el receptor
puede pedir un paquete específico. También, el emisor puede prever un acuse de
recibo de cada paquete.
Un método que puede utilizar es asignar un único número de secuencia a cada
paquete (como el mecanismo de acuse de recibo de TCP). Cuando cada paquete
llega, el código UDP reforzado lo comprueba. Si un paquete se pierde, el programa
puede solicitar una retransmisión con el número de paquete específico. El
problema principal de este método es que el emisor debe guardar un registro de
todos los paquetes enviados. El emisor puede desechar los paquetes más
antiguos, pero ese histórico de paquetes puede llegar a ser muy grande.
Una forma de resolver el tamaño de los históricos de paquetes es acusar recibo
de cada paquete. Este método resuelve el segundo problema y requiere que el
receptor responda a cada paquete con su propio mensaje indicando que recibió
exitosamente ese número de secuencia. Esta técnica es en esencia lo que hace
TCP. Sin embargo, al usar esta técnica, el programa experimenta algo del impacto
de rendimiento que tiene TCP.
El problema es que muchas redes físicas no son verdaderamente
bidireccionales. Cada transmisión ocupa algo del ancho de banda de red,
impidiendo a otros transmitir. Además, cada paquete tiene cabecera IP y UDP
(mínimo 28 bytes). Si el receptor responde a cada paquete individualmente, la
mayor parte de los acuses de recibo tienen sobrecarga fija de cabecera (4 o 5
bytes del ID de la secuencia y 28 bytes de la cabecera UDP/IP). Por lo tanto,
cuanto más tiempo el receptor custodie en silencio ayudará a reducir la congestión
de red.
Una solución puede ser agrupar los acuses de recibo dentro de unos pocos
paquetes. Por ejemplo, si cada paquete es de 1.024 bytes y el emisor tiene que
transmitir un mensaje de 1 MB, el receptor puede recibir alrededor de 1.000
paquetes. Dentro del límite de 1.024 bytes, el receptor puede fácilmente acusar
recibo de 10 paquetes a la vez. Esto limita el histórico a 10x1.024 bytes. Necesita
conocer el rendimiento de la red y las limitaciones de memoria del host con objeto
de implementar esto correctamente.
Se presenta otro problema: ¿cómo conoce el receptor cuándo ha finalizado el
emisor? Por ejemplo, si el receptor acusa un recibo cada diez paquetes y el emisor
sólo tiene cinco paquetes, puede encontrarse con dos problemas:
• El emisor puede suponer que el receptor nunca obtuvo los paquetes, así que
los retransmite.
Para resolver esto, puede indicar al receptor el número total de paquetes a
esperar. Cuando el receptor consigue el último paquete, transmite el mensaje de
acuse de recibo reducido.
104/512
Secuencia de los paquetes
Dentro del protocolo UDP, cada paquete de un mensaje puede llegar al destino
desordenado con respecto a otros paquetes. Esto ocurre porque Internet es muy
dinámico (véase el Capítulo 3). Enlaces caídos, congestionados y fuera de control
pueden forzar a que los routers retarden o vuelvan a encaminar el mensaje.
Una de las soluciones para que un paquete llegue con garantía es incluir un
número único. Esta solución puede ayudar a la secuencia de los paquetes. Para
que esto funcione, el número debe ser secuencial y único entre los mensajes.
Si el paquete se desordena, el protocolo debe de guardar (os paquetes con
numeración superior hasta que los mensajes de numeración menor lleguen. Por
ejemplo, si los paquetes 1, 2, 4 y 5 llegan, los paquetes 4 y 5 esperan hasta que
llegue el 3.
Potencial mente, la red puede desechar el paquete 3 durante la transmisión, en
ese caso el receptor puede pedir específicamente el paquete 3 o acusar recibo de
los paquetes que llegaron. En ambas opciones, el emisor retransmite el mensaje
perdido.
Redundancia de paquete
La red puede dar pie al reflejo de paquetes, dando como resultado que los
duplicados lleguen al destino. La causa más común es cuando un paquete llega
muy tarde después de que el destino solicitara una retransmisión. La secuencia
elimina la redundancia de paquetes. El receptor desecha simplemente el paquete
con el ID de secuencia duplicado.
105/512
con estos elementos:
106/512
programas? ¿Por qué no se utiliza sólo uno? El uso de un sólo programa es
posible, pero cuando quiere enviar y recibir a la vez, necesita esencialmente dos
programas.
Suponga que tiene un programa distribuido (un programa que utiliza la red como
un multiprocesador) que procesa muy rápidamente datos de imágenes. La red
utiliza varias etapas para procesar la imagen: captura de la imagen, eliminación de
defectos, suavizado, muestreo y distribución en serie. Cada uno de estos pasos
está en una computadora distinta, y la información fluye de una etapa a la
siguiente. El equipo de suavizado recibe los datos del equipo que elimina los
defectos de la información y lo pasa al equipo de muestreo. Cada etapa intermedia
debe recibir y enviar casi al mismo tiempo, como un cubo con un agujero en el
fondo.
En este ejemplo, no puede combinar fácilmente dos etapas. Cada etapa tiene
que estar separada, así que la información entrante no consigue enturbiarse con
los resultados salientes. Por supuesto podría enviar en el asunto de error de
imagen: ¿cómo obtiene un mensaje de error al subir la cascada?
la división de la tarea dentro de responsabilidades bien definidas es parte del
concepto de multitarea. Puede ejecutar varios programas a la vez en sus propios
entornos para asegurar que las entradas y salidas no se entremezclan nunca. Esto
evita resultados corruptos. Los ejemplos que se vieron en este capítulo utilizan un
formato de multitarea porque se ejecutan como programas separados.
Los programas son realmente tan similares que podría asociarlos dentro de uno
para que éstos hablen consigo mismo. En ese caso y para simplificar interacciones,
debe crear una tarea de ejecución separada para cada uno, indicando cuál envía el
primer mensaje.
La multitarea está incrustada totalmente en la red de programación. En el
Capítulo 8, "Cómo decidir cuando esperar la E/S", se trata la multitarea con más
detalle.
107/512
sistema reflejan las otras llamadas pero añaden direcciones de origen y destino
Transaction TCP (T/TCP) es esencialmente TCP sin conexión. Si está soportado
(Linux no soporta todavía T/TCP), T/TCP ayuda a evitar la longitud de la
configuración y los tiempos de cierre. Esto acelera la transmisión de los mensajes.
También, el protocolo abreviado fija su atención en el paquete en vez de en el flujo
y reduce Ja interacción a tres paquetes de mensajes. Por omisión UDP es sin
conexión, pero se puede conectar el socket UDP para facilitar la programación. De
forma distinta a TCP, se puede volver a conectar el mismo socket a diferentes
peers.
TCP ofrece fiabilidad pero también transporta cierta sobrecarga fija. Se puede
añadir fiabilidad a UDP para garantizar la llegada de los datos, secuencia de
paquetes, eliminación de la redundancia, verificación de la integridad de los datos y
eliminación de problemas de flujo de datos. TCP y UDP ofrece niveles distintos de
fiabilidad e interacción. TCP permite ver la información como un flujo; UDP da al
programa acceso a los mensajes particulares. Cada una de estos se fundamenta
en el modelo, funcionalidad y arquitectura de IP.
108/512
Capitulo iv
Uno de los postres europeos más delicioso es la torta, un tipo de pastel muy
rico. La torta tiene normalmente varias capas (de cinco a ocho). Cada capa se
apoya en las capas inferiores y tiene un sabor y color diferente. La torta tiene una
capa de escarcha consistente que recubre desde la capa superior hasta la bandeja.
Si mira cómo está diseñado el subsistema de red conceptual (o pilas de
protocolos), encuentra una disposición en capas similar. En la superficie se
muestra una interfaz común con las capas interiores. Cada capa de la red se apoya
sobre otra para soportar sus características. Esta dependencia es muy importante:
si cualquier parte de la capas inferiores falla, el subsistema completo cae o
funciona de una forma menos óptima.
La división de las responsabilidades de red ofrece el soporte para las
características críticas de la funciones de alto nivel. El subsistema de red es una
mezcla grande y complicada de hardware, memoria, seguridad, datos, núcleo y
aplicación. La coordinación de todas estas tecnologías es el desafío de red.
Frecuentemente, a menos que entienda realmente la interacción cooperativa de las
tecnologías, la coordinación de su fusión sobrecarga el desafío de red.
El modelado de red controla el intercambio de red sin perder la capacidad de
transporte. En este capítulo se introduce el desafío de red y se presentan dos
109/512
modelos que intentarán resolverlo.
110/512
tienen todavía que tratar con varios problemas comunes.
Un problema de muchas tecnologías es la atenuación de señal, o pérdida de
línea, a partir de la resistencia del medio. Tipos eléctricos de transmisión son
particularmente susceptibles a este problema. El mensaje puede degenerarse por
el medio. La pérdida de señal de línea es un verdadero problema para ciertas
tecnologías (por ejemplo, cableado de par trenzado o coaxial). La física de la
transmisión eléctrica y de radio atenúa el mensaje al pasar a través del medio.
Otro problema son las colisiones. Varias computadoras compartiendo el mismo
medio (cableado o no cableado) puede causar colisiones cuando transmiten
mensajes a la vez. Si la computadora envía un paquete mientras otra está
transmitiendo, la colisión resultante desfigura ambos paquetes. Las dos
computadoras deben detectar las colisiones, conceder, recuperar y retransmitir.
Los cortes en el conductor causan la pérdida de señal y la degradación del
paquete. Puesto que la física es complicada, si se desconecta un cable coaxial de
una red activa, cualquier señal eléctrica activa rebota en el exterior de la conexión
abierta como si fuera un espejo. Cualquier transmisión finaliza cuando está
completamente distorsionada.
Transmisiones directas como láser, microondas o infrarrojos pueden también
sufrir cortes. Un obstáculo colocado entre el transmisor y el receptor puede
bloquear completamente la señal. Otros elementos como el humo, niebla y pájaros
causan problemas también.
Los hosts se conectan a un medio de red común, compartiendo tiempo y
recursos, así que la identificación hardware puede ser de pronto un problema.
Cada paquete necesita un destino en una red compartida. Si la conexión de red es
sólo dos computadoras (una red punto a punto), el paquete no necesita un destino.
El destino estaría implícito. Sin embargo, la red compartida necesita una dirección
hardware para que la interfaz de red pueda seleccionar sus mensajes de los miles
que pasan a gran velocidad por ahí.
Las tarjetas LAN ethernet ofrecen su propio ID: MAC, una dirección ethernet de
seis bytes. Cuando la computadora envía un mensaje, el subsistema de red divide
el mensaje dentro de varias tramas, el receptáculo más pequeño transmitido. Sobre
una ethernet, cada trama incluye la dirección MAC del destino y del origen. Even-
tualmente la red asocia la IP lógica con una dirección física real (MAC). Con la
dirección física, la tarjeta LAN recoge sus direcciones. Cuando llega una dirección
coincidente, abre sus buffers y permite que el mensaje fluya hacia dentro.
111/512
Problemas relativos a paquetes en el nivel de funcionamiento en red implican
normalmente problemas con el encaminamiento. Puesto que la red cambia
mientras los mensajes se mueven de un lugar a otro, comprender con anterioridad
lo que son los síntomas, pueden ayudarle a identificar el problema y efectuar una
solución.
Los cambios en la topología de red (o dinámica de propagación) es una
preocupación incluso con la mayoría de las configuraciones de red controladas.
Aunque las causas pueden ser diversas, el resultado es invariablemente la pérdida
de las transmisiones. Un ejemplo de propagación dinámica es cuando la propia red
pierde mensajes en tránsito. Tal vez haya experimentado, u oído hablar, de redes
sitas en cubículos donde alguien mueve los pies v acaba con la sesión de otro
usuario, Las redes entre computadoras cambian, y las conexiones entre los
equipos aparecen y desaparecen.
Otra causa de la propagación dinámica es el envejecimiento excesivo del
paquete. El paquete se propaga a través de un camino y cada nodo que reenvía el
paquete comprueba el envejecimiento utilizando el campo de tiempo de vida (TTL).
Un paquete caduca cuando excede el número de saltos de router adjudicados.
Cada paquete puede conseguir hasta 255 saltos; el valor predeterminado es 64
saltos. Habitualmente, ese valor es suficiente para que el mensaje consiga
alcanzar el destino.
El límite en el número de saltos es importante para que la red no se mantenga
superpoblada de paquetes con un tiempo de destrucción grande. Es fácil para un
paquete que no hace seguimiento dar vueltas en círculos indefinidamente. Por
ejemplo, suponga que el router A obtiene un mensaje. La tabla de encaminamiento
del router A indica que el mejor camino es a través de B, así que éste envía el
paquete a B. El router B, sin embargo, pierde ese enlace, así que es posible que
envíe el paquete de regreso a A. El router A no tiene registro de recepción de
paquetes, así que el paquete rebota de un lado a otro (hasta que expira el campo
TTL o los router A y B sincronizan sus tablas). El campo TTL limita el tiempo que el
paquete da vueltas en círculos.
La lista de rutas y la tabla de hosts pueden llegar a ser muy grandes. Una buena
administración de la tabla ayuda a evitar retardos grandes al calcular el mejor
camino hacia el destino.
Aunque la complejidad de la red es un reto, detectar caminos perdidos,
recuperados y cíclicos también es complejo. A menudo, cuando un segmento de
red desaparece, se forman ciclos (o bucles de red) que ocasionan que los
paquetes envejezcan y mueran rápidamente. La red raramente notifica a los hosts
de envío y recepción del problema; el paquete simplemente caduca.
Algunas veces la ambigüedad del camino puede hacer que un paquete de un
mensaje se duplique durante el tránsito {espejo de paquete o ghosting). Este
extraño caso es indetectable a menos que cada paquete posea un ID o número de
secuencia. No sólo la red puede duplicar información innecesariamente, sino que
también puede perder datos a través de routers desaparecidos durante una
transmisión o incluso corromper paquetes, así que un mensaje de error nunca se
envía ai emisor.
112/512
Todo el tiempo se pierden paquetes. Sin contabilidad y acuses de recibo, el
autor nunca puede determinar si el destino recogió el mensaje.
Por último, los segmentos pueden tener incompatibilidades de datos de router.
Algunos routers o redes no soportan completamente bytes de 8 bits o tamaños de
paquete grandes. Los routers deben transformar los paquetes o devolver un error.
Afortunadamente, los asuntos de red se originan sólo cuando utiliza protocolos
de bajo nivel. Si no tiene tiempo o no quiere afrontar estos retos y está satisfecho
con los rendimientos lentos, utilice los protocolos de alto nivel probados y
comprobados. Muchas de las aplicaciones de red los utilizan. Una vez haya
comprobado el algoritmo con un protocolo de alto nivel, debe considerar la
posibilidad de ajusfar el programa para obtener un mayor rendimiento con
protocolos de bajo nivel. Entonces, cuando se vaya la euforia de construcción del
proyecto, afine y ajuste el algoritmo para obtener más rendimiento.
113/512
Además, el núcleo tiene que preparar cada mensaje para la transmisión, o tiene
que descubrir el mensaje de entrada del receptor. El subsistema de bu/íers sigue la
pista a las tramas construidas y las prepara para la red o el proceso cliente (como
una E/S de disco).
Parte de! proceso de empaquetado del mensaje es la asignación de la
identificación. La identificación del hardware (como la MAC) es torpe y difícil de
administrar en una red grande. Así que el subsistema de red ofrece una
identificación lógica (por ejemplo, el número IP).
Cada paquete necesita la identificación para que el sistema operativo pueda
determinar rápidamente dónde enviarlo. La numeración IP ofrece al sistema
operativo de red una forma de agrupar las computadoras juntas dentro de
subredes.
114/512
red desde el programa. Hace varios años, los grupos de programadores afrontaban
los retos de la red e inventaron varios modelos estándar. Los estándares se
fundamentan normalmente uno encima del otro en forma de capas. Capas nuevas
pueden depender de las soluciones de capas inferiores. Como los cascos de una
cebolla, el corazón del funcionamiento en red es el medio de transmisión físico
(electricidad, radio o luz).
Los dos modelos tratados en este apartado, OSI e IP, son similares a otros
muchos modelos de red. Con esta información, puede ver cómo eligen sus
acciones.
115/512
FIGURA 5.1 Mucha gente asocia las capas de red con el modelo 051. Cada capa
añade funcionalidad y fiabilidad mientras se abstrae la red y se reduce el rendimiento.
Capa 1: física
La capa física abarca todas las interfaces relacionadas con el hardware: el
medio y la señal. El medio (por ejemplo, par trenzado, coaxial, fibra) transporta la
señal a su destino.
Parte de la capa física es el adaptador de red. El adaptador de red actúa como
intermediario entre el núcleo y el medio físico. En un extremo, acepta las peticiones
de transmisión desde los controladores del núcleo y transmite los paquetes del
mensaje (tramas). Cuando finaliza, notifica al núcleo por medio de una interrupción.
Los microcontroladores del adaptador observan si la red está inactiva antes de
enviar una trama. Detectan y gestionan colisiones y retransmisiones. Si es
necesario, notifican al núcleo de los problemas de transmisión. Los controladores
del núcleo entonces administran sus propios reintentos o elevan los errores a la
pila de protocolos.
En el otro extremo, el adaptador observa en la red si existen mensajes que
coincidan con su dirección hardware (ethernet). Cuando coincide, el adaptador
llena un buffer interno y emite una interrupción al núcleo. Al hacer esto, la capa
hardware ofrece su propia identificación hardware.
Repetidores de trama de datos aceptan todos los mensajes de la red y los
pasan a un segmento nuevo de red. Los repetidores de datos son mudos ya que no
seleccionan ningún paquete e ignoran las identificaciones hardware. Un repetidor
de datos tiene menos dificultad de programación que un repetidor normal, el cual
116/512
permite selectivamente el paso de paquetes entre segmentos y se ubica en la capa
física. Utilice un repetidor de datos para extender la longitud de la red y amplificar
la señal atenuada.
Finalmente, algunos adaptadores de ethernet incluyen ahora una suma de
comprobación o CRC de su propiedad para realizar comprobaciones de validación
de datos. Si el CRC encuentra un error, la tarjeta señaliza los datos como
sospechosos para que la capa de enlace de datos lo arregle. Esto puede dar un
poco más de confianza en los protocolos de datagrama.
Otras redes físicas pueden tener otros mecanismos. Por ejemplo, la conexión
PPP puede incluir un bit de paridad (la mayoría no). FDD1 tiene sumas de
comprobación o CRC. Algunos protocolos de radio desconocidos envían
simplemente cada carácter dos veces.
117/512
Capa 3: red
La capa de red se apoya encima de la capa de enlace de datos. Su función
principal es funcionar con las tablas de rutas, routers y gateways para encontrar el
host de destino. En una red no encaminada, esta capa es esencialmente inactiva.
La capa de red ofrece un mecanismo de direccionamiento uniforme para redes
heterogéneas. Por ejemplo, si tiene dos redes físicas como AppleTalk y Ethernet,
esta capa los cruza utilizando una transformación de trama.
Los routers de red se ubican en esta capa, ofreciendo una conexión entre redes
físicas homogéneas y heterogéneas.
Capa 4: transporte
La capa de transporte se apoya encima de la capa de red, confiando en la
integridad de los datos y el encaminamiento de las capas de enlace de datos y de
red. Su principal responsabilidad es entregar un mensaje intacto, en orden y sin
duplicación. Esta capa añade muchas funciones para ofrecer integridad en la
comunicación y en la generación de flujo.
La capa de transporte es el último lugar del modelo en la que se puede controlar
cualquier error de datos o de comunicación. (Con los errores de datos aquí se
refiere sólo a la integridad de los datos, no al significado. Por ejemplo, esta capa no
captura una petición HTTP mal construida, pero captura los errores de suma de
comprobación.) Si la capa detecta un paquete perdido o corrupto, pide una
retransmisión al emisor. Al hacer esta comprobación, añade más sumas de
comprobación al paquete.
En esta capa es introduce el concepto de red virtual o multiplexado. Con esta
característica, cada conexión tiene la percepción de tener una conexión exclusiva a
la red (como el concepto de puerto de TCP/IP). El programa obtiene todos los
mensajes que otros programas remotos le envían, a pesar de que el host puede
tener varios programas de red en ejecución.
Debido a la fiabilidad, la capa de transporte es la capa más común usada por las
aplicaciones que se comunican una con la otra sobre Internet. Ofrece todas las
características necesitadas para mantener sesiones de red.
Capa 5: sesión
La capa de sesión depende con exceso de la capa de transporte para ofrecer la
generación de flujo y la Habilidad que necesita. Su principal responsabilidad es
aportar controles sobre la conexión y el flujo de datos.
La conexión puede ser inestable, razón por la cual esta capa ofrece gestión de
puntos de control. Por ejemplo, suponga que, durante conexión, se está
transfiriendo un archivo y llega un momento en que la conexión se pierde. Todo el
tiempo gastado en la obtención de la parte primera del archivo se pierde si al
reconectar el archivo tiene que comenzar de nuevo. El gestor de puntos de control
bloquea las partes inferiores de la transferencia para que la conexión nueva tenga
que recibir sólo las partes perdidas.
118/512
Como se explicó anteriormente, la capa de sesión reestablece automáticamente
las conexiones que se han perdido. También ofrece procedimientos de seguridad y
acceso.
Además ofrece administración de flujo de datos. Uno de los problemas con las
conexiones cliente-servidor es el control de quién habla primero y cómo mantener
el diálogo. Un método que esta capa utiliza es el pase de token: el host que tiene el
token puede realizar operaciones críticas. Este control de flujo reduce la
probabilidad de interbloqueo, inanición o corrupción de datos (tratado en detalle en
el apartado, "Cuestiones concurrentes del cliente-servidor" del Capítulo 10).
Capa 6: presentación
La capa presentación administra los datos y realiza la transformación necesitada
(por ejemplo: descompresión, dar formato, diseño, mostrar imagen, transformación
del conjunto de caracteres). Las llamadas de procedimiento remoto (véase el
Capítulo 15, "Encapsulados de red con llamadas de procedimiento remoto (RPC)")
son un ejemplo práctico de esta capa.
Capa 7: aplicación
La última capa, la capa de aplicación, ofrece servicios de red como transferencia
de archivos, emulación de terminales, gestión de mensajes y administración de red.
Esencialmente, la mayoría de las aplicaciones utilizan esta capa, ya que ofrece la
Interfaz de programación de aplicaciones (API). Sin embargo, no es la aplicación
real de usuario; es el conjunto de servicios y llamadas de biblioteca para ayudar a
las aplicaciones a funcionar con la red. El Sistema de archivos de red (NFS) es un
ejemplo de esta capa que se engancha dentro de la arquitectura del sistema de
archivos virtual del sistema operativo. Se basa en las herramientas RPC para
ofrecer las interfaces de función.
119/512
6 y 7 de OSI (véase la Figura 5.2). Compare los distintos modelos en la Figura 5.1.
120/512
Capa 2: capa de funcionamiento en Internet (IP)
Las capas IP (IPv4 e IPvó) dependen de los controladores de dispositivo para
proporcionar funcionalidad y una interfaz común. Ambas versiones son distintas en
esta capa. Se componen de pilas distintas de protocolos que traducen de IPv4 a
IPvó, y a la inversa conforme se necesite, para asegurar la compatibilidad.
La capa IP se alinea mejor con la capa de red OSI. Ofrece el direccionamiento
lógico y las funciones de encaminamiento de esa capa. El único componente del
que carece la capa IP es la gestión de mensajes de error de la capa de red.
121/512
con puertos. Como se describió en el Capítulo 4, "Envío de mensajes entre
puntos", esta característica particular abstrae la red y permite al programa de red
actuar como si poseyera exclusivamente la conexión. Todas las otras
características son como en la capa IP. Si se coloca correctamente, UDP se sitúa
en las capas de red y de transporte, apoyándose más en la capa de red.
122/512
Tabla 5.3 Capa de transporte contra TCP
Capa de transporte TCP
Datos fiables Datos fiables (sumas de comprobación).
123/512
termina en la capa de sesión.
El modelo IP funciona de forma distinta. Al adjuntar una cabecera por cada capa
hace pesada la cabecera del mensaje, reduciendo el rendimiento. En vez de eso, el
modelo IP coloca el tipo de protocolo en la cabecera IP. Cuando IP obtiene el
mensaje, comprueba ese campo y encamina el mensaje directamente al protocolo
elegido, después de eliminar su propia cabecera.
Además, como se indicó al final del Capítulo 3, cada uno de los protocolos IP
completa un papel específico. De forma distinta al modelo OSI, donde
verdaderamente cada capa descansa encima de las capas inferiores.
De nuevo, el modelo IP utiliza un protocolo para controlar una necesidad particular. ICMP es
para la gestión de mensajes de error, UDP es para mensajes directos y comunicación sin
conexión, y TCP es para la generación de flujo. El IP raw es para el desarrollo de protocolos
nuevos.
Esta interfaz puede ser distinta de sus expectativas. Linux permite acceso de lectura a los
mensajes del controlador de bajo nivel utilizando SOCK_PACKET. Esta característica
específica de Linux le da acceso a todos los mensajes de la red. Utilizó esta característica para
construir su propio snooper de red en el Capítulo 3.
124/512
de capas. Las capas descansan encima de cada otra como las capas de un pastel. El soporte
que las capas inferiores ofrecen incrementa la fiabilidad de los datos de las capas superiores.
Las capas también ofrecen una amplia selección de velocidades contra la fiabilidad.
La capa primera dirige la interacción física entre las computadoras. En sucesión,
cada capa consolida la fiabilidad, llegada y flujo del mensaje. Cuando utiliza las
interfaces de la capas intermedias de la pila de protocolos, puede prever que los
datos llegan correctamente.
Niveles más altos funcionan como si la red fuera la computadora, mientras que
los niveles más bajos funcionan como si estuviera enviando mensajes en una
botella. Cada uno tiene un uso para los datos del programa.
125/512
Parte II
126/512
Capitulo vi
Generalidades sobre el
servidor
En este capítulo
Asignación del socket el flujo de programa general del servidor
Un servidor de eco sencillo
Reglas generales sobre la definición de protocolos
Un ejemplo extenso: un servidor de directorio HTTP
Resumen: los elementos básicos de un servidor
127/512
directorios HTTP pequeño le mostrará como conectarse a un cliente y cómo
empaquetar mensajes HTML.
128/512
FIGURA 6.1 Los flujos de programa para un cliente y un servidor son similares
pero tienen diferencias muy dispares en el modo en que se conectan a la red. Éste es
un esquema de las llamadas de sistema para los programas cliente y servidor.
El Capítulo 4, "Envío de mensajes entre peers" nos introdujo la llamada del
sistema bind(). Este capítulo la define un poco más formalmente. De igual modo,
describe dos nuevas llamadas -listen() y accept().
129/512
Un servidor de eco sencillo
Antes de hablar acerca de las diferentes llamadas del sistema que utiliza para
crear un servidor, querrá ver el flujo de programa general del servidor de ejemplo.
El ejemplo es el servidor de eco estándar. Al igual que el programa Helio, ejemplo
mundial de programación en C, el servidor de eco es el ejemplo para servidores de
socket. Puede encontrar el código fuente completo de este programa en el sitio
web, con el nombre simple-server.c.
La prueba más simple para la mayoría de las comunicaciones es realizar un
eco. Así como un vendedor por teléfono le repite su orden de compra para
asegurarse de que no hay errores, puede utilizar un eco para iniciar su propio
servidor. Básicamente, el escribir el programa de este modo le ayuda a crear y
probar sus aplicaciones críticas.
PARADIGMA DE LA CREACION Y LA PRUEBA
Habrá notado que este libro cita muchas disciplinas basadas en las ciencias de las
computadoras. La programación en red exige una mayor concentración en la prueba,
comprobación, y creación. Está cargada de tantas cuestiones primordiales que debe
utilizar las aproximaciones más simples a la programación para minimizar los errores en
los programas. El paradigma de la creación y la prueba (una pequeña faceta del
desarrollo rápido de prototipos) le permite concentrarse en un problema específico.
Después de resolver el problema, el resultado se convierte en un cimiento para el resto
del desarrollo.
Un servidor genérico requiere que llame a una serie de llamadas del sistema en
un orden particular. El servidor de eco es una buena demostración de este orden
sin enturbiar el ejemplo con procesos específicos del servidor. El siguiente es el
flujo de programa general para el servidor de eco:
1. Crear un socket con socket().
2. Asociar el puerto con bind().
3. Convertir el socket en un socket en espera con listen().
4. Esperar una conexión con accept().
5. Leer el mensaje con recv() o read().
6. Devolver un eco del mensaje con send() o write().
7. Si el mensaje no es bye, volver al paso 5.
8. Cerrar la conexión con close() o shutdown().
9. Regresar al paso 4.
El flujo del programa evidencia la diferencia con una interfaz sin conexión al no
cerrar la conexión hasta que el cliente lo pida (con un comando bye). El Capítulo 4
presentaba una comparación entre sockets basados en conexión y sockets sin
conexión. El servidor completa esta comparación. Puede pensar en un equipo
homólogo en espera sin conexión, como en algo parecido a un servidor.
Este flujo de programa es importante para ver lo que se va a encontrar por
130/512
delante cuando vaya a crear servidores. El primer paso obvio (la creación de un
socket) se trató en el Capítulo 1, "Recetario del cliente de red." Y, tal y como
apuntamos, todas las comunicaciones en red comienzan con ese paso. El siguiente
paso, asociar el puerto, es crítico para su servidor.
131/512
struct sockaddr^in addr; /* crea un nombre de socket de internet */
bzero(&addr, sizeof(addr)); /* comienza con una lista vacia */
addr.sin_family = AF_INET; /* selecciona la pila IP */
addr.sin_port = htons(MYPORT); /* designa el número de puerto */
addr.sin_addr.s_addr = INADDR_ANY; /* permite IP de cualquier host */
if ( bind(sd, &addr, sizeof(addr)) != 0 } /* pide el puerto */
perror("Bind AF_INET");
Utilice el segmento de código del Listado 6.2 para asociarse a un socket
designado (AF_UNIX o AF_LOCAL).
Listado 6.2 Utilización de bind() en un servidor LOCAL
/****************************************************/
/*** Ejemplo de socket LOCAL: rellena la estructura sockaddr_ux ***/
/*** variable addr ***/
/****************************************************/
#include <linux/un.h>
struct sockaddr_ux addr; /* crea un socket designado local */
bzero(&addr, sizeof(addr)); /* comienza con una lista vacía */
addr.sun_family = AF_LOCAL; /* selecciona un socket designado */
strcpy(addr.sun_path, "/tmp/mysocket"); /* selecciona el nombre */
if ( bind(sd, Saddr, sizeof(addr)) != 0 ) /* asocia el nombre */
perror(“Bind AF_LOCAL");
Con este segmento de código en particular, puede mirar en el directorio /tmp y
ver el archivo de sockets. El registrador (logger) de mensajes del sistema, syslogd,
utiliza sockets LOCAL para conseguir su información: los procesos del sistema
abren una conexión al socket del registrador (logger) y le envían mensajes.
Los siguientes son los errores típicos que puede obtener de esta llamada:
• EACCES. El número de puerto pedido está restringido para acceso del root
solamente. Recuerde que si utiliza puertos entre 0 y 1023, debe ejecutar el
programa como usuario root. Para más información, véase la sección sobre
números de puertos de host en el Capítulo 2.
• EINVAL. El puerto ya está siendo utilizado. Debe tener otro programa que se
ha apropiado de este puerto. También puede obtener este error si trata de
volver a ejecutar un servidor instantes después de haber sido parado. El
sistema operativo se toma su tiempo para liberar un puerto adjudicado (¡hasta 5
132/512
minutos!).
En todos los casos, la llamada del sistema bind() intenta adjudicar un puerto o
nombre específico para el socket de su servidor (utilice el archivo /etc/services
como guía de los puertos disponibles o estándar). Los clientes utilizan este puerto
para establecer la conexión y comenzar a enviar y recibir datos.
133/512
(tal y como 60 para 60 segundos).
Los siguientes son los errores devueltos comúnmente por una llamada del
sistema listen();
134/512
Asegúrese de indicar un tamaño para el parámetro addr lo suficientemente
grande como para aceptar la dirección que se espera. No se preocupe sobre la
corrupción debida al desbordamiento del buffer, la llamada no excede el tamaño en
bytes del parámetro addr_size. En la mayoría de los casos, las conexiones de los
clientes son del mismo tamaño que el protocolo (por ejemplo, AF_INET para
sockaddr_in o AF_IPX para sockaddr_ipx). La llamada pasa el parámetro
addr_size por referencia, de modo que su programa pueda determinar el número
actual de bytes utilizados (véase el Listado 6.4).
Listado 6.4 Ejemplo de Accept()
/*****************************************************************/
/*** Ejemplo de accept: espera y acepta una conexión ***/
/*** de cliente. ***/
/****************************************************************/
int sd;
struct sockaddr_in addr;
/*** Crea un socket, lo asocia y lo convierte en un socket en escucha ***/
for (;;) /* repetir esto indefinidamente */
{ int clientsd; /* el nuevo descriptor de socket */
int size = sizeof(addr); /* reasigna el tamaño de addr */
clientsd = acceptfsd, &addr, &size); /*espera a la conexión */
if ( clientsd > 0 ) /* no ocurren errores */
{
/**** Interacciona con el cliente ****/
close(clientsd); /* limpia y desconecta */
}
else /* acepta errores */
...
135/512
Puede obtener la dirección y el número de puerto de addr utilizando las
herramientas de conversión descritas en el Capítulo 2 (véase el Listado 6.5).
Listado 6.5 Accept() con registro de conexiones
/*****************************************************************/
/*** Ejemplo de accept (revisado): añade registro de conexión ***/
/*** de clientes. Comparar con el Listado 6.4 ***/
/*****************************************************************/
/**** (Dentro del bucle) ****/
...
client = accept(sd, &addr, &size);
if ( client > 0 )
{
if ( addr.sin_family == AF_INET )
printf("Connection[%s]: %s:%d\n",/* registra la conexión */
ctime(time(0)), /* marca horaria de la conexión * /
ntoa(addr.sin_addr), ntohs(addr.sin_port));
/*---interacciona con el cliente ---*/*/
...
Si la llamada del sistema accept() produce un error, devuelve un valor negativo
a su servidor. De lo contrario, devuelve un nuevo descriptor de socket. Los
siguientes son algunos errores comunes:
136/512
Listado 6.6 Ejemplo de servidor de eco
/********************************************************************/
/*** Ejemplo de servicio de eco: devuelve todo lo que nos llegue ***/
/*** hasta "bye<ret>". (Comparar con el Listado 6.5) ***/
/********************************************************************/
/**** (Dentro del bucle y después del accept()) ****/
...
if ( client > 0 )
{ char buffer[1024];
int nbytes;
do
{
nbytes = recv(client, buffer, sizeof(buffer), 0);
if ( nbytes > 0 ) /* si hay datos reales, devuelve un eco */
send(cliervt, buffer, nbytes, 0);
}
while ( nbytes > 0 M strncmpí"bye\r", buffer,4) 1= 0 );
close(client);
}
...
137/512
¿Qué programa habla primero?
La mayoría de los servidores hablan primero, Pero los sistemas de alta
seguridad algunas veces esperan a que el cliente envíe el primer mensaje. El
proceso que utilice puede tener la identificación del cliente en sí (más que el puerto
y la identificación de host estándar). Al mismo tiempo, puede querer tener un
sistema más amigable que proporcione algún listado sobre la identificación y el
protocolo de conexión.
Otra consideración es la interacción innecesaria. Si el servidor habla primero,
normalmente dice siempre lo mismo cada vez que obtiene una conexión. ¿Necesita
el cliente saberlo? ¿Disminuye un poco el rendimiento de los canales de
comunicación esta interacción?
138/512
Esta documentación también puede ofrecer información sobre defectos de software
e incluso sobre crakers de sistemas. Un modo sencillo es pedirlo. Algunos
navegadores y servidores Web utilizan métodos más elaborados.
139/512
¿Necesita sincronización horaria?
Aunque se trata de un desafío, la sincronización horaria (coordinación de los
relojes a tiempo real entre hosts) es esencial cuando tiene tareas críticas en el
tiempo, tales como transacciones financieras. Este es un problema feo. Primero,
determine con cuanta exactitud debe producirse la sincronización.
El servidor tiene que establecer su hora en relación a la del cliente, Sin
embargo, la mayoría de los clientes no sincronizan con la hora de la red. Puesto
que rara vez el cliente es exacto, ambos usan un arbitro (un servidor de tiempo).
Sin embargo, esto presenta sus propios problemas (el retraso en conseguir el
tiempo correcto de la red).
Una solución para un problema de sincronización es establecer todas las
transacciones en el servidor (alejar la mayor parte de la responsabilidad del
cliente). Después de que el servidor complete la transacción, responde al cliente
con la hora y fecha de envío.
¿Cuándo ha finalizado?
El servidor y el cliente se han conectado y se han pasado paquetes en un sentid
y en otro. Ahora es el momento de decir adiós. Saber cuando la interacción esta
completa puede no ser una tarea tan fácil cómo parece. Por ejemplo, cuando
escribe un servidor HTTP, completa la interacción enviando dos códigos de nueva
línea. Sin embargo, puede esperar indefinidamente a la petición del cliente cuando
tiene un espacio de buffer insuficiente para una sola lectura y realiza la lectura con
read( o recvf). En este caso, el servidor puede agotar el tiempo de espera y
declarar 1 conexión finalizada.
Igualmente, puede ser difícil decidir qué programa debe cerrar la conexión
140/512
primero. Un cliente obtiene un error de canal roto (EPIPE) cuando el servidor
cierra 1, conexión antes que el cliente complete la transmisión.
Todo se reduce a lo que los protocolos demandan. Las interacciones HTTP
necesitan un par de códigos de nueva línea para indicar un final de transmisión.
Puede tener que definir el suyo propio, puesto que un flujo de sockets le da una
gran libertad de acción.
141/512
/*--- muestra mensaje 'helio' del cliente en servidor ---*//
fprintf(stderr, "%s", buffer);
close(client);
}
else
perror("Accept");
}
142/512
No obstante, se puede evitar la devolución del estado e información MIME, ya
que el cliente asume que se trata de HTML. El programa simplemente envía el
código fuente HTML generado.
Al escribir un servidor de HTTP sencillo, puede encontrarse algunos problemas.
Por ejemplo, el servidor no sabe qué extensión tendrá el documento resultante, por
lo que convierte el canal del socí:ef en una estructura FILE* (véase el Listado 6.8).
Listado 6.8 Algoritmo de un receptor HTTP extendido
/***************************************************************/
/*** Ejemplo HTTP 1 . 0 : acepta conexión, obtiene petición, ***/
/*** abre directorio, y crea un listado de directorio HTML. ***/
/***************************************************************/
...
/**** Crea el socket, lo asocia, lo convierte a un receptor ****/
for (;;)
{ int client;
int size = sizeof(addr);
client = accept(sd, Aaddr, Asize); /* espera a una conexión */
if ( client > 0 ) /* si tiene éxito... */
{ char buf[1024];
FILE *clientfp;
bzero(buf, sizeof(buf); /* limpia el buffer */
recv(client, buf, sizeof(buf),0); /* obtiene el paquete */
clientfp * fdopen(client, "w"); /* lo convierte a FILE* */
if ( clientfp 1 = NULL )/* si la conversión tiene éxito,... */
{
/**** Obtiene la ruta desde el mensaje (buf) ****/
/**** Abre directorio ****/
/**** Para todos los archivos,... ****/
/**** lee cada nombre de archivo ****/
/**** Genera una tabla HTML ****/
fclose(clientfp); /* cierra FILE ptr */
}
else
perror("Client FILE"); /* no puede convertirlo a FILE */
close(client); /* cierra el socket del cliente */
143/512
}
else
perror("Accept"); /* ha ocurrido un error en accept() */
}
...
144/512
Capitulo vii
División de la carga:
multitarea
En este capítulo
Definición de multitarea: procesos frente a threads
Venciendo al reloj: condiciones de carrera y exclusiones mutuas (mutex)
Control de hijos y eliminación de procesos zombis
Ampliación de los clientes y servidores actuales
Llamada de programas externos con el exec del servidor
Resumen: distribución de la carga del proceso
Imagínese todas las tareas que podría realizar si pudiera trabajar en muchas
actividades dispares a la vez. Esta asignación de tareas en paralelo le permite
concentrarse eficientemente en diferentes tareas sin distracción. De hecho, la
mente es capaz de hacerlo. Por ejemplo, puede limpiar e) sótano mientras está
pensando en un algoritmo particularmente duro; o puede preocuparse acerca de un
problema matemático mientras lee alguna lectura de ciencia-ficción. Cada uno de
ellos es un ejemplo de paralelismo - o multitarea.
Hasta el momento, ha creado unos pocos clientes que se conectan con
servidores en red, ha programado equipos homólogos para compartir información,
y ha escrito un par de servidores. Pero en todos estos casos, cada programa
realiza una sola tarea a la vez. El servidor permite la conexión a un solo cliente; y
una conexión entrante de un cliente tiene que esperar mientras el servidor da
servicio al cliente actual. ¿No sería estupendo aceptar más de una conexión al
mismo tiempo? ¿Qué tal conectar su cliente a varios servidores en el acto?
La multitarea es muy potente y puede facilitar la programación si piensa en
tareas múltiples o en paralelo con responsabilidades definidas. Por encima de todo,
escriba su código de forma inteligible por sus equipos homólogos. La revisión de su
código fuente correcto para implementar la multitarea sin una planificación
adecuada puede fácilmente generar un código desagradable y poco manejable.
145/512
Este capítulo se extiende sobre el tópico frecuentemente confuso de la
multitarea, cubriendo tantos procesos como los threads. Además de cómo y
cuando usarlos, este capítulo describe sus usos, diferencias, puntos fuertes y
debilidades. Adicionalmente, nos presenta las señales, el bloqueo, y el
interbloqueo.
Como puede imaginar, los tópicos presentados en este capítulo son bastante
amplios. Podrá encontrar textos educativos completos dedicados a estos temas.
No obstante, este capítulo solo presenta los puntos esenciales de la multitarea para
la programación en red. Por tanto, la discusión se centra en la información que
necesitará para realizar su trabajo de un modo rápido y fácil.
Para una mayor claridad, este capítulo utiliza el término tarea para indicar una
entidad ejecutable del sistema. Bajo esta definición, los procesos y los threads son
tareas para simplificar la discusión y ayudarle a entender la multitarea dentro de
este contexto,
146/512
Las tareas pueden compartir diferentes páginas, dependiendo de cómo fueron
creadas. Para los procesos, Linux utiliza un algoritmo de copia al escribir. Los
procesos no comparten ningún dato, pero antes que copiar todo de inmediato, la
VM sólo copia aquellas páginas que el padre o el hijo examinan. La copia al escribir
reduce los costes fijos de la creación del proceso, incrementando el rendimiento de
su estación de trabajo. Parte del gran rendimiento que podemos observar en Linux
es debido, en parte, a esta copia en memoria retardada.
Los procesos y los threads tienen cada uno sus respectivos papeles y rara vez
se cruzan. Estos papeles pueden ayudarle a decidir cuál utilizar. Por ejemplo,
utilizará un proceso para llamar a un programa externo y obtener información de él.
Por otro lado, puede utilizar el thread para procesar una imagen y mostrarla
mientras la carga. Esta regla simple puede ayudarle a decidir entre procesos y
threads: si necesita compartir datos, utilice un thread.
La Figura 7.1 muestra qué partes de una tarea puede compartir. En todos los
casos, las tareas no pueden compartir la pila y el contexto. La implementación
actual de la biblioteca de threads le indica a un thread que comparta todo excepto
el identificador de proceso (PID).
Durante un intercambio de tarea, el sistema operativo reemplaza las entradas de
la tabla de páginas actual con aquellas de la tarea entrante. Este coste fijo puede
llevar varios ciclos de CPU. Puede observar sobrecargas por intercambio de tareas
entre 1us y 0.1ms, dependiendo del microprocesador y su velocidad de reloj. Eso
puede ser un tiempo muy largo —especialmente cuando el intercambio de tareas
en Linux se produce unas 100 veces por segundo (cada 10ms). Cada tarea
consigue una porción del tiempo de CPU, parte del cual se destina al propio
cambio de tarea.
En algunas versiones de UNIX, los threads se ejecutan más rápidos porque el
intercambiador de tareas tiene que limpiar menos entradas en la tabla de páginas.
Las versiones de núcleo de Linux 2.0 y 2.2 muestran un tiempo de intercambio casi
idéntico. Esta similitud es debido a los algoritmos de intercambio tan bien afinados
que utilizan.
147/512
FIGURA 7.1 Las tareas de Linux poseen varias regiones de la memoria.
Linux y otros sistemas operativos también soportan el multiprocesamiento
simétrico. Si escribe sus programas de forma que soporten multitarea, conseguirá
una carga acelerada automática cuando sean ejecutados en multiprocesadores
simétricos. Las reglas son, básicamente, las mismas que en Linux, sólo que hay
dos o más procesadores listos para ejecutar las tareas. (En el momento de la
redacción de este libro, Linux soportaba un máximo de 16 CPU simétricas.)
148/512
• Realizar otro trabajo. Procesar información o delegar tareas.
• Responder al usuario. Entrada de usuario o informe de estado.
• Dar servicio a otras computadoras o personas. Una tarea espera y acepta una
conexión mientras que otra da servicio a una conexión ya establecida.
Mezclar threads y procesos en el mismo programa puede parece atípico. Sin
embargo, esto es realmente común en aplicaciones interactivas grandes. Los
navegadores de Internet normalmente mezclan ambos, de modo que cada ventana
del navegador es un proceso independiente, mientras que cada petición, tal y como
una carga de página, provoca que el navegador dispare varios threads. Puede
mezclar y emparejar threads y procesos cuando los necesite.
Características de la multitarea
Observe varias características comunes en todas las tareas de un listado de
procesos (utilizando los comandos top o ps aux). Estas características le ayudarán
a trabajar con ellas de un modo consistente.
Primero, cada tarca tiene un padre. Bueno, esto casi es cierto. El listado de la
tabla de procesos muestra un programa llamado init; esta es la madre de todas las
tareas del sistema. Su principal responsabilidad es gestionar las tareas en
ejecución en el sistema. La tarea padre crea y gestiona las tareas que utiliza para
delegar responsabilidades. Cuando el hijo completa su tarea, el padre debe
eliminarlo tras su finalización. Si el padre no la elimina, tendrá que hacerlo
eventualmente init.
Cada tarea utiliza memoria, E/S y prioridades. Los programas más interesantes
trabajan con más información que la que el contexto puede soportar —16-32
registros no son muchos. La memoria se mide siempre en términos de RAM y el
espacio de intercambio que establece en su computadora. Los programas utilizan
esta memoria para almacenar su estado actual (es diferente al contexto de la
tarea).
Sus programas necesitan interactuar con algo, tal y como usted mismo o algún
archivo. Esto implica E/S. Cada tarea consigue tres canales de E/S estándar;
• stdin. Entrada estándar (sólo entrada), típicamente el teclado.
• stdout. Salida estándar (sólo salida), típicamente la pantalla.
• stderr. Error estándar (sólo salida), típicamente la pantalla o el registro.
Puede cambiar (o redireccionar) todos estos canales desde su programa o
cuando lo ejecute. Su elección de destinos de redirección puede incluir otros
dispositivos, archivos, o una tarea diferente. Cuando crea (o genera) una tarea, la
tarea hijo hereda todos los archivos abiertos del padre. Cómo de estrechamente
acoplada es esta herencia depende del tipo de tarea.
Todas las tareas tienen una pila hardware independiente. Esto es muy
importante -especialmente si utiliza la llamada del sistema de bajo nivel de creación
149/512
de tareas (clone(), véase más tarde en este capítulo). Los programas utilizan la pila
hardware para seguir la pista de los valores de retorno de las funciones, las
variables locales, los parámetros, y las direcciones de llamadas de retorno. Si las
tareas compartieran su pila hardware, podrían degradar inmediatamente el trabajo
de otros.
Por último, cada tarea tiene una prioridad única. Puede cambiar la cantidad de
tiempo de CPU que su programa utiliza elevando o disminuyendo la prioridad.
PLANIFICACION DE TAREAS EN LINUX
Los sistemas operativos multitarea utilizan muchos métodos diferentes para planificar las
tareas. Linux utiliza un esquema de planificación round robin organizador por prioridades.
En un round robin, cada tarea consigue una porción del tiempo de CPU por turnos. Para
implementar las prioridades, este esquema de planificación mueve las tareas de prioridad
más alta hacía arriba en la lista de tareas más rápidamente que las tareas de baja
prioridad.
150/512
en la creación de archivos (umask).
Tablas de archivos Las tablas de archivos abiertos nos son
compartidas. Si el hijo cierra un compartidas. El sistema operativo copia las
archivo, el padre pierde el tablas de forma que los dos procesos tengan el
acceso a ese archivo. mismo archivo abierto, pero el cierre de un
canal no afecte al otro proceso.
No tiene señales compartidas. las señales son compartidas. Un thread puede
bloquear una señal utilizando sigprocmask() sin
afectar a los otros threads.
Utilice lo que mejor se ajuste a las circunstancias y con lo que se siente más
cómodo. Existen claras ventajas en conseguir la compartición de datos sin
restricción que ofrecen los threads, pero la simplicidad e independencia de los
procesos los hace igualmente atractivos.
151/512
Listado 7.1 Ejemplo de desdoblamiento de tareas
/****************************************/
/* Padre e hijo siguen caminos únicos */
/* Multitarea estilo división-de-tareas */
/****************************************/
int pchild;
if ( (pchild = fork()) -= 0 )
{ /* Está en el hijo */
/*---Realiza algunas tareas del hijo ---*/*/
exit(status); /* ¡Esto es importante! */
}
else if ( pchild > 0 )
{ /* Está en el padre */
int retval;
/*---Realiza algunas tareas del padre ---*/*/
wait(&retval); /* espera a la finalización del hijo */
}
else
{ /* Algún tipo de error */
perror("Tried to fork() a process");
}
Listado 7.2 Ejemplo de delegación de trabajo
/************************************************************/
/* Padre (servidor) e hijo (procesador del trabajo) */
/* Multitarea estilo delegación de trabajo */
/******************************************************/
int pchild;
for (;;) /* Bucle permanente */
{
/*---Esperar a una petición de trabajo ---*/*/
if ( (pchild = fork()) == 0 )
{ /* hijo */
152/512
/*---Procesar la petición ---*/*/
exit(status);
}
else if < pchild > 0 )
{
153/512
convergencia. Como regla general, evite este estado. La ejecución conjunta
también puede tener lugar si el proceso no finaliza correctamente con una llamada
explícita a la llamada del sistema exit().
IMPLEMENTACIÓN DE TOLERANCIA A FALLOS CON CONVERGENCIA
Puede utilizar esta potente técnica de ejecución conjunta como parte de un sistema
tolerante a fallos. Un sistema tolerante a fallos puede duplicar los cálculos para verificar la
exactitud. Cuando un procesador comienza a degradarse, los resultados computacionales
pueden variar. Puede implementar esta técnica ejecutando conjuntamente dos o más
tareas y asociándolas a una CPU particular (actualmente no soportado en Linux). Cada
tarea ejecuta los mismos cálculos en su procesador designado. Y, cada cierto intervalo de
tiempo, las tareas envían sus resultados actuales para la ratificación por parte de un
árbitro. El sistema cierra el odd-processor-out.
Si obtiene un error de una llamada del sistema fork(), puede que existan
problemas con su tabla de procesos o con los recursos de memoria. Un síntoma
típico de un sistema operativo sobrecargado es la denegación de recursos. La
memoria y las entradas de la tabla de procesos son elementos fundamentales para
un funcionamiento correcto del SO. Ya que estos recursos son tan importantes,
Linux pone una limitación a la cantidad total de recursos que un proceso puede
tener. Cuando no pueda obtener un trozo de memoria de un tamaño razonable o no
pueda crear una tarea nueva, podría tener problemas con su programa o no
disponer de suficiente memoria.
154/512
último argumento en la línea de comandos del ce. Por ejemplo, para compilar
mythreads.c y enlazarlo con la biblioteca Pthreads, utilice lo siguiente:
cc mythreads.c -o mythreads -lpthreads
Al igual que con la llamada del sistema forkQ, en el momento que ejecute con
éxito pthread_create(), tendrá otra tarea en ejecución. Sin embargo, la creación de
threads es más complicada que la llamada del sistema fork(). Dependiendo de
cuantas características quiera utilizar, puede tener que realizar un mayor trabajo
previo. Cada parámetro proporciona el material necesario para iniciar el nuevo
thread (véase la Tabla 7.1).
Tabla 7.1 La funcionalidad de cada parámetro para la llamada de
biblioteca pthread_create().
Parámetro Descripción
child El gestor del thread nuevo. Utilizará esta variable después de la llamada
para controlar el thread.
Como hemos advertido previamente, tras completar con éxito la llamada tiene
dos thread en ejecución -el padre y el hijo. Ambos comparten todo excepto la pila.
El padre mantiene un gestor (child) del thread hijo, y el hijo ejecuta su propia rutina
(fn) con configuración (arg) y modos de actuación (attr). Si establece los atributos a
NULL, aún puede cambiar el modo de actuación del thread más tarde. Tenga en
mente que hasta que cambie los atributos, el thread se comportará en el modo
predeterminado.
Esencialmente, la llamada se reduce a dos parámetros específicos. Puede
comparar los algoritmos básicos para procesos y threads en el siguiente código.
Los algoritmos son prácticamente un espejo el uno del otro para mostrar las
diferencias elementales. Compare los Listados 7.4 y 7.5 para ver las diferencias en
la creación de procesos y threads.
155/512
Listado 7.4 Ejemplos de creación de procesos
/**************************************/
/* Este ejemplo crea un proceso */
/*************************************/
void Child_Fn(void) {
/* hacer cualquier cosa que un hijo haga */
}
int main(void)
{ int pchild;
/* - - - Iniciación-- -*/*/
/* Crea un nuevo proceso */
if ( (pchild = fork{)) < 0 )
perror("Fork error");
else if ( pchild -= 0 )
{/* aqui está el hijo */
/* cerrar las E/S no necesarias */
Child_Fn();
exit(0);
}
else if { pchild > 0 )
{/* aqui está el padre */
/*---cerrar las E/S no necesarias -•-*/*/
/*---hacer otras tareas ---*/*/
/* esperar la finalización del hijo "/
wait();
}
return 0;
}
Listado 7.5 Ejemplos de creación de un thread
/************************************/
/* Este ejemplo crea un thread */
/***********************************/
void *Child_Fn(void *arg)
{ struct argstruct *myarg = arg;
156/512
/* hacer cualquier cosa que un hijo haga * /
return NULL; /* cualquier valor •/
}
int main(void)
{ struct argstruct arg = {};
thread_t tchild;
/*** Observe que los threads no tienen una sección del hijo ***/ç
/* Aún estamos implícitamente en el padre */
157/512
sobre la creación de procesos y threads. Es una llamada específica de Linux que le
permite seleccionar entre seis tipos diferentes de memoria para compartir. Utilice
esta llamada individual para acceder al espectro completo de la compartición de
datos de tareas. Volviendo al diagrama previo de las páginas de memoria virtual,
utilice esta llamada para habilitar/deshabilitar la compartición de cualquier
combinación de las páginas compartibles de una tarea.
TENGA CUIDADO CON _CLONE()
La llamada del sistema _clone() no es apta para personas con problemas de corazón.
Con toda su potencia, puede reventar fácilmente su programa (¡y hacer su tarea de
depuración casi imposible!).
Si aún quiere jugar, _clone() tiene el siguiente aspecto:
#include <sched.h>
int _clone(int (*fn)(void*), void* stacktop,
int flags, void" arg);
Esta llamada necesita un poco de explicación. Similar a la llamada del sistema
fork(), la llamada del sistema _clone() devuelve o bien el PID del hijo o bien un -1 si
ha ocurrido un error. La Tabla 7.2 describe el significado y utilización de cada
parámetro.
Tabla 7.2 Los parámetros de la llamada del sistema _clone()
Parámetro Descripción
fn Similar a pthread_create(), es un puntero a una función que el hijo
ejecuta. Cuando la función termina (con una instrucción de retorno o
una llamada explícita a la llamada del sistema exit()), la tarca
finaliza.
stacktop Este parámetro apunta a la cima de la pila del hijo. La mayoría de
los procesadores (excepto los HP PA RISC) aumentan sus pilas
desde la cima hasta el fondo. Por tanto, necesita pasar el primer
byte válido de la pila.
(Necesita usar compilación condicional para conseguir facilidad de
adaptación)
flags Este parámetro indica qué se va a compartir v que señal emitir
cuando el hijo finalice. La Tabla 7.3 lista las posibles opciones de
compartición. Mientras pueda especificar cualquier señal sobre la
conclusión del hijo, querrá adherirse a SIGCHLD (el estándar).
arg Similar a la llamada de biblioteca pthread_create(). utilice este
parámetro para pasar información al parámetro fn del hijo.
ESPACIOS DE PILA DE CLONES
Puesto que los threads comparten todo y compartir la pila es una mala idea, el padre
tiene que repartir algún espacio para la pila del hijo. Este espacio es aún parte de la
región de datos compartióles de todo el mundo. Si tiene una tarea actuando mal o
158/512
espacio de pila insuficiente para su hijo, su depuración va a resultar todo un desafío.
Volviendo de nuevo al diagrama de la memoria virtual del procesador (Figura
7.1), puede utilizar los señalizadores de la Tabla 7.3 para habilitar diferentes formas
de compartición de datos. Si son omitidos, el sistema operativo asume que quiere
copiar esa región de datos entre tareas.
Tabla 7.3 Tipos diferentes de datos de tareas compartibles con _clone()
Señalizador Descripción
CLONE_VM Comparte los datos entre tareas. Utilice este flag para
compartir todos los datos estáticos, los datos preiniciados, y
la pila de asignación. Por otro lado, copia el espacio de
datos.
CLONE_F5 Comparte información del sistema de archivos -directorio de
trabajo actual, sistema de archivos raíz, y permisos de
creación de archivos predeterminados. Por otro lado, copia
la configuración.
CLONE_FILES Comparte los archivos abiertos. Cuando una tarea cambia el
puntero de archivo, las otras tareas ven el cambio. Del
mismo modo, si la tarea cierra el archivo, las otras tareas no
son capaces de acceder más al archivo. Por otro lado, crea
nuevas referencias para ínodos abiertos.
CLONE_.SIGHAND Comparte las tablas de señales. Las tareas individuales
pueden elegir ignorar las señales abiertas (utilizando
sigprocmask()) sin afectar a los equipos homólogos. Aparte
de eso, copia las tablas.
CLONE_PID Comparte los identificadores de procesos. Utilice este
flageen cuidado; no todas las herramientas existentes
soportan esta característica. La biblioteca Pthreads no utiliza
esta opción. Aparte de eso, reserva un PID nuevo.La
llamada del sistema _clone() es una creadora de tareas que
engloba todo. Si comparte todo, el hijo es un thread; si no
comparte nada, el hijo es un proceso. Piense que la llamada
del sistema _clone() reemplaza efectivamente a la llamada
del sistema fork() y la llamada de biblioteca pthread_create()
si es configurada adecuadamente.
Los Listados 7.6 y 7.7 muestran secciones de código implementado con fork() y
pthread_create() comparados con una llamada del sistema _clone{) equivalente.
Listado 7.6 Código ilustrando fork() y pthread_create()
/*************************************/
/* La llamada del sistema fork() */
/*************************************/
void Child(void) {
159/512
/*--- deberes del hijo ---*/*/
exit(0);
}
int main(void)
{ int pchild;
if ( (pchild = fork()) == 0 )
Child();
else if ( pchild > 0 )
wait();
else
perror("Can’t fork process");
}
/***************************************************/
/* La llamada de biblioteca pthread_create() */
/***************************************************/
void* Child(void* arg)
{
/*--- deberes del hijo---*/*/
return &Result;
}
int main(void)
{ pthread_t tchild;
if ( pthread_create(&tchild, 0, &Child, 0) != 0 )
perror("Can't créate thread");
pthread_join(tchild, 0);
}
Listado 7.7 Código para una llamada del sistema _clone() equivalente
/**************************************************************/
/* La llamada del sistema _clone() equivalente a fork{)*/
/**************************************************************/
void Child(void* arg)
{
/*--deberes del hijo ---*/*/
exit(0);
160/512
}
/*****************************************************************************/
/* La llamada del sistema _clone() equivalente a pthread__create () */
/*****************************************************************************/
void Child(void* arg) í
/*---deberes del hijo ---*/*/
exit(8);
}
#define STACK 1024
int main(void)
{ int cchild;
char *stack-malloc(STACK);
if ( (cchild - __clone(&Child, stack+STACK-1, CLONE_VM |
CLONE_FS | CLONE_FILES | CLONE_SIGHAND | SIGCHLD,
0)) < 0 )
perror("Can't clone");
wait() ;
}
Básicamente, puede utilizar la llamada del sistema _clone() en cualquier lugar
donde pueda utilizar las otras dos llamadas. Sin embargo, observe que esto no es
al 100% compatible con la implementación de Pthreads. Por un lado, una tarea
clónica tan sólo devuelve un entero (como un proceso). Pthreads, por otro lado, le
permite a un thread devolver cualquier valor. Además, la finalización de las tareas
también es diferente. A menos que espere utilizar su código exclusivamente en una
161/512
computadora compatible con Linux, le interesará centrarse en las llamadas
estándar de generación de tareas.
162/512
Memoria compartida/común
Cuando juega al poker, no quita ojo a dos recursos en particular -la apuesta y la
baraja. Estos son recursos compartidos y limitados. Cómo juega y cuándo se retira
depende de esos factores. Durante el juego, el repartidor se apropia de la baraja y
distribuye las cartas entre los jugadores. Los jugadores evalúan la mano que han
recibido y arrojan envites a la apuesta. Del mismo modo, es necesario tener en
mente los recursos de memoria disponibles para su programa.
Puede programar procesos y threads para enviar pasivamente información a
todo el que soporte memoria común o compartida. El threading ofrece memoria
común intrínsecamente: los padres y los hijos (incluso hermanos) comparten las
mismas regiones de datos predeterminadas. La pila es la única región entre tareas
de threads. Sólo ha de asegurarse de que no exista un acceso simultáneo a la
misma zona de memoria.
Puede crear memoria compartida en sus procesos utilizando la llamada del
sistema shmgetQ. La memoria compartida de este modo reparte un bloque de
memoria para la comunicación con otros procesos. Sin embargo, esto reduce la
velocidad del tiempo de computación porque el procese.) tiene que utilizar el
sistema para gestionar el acceso a memoria.
163/512
if ( pipe(fd) != 0 ) /* crear el canal */
perror("Pipe creation error");
...
read(fd[0], buffer, sizeof(buffer)); /* leer del final de la entrada */
...
164/512
char buffer[1024];
...
/*---Crea el thread utilizando pthread_create() •--*//
write(FDs[1], buffer, buffer_len); /' envía mensaje al hijo */
...
/*--- (Deja el cierre del canal al hijo) ---*/*/
pthread_join(pchild, arg);
El proceso hijo o el thread desempeñan diferentes papeles roles. Aunque
similares el uno al otro y debido a las restricciones en los threads, trabajan de un
modo ligeramente diferente con los canales. Compare el Listado 7.10 con el
Listado 7.11.
Listado 7.10 Creación de canales en un proceso hijo
/**********************/
/*** Proceso hijo ***/
/*********************/
int FDs[2]; /* crea la ubicación del nuevo canal pipe */
pipe(FDs); /* crea el canal: FDs(0]=entrada, FDs[1]=salida */
char buffer[1024];
/*---Crea el proceso hijo ---*/*/
dup2(FDs[0], 0); /* sustituye stdin */
close(FDs[0]); /* no se necesita ahora */
close(FDs[1]); /* sin permiso de escritura para el padre */
read(0, buffer, sizeof(buffer)); /* obtiene mensaje del padre */
...
/"---realiza procesamiento ---*/*/
printf("My report..."};
exit(0)
close(FDs[1]>;
Listado 7.11 Creación de canales en un thread hijo
/*******************/
/* Thread hijo ***/
/******************/
int FDs[2]; /* crea la ubicación para el nuevo canal */
pipe(FDs); /* crea el canal: FDs[0]=entrada, FDs[1]=salida */
char buffe r [ 1024];
165/512
/*---Crea el proceso hijo ---*/*/
read(FDs[0J, buffer, sizeof(buffer));/• obtiene mensaje del padre */
...
/*---realiza procesamiento ---*/*/
printf("My report...");
...
close(FDs[0]); /* cierra el canal de entrada "/
close(FDs[1]); /" cierra el canal de salida */
pthread_exit(arg);
Puede crear un canal entre dos tareas utilizando la Figura 7.2 como guía; utilice
los siguientes pasos para entender mejor lo que ocurre:
1. El padre declara un array de descriptores de archivo. Cuando llama a la
llamada del sistema pipe(), la llamada coloca los descriptores en este array.
2. El padre crea el canal. El núcleo crea una cola y coloca el descriptor de
lectura en fd[0] y el descriptor de escritura en fd[1].
166/512
FIGURA 7.2 Cuando crea un canal entre tareas, tiene que seguir una secuencia
particular para redirigir y cerrar canales.
167/512
si quiere comunicación bidireccional real, necesitará llamar a pipe() dos veces -un canal
para cada dirección (véase la Figura 7.3).
3. El padre genera la tarea nueva. El proceso consigue una copia exacta de todo
(incluyendo el canal). El canal entre el padre y el hijo es entrelazado.
4. El padre se comunica con el hijo utilizando el array de descriptores de
archivos.
5. El proceso hijo redirige su propia entrada estándar (esto es opcional) y se
comunica con el padre utilizando el canal abierto. También cierra el canal de salida.
6. El padre sólo necesita el canal de salida del canal para comunicarse con el
hijo, por lo que cierra el canal de entrada.
No puede redirigir los canales de E/S en los threads porque comparten las
tablas de descriptores de archivos. Esto significa que si cierra el archivo en un
thread, esencialmente lo está cerrando en el resto de threads de su programa. Los
procesos, por otro lado, hacen una copia de las tablas de descriptores de archivos,
de modo que al cerrar un canal no afectamos al resto de procesos en el programa.
La Figura 7.2 le muestra cómo crear una conexión unidireccional entre un padre
y el hijo. Para crear una conexión bidireccional, necesita seguir la Figura 7.3.
Este diagrama le ayudará a crear canales y redirecciones en sus
programaciones.
168/512
Una señal es similar a una interrupción; la única información es que la señal ha
tenido lugar. Cuando llega la señal, la tarea para lo que esté haciendo y salta a una
rutina especificada (llamada gestor de señales). Para la señal SIGCHLD, puede
utilizar wait() para limpiar la finalización del hijo. También puede soportar otras
señales, tales como errores matemáticos o interrupciones vía Ctrl+C.
FIGURA 7.3 Su programa puede crear una conexión adicional de vuelta si quiere
comunicación bidireccional entre el padre y el hijo.
Linux soporta dos estilos de señales: System V (una señal de toma única que
revierte de vuelta al gestor predeterminado cuando el sistema llama a su gestor
169/512
habitual) y BSD (que mantiene su gestor hasta que se le diga lo contrario). Si utiliza
la llamada del sistema signal(), la toma única es la predeterminada. Puede no
estar interesado en esta característica -debería prever la captura de la señal con
regularidad.
RESTABLECIMIENTO DEL GESTOR DE SEÑALES
Para restaurar el gestor, algunos programas acostumbran a reparar el comportamiento de
toma única del System-V colocando una llamada a la llamada del sistema signal
directamente en el gestor de señales. Desgraciadamente, esto puede crear una condición
de carrera; una señal podría llegar antes de la llamada para ponerla a cero. Como
resultado, la señal se podría perder.
En lugar de utilizar signal(), la llamada del sistema preferida es sigaction(). La
llamada del sistema sigaction() le proporciona más control sobre el
comportamiento del subsistema de la señal. A continuación vemos su prototipo:
#include <signal.h>
int sigactionfint signum, const struct sigaction *action, const struct sigaction *old);
Utilice el primer parámetro para definir qué señal se ha de capturar. El segundo
parámetro define cómo quiere manipular la señal. Si establece el último parámetro
a un valor no nulo, la llamada almacena la definición de la última acción. La
definición de la estructura de los parámetros segundo y tercero es la siguiente:
struct sigaction
{
/* Puntero de función al gestor */
void (*sa_handler)(int signum);
/* Retrollamada especializada sigaction */
void (*sa_sigaction){int, siginfo_t *, void *);
/* Array de bits de señales para bloquear mientras se esté en el gestor */
sigset_t sa_mask;
/* actuaciones a realizar */
int sa_flags;
/* (desaprobado — establecido a cero) *//
void (*sa_restorer)(void);
};
Para utilizar esta estructura, se establece en el primer campo (sa_handler)
valores diferentes a los de un puntero de función. Si coloca SIG_IGN en este
campo, el programa ignora la señal entrante específica. Además, si utiliza
SIG_DEL el programa regresa al comportamiento predeterminado.
Para permitir o denegar señales en cascada (una señal interrumpiendo a otra),
utilice el segundo campo, sa_mask. Cada bit (hav 1.024 bits posibles) le indica a
una señal que permita (1) o bloquee (0). De modo predeterminado, una señal
170/512
específica se bloquea al igual que las señales mientras se está dando servicio a la
primera. Por ejemplo, si está prestando servicio a una señal SIGCHLD y otro
proceso finaliza, el gestor ignora esa señal por defecto. Puede cambiar el
comportamiento con SA_NOMASK.
El sa_flags determina el comportamiento del gestor. La mayoría del tiempo,
podemos darle un valor de cero:
#include <signal.h>
/* Define el receptor de señales */
void sig_catcher(int sig)
{
printf("I caught signal #%d\n", sig);
}
int main(void)
{ struct sigaction act;
bzero(&act, sizeof(act));
act.sa_handler = sig_catcher;
sigaction(SIGFPE, &act, 0); /* Captura un error de punto flotante */
act.sa_handler - SIG_IGN; /* Ignora la señal */
171/512
signal(SIGINT, &act, 0}; /* Ignora el "C (interrupción) */
/* - - - realiza procesamiento---*/*/
}
172/512
continuar durante la transición. Por ejemplo, no devuelva un objeto construido
desde una variable de pila. En su lugar, utilice el montón o una variable global. De
hecho, siempre y cuando el padre aguarde al parámetro a cambiar y no sea
compartido entre threads, puede utilizar la misma variable de parámetro para
devolver datos.
Condiciones de carrera
Las condiciones de carrera deben serle familiares, en primera instancia se trata
de dos threads en una carrera para obtener sus datos almacenados.
Anteriormente, este capítulo mostró un ejemplo de una condición de carrera en el
restablecimiento de los manejadores de señales. Una sección crítica es la sección
donde ocurre la confrontación por recursos. Considere los ejemplos de los Listados
7.13 y 7.14, asumiendo que el Thread 1 y el Thread 2 se ejecutan a la vez.
Listado 7.13 Condición de carrera para el Thread 1
/**********************************************************************/
/** Ejemplo de una condición de carrera donde dos threads ***/
/*** están compitiendo por una cola de datos. ***/
/**********************************************************************/
int queue[10];
int in, out, empty;
I************ ** Thread 1 ************/
I * Lee la cola */
if ( !empty ) /* evita el agotamiento de la cola */
{ int val = queue[out];
out++;
if ( out >= sizeof(queue) )
out = 0; /* retorno del cursor */
empty = ( out == in );
}
173/512
Listado 7.14 Condición de carrera para el Thread 2
/***********************************************************************/
/*** Ejemplo de una condición de carrera donde dos threads ***/
/*** están compitiendo por una cola de datos. ***/
/***********************************************/
int queue[i0];
int in, out, empty;
/********** Thread 2 ***********/
/* escribe en la queue "/
if ( !empty && out != in ) /* evita el desbordamiento */
{ queue[in| = 50;
in++;
if ( in >= sizeof(queue) )
in = 0; /* retorno del cursor */
empty = ( out == in );
}
174/512
despejado (verde), el thread puede entrar en la sección crítica. Si el flag está
activado (rojo), el trhread se bloquea hasta que se desactive de nueve» (verde).
Para administrar los recursos, los programas utilizan cualesquiera de los dos
métodos siguientes: cierre basto. El cierre basto básicamente impide a todo el
mundo hacer nada mientras el programa está en una sección crítica. Un método
basto desactiva el mecanismo de división del tiempo en intervalos. Con esta
aproximación se enfrenta a dos grandes problemas: se bloquean tareas no
relacionadas, y no se trabaja en entornos de multiprocesamiento.
El cierre fino le ofrece un control sobre los recursos en competición en lugar de
sobre las tareas. Un thread pide acceso a un recurso compartido. Si está sin usar,
el thread se adueña del mismo hasta que es liberado. Si su thread encuentra que el
recurso ya ha sido reservado, se bloquea y espera hasta que el recurso sea
liberado por la tarea que actualmente lo está utilizando.
La biblioteca Pthreads le ofrece una amplia selección de herramientas para
gestionar sus aplicaciones con threads. Se incluye la API Mutex. El método de uso
de exclusiones mutuas es directo (véase el Listado 7.15).
Listado 7.15 Ejemplo de creación de un semáforo
/**********************************************************************/
/*** Define el semáforo global de exclusión mutua. El valor ***/
/*** indica el algoritmo de comprobación a utilizar. ***/
/*********************************************************************/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
...
/* Comienzo de la sección critica */
pthread_mutex_lock(&mutex);
...
/*---Trabajo sobre datos críticos --*/*/
...
pthread_mutex_unlock(&mutex);
/* Fin de la sección critica */
...
El parámetro mutex es el semáforo que utilizará para bloquear la sección. En el
sitio web se incluye un ejemplo con todo el código (llamado thread_mutex.c).
Inícielo con uno de entre los tres siguientes modos de funcionamiento.
175/512
• Recursivo-(PTHREAD_MUTEX_INITIALIZER_NP) Verifica si el dueño está
bloqueando la exclusión mutua de nuevo. Si el así, lleva la cuenta (semáforo de
cuenta) del número de bloqueos. La exclusión mutua tiene que ser
desbloqueado tanta veces como ha sido bloqueada.
• Comprobación de errores —
(PTHREAD_ERRORCHECK_MUTEX_INITIALlZER_NP) Verifica que el thread
desbloqueando una exclusión mutua es el mismo que la bloqueó. Si no es el
mismo, devuelve un error mientras deja el bloqueo intacto.
La biblioteca ofrece una llamada adicional para un cierre sin bloqueo. La
llamada de biblioteca pthread_mutex_trylock() intenta bloquear el semáforo. Si no
puede, devuelve un error EBUSY.
JUEGO LIMPIO EVITANDO EL BLOQUEO DE LAS
SECCIONES CRÍTICAS
Intente limitar los cálculos computacionales en una sección critica solamente a los
elementos por los que se compite. Por ejemplo, no llame a la E/S o manipule datos no
relacionados a menos que ciertamente no pueda evitarlo. Si es necesario, puede copiar los
datos en variables locales para cálculos fuera de la sección. Tenga en mente que la mezcla
de E/S con secciones críticas conduce potencial-mente a un interbloqueo.
176/512
Prevención del interbloqueo
Imagínese dos bebés jugando con algunos juguetes. Un niño ve con ¡o que el
otro está jugando y viceversa. Cada uno quiere los juguetes del otro, pero no
desean abandonar lo que tienen. En esencia, están interbloqueados.
Cuando utilice threads, asegúrese de considerar la posibilidad de la aparición de
competencia por recursos y secciones críticas. Después de aislar y agrupar los
datos críticos, necesita identificar quién necesita qué y cuándo. Algunas veces
puede encontrarse que dos recursos necesitan ser bloqueados (llamado
interbloqueo) antes de que el trabajo pueda proseguir. Si no es cuidadoso, puede
provocar un interbloqueo.
Considere el siguiente ejemplo de interbloqueo:
Thread #1 Thread #2
1. Bloquea los recursos ahorrados. 1. Bloquea el recurso IRA.
177/512
• Bloqueo en orden. Bloquea los recursos numéricamente desde el menor hasta
el mayor.
178/512
Si le gusta el riesgo y no sabe lo que es un proceso zombi, tal vez pueda
interesarle crear uno en su propio sistema. No lo haga si no puede reiniciar su
computadora. No dañarán su computadora, pero pueden apropiarse de recursos
valiosos (la memoria y entradas de la tabla de procesos). Ejecute el código del
servidor de eco multitarea (disponible en el sitio web). Conéctese, y después,
escriba "bye". Este cierra la conexión. Ahora escriba ps auxlgrep <username>
(indicando el nombre de usuario adecuado). Deberá ver una entrada con el estado
de Z. Esto no significa que esté durmiendo. Por lo general puede eliminarla
matando al padre (el servidor de eco).
Cuando el hijo finaliza, lo hace con un valor de retorno numérico. El valor indica
o bien una finalización con éxito o bien algún código de error. Normalmente, tiene
que programar el padre para que espere a la finalización de la tarea hijo utilizando
la llamada del sistema wa¡t(). La llamada wait() recupera el valor de retorno del hijo
para que el padre lo examine. Si no espera al hijo, éste le esperará a usted por
tiempo indefinido.
OBTENCIÓN DE PROCESOS ZOMBIS
El padre debería cuidar de todos sus hijos. Algunas veces no lo hace. Cuando el padre
concluye sin considerar la finalización de todos sus hijos, el programa de iniciación los
hereda. La responsabilidad primordial del programa de iniciación es planificar, ejecutar, y
limpiar los procesos. Algunas veces no puede. Es así cuando los procesos zombis
empiezan a proliferar en su tabla de procesos. No será capaz de utilizar el comando kill()
para eliminarlos. La única solución es reiniciar su sistema. (Puede intentar los comandos
init s o init 1 para limpiar la tabla de procesos, pero no hay garantía de que esto
funcione.)
A diferencia de los procesos utilizando las llamadas del sistema fork() o
_clone(), los Pthreads le permiten expropiar (o desconectar) un thread. La
desconexión de un thread le permite proceder sin tener que esperar a su
finalización. La llamada de biblioteca es simple:
#include <pthread.h>
int pthread_detach(pthread_t tchild);
El parámetro tchild es la referencia que obtuvo de la llamada a
pthread_create().
La llamada devuelve los siguientes errores:
• ESRCH. No pudo ser encontrando ningún thread propio del especificado por
tchild.
179/512
/**********************************************************************/
/* Crea la variable para referenciar al thread */
pthread_t tchild;
...
/* Crea el thread */
if ( pthread_create(Stchild, 0, Child_Fn, Child_Arg) != 0 )
perror{"Could not créate thread");
else
/* Lo separa */
pthread_detach(tchild);
...
Después de llamar a pthread_detach(), su tarea padre puede continuar con sus
propios cálculos. Los procesos, por otro lado, no tienen esa flexibilidad. Pero, al
mismo tiempo, no es necesario esperar a cada hijo; eso contradice el propósito de
la delegación de responsabilidades.
Puede capturar la notificación de la finalización de un hijo de un modo asincrono
con señales. Cuando un hijo finaliza, el sistema operativo envía al padre una señal
SIGCHLD. Todo lo que tiene que hacer es capturarla y llamar a wait() en el gestor.
/***************************************************************/
/*** Ejemplo de gestor de señales para la captura de ***/
/*** avisos de finalización de hijos. ***/
/***************************************************************/
#include <signal.h>
void sig_child(int sig)
{ int status;
wait(&status); /* Obtiene el resultado final */
fprintf(stderr, "Another one bytes the dust\n");
}
Para conseguir cualquier notificación, necesita conectarse al gestor de señales
con la señal, tal y como sigue:
{ struct sigaction act;
bzero(&act, sizeof(act));
act.sa_handler = sig_child;
act.sa_flags = SAJJOCLDSTOP | SA_RESTART;
sigaction(SlGCHLD, Sact, 0); /* Captura el hijo */
}
180/512
ELUDIR EXEC() PROCESOS ZOMBIS
Para disparar la ejecución de un programa externo, debe capturar la notificación de
finalización del hijo, aún cuando utilice la llamada del sistema exec() (abordada en la
sección "Llamada de programas externos con el intérprete de comandos del servidor,"
posteriormente en este capítulo), Puede parecerle que el programa externo no está ligado
al padre por mas tiempo, pero en realidad el padre aún es dueño del espacio de la tarea.
Si quiere observar si algo salió mal con el hijo, compruebe el valor de retorno
-pasado a través de la variable status. El padre, por supuesto, tiene que interpretar
el significado del resultado. Así, por ejemplo, el hijo puede devolver un 0 (cero)
para una finalización con éxito o un valor negativo para un error.
LIMITES DEL VALOR DE RETORNO DEL PROCESO
Tan sólo son guardados los 8 bits de menor orden, y el valor es desplazado 8 bits. Por
tanto, si obtiene un valor de retorno de 500 (001F4), la llamada del sistema wait() obtiene
0F400. La parte superior, 00100, se pierde.
El valor de estado devuelto por wait() no es directamente interpretable. Tiene
estados codificados junto con el código de salida del proceso. Las bibliotecas le
ofrecen un modo sencillo de aislar el código de salida, dos de los cuales son
WiFEXI-TED() y WEXISTATUS(). Échele un vistazo a las páginas del manual para
waitpid() para obtener información sobre estas macros.
181/512
Interfaz de Gateway Común) son en la Web, como se muestra a continuación:
perror("execl failed");
exit(-1);
/* el "if" es redundante -- exec no devuelve un valor si no falla *//
182/512
representando el entorno, Recuerde finalizar la lista de parámetros con NULL
o cero.
/* Por ejemplo: */
char *env[]={"PATH=/bin:/usr/bin","USER=gonzo","SHELL=/bin/ash",MULL};
if { execle("/bin/ls", "/bin/ls", "-aF", 7etc", NULL, env) != 0 )
perror("execle failed");
exit(-1);
• execv(). Tiene dos parámetros. El primero es la ruta completa del comando. El
segundo es un array de argumentos de línea de comandos. La última entrada en el
arrayes un cero o NULL.
/* Por ejemplo: */
char *argsí]={"/bin/ls", "-aF", "/etc", NULL};
if ( execle(args[0], args) != 0 )
perror("execle failed');
exit{-1);
183/512
printf("Connected: %s:%d\n", inet_ntoa(addr.sin_addr),
ntohs<addr.sin_port));
if ( fork() )
close(client);
else
{
184/512
externo finalice, cerrará los gestores de archivos stdin y stdout. Esto afecta a los threads
de los equipos homólogos. Recomendación: utilice fork() en su lugar si quiere llamar a un
programa externo con exec().
185/512
Capitulo viii
En este capítulo
Bloqueo de la E/S: ¿por qué?
¿Cuándo debo bloquear?
Alternativas al bloqueo de la E/S
Comparación de las diferentes interacciones de programación de E/S
Sondeo de la E/S
E/S asincrona
Resolución de bloqueos de la E/S no deseados con poll() y select()
Implementación de los tiempos de espera
Resumen: elección de las estrategias de E/S
Con las exigencias de tiempo de hoy en día, es una buena idea investigar
métodos para ahorrar tiempo en su red. Así que es bueno tener en mente que su
computadora tiene un recurso limitado y esencialmente crítico: la CPU. Toda tarea
en el sistema le solicita tiempo a la CPU, y toda tarea tiene que compartir ese
tiempo en pequeñas porciones. En el nivel más básico, cada tarea comparte
tiempo de CPU con un mecanismo de división del tiempo en intervalos. Linux utiliza
un algoritmo de planificación más dinámico que se ajusta a la utilización de la CPU
y al bloqueo de la E/S.
Cuando se ejecuta un programa, está o bien en ejecución o bien esperando algo
(bloqueado). El programa se encuentra con dos clases de bloqueo; relativo a la E/S
(gasta más tiempo esperando una operación de E/S) y relativo a la CPU (gasta
más tiempo en la computación). Durante el tiempo de inactividad de la E/S, otras
tareas pueden ejecutarse y completar sus asignaciones. Así es como la mayoría de
las tareas cooperan y comparten el tiempo limitado de CPU. Cuando una tarea ha
de esperar a que se produzca una operación de E/S, cede su lugar (se bloquea) de
forma que otra tarea pueda se pueda ejecutar.
186/512
Puede no desear esperar a la finalización de una operación particular de E/S
antes de continuar con otras tareas. Por ejemplo, considere la molestia que
supondría tener un navegador que sólo le permita abandonar si ha cargado
completamente una página. Por otro lado, piense en un cliente que se conecta
simultáneamente y carga recursos de varios servidores. Si administra
adecuadamente su canal, éste estará siempre lleno, y conseguirá el máximo
rendimiento de su conexión a la red.
Cuando retiene el control de la CPU mientras espera a la finalización de una
operación de E/S, puede incrementar la funcionalidad del programa y reducir la
frustración del usuario. Un modo de retener este control es a través de la E/S que
no bloquea y los tiempos de espera. La E/S que no bloquea le permite comprobar
el subsistema de E/S o conseguir que el núcleo le indique a su programa cuándo
está listo para recibir más datos. Esto libera a su programa para concentrarse en el
usuario o en algún algoritmo de utilización intensiva de la CPU.
Una administración eficaz del bloqueo de la E/S puede trabajar con o ser una
alternativa a la multitarea y el multithreading. Se trata de otra herramienta de
programación que le ayudará a realizar su trabajo.
La elección de cuándo bloquear y cuándo no es muy delicada. En este capítulo
se nos presentan algunas ideas y sugerencias para ayudarle a seleccionar las
mejores posturas.
187/512
de 1 o 2 segundos para volver a ponerse en marcha a partir del estado de inactividad.
Del mismo modo, una red de 10Mb transfiere una media de 300KB/s desde una
computadora a otra (ésta es una medida óptima para una red ejecutando TCP/IP).
Una red de lOOMb/s puede mejorar esta media sólo en un factor de 10 (3.0MB/s).
Sin contar la transmisión, el enrutamiento, y el tiempo de procesamiento de la
petición, su cliente puede tener que esperar un tiempo equivalente a 1000
instrucciones (en un sistema configurado análogamente). Con estos otros factores,
puede fácilmente multiplicar esa estimación por un factor entre 1000 y 5000.
Un número promedio de opcodes por línea de fuente en C compilado es 10:1.
En otras palabras, un retardo en la red de 20ms podría significar que su tarea
podría haber ejecutado alrededor de 5.000.000 opcodes, o 500.000 líneas de
código C.
Así que, ¿cómo se ocupa el sistema operativo de estos retardos de tiempo?
Básicamente echa a dormir su tarca (bloqueo). Cuando la petición del sistema
finaliza, su tarea despierta y continua su ejecución.
El bloqueo de tareas ligadas a E/S es, generalmente, la regla y no la excepción.
Para ver cuántas tareas se están ejecutando en la computadora, ejecute ps aux.
Todas las tareas etiquetadas con una R se están ejecutando; las etiquetadas con S
están paradas. Sólo puede tener una tarea en ejecución: el comando ps en sí
mismo.
• Escritura. Un bloqueo de escritura tiene lugar cuando los buffers internos están
llenos y esperando para la transmisión y su programa solícita enviar más datos.
Linux reparte memoria para los buffers para todos los subsistemas de E/S.
Estos buffers almacenan los datos temporalmente hasta que el subsistema de
E/S lo transmite. Si el buffer se llena, las siguientes llamadas se bloquearán
hasta que se liberen algunos buffers.
• Conexión. Un bloqueo de conexión tiene lugar cuando las llamadas del sistema
accept() y connect() no encuentran conexiones pendientes en la cola de
escucha. Actualmente puede ser una buena idea bloquear mientras se espera
una conexión; por otro lado, puede querer subordinar la petición de E/S a un
thread o tarea suelta.
188/512
En los siguientes apartados se abordan otros temas relativos al bloqueo.
189/512
FIGURA 8.1 Cada estilo de control de E/S varía la cantidad de tiempo disponible para
la tarea durante una lectura.
Con el estilo de bloqueo, el programa espera hasta que los datos llegan. El
estilo de sondeo llama reiteradas veces a la llamada del sistema recv() hasta que
los datos llegan. El estilo de tiempo de espera, por otro lado, puede no conseguir
los datos si estos no llegan a tiempo. No obstante, con el estilo de tiempo de
espera, no pierde el mensaje; el núcleo lo mantiene hasta que lo pida de nuevo.
Finalmente, con el estilo asincrono, el núcleo anuncia a la tarea cuando obtiene los
datos.
Observe cómo la forma en que cada estilo maneja la interfaz cuando envía un
mensaje es una historia diferente. Los buffers del núcleo ocupan el periodo de baja
actividad para acelerar el procesamiento en una llamada que no bloquea. En todos
los casos, una llamada que bloquea se para hasta que el núcleo envía
completamente el mensaje. (Esto no es completamente cierto, puesto que el
núcleo realmente espera hasta que puede enviar el mensaje antes de despertar a
la tarea, pero para simplificar la programación puede asumir que el mensaje se ha
enviado cuando la tarea se reanuda.) El sondeo y las llamadas asincronas
almacenan el mensaje en los buffers y regresan inmediatamente. La Figura 8.2 da
por hecho que los datos son demasiado grandes para almacenarlos
completamente en los buffers.
190/512
FIGURA 8.2 La operación de escritura brinda más tiempo a la tarea utilizando
buffers.
Las siguientes secciones le muestran como utilizar el sondeo, los tiempos de
espera, y la E/S asincrona.
Sondeo de la E/S
El programa puede tener varias tareas que realizar además de esperar un
paquete. Una vez que llama a send() para enviar un mensaje, no nos preocupa
cómo o cuándo el mensaje abandona la computadora. Hasta donde le atañe, el
subsistema de E/S envía el mensaje completo e intacto. Éste no es el caso
frecuente; la mayoría de los mensajes son troceados para ajustarse a las
limitaciones de la red. De manera parecida, puede querer no obtener trozos de un
mensaje. En su lugar, quiere rellenar su buffer. En ambos casos, su programa se
para y espera a que la tarea finalice.
El sondeo le ayuda a evitar el bloqueo en la comunicación de E/S comprobando
si el canal está listo. Para utilizar el sondeo, configure el canal en modo de no
bloqueo. A continuación, compruebe el valor de retorno de una llamada del
sistema. Si la llamada devuelve EWOULDBLK (o EAGAIN), pruebe de nuevo un
poco más tarde. Los programas a menudo colocan esto en un bucle que tiene
algún tratamiento muy localizado, tal y como se halla en el siguiente algoritmo de
sondeo:
191/512
/****************************************/
/*** Algoritmo general de sondeo ***/
/****************************************/
...
while ( /*** Transfiriendo datos **"/ )
{
if (/*** comprueba la disposición de un canal ***/)
/*** procesa el canal ***/
/*** Realiza algo de computación ***/
}
...
Un lector de sondeos
El sondeo le permite obtener trozos de información que la tarea puede procesar
mientras está esperando por más. Con un lector de sondeos, el programa obtiene y
procesa los datos. A menudo, el procesamiento y la lectura de datos están
relacionadas, y debe habilitar la E/S que no bloquea para realizar sondeo en el
nivel de lectura/escritura. Utilice la llamada del sistema fcntIO para habilitar la E/S
que no bloquea tal y como se muestra aquí:
#include <fcntl.h>
int fcntl(int fd, int command, int option);
Comparable a la llamada del sistema read(), este comando acepta o bien un
descriptor de archivos (fd) o bien un descriptor de socket (sd). El comando es
F_SETFL, y la opción es 0_NONBLOCK. Esta llamada del sistema tiene muchas
más opciones. Acuda al Apéndice C para ver un listado más extenso.
if ( fcntl(sd , F_SETFL, 0_NONBLOCK) != 0 }
perror("Fcntl—could not set nonblocking");;
192/512
Por ejemplo, suponga que está escribiendo un plug-in para un reproductor de
audio. En lugar de esperar a que el clip completo llegue de Internet, puede querer
reproducirlo mientras está recibiendo los datos. Por supuesto, puesto que los datos
presumiblemente están comprimidos, el programa tiene que convertir los datos en
un formato legible por la máquina. El retazo de programa podría parecer semejante
al código del Listado 8.1.
Listado 8.1 Ejemplo de lector de sondeo
/**************************************************************************/
/*** Ejemplo de lector de sondeo: lectura de un flujo de audio, ***/
/*** procesamiento de los datos, y reproducción. ***/
/*************************************************************************/
...
if ( fcntl(sd, FSETFL, O_N0NBL0CK) != 0 )
perror( "Fcntl —could not set nonblocking"};;
...
done = 0;
while ( ¡done )
{ int bytes;
Queue ProcessingQueue;
Queue OutputOueue;
/*--- Obtiene cualquier dato en espera ---*/*/
if { (bytes = recv(sd, buffer, sizeof(buffer), 0 ) ) > 0 )
QueueData(ProcessingQueue, buffer);
/*--- Convierte un cierto número de bytes de la cola ---*/*/
/*--- de procesamiento —típicamente más rápido que recv() ---*/
ConvertAudio(ProcessingQueue, OutputQueue);
if ( /*** la cola de salida está bastante distante por delante ***/ )
PlayAudio(OutputQueue);
/*--- Si el flujo de datos se ha realizado y la cola de salida está vacia... ---*/*/
if { bytes == 0 && /*---cola de salida vacia---*/ ) )
done = 1;
}
...
Este ejemplo acepta los datos, los encola, y los procesa. Sin embargo, tiene un
problema: si no hay ningún dato en ninguna cola, la instrucción while se convierte
en un bucle cerrado. Para resolver este problema, puede insertar un retardo, dado
193/512
que el audio es un recurso en tiempo real.
Escritor de sondeo
El programa se ejecuta varias veces más rápido que cualquier función de E/S.
(De hecho, para mejorar el rendimiento de su computadora, obtenga más memoria
y reemplace la E/S vieja y lenta. Instalar una CPU más rápida tan solo no es tan
efectivo.) De este modo, cuando su programa trasmite un mensaje o realiza
cualquier petición, el sistema operativo coloca su programa en espera durante un
buen rato.
El escritor de sondeo tiene un algoritmo parecido al del lector. Es una buena
idea asociar la generación y el procesamiento de los datos con el transmisor. La
preparación es la misma: utilice la llamada del sistema fcntl() con los mismos
parámetros.
El problema con el que tendrá que tratar en un escritor de sondeo es justo el
contrario al que presenta el lector. El lector puede no tener datos con los que
trabajar, colocando de este modo al lector en un bucle cerrado. El escritor puede
generar datos con mayor rapidez que la que la llamada send{) puede transmitirlos
(colisión múltiple en el escritor).
Considere, por ejemplo, el algoritmo del Listado 8.2 para enviar fotos desde una
cámara digital a unos pocos clientes.
Listado 8.2 Ejemplo de escritor de sondeo
/*****************************************************************/
/*** Ejemplo de escritor de sondeo: envió de una imagen ***/
/* a varios clientes. ***/
/*****************************************************************/
...
int pos[MAXCLIENTS];
bzero(pos, sizeof(pos));
for ( i = 0; i < ClientCount; i++ )
if ( fcntl(client[i], F_SETFL, 0_NONBLOCK) != 0 )
perror( "Fcntl —could not set nonblocking");;
...
done = 0;
/ * - repetir hasta que todos los clientes tengan el mensaje completo */
while ( !done )
{ int bytes;
done = 1;
/ * - - - Para todos los clientes... ■ - ■ * / * /
194/512
for ( i = 0; i < ClientCount; i++ )
/ * Si todavía hay que enviar más al cliente... * / * /
if ( pos[i] < size )
{
/ * envía msg, rastreando cuantos bytes se envían * /
bytes = send(client[i), buffer+pos[i],
size-pos[i], 0);
if ( bytes > 0 )
{
pos[i] += bytes;
/ * si ha acabado con todos los clientes, salir * /
if ( pos[i] < size )
done = 0;
}
}
}
En este código, hay que seguir la pista a los clientes individualmente, puesto que
pueden estar aceptando datos a diferentes velocidades. El arrav de enteros pos
rastrea la posición del byte en un buffer donde cada cliente se está procesando.
Puede ampliar este algoritmo para incluir tiempos de espera.
Otro problema puede ser que su servidor puede obtener otra imagen mientras la
primera está aún siendo enviada, provocando una colisión múltiple en el escritor.
Para resolver este problema, elija una de estas opciones:
• Añadir el nuevo mensaje al antiguo.
• Cancelar el último mensaje y comenzar a enviar el nuevo.
• Cambiar la frecuencia de imágenes de la cámara.
En todos los casos, haga su elección basándose en el valor crítico de sus datos,
cuántos podrían perderse, y cómo de variables son las conexiones. Véase el
Capítulo 4, "Envío de mensajes entre equipos homólogos", para obtener una lista
de directrices.
195/512
Algunos de los servicios ofrecidos a través de puertos son tan básicos y triviales
que escribir programas para cada uno de ellos es un desperdicio de tiempo,
esfuerzo y memoria. Pero un socket se puede conectar a un solo puerto. En lugar
de eso, piense en escribir un programa de sondeo de socket, tal y como se
muestra aquí:
/*******************************************************************************/
/*** Ejemplo de sondeo de conexiones: comprueba la existencia ***/
/*** de conexiones en varios puertos creando una nueva tarea ***/
/*** para responder a cada petición. ***/
/******************************************************************************/
...
/*--- Establece el modo de no bloque para cada socket - - - * / * /
for ( i = 0; i < numports; i++ )
if ( fcntl(sd[i], F_SETFL, 0_NONBL0CK) != 0 )
perror("Fcntl—cann't set nonblocking on port#%d", i);
...
for (;;) /* Realizar esto para siempre */
{ int client;
for ( i = 0; i < numports; i++ )
if ( (client = accept(sd[i], &addr, &size)) > 0 )
SpawnServer(sd[i], i);
/*** Realiza operaciones de verificación ***/
}
...
Esto puede parecer una gran idea, pero la creación de una tarea separada para
cada llamada accept() puede ser más manejable. De hecho, la mejor solución es
utilizar la llamada del sistema select(), la cual espera a que un número de canales
de E/S se desbloqueen (para obtener más información sobre select(), acuda al
Capítulo 7, "División de la carga: multitarea").
E/S asincrona
Conseguir que el sistema operativo le diga cuando un canal está listo para
recibir más peticiones de E/S fe da un giro favorable al problema de archivar la E/S.
Esto le proporciona más tiempo al programa para consumirlo en cálculos
computacionales y menos tiempo atorado con varias llamadas del sistema.
La lectura asíncrona es similar a la luz parpadeante de un teléfono de la oficina
indicando que tenemos un mensaje en el buzón de voz. De la misma manera, la
característica de retrollamada en un teléfono es similiar a la escritura asincrona.
196/512
Hágase una imagen de la E/S asíncrona como un anuncio de la disponibilidad del
canal.
El sistema implementa esta E/S asíncrona con la señal SIGIO (también llamada
algunas veces como E/S controlada por señales). Los programas obtienen una
señal SIGIO cuando los datos están listos para la lectura o cuando un canal de
escritura está listo para recibir más datos. Al igual que ocurre con todas las
señales, no se sabe nada más que una señal ha tenido lugar. Por tanto, en el caso
de un programa que obtenga una señal SIGIO cuando están abiertos dos canales,
no está claro qué canal se debe comprobar.
Las excepciones presentan otro problema. El núcleo no envía una señal si
ocurre un error en el canal. No es apreciable fácilmente cuándo un programa
pierde la conexión con el cliente, a menos que actualmente esté realizando un
chequeo del canal en el programa.
El programa inicia la E/S controlada por señales dándole instrucciones al núcleo
para que acepte una señal SIGIO. Cuando llega la señal, el gestor de señales
establece un flag que el programa principal vigila. A su vez, el bucle del programa
principal comienza el procesamiento, comprobando ocasionalmente el flag que
indica la llegada de una señal. El gestor puede realizar por sí mismo el
procesamiento de la E/S y a continuación enviar una señal al bucle principal, o
simplemente podría dejar la E/S para el bucle principal.
¿En que se diferencia esto del sondeo? Ambos algoritmos necesitan comprobar
periódicamente si el canal de E/S está listo. Las llamadas de sondeo no bloquean
las llamadas del sistema pero necesitan una comprobación periódica. De igual
manera, el bucle principal tiene que sondear la señal que establece la E/S
controlada por señales.
La diferencia primordial es cuánto tiempo gasta el programa en las llamadas del
sistema. La E/S controlada por señales gasta muy poco tiempo en las llamadas del
sistema send() y recv(): el núcleo le dice a su programa cuando está listo. Por otro
lado, el sondeo tiene que realizar llamadas del sistema con regularidad.
El algoritmo del Listado 8.3 da una idea general sobre cómo funciona el
proceso.
Listado 8.3 Algoritmo de E/S controlada por señales
/*****************************************************************************/
/*** Algoritmo general asíncrono o de E/S controlada por señales ***/
/****************************************************************************/
int ready=0;
...
void sig_io(int sig)
{
/**** Si RECV(): obtener todos los datos en espera ****/
/**** Si SEND(); enviar todos los datos procesados ****/
197/512
ready = 1; /* avisar al bucle principal: "transacción completada" "/
}
...
for (;;)
{
if ( ready > 0 ) {
/*** Bloquear temporalmente la señal SIGIO ***/
ready = 0;
/**** Si RECV(): copiar datos en el área de procesamiento ****/
/**** Si SEND(): rellenar el buffer de salida a partir de ****/
/**** ...la cola de datos procesados ****/
/**** Desbloquear la señal SIGIO ****/
}
El bloqueo de una señal SIGIO puede parecer atípico. Dado que el gestor de
señales y el bucle principal comparten las mismas variables, se encuentra con una
sección crítica (véase la sección "Exclusión mutua (mufex)" en el Capítulo 7). La
desactivación del gestor de señales es un elemento importante de exclusión mutua
[mutex).
MENSAJES EN COLA
Para obtener todos los mensajes pendientes, tiene que l l a m a r v a r i a s veces a las
llamadas send() y recv() en el gestor. El núcleo puede enviar varias señales al momento a
su programa, pero su programa recibe sólo un tipo de señal a la vez. Si dos señales
idénticas llegan muy juntas, su programa recibe sólo a una de ellas.
Puede iniciar los mensajes SIGIO utilizando la llamada del sistema fcntl(). Esta
llamada del sistema no sólo le dice al núcleo que genere el descriptor de socket
asíncrono (recuerde que esto se aplica a todos los descriptores de archivo) sino
que también le indica que dirija las señales a un proceso específico. Por ejemplo, el
siguiente segmento de código activa las señales SIGIO y las dirige a la tarea
actual:
/**************************************************************************/
/*** Fragmento de código para iniciar el flujo de señales SIGIO ***/
/**************************************************************************/
...
if ( fcntlfsd, F_SETFL, 0_ASYNC ¡ 0_N0NBL0CK) < 0 )
198/512
PANIC("Can't make socket asynch & nonblocking");
if ( fcntl(sd, F_SET0WN, getpidO) < 0 )
PANIC(“Can't own SIGIO");
Cuando su programa reclama la propiedad de las señales de E/S, las reclama
todas, no tan solo la señal SIGIO. Otra señal que puede obtener es SIGURG,
utilizada en la obtención de datos fuera de banda, Para más información véase la
sección "Envío de mensajes de prioridad alta" en el Capítulo 9, "Cómo romper las
barreras del rendimiento".
199/512
PANIC("Can't make socket asynch & nonblocking");
/*--- Reclamar la propiedad de las señales SIGIO y SIGURG ---*/*/
if ( fcntl(sd, F^SETOWN, getpid(í) < 0 )
PANIC("Can't own SIGIO");
while ( ¡done )
{
if { ready > 0 )
{
/*** Bloquear temporalmente la señal SIGIO ****/
ready = 0;
FillQueue(Queue, buffer, bytes);
/**** Desbloquear la señal SIGIO ****/
}
/*** Procesar los datos entrantes por un breve espacio de tiempo ****/
/**** en el rasterizador VRML, o hasta que la variable "ready" ***/
/*** cambie. ****/
}
...
Escrituras asincronas
Los buffers del núcleo hacen que la programación de la escritura asincrona
resulte algo diferente, Cuando su programa llama por primera vez sendO, es muy
probable que los buffers del núcleo puedan almacenar el bloque de datos
completo. Pero también es posible que no pueda, y el núcleo guarde tan solo una
parte.
Por tanto, compruebe el valor de retorno de cada llamada writeü para
asegurarse que se han enviado todos los datos. Puede incluso ser esencial
200/512
mantener todos los datos salientes hacia un canal en una sola cola. Esto reduce la
complejidad del código que involucra a múltiples buffers. Aún asi, es igualmente
importante realizar un almacenamiento en buffers flexible, de forma que la cola
pueda crecer y encogerse según se necesite.
En esta situación nos sirve bien un ejemplo de consulta a base de datos. Este
ejemplo genera grandes cantidades de datos que pueden ser procesados o
recopilados mientras se envía la respuesta, tal v como se muestra en el Listado
8.5.
Listado 8.5 Algoritmo de escritor asincrono
/****************************************************************************/
/*** Ejemplo de escritor asincrono de consulta de base de datos: ***/
/*** procesa la consulta mientras genera los resultados. ***/
/****************************************************************************/
int ready-0, bytes, size=0, pos=0;
...
int sig_io(int sig)
{
if ( size > 0 )
{
bytes = send(client, buffer, size+pos, 0);
if ( bytes > 0 )
{
pos +- bytes;
ready = 1;
}
}
}
...
/**** Habilitar la E/S controlada por señales, que no bloquea; reclamación de la señal
SIGIO ****/
while ( !done )
{
if ( /*** canal disponible ***/ )
/*** enviar el mensaje, no se bloquea ***/
else
/*** encolar el mensaje ***/
201/512
}
...
Cada vez que el canal se limpia y está listo para recibir más datos, el núcleo
envía al proceso una señal S1G10. Utilice esa señal como se mostró en el ejemplo
anterior para distribuir los datos hacia afuera bajo demanda.
202/512
...
Mientras que un servidor bajo demanda es útil, la ventaja clara de la multitarea
es que le permite retener las variables locales, y reparte una tarea específica a
cada conexión.
A menos que su servidor sea propenso a la sobrecarga de tareas (el programa
crea tantos procesos que baja el rendimiento), la utilización de conexiones bajo
demanda puede dificultar su programación. Debido a todo el esfuerzo que supone
el diseño e implementación de un servidor de conexiones bajo demanda,
probablemente no conseguirá amortizar el esfuerzo extra. Simplemente es más
fácil utilizar la multitarea o bien la técnica de sondeo.
203/512
Tabla 8.1 Parámetros de la llamada del sistema selectQ
Parámetro Descripción
maxfd Un número más que el del mayor descriptor de archivo del conjunto.
(Véase más abajo para más información.)
to_read El conjunto de descriptores que esperan una operación de lectura.
Este es el conjunto que quiere chequear para la lectura.
to_write El conjunto de descriptores que esperan una operación de escritura.
except El conjunto de descriptores que esperan mensajes de prioridad.
timeout El tiempo en microsegundos a esperar antes de rendirse. (Véase
más abajo para más información.)
fd El descriptor de archivo a añadir, borrar, o chequear.
204/512
FD_SET(4, &set); /* Añade el canal cliente #4 */
FD_SET(6, &set); /* Añade el canal cliente #6 */
timeout.tv_sec = 5; /* Salta el tiempo de espera tras 5.25 segundos */
timeout.tv_sec = 250000;
/* Espera la llamada de select() */*/
if ( (count = select(6+1, &set, 0, 0, Stimeout)) > 0 )
/* Encontrar los descriptores que han cambiado */
else if ( count -- 0 )
fprintf{stderr, "Timed out!");
else
perror("Select");
...
En este ejemplo, cuando la llamada del sistema select() devuelve un valor
positivo, uno de los tres canales tiene datos listos para ser leídos. Para localizar
qué canal está listo, añada el código necesario y lea los datos. Sin embargo, si el
valor de retorno es cero, la llamada excedió el límite de tiempo de 5,25 segundos.
La llamada del sistema poll() es parecida a selectO y puede ser un poco más
fácil de gestionar. Utiliza un array de estructuras para definir su comportamiento:
struct pollfd
{
int fd; /* el descriptor a probar */
short events; /* eventos solicitados para su observación */
short revenís; /* eventos que se dispararon */
}
El primer campo, fd, es el descriptor del archivo o socket. Los campos segundo
y tercero, events y revents, son máscaras de bits para casos específicos:
• POLLIN. Datos entrantes. Devuelto si los datos están listos para la lectura.
• POLLINVAL. fd no abierto. Devuelto si el canal no es un socket o archivo abierto.
• POLLPRI. Mensajes de prioridad. Devuelto si llega un mensaje de prioridad.
• POLLOUT. Disponibilidad del canal. Devuelto si no se bloquea una llamada
205/512
write().
La llamada del sistema poll() tiene el siguiente aspecto:
#include <sys/poll.h>
int poll(struct pollfd *list, unsigned int cnt, int timeout);
El programa llena un array de estructuras pollfd antes de utilizar esta llamada
del sistema. El parámetro cnt es el número de descriptores legales de socket en el
array. (Recuerde mantener juntos a estos descriptores; si uno se cierra, compacta
el array. No todas las implementaciones tratan bien un hueco vacío.) El parámetro
timeout actúa igual que el parámetro con el mismo nombre de selectf) pero en
milísegundos.
Cualquiera de las dos llamadas del sistema, select() o poll(), proporcionan la
potencia para monitorizar varios canales a la vez. Puede evitar algunos de los
problemas de sobrecarga con los que la multitarea se topa con una buena
planificación.
206/512
al gestor de señales, y a continuación la llamada del sistema retorna con un error
EINTR.
Cada tarea puede ser una señal por sí misma, pero la tarea necesita saber qué
hacer con la señal. En algunos sistemas, la llamada de biblioteca sleep utiliza la
llamada del sistema alarm(), indicando en primer lugar a la tarea que capture la
señal SIGALRM. Por favor, observe el siguiente ejemplo:
/***************************************************************************/
/*** Ejemplo: ejemplo de tiempo de espera utilizando la llamada ***/
/*** del sistema alarm{). (Extracto de echo-timeout.c del CD.) ***/
/***************************************************************************/
...
int sig_alarm(int sig)
{/**" no hace nada ***/}
void reader()
{ struct sigaction act;
/* Inicia estructura de señales; restaura conf. por defecto */*/
bzero(&act, sizeof(act)>;
act.sa_handler = sig_alarm;
act.sa_flags - SA_ONESHOT;
/*--- Establece el gestor de señales ---*/*/
if ( sigaction(SIGALRM, &act, 0) != 0 )
perror("Could not set up timeout");
else
/* Si está instalado gestor de señales, activa temporizador */
alarm(TIMEOUT_SEC);
/*--- Llama a la E/S que podria agotar el tiempo de espera ---*/*/
if { recv(sd, buffer, sizeof(buffer), 0) < 0 )
{
if ( errno == EINTR )
perror("Timed-out!");
}
}
El ejemplo mostrado activa el gestor de señales, establece el temporizador, y
llama a la E/S. Si la llamada recv{) no obtiene ningún dato para cuando el
temporizador caduca, la tarea obtiene una señal. La señal interrumpe la llamada
del sistema recv(), y ésta retorna con un error (EINTR).
207/512
Para obtener este comportamiento, deje deshabilitada la opción SA_RESTART
del campo sa_flags. El Capítulo 7 le recomienda incluir esta opción con las
señales. En ese caso, omita esta opción para que el temporizador del tiempo de
espera funcione.
Una nota final: evite la utilización de la llamada del sistema alarm() junto con
sleep(), puesto que puede causar problemas a su programa. En su lugar, utilice la
señal de Linux SIGALRM o bien sleep() -no las mezcle.
208/512
Capitulo ix
En este capítulo
Creación de servlets antes de la llegada del cliente
Ampliación del control con un sclcct inteligente
Investigación a fondo del control de sockets
Recuperación del descriptor de socket
Envío antes de la recepción: menajes entrelazados
Apunte sobre los problemas de la E/S de archivos
Utilización de E/S sobre la base de la demanda para recuperar tiempo de
CPU
Envío de mensajes de prioridad alta
Resumen: discusión de las características de rendimiento
209/512
algoritmos necesita conocer los conceptos presentados en capítulos anteriores.
210/512
PANIC("bind() failed");
if ( listen(sd, 20) != 0 )
PANIC("listen() failed");
for (;;)
{ int client, len=sizeof(addr);
client = accept(sd, &addr, &len);
if ( client > 0 )
{ int pid
if ( (pid = fork()) == 0 )
{
close(sd);
Child(client); /* Prestar servicio al nuevo cliente */*/
}
211/512
/***************************************************************/
/*** Algoritmo revisado para limitar el número de hijos ***/
/*** ("capped-servlets.c" en el sitio web) ***/
/****************************************************************/
int ChildCount=0;
void sig_child(int sig)
{
wait( 0 ) ;
ChildCount - -;;
}
...
for (;;)
{ int client, len=sizeof(addr);
while ( ChildCount >= MAXCLIENTS )
sleep(1); /* Podría en su lugar utilizar "sched_yield()" */
client = accept(sd, Saddr, &len);
if ( client > 0 )
{ int pid
if ( (pid = fork()) == 0 )
{/* -HIJO */*/
close(sd);
Child(client);/* Prestar servicio al nuevo cliente */
}
else if { pid > 0 )
{/* PADRE */*/
ChildCount++;
close(client);
}
else
perror("fork() failed");
}
}
En este ejemplo, el padre sigue la pista del número de hijos. Si hay demasiados
hijos, el servidor simplemente cede la CPU hasta que una de las conexiones se
cierre y el hijo finalice. Puede utilizar este método desahogadamente y, puesto que
212/512
el único escritor de la variable ChildCount es el servidor y su gestor de señales, no
tiene que preocuparse acerca de las condiciones de carrera.
De nuevo, este algoritmo trabaja bien para sus conexiones a largo plazo. Sin
embargo, si tiene un montón de conexiones a corto plazo y transacciones
individuales, el costo de la creación y finalización de procesos hijos puede
desgastar el rendimiento de su servidor. La idea es tener un conjunto de procesos
en ejecución, todos ellos esperando a que llegue una conexión.
213/512
}
main()
{
214/512
reanuda su tarea, crea un nuevo hijo. Si el padre no necesita crear más hijos, cede
su intervalo de tiempo a otra tarea.
215/512
El objetivo de realizar un duplicado previo es minimizar el tiempo de creación y
eliminación después de los servlets. La creación y finalización de ser\'lets
fortuitamente le lleva de vuelta a la duplicación de cada conexión nueva. Tiene que
realizar una planificación cuidadosa para evitar este resultado. Además, su
algoritmo debe tener un número mínimo de servlets en ejecución para que esto
funcione.
Aquí es donde las estadísticas ayudan un poco. El algoritmo adaptado tiene que
afrontar tres posibilidades: el número de conexiones sea estable, esté aumentando,
o esté disminuyendo. Si el número de conexiones es estable, el servidor entiende
que están finalizando el mismo número servlets que han sido creados durante un
periodo de tiempo determinado. Por ejemplo, si su servidor crea un servlet cada
f>0 segundos (con un tiempo de espera desocupado de 30 segundos), puede
presenciar el primer patrón del diagrama de la Figura 9.1.
Si el número de conexiones está aumentando, hay menos conexiones siendo
finalizadas que las que están siendo creadas. Si su servidor no observa una
finalización dentro del tiempo reglamentario, puede asumir que está prestando
servicio a una conexión. En este punto, su servidor dobla !a tasa de creación de
servlet (véase el segundo patrón en la Figura 9.1). Este proceso continúa hasta
que el número de servlets alcance un techo o su servidor alcance la frecuencia
máxima de creación (quizá, cada 5 segundos).
FIGURA 9.1 Los servlets extra son arrojados fuera para ver si hay conexiones en
espera.
De nuevo, suponga que su servidor crea un nuevo servlet cada 60 segundos
(con un tiempo de espera desocupado de 30 segundos). Si el servlet extra no
finaliza dentro del tiempo reglamentario, puede dividir por dos el retraso entre las
creaciones, duplicando de este modo la frecuencia. Puede repetir este proceso
hasta que la tasa de conexiones comience a estabilizarse.
Finalmente, cuando las conexiones comienzan a mermar, las conexiones en
espera (si aún hay alguna) tienen más que suficientes servlets para la cola, y el
servidor comienza a presenciar finalizaciones de servlets. Cuando los servlets extra
comienzan a finalizar, puede restablecer la frecuencia del servidor a la normal.
/***************************************************/
/** Ejemplo de conexiones con cebo ***/
/*** ("servlet-chummer.c" en el sitio web) ***/
/***************************************************/
int delay=MAXDELAY; /* por ejemplo, 5 segundos */
216/512
time_t lasttime;
void sig_child(int signum)
{
wait(0); /* Finalización de acuse de recibo */
time(Slasttime); /* obtiene la marca horaria */
delay = MAXDELAY; /* reinicia el camarada */
}
El asalto al planificador
El asalto al planificador plantea dos problemas que ponen en evidencia los
217/512
límites de la multitarea y las razones por las que los sistemas de una sola CPU
pueden verse agobiados. El primer problema muestra el efecto que una tabla de
procesos grande tiene sobre los servlets. El segundo problema afecta al concepto
de duplicación previa.
La tabla de procesos fácilmente puede gestionar unos pocos cientos de
procesos. En teoría podría tener muchos más, dependiendo de la memoria y del
espacio de intercambio. No obstante, reflexione sobre los límites: Linux conmuta
entre los procesos cada lOms. Lo que no incluye el retraso producido por la
descarga del contexto del proceso antiguo y la carga del nuevo.
Por ejemplo, si tiene 200 servlets en ejecución, cada proceso podría esperar 2
segundos para obtener una pequeña fracción de tiempo de CPU en un sistema que
conmuta tareas cada .01 segundos. Si en cada intervalo de tiempo pudiera enviar
1KB, podría observar un rendimiento de aproximadamente 200KBps en la conexión
LAN de lOMbps (alrededor del par para TCP/IP). Sin embargo, cada conexión
obtiene sólo 512 bytes por segundo. Éste es el principal riesgo de autorizar a la
tabla de procesos un crecimiento sin límite.
El segundo problema se encuentra en el corazón de la duplicación previa: varios
procesos esperando una conexión. Su servidor puede tener 20 servlets bloqueados
en la E/S cuando llegue una petición de conexión. Puesto que todos están
esperando a una conexión, el núcleo despierta a los 20. El primero obtiene la
conexión, mientras que el resto vuelven al estado de bloqueo de E/S. Algunos
programadores llaman a esto un asalto al planificador o una pandilla estruendosa.
En cualquier caso, este problema resalta la necesidad de minimizar el número de
servlets en espera y de ajustes basándose en la carga de conexiones.
RESOLUCIÓN DEL PROBLEMA DE LA PANDILLA ESTRUENDOSA EN EL
NÚCLEO
Uno de los problemas para escribir un libro sobre una tecnología en constante evolución
como es Linux es que la información queda obsoleta muy rápidamente. La versión 2.4 de
Linux resolvió el problema de la pandilla estruendosa en el núcleo levantando tan sólo un
proceso bloqueado en la E/S.
Sobrecarga de select()
Una solución a estos problemas consiste en evitar del todo la multitarea. En un
capítulo anterior pudo ver las llamadas del sistema multiplexoras de la E/S: selectO
y poll(). Estas llamadas del sistema podrían actuar de una forma similar a varios
procesos.
En lugar de tener 20 procesos seniet esperando todos a que se produzca una
conexión, podría ofrecer un proceso que espera una conexión. Cuando llega la
petición de conexión, el servidor coloca la conexión (el canal del cliente) en una
lista de E/S. Estas llamadas del sistema esperan a que cualquiera envíe una
petición o comando. La llamada del sistema regresa a su servidor con los canales
que están listos, y su servidor recupera y procesa el comando.
La tentación de saltarse del todo la multitarea y utilizar estos multiplexores
puede conducirnos a una pérdida en el rendimiento. Cuando se utiliza una sola
218/512
tarea para procesar la información, su programa o servidor tiene que esperar a que
se produzca una operación de E/S en varios momentos a lo largo de la vida del
proceso. Si no se aprovecha de este tiempo de inactividad, sus servidores pueden
perder oportunidades fundamentales para realizar otras tareas. Por ejemplo, si
alguna vez ha compilado el núcleo, puede ver que tarda menos en compilarse si lo
obliga a realizar múltiples tareas con la opción -j de nwke. (Si dispone de la
memoria suficiente, dos o tres tareas por procesador pueden acortar notablemente
el tiempo de compilación.)
219/512
{
/* Si hay una conexión nueva, conectar y añadir a la lista */*/
if ( FD_ISSET(sd, &set) )
{ int client = accept{sd, 0, 0);
if ( maxfd < client )
maxfd = client;
FD_SET(client, Sset);
}
Puede utilizar este algoritmo para crear unos pocos procesos —muchos menos
que el número que puede crear en un servidor netamente multitarea— por ejemplo,
entre 5 y 10 servlets. Cada servlet, a su vez, podría soportar entre 5 y 10
conexiones.
COLISIÓN DE SELECT
Puede haber oído hablar de un problema de la distribución BSD llamado colisión de
select. El algoritmo anterior da por supuesto que el selecto puede prestar servicio a varios
descriptores y despertarse sólo cuando los descriptores cambien de estado. La
implementación 4.4 de BSD tiene una limitación que provoca que todos los procesos
bloqueados en un select() se despierten. Parece que Linux no tiene esta limitación.
Este enfoque podría soportar fácilmente cientos de conexiones sin
interferencias. El equilibrio entre el número de tareas v el número de conexiones
ayuda a reducir el impacto en la tabla de procesos. Sin embargo, el hecho de tener
tantas conexiones puede conducir a problemas si necesita seguir la pista del
estado de su protocolo.
Puede utilizar este algoritmo con servidores libres de estado. Una conexión sin
estado no recuerda nada sobre las transacciones anteriores. Esto exige a todas las
transacciones enviadas a su servidor que no tengan relación con otras
transacciones o que cada conexión tenga una sola transacción. Además, puede
utilizar este enfoque si su servidor necesita conexiones de duración corta o
mediana. Algunos ejemplos son un servidor HTTP, un servidor de consultas de
bases de datos, y un redirector Web.
Problemas de implementación
Como debería haber notado, este enfoque tiene un par de limitaciones. Primero,
el servidor primario no puede balancear el número de conexiones entre cada
220/512
servlet. Puede añadir código para colocar un límite al número de conexiones:
/***************************************************************/
/** Limitación del número de conexiones a un servlet ***/
/***************************************************************/
if ( FD_ISSET(sd, &set> )
if ( ceiling < MAXCONNECTIONS )
{ int client = accept(sd, 0, 0);
if ( maxfd < client )
maxfd = client;
FD_SET(client, &set);
ceiling++;
}
Este enfoque efectivamente cede la conexión a otro servlet. Sin embargo, la
distribución de conexiones entre sus servlets puede ser aleatoria.
El otro problema tiene que ver con el contexto de su conexión. Cuando un
cliente se conecta a su servidor, a menos que sea una conexión de una sola
transacción,
su servidor pasa por diferentes estados. Por ejemplo, el estado uno es la
petición del nombre de usuario y el estado dos es una sesión.
El fragmento de código anterior siempre vuelve al punto de espera y adquisición
de algún mensaje. Si desea incorporar estados, debe seguir la pista de dónde se
encuentra cada conexión. Con esta información, puede programar el servlet para
comparar la conexión con el mensaje que recibe. No es difícil, pero tiene que
planificar un poco más su programa.
La última limitación de este algoritmo es su soporte de conexiones de larga
duración (intensas en transacciones). Este algoritmo permanece estable, si tiene
un cliente y un servidor realizando transacciones simples o interacciones cortas.
Sin embargo, las conexiones intensas en transacciones generalmente necesitan
múltiples estados. Esto complica tremendamente su servidor. Además, puesto que
la conexión está activa durante más tiempo, la posibilidad de que su carga se
desequilibre se incrementa.
Redistribución de la carga
Puede resolver la limitación de la distribución de la carga si utiliza threads en
lugar de procesos. Tal y como se describió más arriba, el servidor no puede
distribuir fácilmente el número de conexiones por tarea. (Puede hacer esto con
IPC, pero no vale la pena.) La utilización de threads le permite compartirlo todo
entre las tareas, incluyendo la tabla de descriptores de archivos.
De una manera similar, su servidor puede tener varios threads en ejecución,
haciendo difícil asignar conexiones. Tiene un array de descriptores y puede utilizar
221/512
la llamada del sistema pofl(). (Puede utilizar la llamada select(), pero el algoritmo
no es tan directo.) Cada thread posee una parte del array. Sólo funciona si el padre
acepta todas las conexiones. Cuando su servidor observa una petición de
conexión, el padre la acepta y la coloca en el array de descriptores.
Dependiendo de la implementación de poll(), sus threads pueden agotar
frecuentemente el límite de tiempo para poder captar cualquier cambio que se
produzca en el array de descriptores. Algunas implementaciones simplemente
comprueban cada descriptor en un modo cíclico; otras (utilizadas probablemente
más en Linux) registran los descriptores dentro de una llamada del sistema. La
llamada del sistema espera e informa sobre la actividad del descriptor. Por tanto, si
el padre añade un descriptor nuevo, el f/ireacidebe llamar de nuevo a poll() para
comenzar a procesarlo.
/*******************************************************************/
/*** Ejemplo de distribución de carga equitativa: el padre ***/
/*** acepta y asigna conexiones a varios threads ***/
/*** ("fair-load.c" en el sitio web) ***/
/*******************************************************************/
int fd_count=0;
struct pollfd fds[MAXFDs]; /* borrado con bzero() */
/* padre */*/
int sd - socket(PF_INET, SOCK_STREAM, 0);
/*** socket bind() y listen()***/
for (;;)
{ int i;
/* Busca un hueco disponible antes de aceptar la conexión. */*/
for ( i = 0; i < fd_count; i++ )
if { fds[i].events == 0 )
break;
if ( i == fd_count && fd_count < MAXFDs )
fd_count++;
/* Si tiene un hueco disponible, acepta la conexión */*/
if ( i < fd_count )
{
fds[i].fd = accept(sd, 0, 0);
fds[i].events = POLLIN ¡ POLLHUP;
}
else /* de otro modo, se espera un rato más */
222/512
sleep(i); /* o, algún retraso producido */
}
/•- --hijo---*/*/
void *Servlet(void *init)
{ int start = *(int*)init; /* consigue el inicio del rango fd-range */
for (;;)
{ int result;
/* espera tan sólo 0-5 segundos */
if ( (result = poll(fds+start, RANGE, 500)) > 0 )
{ int i;
for ( i = 0; i < RANGE; i++ )
{
Esto muestra las responsabilidades del padre y de los hijos. El padre acepta
conexiones y las coloca en el array fds[]. Los hijos esperan en cada rango de
conexiones. (Si el campo events en la estructura pollfd es cero, su llamada a poll()
se salta esa entrada. Si el array no tiene descriptores activos, poll() espera a que
pase el tiempo de espera antes de volver a cero.)
Igualmente, su cliente puede utilizar las ventajas de una conexión realizada con
threads. Tal y como leyó en el Capítulo 7, los threads realizan muy bien el envío de
múltiples peticiones independientes, recabando la información, y resumiéndola. Su
cliente puede realizar todo esto en tándem, porque la mayoría del tiempo
consumido en la comunicación de red se utiliza para esperar a los servidores.
Puesto que los threads comparten intrínsecamente sus datos, puede llevar a cabo
varias peticiones e interpretar algunos resultados mientras espera al resto.
223/512
Investigación a fondo del control de sockets
Controlar sus algoritmos en torno a los sockets y las conexiones es una pieza
del incremento de! rendimiento del servidor y el cliente. También necesita controlar
el socket en sí mismo. Capítulos anteriores mencionaron algunas opciones que
puede utilizar para configurar el socket de acuerdo a sus necesidades. Esta
sección define muchas más.
Puede controlar todas las opciones de configuración del socAcrutilizando las
llamadas del sistema getsockopt() y setsockopt(). Estas llamadas del sistema
configuran opciones generales y específicas del protocolo de su socket. El
Apéndice A, "Tablas de datos", tiene un resumen de todas las opciones disponibles.
Los prototipos de estas llamadas son los siguientes:
int getsockopt(int sd, int level, int optname, void "optval, socklen_t *optlen);
int setsockopt(int sd, int level, int optname, const void *optval, socklen_t optlen);
Cada opción tiene un nivel. Actualmente, Linux define cuatro niveles de socket
SOL_SOCKET, SOLJP, SOL_IPV6, y SOL_TCP. Cada opción tiene un tipo:
booleano, entero, o estructura. Cuando establece u obtiene una opción, coloca la
opción en optval y declara el tamaño del valor en optlen. Cuando obtiene una
opción, la llamada devuelve el número de bytes que utiliza a través de optlen.
Por ejemplo, si quiere deshabilitar SO_KEEPALIVE, utilice lo siguiente:
/***********************************************************/
/* Ejemplo de setsockopt(): deshabilitar keepalive ***/
/***********************************************************/
int value=0; /* FALSO */
if ( setsockopt(sd, SOLSOCKET, SO_KEEPALIVE, &value,
sizeof(value)) != 0 )
perror("setsockopt() failed");
De igual modo, si quiere saber el tipo de socket (SO_TYPE), puede utilizar este
ejemplo:
/****************************************************/
/*** Ejemplo de getsockopt(): tipo de socket ***/
/****************************************************/
int value;
int val_size = sizeof(value);
if ( getsockopt(sd, S0L_S0CKET, S0_TYPE, &value,
&val_size) != 0 )
perror("getsockopt() failed");
En los siguientes apartados se describe cada una de las opciones que puede
224/512
utilizar.
Opciones generales
Las opciones generales se aplican a todos los sockets. El nivel para estas
opciones es SOL_SOCKET.
225/512
• SO_PASSCRED. Habilita-deshabilita el pase de la identificación del usuario
(véase SO_PEERCRED). (Booleano, deshabilitada de forma predeterminada.)
• SO_RCVBUF. Puede utilizar esta opción para cambiar el tamaño de los buffers
de entrada. Para TCP debería establecerlo a 3 o 4 veces el tamaño del MSS
(Tamaño máximo de segmento, véase TCP_MAXSEG más adelante). UDP, en
cambio, no tiene control de flujo, y cualquier mensaje que exceda el tamaño del
buffer receptor se perderá. (Entero, 65,535 bytes de forma predeterminada.)
226/512
• SO_TYPE. Esta opción devuelve el tipo de socket. Este número se corresponde
con el segundo parámetro en la llamada del sistema socket(). (Entero, sin valor
predeterminado, sólo recepción.)
227/512
son comentarios en la cabecera IP que le indican al receptor anotaciones
específicas (por ejemplo, marcadores de tiempo, niveles de seguridad, y
alarmas). Para obtener más información mire los RFC TCPvl y TCPv2. (Array
de bytes, sin valor predeterminado.)
228/512
esta opción, puede obtener las opciones salto-a-salto en el campo de datos
auxiliar. (Booleano, deshabilitada de forma predeterminada.)
229/512
El tamaño máximo de segmento (MSS) especifica la cantidad de datos de cada
porción. El subsistema de red coordina este valor con los MSS de los equipos
homólogos. No puede incrementar este valor por encima de los MSS de los
equipos homólogos, pero puede disminuirlo. (Entero, 540 bytes de forma
predeterminada.)
230/512
perror("setsockopt() failed");
/**** Bind(), listen(), y acceptf) ****/
Otro procedimiento que podría seguir es intentar de nuevo la llamada del
sistema bind() si devuelve el error EAGAIN. Para la utilización de esta opción es
necesario que ningún otro programa esté utilizando su puerto. La opción
SO_REUSEADDR le permite a sus programas compartir los puertos. Eso puede
causar problemas. No querrá tener dos servidores HTTP ejecutándose a la vez,
ambos utilizando el puerto 80. Además, intentar iniciar un servidor que ya está en
ejecución es una metedura de pata común de los administradores de sistemas.
Algunos programadores dejan esta característica deshabilitada e lo intentan
unas cuantas veces para detectar si el programa ya está en ejecución. Si quiere
utilizar esta opción (lo cual es una buena idea en la actualidad), asegúrese de
utilizar un mecanismo de bloqueo diferente, tal y como el PID en /var.
231/512
La utilización de llamadas estándar es muy útil para el desarrollo rápido y la
construcción de prototipos. Ambos son parientes y tienen varias características que
pueden hacer muy fácil su programación. De hecho, cuando realice prototipos de
forma rápida, utilice aquellas llamadas que encajan mejor con las contenidas en la
API sockets. Intente limitar la utilización de aquellas que pueden ser más duras de
convertir, como printf().
El problema con la utilización de llamadas de E/S de archivos proviene del modo
en que las bibliotecas y el sistema las gestionan. Cuando realiza una de las
llamadas de E/S de archivo con un descriptor de socket o utiliza fdopen(), el
sistema copia sus datos varias veces desde los buffers de archivos a los buffers de
sockets. Esto ataca a las prestaciones estelares de su cliente o servidor. Incluso las
llamadas de bajo nivel de E/S de archivo, read() y write(), evalúan su descriptor,
determinan si necesitan E/S de socket, y realizan la llamada adecuada.
Fventualmente, puede querer utilizar la API sockets en modo exclusivo. Lo que
puede ayudar a la legibilidad y al mantenimiento de su código; es muy fácil de este
modo observar que su programa está operando sobre un archivo frente a un
socket.
232/512
a memoria ilegal (la cual detecta rápidamente la llamada) no obtendrá ningún error
hasta la finalización del send(). Puede obtener estos errores de la opción de socket
SO_ERROR antes que la opción de biblioteca errno los consiga.
Puede acelerar la operación del send() con la opción de llamada del sistema
MSG_DONTWAIT. Con esta opción, la llamada copia sus datos e inmediatamente
regresa. A partir de ese momento puede proseguir con otras operaciones. Sin
embargo, es responsabilidad suya comprobar los códigos de error
cuidadosamente.
La única vez que una operación de escritura se bloquea es cuando los buffers
de transmisión están llenos. No suele ocurrir muy frecuentemente cuando tiene
mucha memoria sin utilizar. Si ocurre, puede utilizar E/S asincrona con señales
para hacerle saber cuándo los buffers están disponibles.
Descarga de recv()
A diferencia de la llamada del sistema send(), la mayoría de las llamadas del
sistema recv() bloquean la E/S, porque su programa generalmente se ejecuta más
rápidamente que los datos entrantes. Si los buffers de recepción tienen datos, la
operación de lectura copia la información y devuelve el control. Incluso si un byte
está listo en los buffers, la operación de lectura regresa con ese único byte. (Puede
cambiar este comportamiento con la opción MSG_WAITALL en la llamada del
sistema recv().)
Generalmente, no querrá esperar la llegada de datos cuando su programa
podría estar haciendo otras cosas. Tiene dos opciones: generar threads que
administren la E/S particular o utilizar señales. Si utiliza threads, puede tener otro
proceso que esté bloqueado en la E/S en la llamada recv(), Esto no puede ser un
tópico, pero tenga en mente los recursos del sistema. Si ya tiene varios procesos o
threads en su programa {u otros programas), puede no querer cargar la tabla de
procesos.
La alternativa, E/S asincrona o E/S controlada por señales, permite ejecutar su
programa. Cuando llega un mensaje, el núcleo envía una señal (SIGIO) a su
proceso. Su gestor de señales acepta la señal y emite la llamada de lectura.
Cuando lo ha hecho, el gestor establece un tlag indicando que los datos están
listos.
Recuerde que la señal sólo le dice a su programa que los datos han llegado, no
qué cantidad de datos. Además, el peligro de realizar operaciones de E/S en
gestores de señales es la posibiÜdad de perder otras señales. Puede resolver
estos problemas no obligando a la llamada a dar más datos que los disponibles y
activando el flag y dejando las operaciones de lectura al programa principal.
Otro algoritmo—E/S controlada por señales o threading—realiza esencialmente
las mismas cosas. Ambos obtienen los datos tan pronto como llegan, y ambos
tienen que activar un ñag global cuando los datos están listos.
233/512
otro extremo o parar alguna operación. El protocolo TCP soporta los mensajes
urgentes que esencialmente se saltan la cola de entrada. Estos mensajes urgentes
son los datos fuera de banda (out-of-band, OOB) de los que habrá leído algo hasta
ahora. (Otros protocolos soportan mensaje urgentes, pero utilizan una
implementación diferente.) Esta opción no es tan fascinante como suena. De
acuerdo con las especificaciones, los mensajes urgentes no son permitidos nunca
en mensajes mayores de un byte.
De hecho, si su programa obtiene dos mensajes urgentes consecutivos, el
segundo puede sobrescribir al primero. Se debe al modo en que el subsistema de
red almacena los mensajes urgentes. Tiene sólo un buffer d e un byte para cada
socket.
Originalmente, esta característica prestaba servicio a conexiones basadas en
transacciones como Telnet, la cual necesitaba un modo de forzar una interrupción
(como ^C) a través de la red. Puede utilizarlos para indicarle al cliente o servidor
una operación muy específica, tal y como reset-connection o restart-transaction.
Realmente no puede hacer mucho más aparte de eso. (Aunque con las 256
posibilidades se puede ser muy ingenioso.)
Si no selecciona la opción de socket SO_OOBINLINE, el núcleo avisa a su
programa de la presencia de datos OOB con una señal (SICURG). (Su programa
ignora esta señal de forma predeterminada a menos que la capture.) Dentro del
gestor de señales, puede leer el mensaje con la opción MSG_OOB de la llamada del
sistema recv().
Si quiere enviar un mensaje urgente, necesita utilizar la opción MSG_OOB de la
llamada del sistema send(). Adicionalmente, al igual que la E/S asincrona, necesita
habilitar la captura de este evento con una llamada del sistema fcntI():
/*****************************************************************************/
/*** Reclamación de la posesión de las señales SIGIO y SIGURG ***/
/*****************************************************************************/
if ( fcntl{sockfd, F_SETOWN, getpid(J) != 0 )
perror("Can't claim SIGURG and SIGIO");
Esta llamada le indica al núcleo que quiere obtener un aviso asincrono de
SIGURG y SIGIO. Recuerde que la llamada sólo utiliza un byte de ese mensaje
como datos OOB.
Puede utilizar los mensajes urgentes (OOB) para asegurarse que el receptor
está vivo, puesto que a diferencia de los datos normales, el control de flujo no los
bloquea. Puede utilizarlo para evaluar la calidad de la señal:
/**********************************************************/
/*** Error de calidad de señal entre programas: ***/
/*** un servidor respondiendo a los pulsos ***/
/*** ("heartbeat-server.c" en el sitio web) ***/
/*********************************************************/
234/512
int clientfd;
void sig_handler(int signum)
{
if ( signum == SIGURG )
{ char c;
recv(clientfd, &c, sizeof(c));
if ( c == '?' ) /* ¿Está vivo? */
send(clientfd, "Y", 1, MSG_OOB); /* ¡SÍ! */
}
}
int main()
{ int sockfd;
struct sigaction act;
bzero(&act, sizeof(act));
act.sa_handler = sig_handler;
sigaction(SIGURG, &act, 0); /* conectar la señal SIGURG */
/* establecer el servidor y la conexión del cliente a clientfd */
/*---reclamar las señales SIGIO/SIGURG ---*/*/
if ( fcntlfclientfd, FSETQWN, getpid(l) != 0 )
perror("Can’t claim SIGURG and SIGIO");
I * * * haz tu trabajo ***/
}
235/512
recv(serverfd, &c, sizeof(c));
got_reply = ( c -= 'Y' ); /* obtuvo respuesta */
}
else
fprintf(stderr, "Lost connection to server!");
}
int main()
{ struct sigaction act;
bzero(&act, sizeof(act));
act.sahandler = sig_handler;
sigaction(SIGURG, &act, 0);
sigaction(SIGALRM, &act, 0);
/*** establecer la conexión al servidor a serverfd ***/
/* reclama las señales SIGIO/SIGURG */*/
if ( fcntl(serverfd, F_SETOWN, getpid()) != 0 )
perror("Can't claim SIGURG and SIGIO");
alarm(DELAY);
/*** haz tu trabajo ***/
}
236/512
Resumen: discusión de las características de
rendimiento
Hasta este capítulo, cada uno de los anteriores capítulos le ha ofrecido una
pieza de un gran puzzle que le puede ayudar a construir un servidor y cliente de
alto rendimiento. El programa de red (el cliente, servidor, o equipo homólogo) tiene
que administrar la información que envía y recibe. Algunas veces, la utilización de
multitarea puede limitar el rendimiento si no tiene cuidado.
Cuando se equilibra con el control de E/S y la E/S multiplexada, puede tomar las
ventajas de la potencia de cada una de ellas y perder poco. Esto añade
complejidad a sus programas, pero con un buen diseño y previsión, puede vencer a
los problemas que crea.
Las opciones de socket dan además un control sutil sobre cómo el socket
gestiona los paquetes. Una de las características incluye el envío de un mensaje
incluso aunque el buffemo esté lleno. Mientras envía y recibe mensajes, puede
aumentar la velocidad de ejecución llamando a la API sockets.
Las herramientas que el API sockets le ofrece incrementan el control sobre su
programa. Puede utilizar muchas de ellas un sólo programa de modo que pueda
predecir la fiabilidad de su programa. La Habilidad en un programa de red es muy
difícil de conseguir y requiere varios trucos, consejos, y pistas. El siguiente capítulo
le lleva a otro nivel de fiabilidad.
237/512
Capitulo x
238/512
más típicos de la programación de red.
239/512
Las constantes van todas en letras mayúsculas.
• Utilice syslog para registrar los eventos, errores, y cualquier actividad atípica.
Éstas son sólo algunas. La mejor solución es encontrar funciones ya existentes
e imitar el estilo de su interfaz.
Escribir su programa de una forma familiar y legible hace más fácil para otros su
utilización, revisión, y mejora. Reflexione sobre lo siguiente: Linus Torvalds declaró
que él no planeó ser el dueño del núcleo para siempre. La utilización de
herramientas estándar y la práctica alarga su lapso de vida.
• Todas las E/S (recv(), send(), etc). Estas llamadas determinan si envió con
éxito su mensaje o si el nuevo mensaje es verdadero. Puede devolver un error
de cierre de conexión o una llamada interrumpida (tal y como se discutió
anteriormente). Si utiliza las funciones de alto nivel, tal y como fprintf(), debería
utilizar llamadas que devuelvan alguna clase de éxito o fallo, y debería
comprobarlo. Podría perder la conexión en cualquier momento, provocando una
señal SIGPIPE que mataría su programa.
240/512
es cero (o NULL). Por tanto, si elimina la referencia al puntero devuelto cuando
obtiene un error, su programa aborta con un fallo de segmentación.
241/512
de las dinámicas de la red y de la posibilidad de que el servidor o el cliente puedan
enmudecer. El subsistema de red mantiene el rastro de algunos de estos errores
en los protocolos de TCP, puesto que TCP asegura a cada momento que el canal
está libre para establecer comunicaciones bidireccionales.
Si su programa no envía mensajes durante un rato, necesita comprobar
manualmente la conexión. Si no lo hace, el error aparecerá con el tiempo como un
fallo de E/S. Para acceder a esta información, puede utilizar la llamada del sistema
getsockoptO con la opción 50_ERROR:
int error;
socklen_t size=sizeof(error);
if ( getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &size) == 0 )
if { error != 0 )
fprintf(stderr, "socket error: %s(%d)\n", strerror(error), error);
La utilización de este método puede interceptar los errores antes que lleguen al
subsistema de E/S. Y puesto que capturó el error con tiempo, puede solucionarlo
antes que el usuario descubra el problema.
Otra forma que tiene el sistema para hacerle saber que ha ocurrido un error es a
través de las señales. La mayoría de las llamadas le avisan en cuanto ocurre un
error en el canal. Algunos errores, tal y como un cierre prematuro o un cierre
normal, envían una señal a su programa si no obtiene el mensaje la primera vez.
Su programa puede ignorar algunas de estas señales de un modo predeterminado,
pero la captura de todas las señales relevantes reduce el riesgo de largas noches
de depuración.
Captura de señales
Un programa de red involucra a muchas tecnologías, y algunas de estas
tecnologías invocan señales. Su programa debe soporta la recolección y el
procesamiento de estas señales. La señal más comúnmente pasada por alto que
puede abortar su programa es SIGPIPE.
La interceptación de señales y su procesamiento tiene sus problemas
exclusivos, tal y como se comentó en el Capítulo 7, "División de la carga:
multitarea". El primer problema con el que debe tratar es el hecho de que cada
proceso almacena sólo una señal por cada tipo de señal. Si obtiene más señales
antes de procesar completamente la primera, su programa omite esos sucesos.
Tiene algunas opciones para tratar este problema. Primero, debería hacer lo
mínimo posible en el gestor de señales. Cualquier E/S puede potencialmente hacer
perder señales ulteriores. El riesgo de un bloqueo de E/S en un gestor de señales
puede provocar consecuencias desastrosas (tal y como un cuelgue del programa).
Además, debería evitar los bucles; intente ejecutar de un modo directo a través de
todos sus algoritmos de señales. Esta regla tiene algunas excepciones, pero
generalmente, es buena idea minimizar el procesamiento.
Segundo, puede permitir que su gestor sea interrumpido. Tenga cuidado con
242/512
esta opción; su gestor de señales puede utilizar una pila de hardware diferente
para aceptar cada llamada. Puesto que de forma predeterminada esa pila especial
no es muy profunda, puede desbordar fácilmente esa pila.
Tercero, su gestor podría aceptar la señal y colocarla en una cola para que la
procese su programa principal. Sin embargo, no es tan útil como podría pensar. La
señal tan sólo le indica que algo ha sucedido. Aparte del tipo específico de señal
(Linux define 32 tipos), no sabe nada más. Si encola los mensajes, su bucle de
procesamiento de la cola tiene que definir si muchos eventos de señales significan
algo o podría realmente comprimirlos en un solo evento.
Determinar qué hacer con cada evento de señal depende de cada señal . De los
32 tipos de señales definidos (listados en el Apéndice A y en la sección 7 de las
páginas del man), su programa probablemente se encontrará o solicitará seis de un
modo normal: SIGPIPE, SIGURG, SIGCHLD, SIGHUP, SIGIO, y SIGALRM. Podría
obtener otras señales, pero éstas son las más comunes a la par que más
relevantes.
SIGPIPE
Una señal común es SIGPIPE. Los manuales de UNIX afirman que sólo los
programas de redes nativas ignoran esta señal. En la programación normal, no
concurrente, nunca se encontrará con este evento. Por otro lado, si obtiene esta
señal, puede no ser muy importante para su programa, y es correcto ponerle punto
final. Sin embargo, cuando se escribe un servidor o un cliente, debe vigilar la
conexión cuidadosamente para que su programa pueda recuperarse sin finalizar
prematuramente.
Un error de canal sucede cuando el host destino cierra la conexión antes de que
haya realizado los envíos (véase sigpipe-client.c y sígpipe-server.c en el sitio
web). Obtiene el mismo error cuando canaliza un listado de directorio largo a través
de un programa de paginación tal y como less. Si abandona el programa de
paginación antes de alcanzar el fondo de la lista, obtiene un mensaje de canal roto.
Puede evitar la señal utilizando la opción MSG_NOSIGNAL en la llamada del
sistema send()-Pero esa puede no ser la mejor estrategia.
Lo que hace con la señal depende de lo que necesite. Primero, debe cerrar el
archivo (o establecer un ñag para el bucle principal) puesto que el canal ha
fallecido. Si tiene varias conexiones abiertas y su programa sondea cada conexión,
necesita descubrir qué conexión se cerró.
Pero puede tener negocios con el otro extremo sin finalizar. Si c! cliente o el
servidor conocen su protocolo, es improbable que la conexión se cierre en medio
del flujo de datos. Dos causas posibles son: que un ocurra un error en el lado del
cliente-servidor y que el camino entre ambos de desintegre. Igualmente, si necesita
finalizar la sesión, puede querer restablecer la conexión.
Si el servidor o el cliente se desconectan o tienen una caída, puede restablecer
la conexión tan pronto como esté lista para conectarse de nuevo. Puede querer dar
a otros programas un poco más de tiempo para regresar. Asimismo, una red
defectuosa necesita un poco de tiempo para silenciarse tras reajustar su
243/512
enrutamiento. Si no puede volverse a conectar tras varios intentos, puede avisar al
usuario y dejarle elegir qué hacer (esto es lo que algunos navegadores hacen).
La red puede seguir siendo poco fiable incluso después de una espera. Tras la
reconexión, puede enviar ráfagas de datos breves a modo de como puntos de
control a lo largo del camino. Esto permite capturar el punto de salida del protocolo.
SIGURG
El protocolo entre el cliente y el servidor requiere la elección de todas los
aspectos necesarios para pasar la información. Puede incluir algunas
características clave, tales y como interrupciones del flujo de datos o una
evaluación de la calidad de la señal (véase el Capítulo 9, "Cómo romper las
barreras del rendimiento"), el cual requiere la característica de los datos fuera de
banda (OOB). Pero a no ser que lo planifique y diseñe en el protocolo, no obtendrá
la señal SIGURG. Su modo de actuar es diferente a otras señales. El modo de
actuar predeterminado de una señal es ignorar la señal, y que su programa la
solicite para obtenerla.
La señal SIGURG es específica para la recepción de datos OOB. Su programa
tiene que seguir varios pasos para interceptar este mensaje (véase el Capítulo 9).
Generalmente, si el cliente o servidor la envían a su programa, debe ser por una
buena razón. Por tanto, si obtiene esta señal inesperadamente, querrá capturarla y
registrarla para una revisión posterior.
Si se obtiene más de una ocurrencia de esta señal demasiado rápido para
aceptarlas y procesarlas, perderá el último mensaje de datos OOB. Es así porque
la cola de datos entrantes de soc/cef sólo reserva un byte para un mensaje
urgente.
SIGCHLD
A diferencia de la señal SIGURG donde tiene que solicitar la notificación,
obtendrá la señal SIGCHLD si utiliza multitarea (concretamente, procesos) y el
proceso finaliza. Cuando el progreso del hijo finaliza, el núcleo retiene su contexto
para que el padre pueda revisar los resultados. El núcleo también avisa al padre
con esta señal. Si su programa ignora la señal, estos contextos se acumulan en la
tabla de procesos como zombis (véase el Capítulo 7).
Típicamente, los programas utilizan esta señal para llamar a la llamada del
sistema wait(). Sin embargo, es posible obtener la señal más rápidamente que una
llamada del sistema de una sola línea como wait(). Este es un problema muy feo.
pero es fácil de solucionar. La señal SIGCHLD es la única señal que justifica la
utilización de un bucle para procesar a todos los procesos zombis.
Puesto que wait() puede bloquear su programa (el bloqueo de un gestor de
señales no es una buena idea), utilice la llamada del sistema waitpid():
#include <sys/types.h>
#incluye <sys/wait.h>
244/512
El parámetro pid puede tener varios valores; si utiliza -1, esta llamada actúa igual
que la llamada del sistema ordinaria wait(). El parámetro status es igual al de
wait(). Para evitar que la llamada se bloquee, establezca las opciones a WNOHANG.
Si lia agotado todos los procesos zombis señalados, la llamada devuelve un cero.
El siguiente es un ejemplo de su utilización en un gestor de señales:
/***********************************************************/
/*** Ejemplo de exteminador de zombis mejorado ***/
/***********************************************************/
void sagchild(int signum)
{
while ( waitpid(-i, 0, WHOHANG) > 0 );
}
SIGHUP
¿Qué ocurre con el proceso hijo cuando el padre finaliza? El hijo obtiene una
señal SIGHUP. Su programa debe obtener esta señal si el usuario abandona la
sesión. De modo predeterminado, el proceso que obtiene este mensaje finaliza. En
la mayoría de los casos, no interesa este modo de actuar, puesto que no querrá
que los programas se demoren tras la finalización de los programas controladores.
La mayoría de los servidores se ejecutan mejor en segundo plano como
demonios sin una conexión específica. De hecho, algunos demonios lanzan un
proceso hijo que asume toda la responsabilidad del programa. Cuando el hijo está
listo, el padre finaliza. Es parecido a la utilización del comando nohup, y tiene el
beneficio añadido de no aparecer en el listado de tareas del administrador.
Además, un modo estándar de reiniciar un demonio es enviarle una señal de
cuelgue. Generalmente esto finaliza el proceso en ejecución, e init inicia una nueva
instancia. Si no puede depender de init para mantener su servidor activo y en
ejecución, puede forzar un reinicio con una instancia nueva con exec() (véase
restart-example.c en el sitio web). Antes de hacer esto, debe finalizar manualmente
todos los procesos actuales por debajo del padre más antiguo. (Debe utilizar esta
técnica de reinicio en todas las señales que puedan finalizar su servidor.)
245/512
SIGIO
Puede obtener una ayuda en el rendimiento de su programa de red
descargando la notificación de la E/S del núcleo. Cuando se implementa
correctamente, su programa obtiene la señal SIGIO siempre que los buffers de E/S
no bloqueen más una operación de lectura o escritura. En el caso de la escritura,
ocurre cuando el subsistema de E/S puede aceptar al menos un byte (véase la
marca de nivel bajo de escritura, SO_SNDLOWAT, opción de socket). En el caso de
la lectura, ocurre cuando el subsistema de E/S ha recibido al menos un byte (véase
la marca de nivel bajo de lectura, SO_RCVLOWAT, opción de socket). Véase el
Capítulo 8, "Cómo decidir cuándo esperar E/S", para el modo de implementar esta
señal, y véase el Capítulo 9 para los beneficios en el rendimiento.
SIGALRM
Al igual que la señal SIGIO, sólo obtiene la señal SIGARLM si su programa la
solicita. Esta señal es generalmente el resultado de llamar a la llamada del sistema
alarm(), una alarma con retardo. Los demonios utilizan frecuentemente esta señal
como un punto de vigilancia para asegurarse que el programa está todavía vivo o
para comprobar la solidaridad de la conexión. Véase el Capítulo 9 para la
utilización de la misma como un evaluador de la calidad de la señal de la conexión.
Administración de recursos
Las señales son una pequeña parte de los recursos de un programa. Si rastrea
y captura señales específicas, puede reducir la corrupción del sistema y puede ser
capaz de incrementar el rendimiento. Un programa completo tiene más aparte de
las señales. Incluye archivos, la pila de la memoria, memoria de datos-estática, y
tiempo de CPU. Tiene recursos adicionales, tal y como procesos hijo y memoria
compartida. Un servidor (o incluso un cliente) es fiable en función del cuidado que
tiene con sus recursos.
Administración de archivos
Su programa obtiene automáticamente tres archivos estándar cuando arranca:
stdin, stdout, y stderr. Debe familiarizarse con éstos v con el hecho de que el
usuario puede redíreccionar cualquiera de ellos. Generalmente no es un problema
puesto que el núcleo los limpia y cierra tras la finalización de su programa.
Los sockets y archivos nuevos son un tema diferente. Debe acordarse de
rastrear v cerrar cada archivo. Incluso aunque el núcleo cierra todos los archivos
tras la finalización del programa (tal y como los archivos estándar de E/S), todos
los archivos requieren una porción de la memoria del núcleo y del programa. Todos
los archivos de los que el programa pierda el rastro consumen más y más recursos.
Incluso si está trabajando con un archivo, es una buena costumbre de
programación obligarlo a cerrarse en algún punto del programa. Algún día, durante
una revisión, podrá abrir mas archivos.
La pila de la memoria
246/512
La pila de la memoria (o asignación dinámica de memoria) es equiparable a una
apertura y cierre de archivos en la que debe rastrear cada trozo de memoria que
consigue. Pero la pila de la memoria es propensa a convertirse en un final
incontrolado de muchos programadores. Las perdidas de memoria son comunes y
muy difíciles de rastrear. Hay bibliotecas que ayudan a rastrear estos pedazos
(como Electrichence), pero algunas buenas costumbres puede ayudarle a evitar los
errores más comunes.
El primer, y quizá más común error de programación es olvidar comprobar el
valor de retorno de mallocQ o calloc() (en C++ se ha de capurar una excepción). Si
el tamaño de la memoria que solicitó no está disponible, la llamada devuelve N U L L
(cen>) a su programa. Dependiendo de cuánto necesite ese espacio, puede
diseñar su programa para finalizar, poner orden y volverlo a intentar, avisar al
usuario, etc. A algunos programadores Ies gusta utilizar assert() para cualquier
asignación de memoria. Desgraciadamente, esta llamada siempre aborta su
programa si falla.
Sea consistente con las llamadas de asignación de memoria. Si utiliza calloc(),
utilícela para todas las asignaciones de memoria. No mezcle llamadas. En
particular, el operador new de C-t-+ algunas veces no itUeracrúa bien con malloc{) y
callocO- Cuando llama a delete en un bloque de memoria asignado con mallocO,
puede observar algunos resultados impredecibles.
Más aún, cuando libera un bloque de memoria, asigna un NULL a la variable
puntero. Este es un modo rápido para localizar si tiene una referencia corrompida
(utilizar un puntero después de liberar su memoria).
Cuando asigna alguna memoria, puede o bien obtener lo que necesitaba
exactamente, u obtener lo que espera que puede necesitar. Estas son dos
aproximaciones diferentes al mismo problema. La primera aproximación
(asignación precisa) logra exactamente el espacio que la variable necesita. El
programa que utiliza la otra aproximación (asignación generosa) generalmente pide
un gran trozo de memoria y a continuación utiliza ese trozo para el procesamiento.
La Tabla 10.1 resume los beneficios de cada estilo de programación. A diferencia
de la mezcla de llamadas de asignación, puede mezclar la asignación precisa y
generosa, pero asegúrese de documentar su utilización.
Tabla 10.1 Comparación de la asignación
247/512
Efectiva en módulos o programas Muy útil en un procedimiento de una sola
con utilización extensiva de llamada donde utiliza un tro^o de memoria y
memoria. a continuación la libera.
Puede malgastar espacio de Sólo genera una cabecera de asignación
asignación porque cada bloque para el bloque completo.
asignado necesita un descriptor de
asignación. (Así es como el
subsistema de asignación rastrea la
utilización de la memoria.)
Trasladable a otros sistemas. También es trasladable, pero se aprovecha
de la asignación latente de Linux. (Sólo las
páginas de 1KB que son utilizadas están
enlazadas de hecho a la memoria real.)
MALLOC() GENERACION DE FALLOS DE SEGMENTACION
Si obtiene un fallo de segmentación en la llamada del sistema mallocO, su programa ha
corrompido el bloque de asignación en alguna memoria asignada. Este fallo generalmente
ocurre a causa de una operación de cadena con mal funcionamiento o por un a r r a y
f u e r a de límites. Para evitar este problema pruebe el índice del a r r a y y limite las
longitudes de las cadenas.
Su programa puede tener varios módulos que utilicen la asignación de memoria
de formas diferentes. Si está interactuando con otros programadores, asegúrese de
establecer quién es el dueño de qué. Por ejemplo, si pasa un puntero a memoria a
una rutina, ¿es la rutina la dueña del puntero ahora? O, ¿deberá hacer una copia?
Una estrategia típica es dejar la posesión a la rutina-módulo que la creó. Esto
significa que si pasa la referencia a otro módulo que necesita su versión, debe
proporcionar un modo de clonar el bloque. Algunos programadores de objetos
llaman a esto una copia profunda porque todas las referencias dentro del bloque
tienen que copiarse igualmente.
248/512
CPU, memoria compartida, y procesos
Los tres últimos recursos, la CPU, la memoria compartida, y los procesos, sólo
requieren unas pocas notas. Véanse los Capítulos 7, 8, y 9 para más información.
• Memoria compartida. Similar a la administración de archivos. Abre, bloquea, y
cierra el acceso a las regiones compartidas de la memoria.
• CPU. Los programas pueden controlar fácilmente el tiempo de planificación de
la computadora. Libere algún tiempo bloqueando en el momento justo.
• Procesos. Si utiliza la multitarea, el sistema le indica el estado de cada proceso
hijo. Necesita aceptar y actuar sobre las notificaciones de modo que no llene la
tabla de procesos con zombis.
Servidores críticos
Conocer los eventos externos e internos (las señales, por ejemplo) le ayudan a
cómo trabajar desde una perspectiva a nivel de sistema. A medida que programa
sus dientes y servidores, necesitará definir qué ha de ocurrir y cuándo. Esto define
el modo de actuar que su programa debe tener en cada circunstancia. La dificultad
primordial en la predicción del modo de actuación es que cada computadora puede
tener una configuración diferente—incluso aunque la instalación de la distribución
pueda ser la misma. (Es un hecho admitido que todo administrador de sistemas
tiene que realizar pequeños ajustes en su estación de trabajo.)
La variabilidad en una computadora individual es en si misma desmoralizadora,
pero cuando se introduce en la programación de red, poco menos que tiene que
declarar que todas las apuestas están fuera de sitio. Sin embargo, tiene algún
control. Si tiene una comprensión completa de lo que se supone que el programa
ha de hacer y con qué interactúa, puede cancelar las restantes variables. Puede
querer fijarle al usuario que las dependencias externas deben actuar correctamente
con sus propias dependencias.
Por ejemplo, suponga que tiene un servidor que ha de ejecutarse como un
demonio. Si su servidor necesita acceso a determinados archivos en el sistema
destino, puede querer pactar que aquellos archivos deben residir en un directorio
particular y deben detentar un formato especificado. Esto ayuda al administrador
del sistema a saber que si algo va mal, él puede mirar en los archivos para
verificarlos.
Los servidores son diferentes en la expectación y las prestaciones. Los usuarios
esperan que su servidor esté disponible cuando contacten con él. También esperan
que responda dentro de un periodo de tiempo razonable. Para mantener
satisfechos a ios usuarios, necesita saber los detalles específicos de lo que
significa "razonable". Más aún, puede querer definir cuánto tiempo de actividad
debe tener su servidor. 5i su servidor se apaga, ¿con qué rapidez espera el usuario
que el sistema vuelva a levantarse? Todo esto depende de cómo de crítico es su
servidor.
249/512
¿Qué se califica como un servidor crítico?
A diferencia de los clientes que pueden apagarse y levantarse más
frecuentemente, los usuarios esperan que los servidores permanezcan levantados.
Los clientes se pueden conectar, establecer protocolos, transferir información, y
apagarse de un modo comparablemente rápido. Por supuesto, en un cliente, podría
relajarse más en la administración de archivos y memoria. Algunos programas
Windows lo son. Cuando el programa finaliza, el administrador de la memoria libera
todos aquellos recursos (por lo menos en Linux).
Los servidores, por otro lado, deben permanecer levantados y en ejecución
indefinidamente. Al igual que el genio en la lámpara, espera que su cliente pueda
conectarse al servidor en cualquier momento y cumplimente cualquier deseo
(dentro de lo razonable). El genio no puede decir, "Agarre a sus caballos mientras
reinicio." El servidor debe estar disponible y listo.
La disponibilidad esperada determina cómo de crítico es su servidor. Unos
servidores son más críticos que otros. Af igual que un servidor HTTP, algunos
servidores simplemente tienen que responder dentro de un cierto periodo de
tiempo. Otros deben seguir la pista de las transacciones de forma que ni el cliente
ni el servidor pierdan información, y todas las sesiones aparezcan sin costuras; un
ejemplo de esto es una adquisición con tarjeta de crédito o una transferencia de
dinero.
Interrupciones físicas
La red tiene tantos tipos diferentes de conexiones físicas y modos diferentes de
dar de baja la portadora (perder la señal eléctrica u óptica que transporta los
paquetes) que incluirlos en una lista sería muy laborioso y no muy útil. El tema lisa
y llanamente es que la conevión entre el punto A y el punto B puede perderse.
TCP trabaja muy bien con estos eventos. No le importa si el medio físico es un
cable, fibra, u ondas de radio. Los diseñadores originales provenían de los días la
locura nuclear, donde la posibilidad de perder redes enteras modeló la forma en
que el router envía los mensajes. Si la red tiene una ruta diferente al mismo
destino, la red la detecta y reprograma los routers. El problema es solamente que
lleva tiempo descubrir la nueva ruta y provoca el enrutamiento.
250/512
Sus mensajes críticos pueden encontrarse con este problema y a menudo TCP
se recupera sin su intervención. Sin embargo, si la red no puede establecer una
nueva ruta, su programa puede tener que recuperar la sesión por sí mismo.
Blips de enrutamiento
Las interferencias físicas provocan blips de enrutamiento con el formato de
ciclos. El mensaje puede rebotar entre routers hasta que es descubierto y
arreglado. Esto provoca la duplicación y la pérdida de paquetes. De nuevo, durante
una conexión TCP simple, su programa no se encuentra con estos problemas
porque el protocolo los arregla antes que lleguen a su programa.
251/512
El último problema típico con el que debe tratar es el procedimiento de
restablecimiento de la sesión. Frecuentemente, las sesiones no son seguras y no
requieren una transición a través de diferentes muros de seguridad. Las
conexiones inseguras no necesitan nada más que una reconexión. Sin embargo, si
el suyo es un servidor crítico, tiene que Autenticarse. La autenticación verifica que
el cliente tiene derecho a conectarse. Esto puede ocurrir a través de una petición
de identificación de usuario, una forma común de seguridad.
La otra forma establece y verifica que el cliente es realmente quien dice ser
(certificación). El proceso de certificación requiere de un tercero, un servidor en
quien se confía (un certificador). Cuando el servidor acepta una conexión y
comienza el proceso de identificación del usuario, el servidor pide al cliente un
certificado de autenticidad. A continuación el servidor reenvía el certificado al
certificador. El certificador comprueba el certificado y responde con su autenticidad.
Debe considerar estas cuestiones generales y otras que puede encontrar antes
de la implementación. Si reflexiona sobre la seguridad y los protocolos de sesión
después de la implementación, estará dejando lagunas insalvables en su diseño.
252/512
El usuario trabaja con la sesión en el formato de transacciones. Estas
transacciones pueden obtener datos o revisar datos del servidor. Para asegurarse
que los datos son siempre exactos, el cliente y el servidor siguen la pista de cada
revisión-transacción. Como diseñador del programa, no necesita seguir la pista de
la recuperación de datos (a menos que eso sea parte de su política de seguridad).
Puede evitar la pérdida de transacciones etiquetándolas, registrando, y
obteniendo acuse de recibo de todas las transacciones. Para acusar su recepción,
la transacción debe tener una etiqueta única de ID. La etiqueta única es diferente al
ID de mensaje de TCP porque la etiqueta es única para todas las sesiones. (La
etiqueta realmente no tiene que ser única para todas las sesiones. Podría reutilizar
las etiquetas después de un tiempo predefinido. Esto simplifica un poco la
implementación.)
Por ejemplo, el cliente envía una petición para retirar los fondos de una cuenta
de tarjeta de crédito. Al mismo tiempo, registra la transacción local mente en el
sistema de almacenamiento permanente. El servidor sigue dos pasos. En primer
lugar, acusa la recepción del mensaje (la transacción ha sido enviada). Tras
completar la transacción, el servidor envía otra acuse de recibo (la transacción ha
sido llevada a cabo).
Después que el servidor acuse la recepción de la transacción, el cliente descarta
la transacción pendiente. (Los programas nunca deberían descartar las
transacciones críticas. En su lugar, se deberían mover a un archivo seguro.) El
cliente puede utilizar el segundo acuse de recibo como un flag para actualizar los
registros actuales.
CLIENTES PEQUEÑOS
Si no quiere tener el riesgo de perder la sincronización con el servidor, puede hacer que
su cliente sea un cliente pequeño no manteniendo información (ocalmente. El cliente que
programe aún mantendrá el rastro de las transacciones, pero en lugar de almacenar la
información localmente (tal y como las instrucciones actuales), cada refresco de
información se convierte en una petición al servidor.
Si la sesión se pierde, el cliente y el servidor deben recuperarla. Tras recuperar
la sesión, el cliente y el servidor tienen que trabajar a través de las transacciones
señaladas. Ambos comparan las etiquetas de transacción, descartando aquellas
que han finalizado.
Este proceso también ayuda a eliminar el problema de las transacciones
duplicadas. El cliente y el servidor sólo eliminan aquellas transacciones que
comparten. Las restantes son señaladas y necesitan enviarse de nuevo.
Puede encontrarse otros asuntos que necesiten una gestión adicional. Los que
esta sección cubre son estándar para la mayoría de los servidores críticos. Para
descubrir otros, cree ejemplos de utilización con escenarios asociados e intenten
descubrir sus necesidades específicas.
253/512
Cuestiones sobre la concurrencia
cliente/servidor
Lma sesión puede tener un funcionamiento defectuoso no sólo a causa de algo
tan catastrófico como un fallo del sistema o un aborto en el programa. Cuando
trabaje con programas en ejecución concurrente, se encontrará con cuestiones
relativas a la concurrencia que pueden provocar interbloqueos e inaniciones
comparables a las que provocan los threads y los procesos (multiprogramarión).
Sin embargo, éstas son más difíciles de detectar.
Necesita entender que los problemas de concurrencia que se pueden encontrar
en la programación de red no son exactamente iguales que ios de la mult i
programación. La razón es sencilla; a diferencia de un programa con muchos
threads, los equipos homólogos y las parejas clientes-servidores no comparten en
realidad recursos. Están aislados y separados. El único enlace es el medio físico
de la red. Más aún, el interbloqueo típico de la multiprogramación es muy difícil (si
no imposible) de liberar. Puede liberar un interbloqueo de red, pero a pesar de eso
se puede producir un interbloqueo e inanición mutuo.
Interbloqueo de la red
La mayoría de las comunicaciones de red definen un protocolo para indicar
quién habla primero. Por ejemplo, normalmente uno envía peticiones mientras el
otro responde. Esa denominación define qué extremo de la conexión dirige el
protocolo.
Para ilustrar la interacción, considere por favor los servidores de HTTP y Telnet.
Un servidor HTTP acepta peticiones del cliente, convirtiendo al cliente en el
conductor del protocolo. Una sesión de Telnet, por otro lado, le pide al usuario un
nombre de usuario y contraseña. Una vez conectado, el único momento en que el
usuario sabe que el servidor puede responder es cuando ve el indicador de
comandos. Una sesión Telnet puede aceptar un comando asincrono utilizando una
interrupción por teclado (Ctrl+C).
El interbloqueo de red es sencillo: la pareja cliente-servidor o los equipos
homólogos se olvidan de a quien ¡e tocaba hablar el siguiente, así que acaban
esperándose el uno al otro indefinidamente. Esta forma de interbloqueo es difícil de
detectar porque muestra los mismo síntomas que una red muy congestionada
-ambos extremos no escuchan nada durante un rato.
Podría colocar un tiempo de espera en las conexiones para solucionar este
problema. Es una muy buena idea porque ningún cliente o servidor (especialmente
estos últimos) debería esperar eternamente en la red. Pero los tiempos de espera
no le indican ninguna otra cosa aparte que un mensaje está tardando demasiado
tiempo en llegar.
Otra forma de tratar el interbloqueo es intercambiar los papeles muchas veces.
Sí el servidor está dirigiendo la sesión, permita al cliente dirigirla transcurrido un
tiempo determinado. Si ocurre un interbloqueo, con el tiempo ambos intercambian
sus papeles y comienzan a transmitir. Una vez que hacen esto, pueden restablecer
254/512
rápidamente el protocolo.
Un modo efectivo de prevenir, detectar, y reparar el interbloqueo es utilizar el
algoritmo de evaluación de la calidad de la señal presentado en el capítulo anterior.
En lugar de utilizar el envío de una consulta "Estás vivo", ambos extremos de la
conexión se envían el uno al otro un mensaje indicando si está en modo escucha o
no (por ejemplo, escucha del conductor o escucha del contestador). Puede incluso
ampliar los mensajes para indicar el estado del protocolo.
Inanición de la red
El otro problema de red que puede afrontar es la inanición. La inanición de la
red, al igual que el interbloqueo de la red, es un defecto en la comunicación con el
cliente. Suponga que tiene un servidor que tiene diez conexiones en un select(), y
el servidor sólo tiene intervalos de tiempo suficientes para responder a cinco. Es
posible que a una conexión nunca se le preste servicio.
Se encontrará frecuentemente con este problema cuando se conecte a sitios de
Internet muy concurridos. El único síntoma que se observa es similar al de un
interbloqueo; se conecta y luego no escucha nada durante mucho tiempo. Es
diferente a la simple conexión y espera sin datos transferidos, lo cual provoca el
protocolo TCP cuando establece y encola la petición de conexión. En su lugar, verá
unos cuantos bytes enviados a su cliente y a continuación—silencio.
La solución a este problema no es tan directa como la solución del interbloqueo.
Igual que intentar decirle a un subjefe que necesita delegar algunas tareas, un
servidor que acepta más conexiones de las que realmente puede soportar parece
productivo. En su lugar, tan solo mata de hambre una conexión tras otra.
El primer paso es asegurarse de tener un equilibrio entre las prestaciones del
procesador-sistema y el número de conexiones activas. Cuando establece la
profundidad de la cola de escucha, recuerde que las conexiones en espera están
bien —siempre que le preste servicio antes de que agoten su tiempo de espera.
Debe medir cuánto dura cada sesión y ajusfar la profundidad de la cola en
consecuencia.
Puede seguir otra aproximación para evitar la inanición de las conexiones. Es
parecida al modo que los procesos obtienen tiempo de CPU a través del
planificador. El concepto que debería probar es la planificación dinámica o las
prioridades. Cuando tiene, digamos, tres procesos que utilizan por igual la CPU,
sólo uno se puede ejecutar a la vez. De esta manera, el planificador aumenta la
prioridad efectiva de los procesos que pasa por alto. Esto asegura una distribución
justa de la CPU.
De igual manera, puede utilizar algo así como un esquema de prioridades en
cada una de sus conexiones. Lo que es beneficioso porque puede utilizar las
prioridades de modos diferentes. Aquellas conexiones que tienen datos listos
consiguen prioridades más altas que el resto.
255/512
Los autores de ataques a la red utilizan una forma degenerada del interbloqueo
de red o la inanición. Necesita saber algo de esto, incluso aunque sea uno de los
ataques de red más antiguos. Tal y como se describió en secciones anteriores, el
subsistema de red acepta una petición de conexión a un puerto específico desde
un cliente. Este puerto representa a un servidor y tiene una cola de escucha.
El agresor se conecta a este puerto. El intercambio de señales de tres
direcciones de TCP finaliza, y el subsistema de red redirige la conexión a la cola de
escucha. Su servidor obtiene la conexión y la acepta (recuerde que la conexión no
está completamente finalizada hasta la aparición de la llamada del sistema
accept()). A continuación su servidor libera la conexión a un servlet.
Aquí está el problema. Si el agresor no hizo nada, no envió datos, la pila de
protocolos de TCP/TP con el tiempo agota el tiempo de espera de la conexión y da
de baja al agresor. Es lo adecuado. Sin embargo, el agresor es más inteligente;
envía unos pocos bytes -insuficientes para rellenar cualquier buffer, poro
suficientes para forzar a un bloqueo de la E/S (un atasco de la E/S).
Su servidor obtiene los primeros bytes y a continuación se bloquea para leer el
resto. El agresor sabe que su servidor se ha bloqueado en esa conexión. Su
programa prosigue realizando más conexiones. Finalmente, su servidor podría
agotar los recursos del sistema sin conseguir nada. Incluso si pone un tope al
número de conexiones, el programa del agresor podría finalmente tener el control
de los recursos del servidor y la cola. En ese momento se encuentra bloqueado y
no puede prestar servicio a nadie más.
Puedo tomar tres medidas para prevenir esto:
1. Coloque siempre un tiempo de espera en todas sus llamadas de E/S. Los
tiempos de espera son fáciles de implementar, y le protegen de la pérdida del
control sobre su servidor.
2. Dejar siempre un proceso libre (generalmente el padre) para controlar los
procesos hijo. Puede delimitar cuantos procesos se han atascado.
3. Limitar el número de conexiones desde un ID de host o subred particular.
Cada conexión incluye el ID del host origen. Esta es una medida de prevención
de poca fuerza porque los agresores son muy ingeniosos, y puede perjudicar
accesos inocentes.
Estas medidas pueden ayudarle a localizar y deshabilitar un ataque de red.
Puede encontrar otros tipos de ataques; debería observar sus registros de
conexión y tasas de acierto.
256/512
En este capítulo se muestran unas cuantas sugerencias de programación
robusta. La mayoría son de sentido común: las herramientas de conversión ayudan
a la transportabilidad, y siempre es importante observar los valores de retorno.
Pero conocer qué valores de retorno hay que observar le permite preocuparse
menos sobre los errores y más acerca del programa. Así mismo, conocer que las
herramientas de conversión no provocan pérdida de prestaciones en computadoras
big endian reduce su riesgo.
Conocer cómo trabaja su servidor y cómo interactúa con el cliente es un modo
estupendo de afinar su servidor y hacerlo irrompible.
257/512
Parte III
En esta parte
11 Cómo ahorrar tiempo con objetos
12 Uso de la API de red de Java
13 Diseño y uso de un marco socket en C++
14 Limitaciones de los objetos
258/512
Capitulo 11
En este capítulo
La evolución de la ingeniería del software
Cómo llegar a la programación Nirvana
Presentación de los fundamentos de los objetos
Características de los objetos
Extensión de los objetos
Formatos especiales
Soporte del lenguaje
Resumen: mentalidad orientada a objetos
259/512
NOTA
Este capitulo precede a los capítulos que presentan los sockets para implementadones
específicas. Por tanto, proporciona la información básica acerca de los objetos que
permite cubrir, de forma apropiada, las implementaciones específicas de Java y C++.
Normalmente, la mayoría de los programadores no entienden esta filosofía y sus
implementaciones se alejan ligeramente del intento original de la tecnología orientada a
objetos.
260/512
sistema. Los Niveles 1 y 2 modelan y definen la arquitectura y componentes
principales del sistema. Los datos no se consideran concretos sino información
general utilizada por cada componente para completar una tarea. En esta etapa, el
modelo utiliza círculos para representar operaciones o funciones conectados
mediante arcos que representan las rutas de los datos (diagramas de flujo de
datos).
El Nivel 3 representa una fase de transición entre el SSA y el SSD. Utilice este
nivel para definir cualquier interfaz dependiente del sistema, propio del sistema o
de otros fabricantes. Algunas interfaces del sistema incluyen el usuario y la interfaz
de red.
La última fase, SSD, obtiene los detalles significativos identificando la forma de
dividir el problema en funciones o procedimientos. Cuando utilice este enfoque,
puede llegar hasta escribir los algoritmos, desde un punto de vista general, o
utilizar los diagramas de flujo.
SSR, SSA y SSD pueden llegar hasta ocho niveles (el Nivel 7 representa el
mayor detalle), pero sólo aquellos proyectos que son muy grandes (millones de
líneas de código) llegarán hasta los niveles más avanzados. En este sentido, el
modelo es muy flexible; el diseñador sólo tiene que llegar hasta el nivel que sea
necesario.
Por otro lado, cada programador utiliza algo parecido al diseño funcional, incluso
para escribir sólo el código. Un diagrama de flujo le puede ayudar a organizar los
pasos necesarios que permiten conocer en detalle el programa. Además, el Nivel 0
es critico, tal y como se describe posteriormente en este capítulo.
Desafortunadamente, la correspondencia actual entre el diseño y código fuente
no está muy clara. De hecho, tenga en cuenta que incluso en la fase SSD, no se ha
escrito todavía ningún código fuente del programa. A pesar de que las etapas
muestran una transición cidra entre la parte abstracta y el detalle, el programador
que utiliza este diseño se esfuerza, a menudo, en la realización de la conexión
entre e] papel y el código.
261/512
entonces todas las secciones que dependen de esta variable también deben
modificarse. (Esto no se aplica a datos globales de sólo lectura, dado que el
programa no puede revisarlos.)
La programación modular (una pequeña extensión de la programación
estructurada) establece reglas de ámbito que deben seguir todos los
programadores. El programador debe limitar todas las variables con un conjunto de
implementaciones e interfaces (descritas posteriormente en este capítulo en la
sección "Interfaces"). La programación modular también se conoce como
encapsulación, presentada posteriormente en este capítulo en la sección
"Encapsulación de la implementación". La programación modular extiende el
concepto más allá admitiendo la concurrencia, en concreto los monitores. Un
monitor es una función o procedimiento que permite sólo la ejecución de un thread
en un instante de tiempo.
VARIABLES LOCALES FRENTE A VARIABLES LOCALES
No piense que nunca podrá utilizar variables globales. En algunas situaciones (por
ejemplo, programación de sistemas incrustados), debe tener mucho cuidado con el
desaprovechamiento de memoria. Los programas, normalmente, utilizan espacios de
memoria para los cómputos temporales. Si es cuidadoso, podrá compartir variables
locales sin provocar una violación del ámbito de cada una de ellas. Recuerde que esto
puede afeclar seriamente al programa concurrente (o entrelazado).
La programación modular le ofrece la interfaz con el mundo exterior. Si organiza
sus programas en módulos e interfaces, podrá modificar un módulo e insertar uno
nuevo y mejorado sin necesidad de volver a plantear todo el proceso de la
ingeniería.
No obstante, la programación modular también presenta sus inconvenientes. No
obstante, introduce un concepto que lia ayudado bastante en la definición de
muchas bibliotecas reutiiizables. Por otro lado, la escritura de módulos conectables
por otros programadores se convierte en algo muy solicitado puesto que
proporciona interfaces fiables raramente modificables. Además, el programador no
necesita conocer los detalles de la implementación aceptando bastante mejor las
revisiones del código.
262/512
implementaciones de una cola. Con la abstracción de datos, los programadores
podrían evitarse estas duplicaciones y centrarse en otros problemas mucho más
interesantes.
Cuando se programa algún problema, resulta interesante comprobar la
posibilidad de generalizar algún aspecto del mismo. Si realiza un análisis y una
práctica detallada, podrá encontrar secciones que pueden resultar muy útiles en
cualquier otra parte. Además, puede encontrar, ya escritos, muchos algoritmos
generalizados.
El hecho de examinar los datos desde un punto de vista abstracto constituyó un
paso muy significativo en el campo de la informática. Con la abstracción, el
programador no necesita conocer los detalles de los datos que utiliza el programa.
Simplemente se centra en aquello que debe realizar.
263/512
Los problemas a los que se enfrenta día a día como programador no difieren
mucho de aquellos a los que se han enfrentado otros programadores con más
experiencia. De hecho, la mayoría de los problemas son pequeños cambios con
respecto a diseños anteriores. Algunos diseños reutilizables incluyen servidores o
generadores de mensajes TCP, como las peticiones HTTP o e-mail.
Lamentablemente, muchos de sus diseños eslán bloqueados por los obstáculos
que plantean los des-arrolladores de software.
Ea potencia de la reutilización es la posibilidad de utilizar el trabajo de otros y
poder desarrollar sus propios trabajos a partir de este trabajo. Considere el tiempo
que necesitaría un arquitecto para construir su nueva casa si tuviera que
encargarse de echar el hormigón para hacer los cimientos. ¡Hoy en día, las
constructoras pueden construir en una fábrica casa preciosas, robustas e
impresionantes y transportarlas al lugar en sólo tres días! Ahora, ¿esto es también
posible en la industria de la informática? Es posible, pero debe seguir una cierta
disciplina.
NIH: NO SE INVENTA AQUÍ
Cuando trabajaba en mi primer trabajo, me encontraba entre los ingenieros de mayor
talento. Los intentos por destacar entre todas las personas fueron una constante lucha.
No obstante, había una característica que muchos de ellos compartían y limitaba su
potencial: NIH. No se inventa aqui es un pensamiento que estipula lo siguiente :"si
nosotros no lo hicimos, probablemente está incorrecto {o no perfecto)". He aprendido que
este punto de vista erróneo (cuando no arrogante) prevalece, de forma muy cercana, en
todas las compañias de software.
Es posible que quiera considerar dos perspectivas de reusabilidad, la aceptación
de los trabajos de otros así como promover los suyos propios. E¡ primero es
realmente factible v sólo requiere una confianza entre el programador y el
proveedor. Es adecuado siempre que el proveedor responda a sus necesidades.
La otra perspectiva le convierte en el proveedor del programador-usuario. Para
promover su trabajo, las bibliotecas deben ser robustas y es necesario que
responda rápidamente a los usuarios. Un diseño bueno e intuitivo facilita al cliente
la vuelta a las últimas versiones. Además, el hecho de ofrecer rutas de
actualización claras ayuda a realizar, de una forma mucho más sencilla, una
transición difícil.
Por último, la generalización del diseño simplifica la reusabilidad. Piense el
problema como se fuese un conjunto de piezas de un rompecabezas. Cuanto más
cerca estén las piezas a un formato general, probablemente mayor posibilidad
exista de que sus usuarios pueden reutilizar su trabajo.
264/512
de litio. Cada una usa un estilo de interfaz basada en la polaridad, positivo y
negativo. La celda seca es mucho más barata y el ion de litio es más eficiente,
recargable y tiene una mayor duración.
Teniendo en cuenta este estilo, es posible seleccionar una interfaz y resistir a las
modificaciones de la misma. Por tanto, sin importar lo que pueda ocurrir, puede
mejorar la implementación (aumentando el rendimiento, incrementando la
fiabilidad, solucionando defectos). La posibilidad de modificar bibliotecas y módulos
de entrada y salida se denomina transportabiíidad.
Para generar código transportable, es necesario que siga unas ciertas reglas.
Primero, defina una interfaz e incorpórela. Un conocimiento detallado de la interfaz
determina la longevidad del módulo. Si la interfaz no engloba todas las
necesidades que se desarrollan, entonces los diseñadores tendrán que descartarla
por una tecnologia más reciente (en algunas ocasiones menos utilizada). Un buen
ejemplo (pero, a menudo, confuso) de una interfaz ñexible es la propia llamada al
sistema socket(). Inicialmente puede resultar menos intuitiva pero se trata de una
interfaz muy flexible que ha sobrevivido varios años al desarrollo de redes así como
a muchas tecnologías de interconexión de redes.
La segunda regla del código transportable es el minimalismo, desarrollando la
interfaz tan sencilla como sea posible incrementando la probabilidad de
aprobación. El acopiamiento se corresponde con el número de conexiones
diferentes que tiene un sistema (o, desde el punto de vista de módulos, el número
de elementos de datos distintos que se pasan). El incremento del acoplamiento
supone un aumento de la dependencia y reducir la adaptabilidad. Para tener un
socket operativo, debe realizar diferentes llamadas a! sistema (hasta siete
llamadas). A pesar de proporcionar una adaptabilidad importante, realmente no
ayuda nada al programador. La única razón por la que sobreviven los sockets
durante tanto tiempo es su capacidad de modificación.
La tercera y última regla es la disposición. La disposición realiza algo más que la
modularización de un conjunto de datos; realmente coloca un único límite alrededor
de toda la tecnología. Los modelos de disposición de niveles OSI o TCP
constituyen un buen ejemplo de disposición en la tecnología. Cada nivel presenta
unas reglas y cada regla incluye unas interfaces. Las reglas siempre van a prever
un producto que puede adaptarse a las nuevas tecnologías.
265/512
familiarizado con algunos de estos fundamentos y otros puedan resultarle
novedosos. La programación orientada a objetos se centra en cuatro conceptos
fundamentales: abstracción, polimorfismo, herencia y eneapsulación. Puede
relacionar estos conceptos con el término A-PIE. (Algunos puristas de los objetos
afirman que realmente son sólo tres pilares, puesto que la abstracción es esencial
para el polimorfismo, herencia y encapsulación. La abstracción todavía resulta
importante a lo largo de la evolución de la informática )
Esta sección también presenta ejemplos de cada uno de los fundamentos. Lo
más interesante de la programación orientada a objetos es su capacidad para
aplicarse en cualquier lugar, con o sin un lenguaje orientado a objetos.
Traducción: puede utilizar la mayoría de estos conceptos en prácticamente
cualquier lenguaje de programación, incluyendo el ensamblador. Todos terminan
dirigiéndose a esta disciplina.
Encapsulación de la implementación
El primer fundamento de la tecnología orientada a objetos es la encapsulación.
Ocultar todos los detalles de la implementación actual en una interfaz de comandos
protegida constituye un requerimiento crítico para la reusabilidad. Con el mayor de
los respetos, los datos globales no se corresponden con programación orientada a
objetos. Su programa debe forzosamente vincular los datos a la implementación. Si
el programa rompe este vínculo, perderá reusabilidad y transportabiíidad.
OBJETOS GLOBALES
Los datos globales no constituyen una buena regla a seguir. Asegúrese que sólo puede
modificar estos datos el propietario. Cuestión: ¿Un objeto global se corresponde con
datos globales? Por ejemplo, suponga que tiene una instancia denominada MyAccount,
presente en una variable global. La respuesta es que un objeto global no se corresponde
con datos globales. Como cuestión de hecho, debe saber que la creación de objetos
globales es una práctica común y, a menudo, requerida.
La encapsulación oculta aquella información no necesaria para el exterior o
protege el acceso a ciertos datos. Normalmente, existen dos tipos diferentes de
información no necesaria para el mundo exterior: datos internos e implementación
interna. Los datos internos (todos los datos deberían ser internos) incluyen
información sobre la estructura y disposición de los mismos.
Por ejemplo, si tiene dos variables persistentes dentro de un objeto que crea un
contador, resulta lógico que no quiera que se conozcan detalles sobre la forma que
tiene el programa de almacenar los datos. Realmente, esta información no va
afectar el rendimiento. Ahora, si otra parte del programa accede a las variables
directamente, entonces aumenta el acoplamiento entre las dos secciones del
programa. Si, además, necesita modificar el funcionamiento de los contadores,
también deberá cambiar las referencias externas.
La implementación interna incluye todos los procedimientos o funciones locales
que admite el objeto. En el proceso de escritura de un programa, puede que resulte
necesario escribir un conjunto de rutinas de soporte para sólo simplificar la
codificación. Estas rutinas dividen la carga de procesamiento en diferentes pasos o
266/512
funciones sin ninguna relación con el mundo exterior.
Todas las interfaces con el mundo exterior deberían mostrar la relevancia de los
objetos o la responsabilidad que presentan. Normalmente, la industria denomina
estas funciones como servicios. El resto no debe estar visible sino encapsulado por
el objeto.
Herencia de métodos
Supongamos que quiere escribir un módulo igual que otro modificando un par de
detalles. Por ejemplo, un conmutador de una aplicación personal es sólo una
extensión de un botón que especifica un estado de pulsado o liberado. La herencia
a objetos permite este tipo de situaciones. Existe un viejo refrán que expresa este
pensamiento: "De tal palo, tal astilla".
Los objetos le permiten definir un objeto con un conjunto de atributos y
comportamientos (métodos). La herencia le permite reutilizar este trabajo
incorporando derivaciones o extensiones especificas a partir del trabajo existente.
Cuando el programa crea un objeto nuevo a partir de otro, la herencia permite al
nuevo objeto tener todos las características del padre (o svpcrclase). La Eigura
11.1 muestra la superclase Device que constituye el padTe de otras clases más
específicas.
267/512
Desde un punto de vista de programación podría decirse "SerialPort es un
CharDevice y un CharDevice es un Device". Device debería definir las interfaces
básicas para initializeQ. open(), read{), writeO y closef). CharDevice extendería la
interfaz de Device con ¡octl{). Por último, SerialPortf) define realmente la imple-
mentación de cada interfaz, aplicando las particularidades del puerto serie.
que Disk define esta interfaz. Para acceder a esta interfaz, es necesario revisar
el código:
BlockDevice *dev = new Disk();
dev->Address();
Especifique el acceso y las interfaces definiendo la interfaz como parte de la
declaración de variables. Por supuesto, puede hacer una conversión de la variable
al tipo apropiado, pero no todos los lenguajes admiten la seguridad de tipos
durante la conversión.
Polimorfismo de métodos
El cuarto y más importante fundamento de la tecnología orientada objetos es
similar a la abstracción y se denomina polimorfismo. Este fundamento no mejora la
potencia del paradigma de programación. Realmente, simplifica la interfaz definida
permitiendo recordar, de forma más sencilla, los nombres de los métodos.
El polimorfismo utiliza el nombre y los parámetros de una interfaz para
identificar, de forma única, el comportamiento o método. Por ejemplo, podría tener
dos métodos denominados PlayVideo() y PlayMIDK). Con el polimorfismo, tendría
un sólo nombre, PlayO, para los dos métodos. Los parámetros van a definir la
utilización del correspondiente método, Play(Video) y Play(MIDI).
Los enlazadores que utilizan los compiladores no permiten todavía controlar la
correspondencia entre el nombre y los parámetros de un método, de forma que el
compilador pueda proporcionar el adecuado. La implementación de estas
características en un lenguaje supone modificar el nombre del método para incluir
su nombre y el tipo de parámetro. Esto se denomina ajuste de nombre. EL nombre
resultante sólo se parece parcialmente al nombre original del método.
La clase o el objeto
Las primeras dos características, a menudo, confunden a los programadores. La
clase se corresponde con la descripción del objeto y el objeto con aquello que está.
268/512
Abstracción de datos
El tercer fundamento, abstracción de datos, es similar al modelo de
programación abstracta descrito anteriormente, aunque extiende un poco más este
concepto. La programación abstracta permite al programador centrarse en la
función ignorando el significado de los datos. Esto permitió desarrollar
implementaciones estándar de estructuras básicas de la informática, como pilas o
diccionarios.
La programación orientada a objetos extiende este concepto utilizando su propia
filosofía. Centrándonos en la encapsulación, la programación orientada a objetos le
permite trabajar dentro de la esfera que rodea al módulo que necesita escribir y
perfeccionar. La abstracción trabaja con las funciones y operaciones desde un
punto de vista genérico.
La herencia ha supuesto un tremendo empujón para la abstracción. De hecho, la
herencia y la abstracción le permiten crear objetos generalizados comportándose
como marcas distintivas que realmente no existen. Utilizando un ejemplo de la vida
misma, un chihuahua es un perro y un perro es un mamífero. Ni el perro ni el
mamífero existen como tales; sin embargo proporcionan una marca distintiva para
poder distinguir el perro de un gato o árbol. Un objeto abstracto identifica las
responsabilidades principales del objeto permitiendo dejar algunas sin implementar.
A diferencia de los objetos abstractos, los objetos normales deben definir e
implementar todos los métodos restantes. En el ejemplo Device mostrado
anteriormente, CharDevive y Device no son objetos reales sino que constituyen
realmente abstracciones proporcionando un proyecto previo a las interfaces y
métodos específicos. Los métodos constituyen las interfaces básicas mencionadas
anteriormente. Tenga en cuenta, que sería posible programar métodos por omisión
para un conjunto de interfaces y esto no reduciría la potencia de la abstracción. De
hecho, si se realiza de forma apropiada, los métodos por omisión permitirán
mejorar los objetos heredados.
El poder de abstracción le permite llamar a una única interfaz (por ejemplo,
read() del ejemplo anterior) sin conocer los detalles de implementación. La
definición de la jerarquía de Device (Figura 11.1) incluye dos objetos, Disk y
Network. La abstracción le permite crear cualquiera de los objetos no abstractos ya
tratados en la versión abstracta. Por ejemplo, puede realizar lo siguiente:
/*********************************************************/
/* Crea un objeto Disk y le coloca en la referencia */
/* abstracta Device */
/*********************************************************/
Device *dev = new Disk();
dev->Initialize();
Aunque Device no define el método ln¡t¡alize() y sólo aporta la interfaz, el
lenguaje es lo suficientemente listo como para llamar a la implementación de Disk.
Tenga en cuenta que la interfaz AddressQ no está visible para Device, a pesar de
269/512
alojado o asignado. La correspondencia podría ser un anteproyecto (la clase) y la
casa (el objeto) construida a partir del anteproyecto.
Un programador con experiencia en C conoce perfectamente el elemento del
lenguaje struct que describe el contenido de una estructura de datos compuesta.
De forma similar, la clase describe ios elementos que tendrá el objeto cuando se
crea. La creación de un objeto a partir de una clase se corresponde con la creación
de una variable utilizando la etiqueta de la estructura definida.
NOTA DE USO
Hasta este momento, este capítulo ha utilizado el término objeto para hacer referencia a
la clase y al objeto. Esto se ha realizado de forma intencionada para evitar la confusión.
La jerarquía de la herencia incluye dos tipos especiales de clases: la superclase
y la subclase. La superclase es la clase que aparece en la raíz del árbol de
herencia y, normalmente, es una clase abstracta. Cada clase que deriva sus
métodos a partir de alguna clase padre se denomina subclase.
Atributos
Los campos individuales de una estructura en C son similares a los atributos de
una clase. Normalmente, los atributos constituyen todos los campos públicos y
ocultos de la clase. Los elementos de una clase que están visibles al mundo
exterior se denominan públicos y forman parte de la interfaz. Normalmente, resulta
adecuado ocultar (o encapsular) todos los atributos de una clase.
Propiedades
Algunos atributos pueden estar visibles a la interfaz. Estos atributos constituyen
las propiedades de la clase. No utilice esta posibilidad, a menos que quiera
exponer parte de su clase al mundo exterior. Normalmente, las propiedades
constituyen definiciones de sólo lectura o están bajo la protección de Get() y Set{).
Nunca debe hacer pública una variable.
PROGRAMACION CRUD
Los métodos Get() y Set() no son realmente métodos. Forman parte de un conjunto
especial de funciones que se encargan de crear, leer, actualizar o eliminar información
(CRUD). Los métodos tienen sus responsabilidades: realizan algo. CRUD no ofrece
ningún comportamiento adicional y siempre se asumen. Por tanto, no se consideran
dentro dei conjunto de todos los métodos.
Las subclases heredan las propiedades y atributos de sus clases padre.
Métodos
Los métodos son las responsabilidades o comportamientos de los objetos.
Mediante el polimorfismo, puede anular los métodos heredados, añadir nuevos
métodos o mejorar las interfaces existentes. Cuando anula un método heredado,
resulta habitual llamar al método original de la clase padre. Esto ayuda al padre a
iniciar sus atributos y comportamientos o métodos privados.
Los atributos, propiedades y métodos constituyen lo que se denomina
270/512
elementos de la clase.
Derechos de acceso
Los derechos de acceso aseguran que el programa accede a la parte de la
interfaz sobre la que tiene permiso. Tanto Java como C++ ofrecen tres niveles de
acceso: prívate, protected y public.
• prívate. Aisla los elementos a todos excepto al propietario.
• protected. Permite al propietario y subclases acceder al elemento.
• public. Permite a todos el acceso al elemento.
Puede eliminar las restricciones de las reglas de acceso configurando relaciones
de confianza entre las clases.
Relaciones
Los objetos interactúan entre sí de tres formas diferentes. Una de ellas es la
herencia que se ha descrito anteriormente en este capítulo. Puede recordar estas
relaciones con tres claves: es-un/a, tiene-un/a y utiliza. Los profesionales utilizan
estas claves durante el análisis y diseño de los objetos y tienen su propia
representación gráfica (esta representación está fuera del ámbito de este libro}.
• es-un/a. Herencia: una clase deriva sus métodos de otra.
• tiene-un/a. Contiene: una clase es propietaria de otra.
• utiliza. Trabaja: una clase trabaja o utiliza otra.
Todos las interacciones entre objetos incluyen exclusivamente estas claves.
Tenga cuidado con las interacciones que permite entre sus objetos: dos objetos no
deberían tener más de una interacción. Por ejemplo, el Objeto A no debería
contener y utilizar el Objeto B. Si un análisis muestra este tipo de relación, es señal
de que existe un problema.
La dualidad de las relaciones especifica un análisis incorrecto. Los objetos
deben tener una sola responsabilidad. Un objeto que tenga más de una
responsabilidad distinta puede presentar esta dualidad. La solución es sencilla,
dividir el objeto en más de uno.
271/512
Plantillas
La abstracción de clases y el polimorfismo utilizan un conjunto de funciones
aplicándolas, de forma apropiada, a los datos. Imagine la posibilidad de extender
este concepto permitiendo crear una clase que genera su propia versión sobre los
datos nuevos. Las plantillas (disponibles en C++) le permiten definir una
responsabilidad genérica sin considerar las características de los datos.
Una plantilla establece el método y la responsabilidad. Puede crear una clase
nueva en función de esta plantilla proporcionando un tipo específico. Un uso
habitual de las plantillas es la creación de contenedores genéricos de objetos. Por
ejemplo, una cola o pila tiene un método (introducir y obtener los datos ordenados)
independiente del tipo de dato.
La creación de una plantilla a partir de un indicio resulta obsoleto y la sintaxis es
extraña. La forma más sencilla de crear una plantilla es generar una instancia de la
clase y generalizaría. Después de comprobarla, debe ir al comienzo de la clase
para incorporar la notación relativa a la plantilla.
Persistencia
La mayoría de las veces, los programadores consideran que los objetos viven
durante la ejecución del programa. No obstante, en algunas ocasiones los
programas incluyen parámetros que ayudan a definir sus métodos. Normalmente,
estos métodos se ajustan a las necesidades específicas del usuario. Utilice un
archivo cuando necesite guardar este tipo de información mientras no se esté
ejecutando el programa.
La persistencia le permite abstraer este método. Cuando el programa se inicia,
carga los parámetros y continua donde el usuario se quedó. La persistencia resulta
de mucha ayuda en la recuperación de un sistema. Puede incluso restablecer
conexiones pérdidas por un fallo del sistema.
Generación de flujos
Otra extensión de los objetos es la generación de flujos. Imagine la posibilidad
de indicarle al objeto que se empaquete y se guarde o envíe a alguna otra parte.
Cuando llegue o lo cargue un programa, automáticamente se abre indicándole lo
que puede realizar. Esto resulta muy útil en la persistencia y en la programación
distribuida.
La generación de flujos forma parte de Java. Puede llevar a cabo algunas
técnicas de generación de flujos en C++, pero no es posible la identificación de
objetos (denominada en Java introspección).
Sobrecarga
Muchas de las extensiones se centran en un mismo fundamento. Por ejemplo, la
sobrecarga de operadores (permitida en C++) es una extensión del polimorfismo.
Algunos programadores, de forma equivocada, comentan que el polimorfismo
engloba la sobrecarga de operadores. Esto realmente no es así; es simplemente
272/512
una extensión. Java no admite la sobrecarga, y, sin embargo, es considerado un
lenguaje orientado a objetos.
La sobrecarga de operadores le permite ampliar (no redefinir) el significado de
los operadores internos. Utilizando el polimorfismo, puede usar de la forma que
quiera estos operadores siempre y cuando constituyan nombre de métodos. No
obstante, la extensión debe incluir restricciones sobre la nueva definición.
Interfaces
Una extensión realmente útil permite al objeto mantener su identidad mientras
se encarga de ofrecer servicios a otros. Un problema que tiene C++ es que si la
Clase A utiliza la Clase B, entonces la Ciase A debe conocer la Clase B cuando el
programador diseña la Clase A, o la Clase B debe heredar de una clase que
conoce la Clase A. Es posible que se generen problemas de herencia realmente
peligrosos, cuando la Clase B tiene que admitir diferentes interfaces.
Las clases que deben tener las interfaces más flexibles son aquellas que tienen
el mayor número de dependencias. Java y otros lenguajes permiten que la clase
nueva admita una interfaz determinada. Esta declaración no incorpora mucha
sobrecarga sobre la clase nueva.
Eventos y excepciones
La última extensión habitual de los objetos permite al programador preocuparse
más de los aspectos del programa y obviar un poco los detalles de recuperación de
errores. Si ha programado mucho en C++, es bastante probable que haya
trabajado con el problema persistente de la resolución de errores. Cuando se
produce un error (interno o externo), el lugar más apropiado para controlar dicho
error no es precisamente el lugar donde se ha generado.
Las excepciones le permiten definir cuándo y cómo quiere controlar los errores
particulares sin tener que estar ejecutando el controlador de errores.
Lamentablemente, todos estos lenguajes no admiten una utilidad de resumen,
provocando que si el controlador detecta un error, el programa no puede continuar
a partir de donde se quedó.
273/512
Al igual que las excepciones, los eventos constituyen situaciones asincronas
internas o externas que provocan una redirección en el programa. Aparecen
normalmente en los programas CU1 y la programación basada en eventos
especifica etapas que permiten responder a los eventos, al tiempo de espera para
su generación y al procesamiento que debe llevar a cabo entre cada evento.
Formatos especiales
La tecnología orientada a objetos tiene los mismos problemas de datos y
diseños planteados por otras tecnologías. No todos los problemas relacionados con
los datos se ajustan a las características del objeto. Estos formatos degenerados
no constituyen necesariamente diseños inadecuados, pero la mayoría de ellos
indican una ausencia importante de entendimiento entre los objetos.
Esta sección define los tipos de clases degeneradas, aportando un ejemplo y
ofreciendo sugerencias muy interesantes sobre cómo evitarlas en la media de lo
posible.
Registro/Estructura
Una regla ya antigua establece que una clase sin métodos es simplemente un
registro. Algunos registros son necesarios. Si no puede pensar desde un punto de
vista de responsabilidad, es posible que no tenga ninguna. Por ejemplo, las bases
de datos sólo almacenan datos; no almacenan métodos. Naturalmente, no existen
objetos sin métodos. En realidad, cualquier cosa puede realizar algo.
Un registro o estructura es sólo una colección de datos. No tiene métodos más
allá de los métodos CRUD. Puede utilizar algunas de estas clases degeneradas
aunque no es algo que resulte muy habitual. Si lo hace, intente comprobar estas
clases planteando las siguientes cuestiones:
• ¿Está analizando una parte de un sistema grande? Las bases de datos son
ejemplos de grandes colecciones. Cada relación tiene que mantener unas
reglas de negocio que garanticen la integridad de los datos. Si examina sólo
una parte del mapa de relaciones, puede concluir que no existe una función a
nivel global. La realidad puede ser la pérdida de la integridad de los datos.
Si ha respondido "Sí" a todas estas cuestiones, no se preocupe y cree el
registro. Asegúrese de encapsularlo con CURD.
274/512
Colección de funciones
La colección de funciones es lo contrario a la colección de datos o registro. No
tiene ningún dato pero está constituida por bastantes funciones. Un ejemplo ideal
de colección es la biblioteca matemática. Podría pensar que float está relacionado
con las funciones de la biblioteca matemática. Realmente, existe esta relación pero
no están acoplados. Para crear una clase, de forma apropiada, debe existir un
perfecto acoplamiento entre los datos y los métodos. Si no existe, no podrá tener
un objeto. La función exp() está relacionada pero no acoplada con el tipo de dato
punto flotante (todavía puede tener un float sin disponer de la función exp()).
A diferencia de las colecciones de datos, las colecciones de funciones existen
de forma natural. Puede localizar estas colecciones en cualquier libro de física o
cálculo. Realmente, existen con una necesidad sobre los datos.
Si deduce una colección de funciones, compruebe su validez mediante las
siguientes preguntas:
• ¿Tiene acoplamientos "saludables" entre las clases? Al igual que las tareas, si
dos o más clases dependen entre sí, pueden generar un bloqueo o
dependencia cíclica. Estas dependencias indican que los datos o funciones
están en la clase incorrecta.
• ¿ Tienen las funciones y los datos los propietarios adecuados? Si los datos y
las funciones no tienen una dependencia clara, o puede particionarlos de forma
sencilla, entonces es posible que no tenga especificadas las responsabilidades
en los lugares adecuados.
275/512
todos aquellos que quieran plantearse un reto). Los lenguajes orientados a objetos
clásicos son SmallTalk y Eiffel. Lisp es un lenguaje que admitía una filosofía
orientada objetos antes de que se produjera el apogeo de los objetos. Los
siguientes dos capítulos presentan la API de Java y C++ para los sockets.
276/512
Cómo incluir objetos en los lenguajes no
orientados a objetos
La tecnología orientada a objetos es muy recomendable cuando decida
programar utilizando un lenguaje de programación que admita objetos {como C++ o
Java). Puede obtener un gran número de ventajas a partir de las extensiones y
fundamentos y, utilizada de forma correcta, hace que su código sea más
reufilizable. No obstante, no todos los lenguajes admiten o permiten orientación a
objetos. ¿Qué puede hacer ante esta situación?
Lo crea o no, la mayoría de las características y fundamentos de los objetos
(excepto la herencia) se pueden implantar en cualquier lenguaje de programación,
incluyendo COBOL y ensamblador. Se trata sólo de seguir una disciplina y un estilo
particular de programación. Desafortunadamente, casi todas las extensiones se
pierden.
Los lenguajes actuales incluyen estas posibilidades puesto que la mayoría de
las características de la orientación a objetos se basan en tecnologías previas
(abstracción, programación modular y demás). A continuación, se especifica cómo
obtener tístas ventajas:
• En caps u/a don. Asegúrese de que todas las interfaces con sus módulos se
corresponden con procedimientos o funciones. Confíe en que los
programadores no destrocen su código y utilice las variables y otros elementos
privados. Además, asigne a los métodos nombres tales como
<NombreMódulo>_<NombreProcedimiento>(). Sin embargo, algunos
compiladores incluyen algunas limitaciones sobre la longitud del nombre y, por
tanto, sería conveniente que comprobara este aspecto.
277/512
parámetros.
• Plantillas. No disponibles.
• Persistencia.Puede realizar un seguimiento del estado y cargarlo, de forma
manual, cada vez que se inicie el programa.
278/512
Capitulo 12
En este capítulo
Exploración de diferentes sockets Java
Conexión a través de E/S
Configuración del socket Java
Multitarea de los programas
Limitaciones de la implementación
Resumen: programación de redes de tipo Java
279/512
Java es un lenguaje muy potente, simple y divertido. Si sabe programar en C/C+
+ y conoce la tecnología de objetos, ya puede comenzar a programar en Java. Las
bibliotecas de Java son expansivas v cubren muchas operaciones. Es fácil que
llegue a perderse con todas las clases disponibles. Aunque aprenda Java y se
familiarice por completo con las bibliotecas de clases, puede que tenga que
mantener a mano los documentos de ]ava Developers Kit ÍJDK) y algunas
referencias impresas.
En el capítulo anterior vio el propósito de la tecnología de objetos y la forma de
diseñar con ella. Este capítulo explica una implementación de socket Java; el
capítulo siguiente define estructuras de socket personalizadas en C++. Ya que un
tema sobre Java resultaría excesivo, en este capítulo se supone que ya conoce
Java y desea adentrarse en la programación de red. Algunos de los ejemplos del
sitio web contienen el código del programa Abstract Windowing Toolkit (AWT). Las
interfaces gráficas del usuario (GUI) y los eventos quedan fuera del ámbito de este
capítulo.
Las siguientes secciones tratan las clases que ofrece Java para los sockets,
entrada-salida relevante y ejecución entrelazada. Este capítulo también discute
algunos de los métodos más novedosos que soportan la configuración de sockets
(como se vio en el Capítulo y, "Cómo romper las barreras del rendimiento").
280/512
Socket s = new Socket("locainost", 9999);
Eso es. No tiene que hacer nada más que conectarse a un servidor. Cuando la
JVM crea el objeto, asigna un número de puerto local, realiza la conversión del
orden de bytes de red y conecta con el servidor. Incluso puede especificar la
interfaz de red local y el puerto a utilizar con las siguientes extensiones:
Socket s = new Socket(String Hostname, int PortNum,
InetAddress localAddr, mt localPort);
Socket s = new Socket<lnetMaress Mür, int PortNum,
InetAddress localAddr, int localPort);
La clase InetAddress convierte el nombre del host o la dirección IP en la
dirección binaria. La mayoría de las veces, no se debería utilizar directamente el
objeto InetAddress en la llamada a menos que se comience con él. En su lugar,
suele utilizarse un constructor que pasa Hostname. La mayoría de los programas
se conectan mediante la información que un usuario proporciona al programa- Esta
suele consistir en el nombre del host o la dirección IP.
SOPORTE IPV4/IPV6 DE JAVA
En la actualidad. Java soporta IPv4 directamente y, de acuerdo con el proyecto Merlin
(véase java.sun.com), debería estar disponible el soporte IPv6 cuando el sistema
operativo lo soporte. Las clases como InetAddress, que gestionan los nombres y la
conversión, deberían adaptarse fácilmente a otros protocolos. Sin embargo, debe tener
en cuenta que puede que la versión que tiene no lo soporte todavía. Algunas interfaces,
como lnetAddress.getHostAddress(), necesitan cambios para soportar la dirección
extendida y el formato nuevo.
Envío/Recepción de mensajes
A pesar de haber creado el socket, el programa no puede enviar o recibir datos
directamente a través de esta clase. Obtenga el InputStream o el OutputStream
desde la instancia de clase:
InputStream i = s.getlnput3tream();
OutputStream o = s.getOutpjtStream();
Utilizando estos métodos, puede leer o escribir arrays de bytes:
byte[] buffer = new byte[1024];
int bytes_read = i.read(buffer); //--Lee bloque del socket
o.write(buffer); //--Transmite el array de bytes a través del socket
Incluso se puede utilizar InputStream para determinar si los datos esperan en
los buffers del núcleo con InputStream.availableQ. Este método devuelve el
número de bytes que puede leer el programa sin bloquearse.
if (i.availablel) > 1 0 0 )//--No lee a menos que espere 1 0 0 bytes
bytes = i.read(buffer);
281/512
Cuando se haya completado, se puede cerrar el socket (así como todos los
canales de E/S) con una simple llamada a Socket.close():
/ / - -Limpia
s.close();
Java utiliza new para todas las llamadas. La JVM pasa todo (salvo los escalares) como
punteros y éstos nunca se exponen en su programa. Todo debe apuntar a algún dato o a
un valor nulo (null). La JVM llama a un proceso recolector de basura cuando empiezan a
escasear los recursos. Todas las referencias a memoria perdidas durante este proceso se
almacenan en una zona de memoria disponible. Esto significa que nunca tiene que liberar
la memoria que reserve. Sin embargo, no se aplica a sockets. Se deben cerrar
manualmente todos los sockets. Si el programa no cierra un socket muerto, puede salirse
finalmente de los descriptores de archivo disponibles. El comportamiento del programa
resultante puede no ser completamente imprevisible.
Con estas llamadas y objetos, puede crear un cliente de eco como se muestra
en el Listado 12.1.
Listado 12.1 Ejemplo de cliente de eco sencillo en Java
//************************************
// Extracto de SimpleEchoClient
//************************************
Socket s = new Socket("127.0.0.1 ", 9999); // Crea el socket
InputStream i = s.getlnputStream(); // Obtiene el flujo de entrada
OutputStream o = s. getOutputStream(); // Obtiene el flujo de salida
String str;
do
{
byte[] line = new Cyte[l00];
System.in.read(line); // Lee desde el teclado
o.write(lineI; // Envía el mensaje
i.read(line); // Vuelve a leer
str = new String(line); // Convierte a cadena
System.out .println(str". trim( ]); // Imprime el mensaje
}
while ( !str.trim(|.equals("bye"| ];
s.close(); // Cierra la conexión
El Listado 12.1 muestra un sencillo bucle de lectura y envío. Aunque no es
seguro que funcione, al menos sirve de ejemplo para apreciar la funcionalidad. Si
intenta compilar este retazo, el compilador se queja por no considerar ciertas
282/512
excepciones. Todas las operaciones de red deben capturar excepciones. Para
hacer esto, introduzca el archivo fuente del Listado 12.1 en el lugar indicado dentro
del siguiente código:
try
{
// <--Inserte aqui todo el archivo fuente 1
catch (Exception err)
{
System.err.println(err);
}
283/512
//--------------------.....................
try
{
ServerSocket s = neiv ServerSocket (9999); / / Crea el servidor
while (true)
{
while ( lstr.trim().equals("bye") );
c.close(); I I Cierra la conexión
}
}
Implementación de mensajeros
La sección anterior cubre los protocolos de flujo (o TCP). Para las
comunicaciones basadas en conexiones, los flujos deberían funcionar de forma
apropiada. Sin embargo, pueden prestarse algunos mensajes a un protocolo
basado en mensajes, como los datagramas (UDP). El paquete de red Java dispone
de un par de clases que permiten crear sockets y enviar mensajes. Estas son
DatagramSocket y MulticastSocket.
La clase DatagramSocket permite crear un socket con una única llamada:
DatagramSocket s - new DatagramSocket();
284/512
Al igual que para la clase 5ocket, se puede definir manualmente el puerto local y
la interfaz de red:
DatagramSocket s = new DatagramSocket(int localPort);
DatagramSocket s = new DatagramSocket(int localPort,
InetAddress localAddr);
Una vez creado el socket datagrama, se pueden enviar y recibir mensajes.
Estos mensajes no interactúan directamente con el paquete de E/S Java. En su
lugar, se deben usar arrays de bytes. El paquete de red incluye la clase
DatagramPacket para administrar los datos y la información de destino de cada
mensaje con:
DatagramPacket d = new DatagramPacket(byte[1 buf, int len);
DatagramPacket d = new DatagramPacket(byte[j buf, int len
InetAddress Addr, int port);
Utilice el primer constructor para crear un objeto de lectura. El segundo
constructor es útil para crear mensajes que se intenten enviar y añade una
dirección-puerto de destino, buf hace referencia a un array reservado de bytes y len
se refiere a la longitud del array o la longitud de los datos válidos.
Para demostrar el uso de DatagramSocket y DatagramPacket, considere un
ejemplo peer de origen y peer de destino. Un programa peer de origen envía un
mensaje al peer de destino. Esto es similar a la programación de datagramas
demostrada en el Capítulo 4, "Envío de mensajes entre puntos". El Listado 1 2 .3
muestra un ejemplo de un peer de origen utilizando datagramas.
Listado 12.3 Crea un socket datagrama y envía el mensaje
//************************
/ / Extracto de SimplePeerSource
//----------------------------------
285/512
parecer un error, todos los sockets permiten enviar algo y a continuación cerrar
(incluso antes de que el mensaje abandone su iiosf local). La cola de mensajes del
socket permanece activa hasta que hayan salido todos los mensajes.
El peer de destino suele hacer lo mismo que el cliente, sólo que lee el mensaje
en lugar de enviarlo. El Listado 12.4 muestra un peer de destino sencillo.
Listado 12.4 Recibe el mensaje del datagrama y lo muestra
//****************************
/ / Extracto de SimplePeerDestination
I I ---------------------------------------
DatagramSocket s = new DatagramSocket(9998); / / Crea el socket
byte[] line = new byte[100];
DatagramPacket pkt = / / Crea un buffer para los mensajes de entrada
new DatagramPacket(line, line.length);
s.receive(pkt); / / Obtiene el mensaje
String msg = new String(pkt.getData()); / / Convierte los datos
System.out.print( " Recibió el mensaje: "+msg);
s.close(); // Cierra la conexión
Java soporta el socket datagrama hasta el punto de que se pueden crear
sockets y enviar mensajes, pero sólo eso. Si desea una capacidad de E/S más
robusta, Java no la puede ofrecer directamente. En su lugar, se pueden utilizar las
clases de E/S de memoria para construir los mensajes en un array (descrito en la
sección "Clasificación de las clases de E/S", más adelante en este capítulo).
Las ventajas de enviar mensajes directos en lugar de un conducto abierto son
esencialmente las mismas que ofrece UDP. (Para más detalles sobre estas
diferencias, por favor véase el Capítulo 3, "Distintos tipos de paquetes de Internet"
y el Capítulo 4.) Java proporciona un wrapper sencillo sobre los protocolos
datagrama.
286/512
multidifusión permite que a un programa se asocie a una dirección IP especial
reservada para grupos de multidifusión. Todos los programas de este grupo
obtienen mensajes enviados a la dirección. Para crear un MulticastSocket, se
puede utilizar uno de los dos siguientes constructores:
MulticastSocket ms = new MulticastSocket();
MulticastSocket ms = new MulticastSocket(int localPort);
Aunque puede crear un socket de multidifusión sin un puerto específico, su
programa debe seleccionar un puerto antes de poder esperar ningún mensaje. La
razón es que todos los datagramas de multidifusión utilizan el puerto para filtrar los
mensajes no deseados.
Una vez creado el socket, éste actúa exactamente igual que un
DatagramSocket. Se pueden enviar y recibir mensajes directos. Para obtener la
funcionalidad de multidifusión extendida, necesita un enlace con un grupo. IPV4
define un rango de direcciones de multidifusión legales: 224.0.0.0-239-255-255-
255.
MulticastSocket iris = new MulticastSocket(16900);
ms.ioinGroup(InetAddress.getByName("224.0.0.1"));
ms.joinGroup(InetAddress.getByName("228.58.120.11”));
A partir de este momento, su socket obtiene mensajes enviados a las
direcciones 224.0.0.1:16900 y 228.58.120.11:16900. Como en otros datagramas, el
programa puede tomar cada mensaje y responder directamente al emisor o bien
responder al grupo.
No tiene que realizar un enlace con un grupo para enviar mensajes. Puesto que
el socket de multidifusión continúa siendo un socket UDP, puede enviar mensajes
al grupo sin realizar realmente un enlace con el grupo. Para recibir mensajes de
multidifusión, sin embargo, el socket debe enlazarse al grupo.
OBJETOS Y PLANIFICACIÓN PARA EL FUTURO
IPv6 soporta una multidifusión basada en UDP e intenta soportar una multidifusión
basada en TCP. Esta extensión elimina la falta de fiabilidad del protocolo UDP. Pero
puesto que MulticastSocket deriva de DatagramSocket, la jerarquía actual no puede
soportar los planes IPv6 para los datagramas fiables. Este diseño es un buen ejemplo de
falta de planificación para el futuro. Cuando diseñe y escriba algún marco de trabajo de
objetos, intente mirar un poco hacia adelante. Ignorar algunos aspectos que necesitan
actualizarse o extenderse es más fácil que fijar una jerarquía. MulticastSocket no puede
soportar un socket T/TCP IPv6 simplemente porque hereda de un datagrama.
El Listado 12.5 muestra un ejemplo de cómo crear y configurar un socket de
multidifusión.
Listado 12.5 Crea un socketde multidifusión y le asigna el puerto #16900, se enlaza
al grupo y espera mensajes
//*******************************
/ / Extracto de SimpleMultisastDestination
/ / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
287/512
MulticastSocket ms = new MulticastSocket(16900); / / Nuevo socket
ms.joinGroup(lnetAddress.getByName( "224.0.0.1" ) ) ; / / Se une a un grupo
String msg;
do
{
while ( !msg.trim().equalsf"fin") ) ;
ms.close(); / / Cierra la conexión
El Listado 12.5 crea un socket para la multidifusión y solicita un puerto a través
del cual pueda obtener los paquetes. Una vez que se enlaza al grupo 224.0.0.1
(host local), crea un paquete para los siguientes paquetes.
288/512
dispositivos hardware; en su lugar, se utilizan arrays en RAM como zonas
virtuales de E/S. Esto ayuda al análisis de cadenas o incluso a la creación de
un buffer para los datagramas. Entre los ejemplos se encuentran Byte Array
InputStream. Byte Array OutputStream, CharArrayReader, CharArrayWriter,
5trÍngReader y 5tringWriter.
289/512
sólo ofrece dos clases de E/S: OutputStream e InputStream. Estas proporcionan
estructuras mucho más elementales en la lectura y escritura de arrays de bytes.
Para obtener las diferentes características, necesita convertir estas clases.
Suponga que desea leer cadenas, no arravs de bytes. BufferedReader dispone
de una buena interfaz String. Necesita convertir de la clase abstracta InputStream a
la clase abstracta Reader. La clase InputStreamReader actúa como intermediaria de
esta conversión:
Socket s = new Socket(host, port);
InputStream is = s.getInputStream();
inputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String l = br.readLine();
También puede utilizar la siguiente abreviatura:
BufferReader br = new BufferReader(new InputStreamReader(
s.getInputStream!)));
El envío de una cadena (sin conversión real de la cadena) a través de un canal
es un poco más fácil:
String msg = new String(
"<html><body>Bienvenido a mi sitio web Java</body></html>" |;
SocketServer ss = new SocketServer(9999);
Socket s = ss.accept();
PrintWriter pw = new PrintWriter(s.getOutputStreamj), true);
pw.println(msg);
s.close();
El primer parámetro proporciona a la instancia nueva de PríntWriter una
referencia al flujo del socket. El segundo parámetro indica al ob|eto que se limpie
automáticamente siempre que el programa llame a println(). Normalmente, la E/S
en PríntWriter almacena los datos en buf/erhasta que haya suficientes para
minimizar el gasto de ancho de banda. La especificación de la limpieza automática
indica que se desea especificar cuándo se envía un buffer.
Mediante estas clases de E/S, puede crear mensajes de E/S que pueden
interpretar los programas que no soportan Java. Sin embargo, aún puede enviar-
recibir objetos a-desde otro programa Java mediante las clases
ObjectOutputStream/ObjectlnputStream. Puede transmitir cualquier objeto que
implemente la interfaz Serializable.
String msg = new String("Test");
Socket s = new Socket(hostname, port);
ObjectOutputStream oos = new ObjectOutputStream(
290/512
s.getOutputStreaml));
oos.writeObject(msg);
El fin de la recepción puede tomar el mensaje como un objeto v convertirlo a un
tipo conocido:
Socket s = ss.accept();
ObjectlnputStream ois = new ObjectInputStream(
s.getInputStream());
String newMsg = (String)ois.readObject();
Si el mensaje no coincide con el tipo al que se está convirtiendo, el intérprete
lanza una excepción ClassCastException. De esta forma, si el programa obtiene
una clase que no preveía, puede utilizar la reflexión de clases para obtener el tipo
correcto. Si el programa sigue sin conocer el tipo, debe profundizar más en la clase
y obtener los métodos, herencia e implementaciones. Aunque complique el código,
el uso de este enfoque puede ofrecer un producto más extensible.
291/512
Los siguientes métodos determinan si el soeAet debería mantenerse una vez
cerrado y con qué tamaño. Estas llamadas exponen la opción SO_LINGER IP del
socket.
getSoLinger()
setSoLinger(boolean on, int linger)
El siguiente código activa y desactiva el algoritmo de Nagle y espera a que la
red envíe un paquete antes de enviar el siguiente. Estas llamadas exponen la
opción TCP.NODELAY del socket.
getTcpNoDelay()
setTcpNoDelay(boolean on)
292/512
simplemente lo siguiente:
public class TestThread extends Thread
{
public void run()
{
/** aquí se ejecuta el thread **/
}
Cada vez que desee crear un thread, llame a Start(). Como su nombre indica,
todos los threads comparten todos los datos. Para crear un espacio de trabajo
individual para un thread, puede crear un objeto nuevo:
Thread t = new TestThread();
t.start();
JUEGUE LIMPIO CON LOS THREADS JAVA
De la misma forma que las tareas pueden tomar posesión de la computadora, los threads
tava pueden dominar fácilmente el tiempo de CPU (esto es más probable en Windows
que en Linux/UNIX). Asegúrese de seguir las directrices marcadas en el Capitulo 7,
"División de la carga: multitarea", de forma que otras tareas tengan la posibilidad de
ejecutarse.
Cualquier parte de su programa, incluyendo los controladores de eventos y
excepciones, pueden crear e iniciar un thread. Sin embargo, recuerde que desea
mantener la referencia sobre cualquier objeto creado. Por otro lado, cuando ocurre
una recolección de basura aleatoria, su thread colgante puede terminar
repentinamente.
293/512
programa sólo cambia ligeramente.
public class TestFrameThread extends Frame
implements Runnable
{
}
El texto en negrita indica los dos cambios a realizar sobre el fuente. El primero
es la herencia de Frame y la implementación de Runnable. Esto declara la relación
que necesitamos para obtener el funcionamiento correcto del programa. El
segundo cambio es la creación de un thread. La clase Thread tiene un constructor
adicional que acepta cualquier objeto que implemente la interfaz Runnable y genera
un objeto Thread a partir de ella. La llamada a start() hace lo mismo que si se
derivara la clase desde Thread.
Todos los threads que se ejecutan en una clase pueden realmente no tener
relación con la funcionalidad básica de la clase. Realmente no tienen ninguna
restricción. Pero si encuentra distintos grupos de responsabilidades, asegúrese de
que en realidad no tiene dos clases diferentes. Dos formas de determinar si
realmente tiene clases distintas incluyen la cantidad que comparten los threads
(padre e hijo) independientes y si el thread hijo puede existir sin los datos y las
características del padre.
La creación de clases independientes para cada responsabilidad principal
constituye un diseño de objetos correcto y realmente ayuda en un entorno de
multiprogramación. Si necesita acceder a los métodos o recursos de cada objeto,
cree méiodos de control de propiedades (set{)/get()). Centralizando todo el control
de atributos a través de métodos, realmente construye la encapsulación y hace
más fácil la sincronización de threads.
Sincronización de métodos
Una de las principales ventajas de los threads es su capacidad intrínseca para
compartir recursos. Esto provoca los mismos problemas de exclusión mutua que se
describen en el Capítulo 7. Java resuelve este problema utilizando métodos
sincronizados.
294/512
El método sincronizado reemplaza a los semáforos utilizados en pthreads. La
declaración es:
public synchronized changeSomething()
{
La palabra clave synchronized fuerza que todas las entradas del método ocurran
de una en una. Antes de que otro thread pueda introducir el método, el primero
tiene que completar su llamada.
A veces se necesita un poco más de control. Por ejemplo, suponga que tiene
tres threads (un padre y dos hijos) que controlan las entradas y salidas de buffers a
través de un socket. Puede que no quiera enviar un buffer hasta que no esté lleno y
los threads hijos controlan los buffers de salida. Un método sincronizado obtiene el
contrnl de un buffer, pero eso no es suficiente. El método podría devolver un valor
erróneo, forzando al hijo a volver a intentar la llamada. De forma alternativa, el
método podría guardar el recurso y forzar al thread a ceder temporalmente el
control.
Retrocediendo un momento, cuando los threads compiten por un recurso
controlado, se introducen en una cola de espera. Cuando el propietario del recurso
actual sale del método sincronizado, el siguiente thread de la cola obtiene el control
del recurso. Esto continúa mientras exista competencia entre threads.
En las ocasiones en que necesite más control, el thread obtiene el control sólo
para descubrir que no tiene una parte suficiente del recurso para hacer algo. En
este caso, este puede ceder el contTol y desplazarse automáticamente al final de la
cola de espera. Las herramientas que utiliza son wait() y notifyAII().
public synchronized changeSomething ()
{
while ( buffe r n o t f u l l ) / / ¡Existen datos suficientes?
{
try { wait()} / / No, se desplaza al final de la cola
catch (Exception err)
{ System.err.println(err); }
}
295/512
threads en espera c¡ue ya pueden intentar acceder al recurso.
Limitaciones de la implementación
La implementación en Java de la programación de red es un gran modelo para
el desarrollo rápido y sencillo. Es fácil crear sockets, enviar mensajes y gestionar
las excepciones. Sin embargo, tiene algunas limitaciones.
• E/S confusa. El paquete de E/S no es tan intuitivo como podría serlo. La posible
razón es el uso de verbos como nombres de clases. La separación de los
diferentes tipos de E/S es grande, pero el resultado fue una explosión de clases
(las clases se permutan entre tipos de entrada/salida y las clases básicas), lo
que hace que la implantación no sea realmente directa.
296/512
confusa, tiene una potencia tremenda para liacer más rápida la programación de
red.
La capacidad de Java para utilizar threads simplifica la programación de sockets
y facilita el desarrollo del servidor. Puede crear threads de cualquiera de las dos
formas siguientes; heredando Thread o implementando Runnable. Con eso, la
llamada al método start() crea un nuevo thread de ejecución e inicia la tarea en la
imple-mentación personalizada de run(). La sincronización de una clase
entrelazada es bastante fácil con dos herramientas: métodos sincronizados y
cesión de la ejecución.
El análisis y el uso de Java como base de programación ayuda a ver la forma de
implantar sockets en un lenguaje que permita objetos. El siguiente capítulo propone
y traza una implementación en C++.
297/512
Capitulo 13
En este capítulo
¿Por qué utilizar C++ para la programación basada on sockets?
Disposición del marco de trabajo
Prueba del marco de trabajo basado en sockets
Limitaciones de implementación
Resumen: un marco de trabajo basado en sockets de C++ simplifica la
programación
298/512
¿Por qué utilizar C++ para la programación
basada en sockets?
Un marco de trabajo basado en C++, que inicie la API de sockets, ofrece
ventajas en la generación, de forma manual, de llamadas al sistema. Como habrá
podido comprobar, el proceso de creación, configuración y uso de sockets varía
muy poco según el tipo de aplicación.
299/512
permitiendo al sistema desarrollarse internamente sin afectar a las interfaces con el
mundo exterior.
Al igual que en todos los lenguajes que admiten objetos, C++ obliga a utilizar las
reglas que establecen los propietarios de dichos objetos, permitiendo al programa
acceder sólo a aquello que pueda ver. Con una planificación adecuada, un marco
de trabajo abre la posibilidad de admitir todas las formas posibles de redes
basadas en sockets.
300/512
enlazado-no utilizable/no alcanzable). El código inservible ya no aparece en
muchos programas. Los enlazadores más novedosos e inteligentes eliminan todo
el código que aparezca como inservible.
301/512
Envío de mensajes sencillos
La segunda característica es crucial para un buen funcionamiento. El marco de
trabajo debe ser capaz de enviar y recibir mensajes. Los mensajes tienen tres
partes: direcciones, canal y cuerpo del mensaje.
Los capítulos anteriores describen los métodos para el envío y recepción de
mensajes. Las llamadas a estos métodos implican ciertos estados en los sockets.
Por ejemplo, para la llamada de sistema send(), debe tener un socket conectado.
El prin-:ipal objetivo de la recepción de mensajes es la simplificación de las
interfaces.
La simplificación de las comunicaciones pasa por disponer de un mensaje que
se puede enviar él mismo. Una de las características presentadas en el Capítulo 11
fue la generación de flujos para los datos, es decir, el empaquetamiento y
desempaquetamiento de los datos. La clase debería conocer mejor sus detalles
internos y debería asumir este papel.
El lenguaje C++ no es adecuado para proporcionar la generación de flujos de
datos. Tiene una limitación importante: necesita conocer los datos para poder
usarlos. Comparado con Java, éste permite definir una clase e incorporar una
interfaz Serializable. A partir de este momento, la clase se empaqueta-
desempaqueta de forma automática. Esto lo puede realizar puesto que utiliza
código interpretado. Asume que no aparece ninguna información específica relativa
a la máquina.
Un programa C++ debe suministrar los procedimientos de empaquetamiento y
desempaquetamiento. Además, dado que C+-+ no admite interfaces, debe heredar
a partir de una clase común que describa estos procedimientos.
Manejo de excepciones
La tercera característica más importante debe controlar las diferentes
excepciones y errores que la red introduce en la programación. Es posible que
pueda aparecer cualquier número de errores asincronos, cuando crea y configura
sus sockets así como cuando envía y recibe sus mensajes.
Todos los errores se originan a partir del socket, la conexión, la ruta o el otro
host. La identificación rápida y eficiente de todos los errores ayuda al programador-
usuario a aislar el problema y recuperar el sistema de forma apropiada.
El marco de trabajo utiliza la construcción de C++ try...caten() para las gestión
de excepciones, similar a la que presenta Java. El programador-usuario debe
capturar todas las excepciones que genere el marco de trabajo; en cualquier otro
caso, el programa finaliza con un error no controlado. Puede capturar todas las
excepciones no esperadas al comienzo del programa y generar algún mensaje
significativo que permita mejorar la depuración.
302/512
Todos los tipos de sockets tienen diferentes parámetros modificables de
configuración. Las diferentes configuraciones modifican el comportamiento del
socket o de los canales de envío y recepción de mensajes.
FIGURA 13.1 Todas las clases de excepciones que derivan de Exception capturan
sólo una cadena de caracteres y un código de error.
Una jerarquía de clases de excepciones define, de forma no muy frecuente,
cualquier diseño ingenioso o iluminado. De hecho, si emplea una jerarquía de
clases para las excepciones, tenga mucho cuidado de no crear excepciones
303/512
recursivas. Por ejemplo, si aloja memoria o ha asignado el controlador de memoria
a una excepción, podría provocar una caída del programa si realiza una llamada
errónea a new(). Deje el diseño de las excepciones de la forma más sencilla
posible (poco más que valores y un breve mensaje).
304/512
lugar pl protocolo que esté utilizando. Si no lo encuentra, busque en la clase padre.
Además, las clases asumen que está siguiendo las reglas apropiadas. En el
ejemplo anterior de la llamada a send(), las clases no comprueban si el socket está
conectado. En su lugar, el programa genera una excepción.
Relaciones
El primer paso es identificar las relaciones entre los componentes y las clases.
Utilice términos generales, tales como utiliza, es-un/a, tiene un/a y genera, para
etiquetar las conexiones entre componentes y clases.
En la Figura 13.2, se conectan los diferentes componentes para mostrar las
interacciones existentes entre ellos. Algunos sistemas de diagramas utilizan líneas
especiales (como por ejemplo, flechas). Utilícelas si es más cómodo. El factor más
importante es la claridad.
305/512
solamente algunas características (es pecializ ación) sin tener que reescribir la
clase completa. Esta es la base más importante de la tecnología orientada a
objetos.
El único componente que tiene algunas clases interesantes es Socket. Incluye
cinco clases que están claramente relacionadas. Véase la Figura 13.3. Las clases
Broadcast y MessageGroup (multidifusión) utilizan sockets UDP y tienen una
funcionalidad similar, dado que se derivan de la clase Datagram. Pero SocketClient
y SocketServer no funcionan de la misma forma a pesar de que usan TCP. El
cliente se conecta de forma activa mientras que el servidor espera las conexiones.
Identificación de abstracciones
Volviendo a la jerarquía de socket, las clases se ajustan perfectamente, pero
requiere algunas clases adicionales. Claramente, la clase 5ocket debe ser la
superclase, la madre de todos los sockets. Realmente, la única función que tienen
es proporcionar un conjunto común de caráeterít.ticas para (odas las clases hijas.
La superclase 5ocket aporta todas las funciones get y set. Además, incluye las
funciones creation, send, receive y cióse. Sin embargo, no puede existir en espacio
y tiempo real. La razón es que no tiene protocolo. El protocolo se define en otra
parte.
ADAPTAR LA JERARQUIA A LOS SOCKETS RAW
Podría revisar ta jerarquía, de forma que socket tenga la posibilidad de crear, el mismo,
un socket raw o difícil. No obstante, es posible que tenga problemas. Primero, la
funcionalidad de estos sockets de bajo nivel está más cerca a UDP que a TCP. Tendría
que desactivar la funcionalidad de las clases SocketClient y SocketServer. Esto es
horroroso. También, podría abstraer esta funcionalidad incorporando un nuevo nivel a la
parte UDP de la jerarquía. De todas formas, el componente se hace mucho más
complicado. Es un problema real en el desarrollo de los marcos de trabajo: cuantas más
características se incorporaran, más complicado se hace y más difícil de utilizar.
SocketClient y SocketServer presentan suficientes diferencias para que la unión
de ambas directamente en Socket no ofrezca la funcionalidad TCP. Además,
presentarán más diferencias funcionales que las que tienen cuando se relacionan
306/512
directamente.
Puede resolver esta relación cercana entre SocketCIient y SocketServer
mediante una clase intermedia (SocketStream) que defina las características de un
socket TCP sin perder la identidad de la superclase. La Figura 13.4 muestra el
diagrama completo.
Las dos clases, Datagram y SocketStream, proporcionan la configuración y
características que requieren sus protocolos. La separación de protocolos facilita la
incorporación de otros protocolos, como SOCK_RAW, o incluso protocolos nuevos,
como RDM (Mensajes entregados con fiabilidad).
FIGURA 13.4 La jerarquía completa de la clase Socket incluye dos clases virtuales:
Socket y SocketStream.
307/512
revestimiento para la API de sockets, es necesario que identifique las
características y atributos disponibles.
Las características de los sockets se reflejan principalmente en las opciones que
presentan. El Capítulo 9, "Rompiendo las barreras del rendimiento" y el Apéndice
A, "Tablas de datos", muestran las opciones estándar de los sockets así como
aquellas relacionadas con Linux. Estas opciones constituyen buenos candidatos
para los atributos de la clase Socket. En realidad, la clase no mantiene los
atributos como entidades separadas en la clase. En su lugar, el descriptor del
socket incluye cada configuración. Cada par get/set, que acompaña normalmente
a los métodos CRUD (Crear/Leer/Actualizar/Borrar), se encarga de realizar,
realmente, la llamada a getsockopt()/setsockopt().
La Figura 13.5 muestra que la función get/set realiza la llamada para cada
atributo expuesto. Algunos atributos sólo activan o desactivan una característica,
de forma que el diseño no incluye una función get{). Una clase no tiene la
necesidad de exponer todos los atributos; normalmente, expone aquellos atributos
que modifican el comportamiento de Ir. clase. Tiende a mantener otros atributos
internos que podrían modificar el estado de la clase.
Un último atributo especial requiere algo más que modificar un valor. Se trata de
propiedades que parecen atributos pero que, realmente, realizan operaciones
diferentes. Por ejemplo, MinimizeDelay(), MaximizeThroughput(), MaximizeRe-
líability() y MinimizeCost() llaman a todas las interfaces en Datagram con una
única llamada setsockopt(). Pero, con la idea de simplificar la interfaz, calculan y
establecen valores de bit adecuados de forma que no tenga que hacerlo el
programador.
Las clases en el componente socket pueden incluir otros atributos relativos a la
implementación específica de la clase. Por ejemplo, SocketServer puede incluir la
rutina de refrollamada que registra las conexiones aceptadas. Además, los sockets
de multidifusión no tienen una forma clara de liberar grupos y, por tanto,
MessageGroup incluye un array de grupos unidos.
Socket
Bind(HostAddress)
Send(Message, Options)
Receive(Message. Options)
Closelnpul()
CloseOutpul()
308/512
int GetTTL() SetTTL(int)
PermitRoute{bool)
KeepAhve(bool)
int GetType()
int GetError()
FIGURA 13.5 :ada atributo en una clase puede tener asociada una función get...
()/set...().
309/512
Tabla 13.1 Métodos especiales implementados en el componente Socket
Clase Método Descripción
310/512
/ / * * * crea un socket y coloca la referencia en el atributo
//*** SD.
Socket::Socket(ENetwork Network, EProtocol Protocol)
{
Cuando ocurre algún error durante la creación, con los métodos convencionales
no tiene posibilidad de enviar de regreso un error. En su lugar, los marcos de
trabajo utilizan excepciones que permiten enviar directamente el error al
controlador. Si todo está correcto, la construcción finaliza y devuelve el control al
proceso que genera la llamada.
No todas las operaciones están disponibles para un constructor. No puede llamar
a los métodos de la clase que esté creando a menos que los declare estáticos.
Cada clase incorpora un contexto y estado. Cada método asume que el contexto
es completo y estable, mientras que el compilador considera un contexto no
completo e inestable en el constructor.
Los métodos estáticos de una clase no dependen de un contexto y, por tanto,
puede utilizarlos en cualquier momento y en cualquier sitio. De hecho, incluso no
necesita instanciaT un objeto para poder utilizarlos. Por tanto, las relaciones
existentes entre los métodos estáticos y la clase son muy débiles, casi
subordinadas. Muy raramente, las clases utilizan métodos estáticos debido a esta
relación débil. No obstante, resultan muy útiles en ciertas situaciones. Algunas
llamadas al sistema, como los controladores de señales, no admiten los contextos
que requieren los métodos estándar. Si, por ejemplo, quiere capturar varias señales
en una clase, debe declarar los controladores de señal como métodos estáticos.
311/512
//*** Destructor Socket
Socket::-Socket(void)
{
if ( close(SD) !=0 )
throw FileException("Can’t close socket");
}
Debería declarar todos los destructores como virtual para que todos los
destructores en la jerarquía de herencia lleguen a ejecutarse. Un destructor no
virtual especifica que no quiere utilizar los destructores de la clase padre cuando
elimine un objeto. Simplemente, puede especificar el destructor de la superclase
como virtual. Esto convierte a virtual todos los destructores heredados. Sin
embargo, por cuestiones de claridad, puede que quiera declararlos como virtual de
forma explícita.
El cliente/servidor de eco
El primer ejemplo (v más citado en el texto) es el cliente/servidor de eco. La
razón real de utilizar un cliente/servidor de eco es comprobar que las
comunicaciones funcionan correctamente. Una vez que entienda la parte que se
presenta continuación, el resto resultará bastante más sencillo.
El Listado 13.1 muestra el cliente básico.
Listado 13.1 Echo-Client.cpp
//*****************************************************************************
// Cuerpo principal del Cliente
HostAddress addr(strings[1]); // formato <address:port>
try
{
312/512
char line[l00];
client.Receive(msg); // Obtiene el mensaje de bienvenida o la petición
printf("msg=%s", msg.GetBuffer()); // Muestra el mensaje
fgets(line, sLzeof(line), stdin); // Obtiene la linea
msg = line; // Coloca la linea en el mensaje creado
client.Send(msg); // Envia el mensaje al servidor
}
fprintf(stderr,"Unknown exception1\n");
}
Esta definición captura todo aquello que no ha previsto.
El servidor podría ser un servidor de eco predefinido, de correo o HTTP. La clase
TextMessage es compatible con cualquiera de estos servidores. El Listado 13.2 le
muestra la versión basada en objetos del servidor de eco.
Listado 13.2 Echo-Server.cpp
//*************************************************************************
// Cuerpo principal del Servidor try
try
{
313/512
}
314/512
imple-mente la multitarea. El clásico ejemplo es el datagrama que debe aceptar
cualquier número de mensajes que se envían (véase Listado 13.4).
Listado 13.4 Peer.cpp
//****************************************
// Envió de mensajes con Datagramas
try
{
HostAddress addr(strings[1 ]); // Definir su propia dirección
Socket "channel = new Datagram(addr); // Crear el socket
if ( !fork() ) // Crear el proceso
receiver(channel); // Llamada al receptor de datos
channel->CloseInput(); // Cerra el canal de entrada
HostAddress peer(strings[2]); // Definir la dirección homóloga
TextMessage msg(l024); // Crear el buffer msg
do
{
char line[l00];
fgets(line, sizeof(line), stdin); // Leer la línea
msg = line; // Colocarla en el buffer
channel->Send(peer, m s g ) ; I ! Enviarlo.
}
315/512
un nuevti thread puede provocar resultados imprevisibles.
La multitarea tiene sus ventajas y puede incluso usar C++, pero tenga cuidado.
Limitaciones de implementación
El marco de trabajo es sólo un ejemplo funcional, pero incompleto, de cómo
podría implementar sockets en una biblioteca de C++. Debería proporcionarle
algunas ideas de cómo adaptar el marco de trabajo (o escribir el suyo propio) para
considerar todas sus necesidades de programación.
Como ha podido comprobar, puede crear sockets de cliente, servidor,
datagramas y difusión-multidifusión. Además, puede configurar los sockets
utilizando las opciones que tienen disponibles. Incorpore una abstracción del
proceso de envío y recepción de mensajes que le permita escribir sus propios
formatos de mensajes. Las siguientes secciones presentan un par de ideas que
permiten enriquecer, aún más, estas características.
Incorporación de la multitarea
Otra limitación, como se ha mencionado anteriormente, es la ausencia de
soporte para la multitarea. La mayoría de los programas que trabajan con la red,
realizan tareas al mismo tiempo para mejorar el rendimiento. El hecho de ofrecer
procesos y threads como característica intrínseca del marco de trabajo implica que
la programación resulte mucho más sencilla.
El problema proviene de la mera definición de una clase: algo que realiza y
316/512
conoce alguna cosa. Así, si define una clase de sockets, ¿tendría un socket
multitarea y otro de entrelazado múltiple? Además, ¿se corresponde una tarea con
un objeto? Si es así, existen realmente dos clases derivables: Socket y
Process/Thread. El resultado genera un problema de herencia múltiple {pese a la
incredulidad de la mayoría de los fundamentalistas en la tecnología orientada a
objetos).
Quizá, el problema es más sencillo de lo que parece. Sin embargo, no resulta
muy adecuada la idea de combinar el cliente, servidor, datagrama, difusión y
multidifusión con los procesos, threads y la no-disponibilidad de la multitarea.
317/512
Capitulo 14
318/512
Comience con buen pie
El beguine es un baile español que precede al tango. Como en este baile, si no
comienza con el pie apropiado en un proyecto de objetos, podría también bailar
con la muerte. Comprender lo que quieren los usuarios es el paso más importante
que se debe dar. La interacción con su usuario es como un baile en el que él dirige.
El usuario es la persona o sistema que interactúa con su programa. Si no
identifica y solidifica las interfaces, el baile puede volverse contra usted y dejar
descontento a su pareja. Ésta no es una buena postura; el resultado será
inevitablemente distinto al deseado por el usuario.
Puede utilizar cuatro herramientas esenciales para definir los deseos y
necesidades del usuario: el contexto, la lista de funciones, los casos de
uso/escenarios y el flujo del sistema. Consiga un buen libro sobre la obtención y
administración de los requerimientos para ver de forma detallada cómo hacerlo.
Estas herramientas le ayudarán a comprobar que el programa funciona de acuerdo
a las expectativas del usuario.
El contexto simplemente define lo que el programa no hace. Identifica a todos
los usuarios, el flujo de entrada y salida de la información y las funciones que son
responsabilidad del usuario. Esto es crucial para el éxito del proyecto. Si no
identifica lo que va a omitir, el usuario puede suponer que el programa lo hará.
El contexto le conduce directamente a la lista de funciones que hará el
programa. Cada función es un par verbo-objeto. Por ejemplo, una función válida
sería "Imprime un informe estadístico para el usuario". El sujeto implicado es el
programa.
La lista funcional lógicamente conduce a los casos de uso. Los casos de uso
suelen describir la forma en que el programa realiza la función. Los casos de uso
siempre comienzan y terminan con el exterior; los pasos intermedios describen la
transformación requerida. Puede subdividir los casos de uso en escenarios
individuales. Recuerde que el caso de uso es la función y el escenario establece
las diferencias en los datos. Considere los siguientes ejemplos:
Caso de uso #9: "Enviar un mensaje a una persona de la red"
[El usuario escribe el mensaje y pulsa Enviar]
Abrir la conexión con el destino
Contactar con el receptor
[El receptor acepta la interrupción]
Enviar el mensaje
Devolver confirmación al usuario
[El usuario recibe el estado]
Escenario #9.1: "Enviar un mensaje; mensaje denegado"
[El usuario escribe el mensaje y pulsa Enviar]
319/512
Abrir la conexión con el destino
Contactar con el receptor
[El receptor rechaza la interrupción]
Devolver rechazo al usuario
[El usuario recibe el estado]
Escenario #9.2: "Enviar un mensaje extenso a una persona de la red"
[El usuario escribe el mensaje y pulsa Enviar]
Abrir la conexión con el destino
Contactar con el receptor
[El receptor acepta la interrupción]
Enviar el mensaje en partes
Devolver confirmación al usuario
[El usuario recibe el estado]
Los escenarios de este ejemplo son simples mutaciones del caso de uso
original. Los casos de uso suelen incluir casos de excepción o negativas. Un caso
de excepción es aquel que muestra la forma en que responderá el programa a un
problema con los datos o la transacción.
El último paso toma los casos de uso y muestra gráficamente la funcionalidad
del programa. Es importante que incluya cada caso de uso y muestre cómo van
cambiando los datos hasta que alcanzan el destino final.
Un mezclador no es un objeto
Otro problema que se encuentran los diseñadores es asignar el nombre
apropiado. Una vez comprendido lo que desea el usuario, puede comenzar el
proceso de análisis de las "cosas" del sistema del programa. Este sistema de
cosas tiene responsabilidades ("Qué sabe" y "Qué hace") que determinan su
comportamiento.
La utilización de nombres representativos para los objetos determina su
comportamiento actual y su evolución. Un buen nombre siempre es un sustantivo.
Por ejemplo, llamar 5ocket a un objeto es mejor que llamarlo NetIO puesto que
puede hacer mucho más en la red que una simple entrada-salida. Pero si utiliza un
verbo como nombre del objeto (Netl/O es una abreviatura de entrad a-salid a de
red, con dos verbosl, limita la eficacia y la evolución del objeto. Con Socket, no se
refiere sólo a la lectura y escritura de mensajes; puede incluir control de la red,
suscripción a redes de multidifusión y demás.
La mayoría de sustantivos funcionan muy bien. Sin embargo, el inglés tiene un
problema con ciertas palabras: puede llamar a un objeto falso o desorientador.
Nombres que suelen ser desorientadores son los sustantivos verbales (palabras
que actúan como sustantivos pero que realmente son verbos). Por ejemplo, un
320/512
mezclador no es un objeto.
¿Qué hace un mezclador? Mezcla. Por tanto, ¿qué métodos debería tener un
mezclador? ¿blend()? No es suficiente para que sea un buen objeto. Encontrar un
buen nombre es muy difícil. Cuando asigna un nombre a un objeto en función de lo
que hace, es muy difícil olvidarse de lo que podría hacer en el futuro.
A veces no se puede identificar un nombre que se ajuste a su objetivo. Una
solución muy simpe (aunque suele ser efectiva) es asignarle un nombre que no
tenga relación como bob. Entonces se deja que las responsabilidades dirijan el
objeto.
321/512
superficial del sistema. Trabaje directamente con el personal del negocio y
muéstreles cómo funciona su solución de alto nivel para hacer lo que piden. De
forma inevitable, descubren que lo que hizo es exactamente lo que pidieron y no lo
que pretendían. Si alcanza un nivel de detalle más profundo, los oyentes pueden
llegar a perderse o no saber centrarse en los casos de uso.
Si sus clientes (el personal del negocio) desean conocer más detalles acerca de
la forma en que intenta implementar el producto, puede ser una buena señal de
que su trabajo es bueno. Puede informarles de que les permitirá revisar el trabajo
una vez completado el diseño. En ese momento, pueden ver todos los detalles que
deseen.
La explosión de la herencia
Durante la etapa de diseño, tome las clases que definió v proporcione los
detalles. Debe tener cuidado en este momento; la tentación es simplemente
heredar todas las clases a un detalle de nivel de diseño. La razón fundamental es
"Tenemos un análisis puro y portable. Mantengámoslo durante el diseño". De
hecho algunos expertos en la tecnología de objetos promueven fuertemente esta
explosión de objetos. Sin embargo, rara vez es necesario.
La mayoría de las implementaciones no hace más que utilizar las clases
contenedor y añadir una interfaz de usuario. El análisis sigue siendo puro y el
trabajo que hizo no pierde consistencia. Proceda simplemente añadiendo las
piezas necesarias para asignar el análisis a la arquitectura del sistema.
Además, si permite una explosión de objetos, el resultado es inevitable: la
anarquía de objetos y un mantenimiento muy complicado. Sólo debería heredar
una clase en el caso de que necesite transformar un comportamiento o añadir
funcionalidad. Si la funcionalidad no forma parte del análisis sino del diseño,
asegúrese de que no fue un error.
La anarquía de objetos realmente demuestra una modelización ausente de
responsabilidad. La herencia de cada clase incrementa la probabilidad de enredar
la web cuando cada clase intenta establecer la asociación apropiada.
El mantenimiento de este nido de conexiones es muy difícil, aunque
dispongamos de la documentación. Y cuando intente revisar un objeto del árbol de
herencia, puede afectar a otro, de forma que abata el propósito de la
encapsulación.
Reutilización/Desaprovechamiento
Un mito común que mantienen los aficionados a los objetos es su aseveración
de que pueden reutilizarlo todo. Esta opinión es muy poco realista. Al igual que
convertir un martillo en un taladro de dentista es en el menor de los casos un error,
algunas clases son más apropiadas en un lugar (su diseño deseado).
Si desea reutilizar una clase, asegúrese de que el objetivo junto con la interfaz
se ajustan ampliamente a sus necesidades. Si tiene que volver a ajusfar gran parte
de la interfaz, puede que no haya considerado un buen ajuste o que el diseño
original sea deficiente. A continuación se muestran algunos aspectos a considerar:
322/512
La función pretendida para la clase ¿se ajusta a la suya propia?
¿Debería tener que volver a ajustar sólo una o dos interfaces?
¿Satisfacen las clases padre y las superclases sus necesidades?
¿Utiliza los datos existentes de la forma apropiada?
Si responde "no" a cualquiera de estas preguntas, puede tener un problema con
la re utilización-desaprovechamiento. Una solución es encontrar una clase padre
distinta que se parezca más a lo que está buscando. Otra solución es olvidarse por
completo de la jerarquía y comenzar un nuevo camino de desarrollo.
El operador sobrecargado
Hasta la sobrecarga de operadores se considera ahora innecesaria y no
deseada. Esta no rompe ninguna regla cardinal de los objetos, pero hace que el
código sea menos legible y complica el mantenimiento. El propósito global de la
mejora de la tecnología de la ingeniería del software es incrementar la reusabilidad
y la transportabiíidad. Los operadores sobrecargados suelen hacer exactamente lo
contrario. Su uso y mantenimiento son confusos.
Aún así, si desea utilizar la sobrecarga de operadores, recuerde bien las
siguientes reglas:
• No pase datos por valor o punteros. En su lugar, páselos por referencia (&)
siempre que sea posible. Si no lo hace, puede tener problemas con los
punteros, obtener un rendimiento degradado cuando llame ai constructor y
destructor una y otra vez.
323/512
• Nunca sobrecargue los operadores externos. Las llamadas a función {"()") y los
indicadores de campo ("." y "->") en contadas ocasiones aclaran el uso de la
interfaz.
En conjunto, intente utilizar una programación significativa. Si desea que la
gente haga un buen uso de su trabajo, hágalo inteligible para ellos.
Infección de la herencia
Los objetos introdujeron el concepto de herencia, reutilizando el comportamiento
de las clases establecidas. Desafortunadamente, algunos programadores creen
que ésta es la panacea de la programación (por lo que lo heredan todo). Estos
programadores intentan colocar todo de forma jerárquica, aún cuando cada
aspecto individua] no tenga ninguna relación con los demás.
Un programa de tamaño normal tiene entre tres y cinco componentes principales
y cada componente puede tener entre dos y siete objetos. Tener todas las clases
logística mente relacionadas es extremadamente raro y forzar esta relación es
innecesario. (Por favor, observe que esto no se aplica a javo, que hereda todas las
clases de Object.)
Código inalcanzable
La tecnología de objetos puede responder a muchos problemas en un dominio
de programación particular. Muchos de los problemas generados son generales y
probablemente no pueden cambiar. Se debe a que la reutilización es bastante
importante. Alguien dijo una vez que es probable que la mayoría de los programas
ya se han escrito. El trabajo que se realiza actualmente se centra las interfaces
humanas y en tecnologías nuevas.
Sin embargo, la tecnología de objetos no puede resolver un conjunto pequeño
de problemas. Estos problemas se ajustan más a la naturaleza que la definición
actual de los objetos. Esta sección introduce estas limitaciones y las posibles
324/512
formas de evitarlas.
Clase parcial
La primera limitación es la clase parcial. Por naturaleza, si elimina todas las
patas de una silla menos una, seguirá siendo una silla. Sin embargo, si una clase
pierde parte de su funcionalidad, no mantendrá la capacidad de la clase original.
Aunque dispone de la interfaz original, no es una herencia de la forma original.
Funciones nulas
La clase parcial también se extiende a las funciones nulas. La función nula
permite desconectar un método para que no continúe ofreciendo su servicio.
Aunque herede de una clase padre, puede encontrar que ahora un servicio
particular está obsoleto o incluso es destructivo para la clase nueva. Una función o
método nulos poda completamente el método de la clase nueva y a partir de ahí
queda olvidado.
Si le gusta esta característica, algunos programadores ignoran el método y no le
asocian ningún comportamiento. Aunque efectivamente esto consigue el mismo
objetivo, el método sigue estando allí, ocupando espacio de definición.
Mutación de objetos
Otra limitación es la mutación de objetos, que permite al programador cambiar
un objeto por otro sin utilizar la conversión de tipo. La conversión de tipo es muy
peligrosa, incluso en el mundo fuertemente tipado de los lenguajes orientados a
objetos como C++ y Java. La mutación es una forma agradable de realizar una
conversión de tipo que dispone de ciertas reglas para asegurar que pasa de A a B
sin demasiado problema.
La mutación es una herramienta que permite crear un objeto genérico y,
conforme se va descubriendo más sobre él, puede cambiarlo o mutarlo poco a
poco (incluyendo incluso una clase global nueva y previamente desconocida). Un
lugar perfecto para esta clase de herramienta debería ser el flujo de objetos, donde
puede recibir un conjunto general muy grande de datos y tener que descifrarlos. En
parte, puede crear un constructor traductor que toma el otro formato y crea una
clase nueva a partir de él, pero no debería ser necesario escribir un constructor
traductor para cada una de las otras clases relacionadas (un problema de
combinatoria).
El motor de mutaciones requiere los siguientes criterios;
325/512
• El motor de descubrimiento debe estar capacitado para crear una instancia de
una clase abstracta. Puede que encuentre esto difícil de aceptar, pero durante
el proceso de descubrimiento (justo cuando esté jugando a las veinte
preguntas) puede tener que comenzar con la abstracta.
• El resultado siempre debe ser una instancia de una clase concreta. La razón es
obvia: no puede existir ningún objeto en una clase incompleta.
Algoritmos heurísticos
La tecnología de objetos no soporta directamente los algoritmos heurísticos. Los
algoritmos heurísticos son problemas basados en objetivos en los que el usuario
describe el problema, y la computadora resuelve el objetivo. Gran parte se puede
simular, pero una premisa global de resolución no determinista es inalcanzable
desde un punto de vista computacional.
Puede crear algoritmos heurísticos y utilizar un generador de números seudoa-
leatoríos para aproximar la resolución no determinista. Sin embargo, la simulación
está condicionada por la eficacia de los números aleatorios.
326/512
aspectos de los objetos y lo que la historia ha mostrado.
327/512
Otra causa común de la herencia múltiple es la utilización de un verbo como
nombre de una cla^e. Por ejemplo, si tiene StreamReader y StreamWriter, debe
combinar las dos para obtener un Stream de acceso aleatorio. Los nombres
-Reader y -Writer son el problema.
Puede resolver la mayoría de los problemas de herencia múltiple extrayendo las
características comunes de una superclase abstracta o utilizando contenedores.
Normalmente, la clase nueva se asemeja más a una clase padre que a otra.
Herede desde ln clase más similar e inserte la otra.
Las clases-tarea demuestran ser una pequeña excepción a esa regla. Si tiene
una clase que está relacionada con una clase padre que además es una tarea
(proceso o thread), puede insertarla mediante herencia múltiple. La razón es que la
clase no puede contener una clase/tarea: la programación no funciona muy bien.
Su subclase suele estar más relacionada con la otra clase.
Para resolver el problema de las clases-tarea sin herencia múltiple, puede que
tenga que forzar a la subclase a heredar desde la tarea e insertar la otra clase. Es
factible pero puede resultar engorroso.
328/512
• Limíte las funciones en línea. (Hay quien dice que siempre debería evitar las
funciones en línea explícitas y permitir que el compilador seleccione esa
característica.)
Estas indicaciones pueden ayudarle a minimizar el tamaño de sus códigos fuen-
e y objeto. A pesar de todo, puede esperar un incremento; el motor de resolución ie
nombres y las bibliotecas de soporte son más grandes que las correspondientes a
os lenguajes funcionales o modulares.
329/512
involucrado a lo largo de todo el proyecto.
330/512
zonéctelos. Cuando se tope con problemas que necesiten clarificación, trabaje con
el abogado del cliente e incluso los propios clientes puesto que un diseño JIT
requiere jna interacción mucho más fuerte.
Sin embargo, no caiga en la trampa de crear un prototipo desechable para mos-
rarlo a sus clientes. Eso nunca funciona. Si crea un prototipo (especialmente duran-
e el diseño JIT), anticipa su uso y nunca tendrá tiempo de "hacerlo de forma correc-
a más adelante".
Prueba de (des-)integración
El ciclo de desarrollo de software global depende de una secuencia de
deducción / validación de necesidades. La primera parte del proceso describe las
necesidades / un proceso supervisor garantiza que el proyecto las codifica. Este
proceso super-/isor suele llamarse prueba o garantía de calidad.
En realidad, la validación hace mucho más que una simple prueba. Esta recopila
/ archiva los productos de trabajo; comprueba y verifica las liberaciones del pro-
iucto; y garantiza la conformidad del negocio y la línea del producto. De todas las
esponsabilidades de un proyecto, el representante de la calidad tiene la más difícil.
La tecnología de objetos no ayuda demasiado. De hecho, multiplica por un valor
íiitre dos y cuatro la complejidad del nivel. Por un lado, las pruebas de la caja
blan-:a y de la caja negra (definidas en la sección siguiente) y la validación ya eran
sufi-:ientes. Pero los módulos introducen la prueba de la caja gris. Los objetos
añaden la Jruebas de herencia, polimorfismo, marco de trabajo, conformidad de la
reutiliza-:ión e interfaz. La complejidad global de la tecnología de objetos en el
campo de la :alidad del software ha hecho que se vuelva a plantear por completo la
prueba.
El primer error que se suele cometer es dejar sólo al diseñador y codificador que
jrueben su trabajo. Este problema debería ser obvio: Los mismos puntos oscuros |
ue provocaron el defecto pueden interferir posiblemente en el descubrimiento de os
mismos. La probabilidad de esta coincidencia es demasiado elevada para dejarla
jasar. Consiga una buena prueba realizada por ingenieros experimentados para
ontraprobar el trabajo de los programadores.
Cuando crea un proyecto de objetos, usted y sus clientes deben identificar la
torna en que se realizará la prueba. Sea consciente de que la validación consume
recursos y tiempo. Considere que el esfuerzo {número de personas-hora) requerido
para un proyecto normal se duplica o triplica cuando realiza una transición a un
proyecto de objetos.
Niveles de gris
La sección anterior menciona las pruebas de la caja blanca, de la caja gris y de
la caja negra. La prueba del software utiliza técnicas similares a las que se usan
con objetos, siendo una de ellas la encapsulación. La definición del nivel de gris del
sistema expone la cantidad de cualidades intrínsecas visibles. Con iluminación total
(caja blancal, el probador explora todas las funciones, caminos, decisiones y
sentencias. A menudo, el programador o diseñador pueden hacerlo con seguridad.
331/512
La prueba de la caja negra percibe el sistema desde el punto de vista del
usuario. La caja negra es similar a la prueba de aceptación donde los casos de uso
deciden la conformidad.
La última es la prueba de la caja gris, que se encuentra en medio. Ésta dispone
de un mayor conocimiento de la implementación (desciende a nivel de módulo o de
función, pero no más allá) y depende de los casos de uso para los caminos de
validación.
Los objetos enturbian un poco estas definiciones. Cor ejemplo, ¿dónde se
establece la prueba de herencia, de sobrecarga y de polimorfismo? ¿Qué ocurre
con la prueba de reutilización? El resultado está claro; todos tienen que probarlo
todo.
332/512
satisfecho ¡uiere más y un equipo satisfecho funciona de forma más compenetrada
(como una náquina bien engrasada).
333/512
Parte IV
334/512
Capitulo 15
En este capítulo
Repaso del modelo OSI
Comparación de programación de red y procedimental
Suministro de métodos middlewarc
Creación de RPC con rpcgen
Creación de llamadas con estado con conexiones abiertas
Resumen: creación de una caja de herramientas para RPC
335/512
Este capítulo muestra la forma de escribir RPC desde dos enfoques distintos. El
primer enfoque demuestra la forma de escribirlos sólo con sus manos, sin ninguna
herramienta. El segundo introduce la herramienta rpegen. Antes de todo esto, la
siguiente sección repasa los conceptos del modelo de red para mostrar cómo se
ajustan las RPC.
336/512
Para crear una biblioteca de herramientas preparadas para red, tiene varios
obstáculos. Los obstáculos tienen relación con el rendimiento y la fiabilidad;
también pueden involucrar simples limitaciones en la tecnología.
337/512
DEVOLUCIÓN DE VECTORES POR VALOR
El lenguaje C permite devolver un vector por valor. Sin embargo, la implementación es
especifica del compilador y puede sorprender. Debido a la forma en que algunos
procesadores manejan la pila hardware, el compilador no puede realmente copiar el
resultado a la pila. En su lugar, puede crear una variable local o estática y pasar ta
referencia a la variable. Una vez devuelto, et llamador copia el valor en el destino
apropiado. Esto puede tener un efecto lateral significativo en los programas entrelazados.
De forma alternativa, algunos compiladores pueden incluso ser "inteligentes" y anotar el
destino antes de la llamada a la función. Cuando la función accede al vector, realmente
está revisando el destino actual.
Los propios parámetros son un problema particular. La mayoría de los
programadores no indican los parámetros que son sólo de entrada, sólo de salida o
de entrada/salida. Pero, puesto que la interfaz tiene que determinar los parámetros
que envía y los que rellena, necesita indicarlo de alguna manera en la iniería?.. A
menudo la documentación es la interfaz. Considere el siguiente ejemplo:
/**************************************/
/*** Ejemplo de interfaz de red ***/
/*************************************/
/**/
/*............................................................................*/
/* NOTAS DE PUBLICACION */
/* getuserinfo - obtiene la información de un host */
/* user - (entrada) indentificacion de conexión del usuario */
/* host - (entrada) el nombre externo del host */
/* data - (salida) el resultado de la llamada */
/* VALOR DEVUELTO: correcto o fallo {errno de comprobación) */
338/512
La sesión de red incluye garantías implícitas de lo que la mayoría de gente
comprende y espera. El Capítulo 5 introduce alguna de estas garantías. En las
llamadas de servicio RPC. tiene que desplegarlas.
Establecimiento de un diálogo
El primer paso para aportar la funcionalidad del nivel de sesión es determinar el
orden de comunicación y mantenerlo. La mayoría de las conexiones RPC requieren
alguna forma de autenticación, pero el cliente dirige la comunicación.
La conexión inicial entre el cliente y su servidor suelen requerir un proceso de
conexión. El cliente debe proporcionar algún nivel de verificación (como nombre de
usuario y contraseña). Por razones de seguridad, debe garantizar que este
intercambio está asegurado a través de alguna forma de encriptación, puesto que
alguien de la misma subred puede ver los mensajes entre cliente y servidor. Tras la
conexión, el cliente y el servidor establecen quién se comunica primero.
A veces la pérdida de un mensaje puede confundir la conexión. Tanto el cliente
como el servidor final se esperan mutuamente. Como se indicó en el Capítulo 5 y
se describió en el Capítulo 10, "Diseño de sockets Linux robustos", puede hacer un
seguimiento de los turnos que se siguen en la comunicación. El método más
sencillo es enviar periódicamente un mensaje indicándose mutuamente el estado
mediante los protocolos de datos OOB.
Control
Algunas conexiones dirigidas por servicio pueden tener transacciones críticas.
Las transacciones no pueden permitir la pérdida de datos (por ejemplo,
transacciones bancarias). Una vez que el cliente y el servidor han establecido un
diálogo, podría ser conveniente forzar periódicamente un control.
El control es como el comando commit de algunas bases de datos. Durante la
sesión, puede tener varias transacciones. Estas transacciones cambian el estado
del servidor (como una transferencia de fondos o el pago de divisas). Tanto el
cliente como el servidor hacen un seguimiento de las transacciones y los
resultados esperados. El control es como la comprobación del balance tras varias
transferencias.
Puede experimentar otro giro en el control. Los clientes suelen llevar registro
local de las transacciones en un archivo para una posible revisión. Si es éste el
caso, podría ser conveniente encriptar cada transacción.
Conexiones recuperables
Una sesión requiere una conexión simple que permanezca aparentemente
abierta en todo momento. Ese requerimiento puede no ser siempre posible; la red
puede dejar una conexión en cualquier momento. Además, la API del socket no
indica directamente cuándo se ha perdido una conexión.
Tener una conexión recuperable significa que no sólo se intenta reconectar, sino
que también significa que el programa debe determinar la existencia de la conexión
(el canal está limpio y activo). El Capítulo 10 describe algunas formas de
339/512
comprobar la existencia de una conexión.
La recuperación de una conexión es fácil si han diseñado los requisitos del
programa. Parte del problema asociado a la pérdida de una sesión es intentar
hacer un seguimiento del lugar en que se encuentra en cada momento. Es
necesario marcar el lugar en que se abandona. Tanto el cliente como el servidor
deben soportar el marcador o identificador de sesión. Sin el identificador de sesión,
puede encontrar que la recuperación de la conexión se hace muy difícil si no
imposible.
Una vez que su programa encuentra que se ha derivado la conexión, ésta
simplemente puede volver a conectarse al servidor. Hacer esto con una cantidad
mínima de interacción de usuario forma parte de las conexiones independientes.
Conexiones independientes
El objetivo aquí es ofrecer al usuario una interfaz fácil de utilizar con la cantidad
mínima de conocimiento de usuario. Por contra, algunas aplicaciones tienen un
enfoque más transparente que requiere que el usuario vuelva a introducir las
contraseñas o establezca manualmente la conexión de red.
Necesita proporcionar al usuario sólo tanta interacción con la red como éste
desee. Puede significar que hace u n seguimiento de la información de conexión
durante una sesión definida. Por ejemplo, si define una sesión con la duración de la
ejecución de su programa cliente, podría observar todas las conexiones de usuario,
recordándolas para su uso futuro.
Sin embargo, debe ser completamente consciente de los aspectos de seguridad.
Mantener las contraseñas en un archivo para la conexión automática suele ser un
riesgo de seguridad innecesario. {¡Es sorprendende que algunas aplicaciones
profesionales ofrezcan esta característica, y por defecto!) Igualmente, puede
decidir derivar la conexión y olvidar la información de conexión después de un
cierto periodo de inactividad. Como situación extrema, si la conexión debe ser
ultrasegura, puede tener que forzar una conexión bajo cada reconexión. Utilice su
mejor juicio y consulte con sus usuarios finales.
340/512
popular.
Puede crear interfaces a largo plazo {críticas para todos los proyectos serios)
identificando la tunda mentación (un conjunto de objetivos, pretensiones, filosofías y
teorías) para los servicios. Una vez que haya solidificado su fundamentación,
puede trabajar en los stubs de la red.
341/512
de conectividad suelen añadir dos interfaces más, una llamada de servicio de
sesión abierta y otra cerrada.
342/512
llama el módulo del servidor. El programa servidor debe definir ese procedimiento
como un punto de entrada de su procesador de peticiones. El problema de este
enfoque es la poca flexibilidad del uso de nombres de procedimiento predefinidos.
Conforme evolucionen sus programas, es posible que el nombre de la interfaz
pueda perder su significado.
Otro enfoque consiste en ofrecer un servicio de registro. En la iniciación, antes
de que el servidor se active y espere conexiones, el programa servidor podría
registrar todas las rutinas del procesador de peticiones. Ésta es la mejor forma de
superar el problema de la interfaz invertida. Asegura la longevidad de las
interfaces.
343/512
segundos desde el 1 de enero de 1970), podría definir la interfaz de la siguiente
forma:
/******************************************************************/
/*--- Define la interfaz para obtener la hora del servidor ---*/
/******************************************************************/
program RPCTIME
{
versión HPCTIMEVERSION
{
long GETTIME() = 1;
} = 1;
} = 2000001;
La primera línea de la declaración define el nombre del programa (RPCTIME). Su
declaración en mayúscula es un convenio estándar; sin embargo, rpegen convierte
todos los nombres a minúscula. Puede tener varias versiones de la misma interfaz,
de forma que la siguiente declaración define la primera versión de la interfaz.
Dentro de ésta, se encuentra la interfaz de procedimiento real.
A cada sección de la declaración se le asigna un identificador numérico. Este
identificador es importante para la comunicación del cliente al servidor. Puede
utilizar cualquier número para la versión y el nombre del procedimiento, pero
algunos identificadores de programa están reservados. Puede experimentar con
seguridad con números entre 2.000.000 y 3.000.000.
Puede observar en el ejemplo que la llamada devuelve un valor long en lugar de
uno time_t. La herramienta rpegen admite cualquier tipo (realmente lo ignora)
sintácticamente y deja la comprobación semántica (declaración de tipos y
significado) al compilador de C. Sin embargo, rpegen supone que conoce algo
sobre el tipo para traducirlo correctamente. Podría definir un tipo (como en C) de
forma que el programa utilice la definición estándar:
typedef long time__t;
344/512
TENGA CUIDADO AL EJECUTAR MAKE CLEAN
Si se plantea utilizar los archivos de ejemplo en su desarrollo, no llame a make con la
opción estándar clean. Makefile elimina erróneamente estos archivos.
Mirando los archivos, puede observar que la llamada a procedimiento GETTIME
ahora se llama gettime_1(). El _1 es el número de versión de este procedimiento.
Cuando llame a estos procedimientos, necesita especificar el número de versión de
la interfaz. Aunque sería agradable llamar simplemente a gettime() y permitú que el
sistema elija la interfaz correcta en base a unos parámetros o incluso la última
versión, rpcgen no funciona así.
El siguiente paso es añadir al ejemplo el código relativo a la hora una vez que se
ejecuta rpcgen -a (para generar los archivos de ejemplo del cliente y el servidor).
Abra el archivo rpctime_client.c y añada el siguiente código:
/***********************************************************************/
/*** Fragmento de código del cliente para la RPC de la hora ***/
/***********************************************************************/
/*... Esto se genera automáticamente ...*/
result_1 = gettime_1 ((void*)&gettime_1_arg, clnt);
if (result_1 == (long *) NULL) {
clnt_perror (clnt, "er-or en la llamada");
}
345/512
muy sencilla de utilizar y desarrollar.
int Err;
unión
346/512
{
char *Data;
} proc_res_u;
};
La unión es muy útil si desea devolver un código de error específico sin tropezar
con las rutinas XDR. El código del servidor siempre devuelve un puntero al resulta
do, no el propio resultado. Si el servidor falla, devuelve NULL Primero, un valor
NULL no es muy descriptivo, y segundo, las llamadas de bajo nivel pueden
interceptar un valor NULL devuelto.
El siguiente ejemplo utiliza la estructura unión para devolver la información de
un archivo en /proc o un código de error. El archivo X completo es el que aparece a
continuación:
/******************************************************/
/*** Declaración del archivo X RPCProc RPC ***/
/******************************************************/
unión proc_res switch (int Err) {
case 0: /*--- si no hay error, devuelve la cadena ---*/*/
string Data<>; /*---sin limite---*/*/
default: /"---si hay error, no devuelve nada---*/*/
void;
};
program RPCPROC
{
versión RPCPROCVERSION
{
procres READPROC(string) = 1;/* nombre de archivo /proc */*/
} = 1;
} = 2000026;
El servidor acepta el nombre de archivo que apunta al sistema de archivos
virtual /proc y abre el archivo. Si no ocurre ningún error, el servidor lee el contenido,
cierra el archivo y devuelve el resultado. Puede encontrarse con un pequeño
problema aquí. Primero, el servidor devuelve la referencia al resultado, de forma
que los datos no deben estar en la pila. Todos los valores devueltos se hacen
estáticos para garantizar que los datos siguen estando activos. Sin embargo, las
cadenas siempre se convierten en char*.
Cuando rellene el registro de devolución, debe reservar memoria para insertar
las cadenas. Significa que debe liberar la memoria de alguna manera; en otro caso,
su programa genera pérdida de memoria por fragmentación. Las rutinas XDR
347/512
ofrecen una forma de limpiar la llamada anterior:
xdr_free(|xdrproct)xdrprocres, (void*)&result);
El único parámetro que cambia es el xdr_<tipo-devuelto> (xdr_proc_res, en este
caso). Llame siempre a esta función antes de hacer otra cosa. Es lo
suficientemente inteligente como para no hacer nada en el caso de que ya haya
sido llamada previamente.
348/512
struct NodeStruct
{
string Name<>;
TNode *Next; /*--- Esto no es válido ---*/*/
};
El sitio web incluye varios ejemplos con un comando ps de RPC que devuelve la
información en una tabla de asignación dinámica con filas y campos.
• Camino o ruta de! estado. La sesión debe tener claramente un camino o ruta
particular. Los pasos hacia adelante y hacia atrás deben ser deterministas y
claros. Si no es así, la recuperación del estado o los pasos siguientes se
349/512
vuelven inciertos y confusos.
350/512
Recuperación desde un estado erróneo
De los tres problemas diferentes que presentan las conexiones con estado, la
recuperación desde un estado erróneo es el más difícil. La red introduce
particularmente muchas formas diferentes de perder un estado. Entre las causas
de la pérdida de estados se encuentran el hardware, el enrutamiento, la
conectividad y los defectos del programa. El primer problema es el reconocimiento.
La pérdida de estados puede ser un problema grave si no se planifica. En primer
lugar, ambos programas deben comprobar mutuamente el identificador de estado
del otro. El identificador de sesión también debe incluir algún nivel de seguridad,
vencimiento e identificación de estados. Incluso puede contemplar el último estado
y los siguientes estados potenciales. Si alguna de estas claves se sale de la
sincronización, su programa puede determinar que se ha perdido ese estado.
La recuperación de un estado perdido conlleva su propio conjunto de
dificultades. Una vez que haya determinado que se ha perdido el estado, se
enfrenta al reto de qué hacer a continuación. A continuación se muestran algunas
ideas:
351/512
todos los datos manualmente.
La caja de herramientas rpegen le permite centrarse en el programa en lugar de
en el transporte. Puede elegir entre protocolos TCP o UDP, pero sin considerar el
transporte. Esto le ofrece una gran cantidad de conversiones automáticas incluyen
do la capacidad de enviar punteros. Las herramientas son muy sencillas de usar y
facilitan bastante la programación.
Finalmente, la mayoría de conexiones RPC son sin estado (un esquema simple
de preguntas y respuestas). Sin embargo, algunas conexiones requieren
interacciones con estado más complejas (la llamada actual depende de las
llamadas previas). Para conseguir este nivel de interfaz, debe implementar los
estados, las sesiones y la recuperación.
El Secure Socket Layer (SSL), que incorpora muchos estados incrustados en su
protocolo, depende fuertemente de las RPC. El capítulo siguiente trata sobre la
seguridad y SSL.
352/512
Capitulo 16
En este capítulo
Asignación de permisos para trabajar
El problema de Internet
Cómo garantizar la seguridad en un nodo de red
Cifrado de mensajes
Seguridad a nivel de socket (SSL)
Resumen: el servidor seguro
353/512
Asignación de permisos para trabajar
La programación en red gira alrededor de la idea de compartir información y
distribuir el trabajo. Para compartir información, necesita conocer quién está ínter-
actuando con el servidor v si se trata de una persona autorizada o se considera
una amenaza. El incremento del número de ataques a los sistema actuales indica
claramente que las amenazas están creciendo de forma preocupante y, ésta es la
razón que justifica el hecho de tener, hoy en día, más prioridad el diseño en
seguridad que la implementación de las características del producto.
La interacción implica reconocimiento y confianza. La seguridad responsabiliza
el proceso de identificación en el cliente y en el servidor. Después de realizar el
proceso de identificación, el cliente puede enviar o recuperar información o,
incluso, colaborar en los procesamientos realizados.
Para gestionar el ámbito de la seguridad, necesita conocer cuáles son los
niveles de seguridad y dónde es obligatorio establecer esta seguridad.
Niveles de identificación
El proceso de identificación del usuario constituye una parte fundamental en la
creación de un sistema seguro. 1.a forma más sencilla de seguridad es la
autenticación que simplemente identifica al cliente. Constituye la primera puerta
que tienen que pasar un usuario a través de un sistema seguro. La entrada a un
sistema remoto es un ejemplo claro de programa que autentifica una conexión. El
procedimiento de entrada o conexión sólo comprueba el nombre de usuario y la
contraseña.
El siguiente nivel de seguridad, autorización, permite o deniega el acceso a los
servicias del sistema. La entrada o conexión remota de Linuv considera
autenticación y autorización conjuntamente mediante la incorporación de perfiles
de usuario y permisos de grupos. Algunos sistemas operativos no distinguen las
conexiones remotas e, incluso, proporcionan una autorización muy limitada. Por
ejemplo, cuando comparta archivos en Microsoft Windows 95 y 98 con un acceso
no sólo de lectura, el sistema únicamente comprobará la autenticación del cliente.
El cliente, una vez autentificado, puede hacer lo que quiera con sus archivos e
impresoras.
El problema que plantea la autenticación y la autorización es que el cliente no
puede determinar si el servidor es un Troyano. La certificación ofrece mayor nivel
de seguridad con respecto a la autenticación. Requiere una tercera parte de
confianza que permita comprobar la identidad del cliente y del servidor. Esta
tercera parte es un servidor de certificados que comprueba que el cliente y el
servidor son realmente los que están reclamando los servicios.
Formas de intercambio
Después de establecer la identidad del cliente, necesita examinar la información
que intercambian las computadoras. El intercambio de datos se realiza de dos
formas diferentes: escrutinio e intrusión. El escrutinio de datos puede resultar
problemático cuando la comunicación es restringida. La revelación de datos
354/512
restringidos, como nombres de usuario v contraseñas, puede generar problemas
serios. La intrusión de datos constituve un problema mayor, dado que el intruso
puede incorporar datos e, incluso, revisarlos o modificarlos.
Normalmente, la comunicación es pública dado que los datos que pasan a
través del canal de comunicación no suponen compromiso alguno para el cliente o
el servidor. A este nivel, trabajan los servidores Web y otros servicios sencillos.
Un intercambio público de información no significa que todo el mundo pueda ver
los datos. Realmente, significa que los programas no toman precauciones con
respecto a la privacidad. Cualquier computadora en la misma conexión LAN física
puede ver los mensajes intercambiados. Este tipo de comunicación es muy
propensa al escrutinio e intrusión en la red.
Otra forma de intercambio de datos es mediante la utilización de grupos. La
difusión de los mensajes compartidos y el aislamiento del tráfico dependen de los
protocolos TCP/IP. Todas las computadoras del mismo segmento de red pueden
ver estos mensajes. Los mensajes del grupo se aislan en el segmento de destino
puesto que los rouíers filtran los mensajes de difusión externos. Esto resulta más
seguro que el intercambio público pero está sujeto todavía al piratero de la red
(dentro del mismo segmento de red). En este caso, la intrusión no es la cuestión
más peligrosa puesto se asume que en el segmento están conectados sólo
amigos.
Los mensajes privados intentan limitar el acceso a la información que pasa
desde un origen a un destino. Normalmente, estos mensajes incorporan
información crítica donde el espionaje externo puede provocar problemas muy
serios. Los mensajes privados requieren medidas extras, físicas y lógicas, que
permitan garantizar la seguridad e integridad de ios datos.
Otra forma importante de intercambio de mensajes a mencionar, es un formato
especial de datos que distribuye la ejecución de un programa entre diferentes
sitios. Estos comandos incluyen comandos RPC y directos (como telnet, rlogin o
rsh). La función de los datos que manejan estos comandos identifica claramente
sus diferencias con respecto a los datos habituales. Los datos normales (o datos
inactivos) sólo modifican el significado o, si están dañados, su interpretación. Los
comandos distribuidos (o datos activos), cuando están dañados, modifican la
ejecución e, incluso, pueden comprometer la seguridad del host El virus Love Bug
en Microsoft Outlook es un ejemplo claro de datos activos dañados.
De los diferentes tipos de interacciones de datos, los datos activos presentan los
mayores riesgos de seguridad. Debe tener mucho cuidado cuando necesite incluir
instrucciones y comandos remotos, verificando que el host remoto es de confianza.
El problema de Internet
Por otro lado, Internet presenta sus propios problemas. Su naturaleza es el
corazón del problema. Se trata de un diseño anárquico para intentar resolver
problemas anárquicos. Existen diferentes historias relativas a las creación de
Internet, pero, quizá, el hecho más relevante fue que se diseñó para soslayar el
problema de un apagón provocado por una explosión nuclear. Esta es la razón que
355/512
justifica las rulas dinámicas existentes entre los hosts.
Ninguna ruta a través del sistema es siempre la misma. No puede indicar al
sistema de red: "Lleva este paquete a través de estos routers a su destino." Cada
paquete puede seguir un camino o ruta diferente. Su camino a través de Internet
puede entrar o salir de territorio enemigo.
Formas de ataque/intrusión
Conocer las formas de ataque habituales, le puede ayudar a proteger su cliente
y servidor. Todas giran alrededor del formato básico de comunicación y siguen
exclusivamente la siguiente premisa: el cliente comunica con el servidor.
356/512
categoría.
357/512
El protocolo TCP/IP no restringe otra secuencia de numeración que asegure la
numeración única de cada paquete (durante el tiempo de duración del paquete).
Cuando es previsible el algoritmo que permite la determinación del siguiente
número, el canal es vulnerable. (Por favor, tenga en cuenta que el número de
secuencia, por si mismo, no es vulnerable, la debilidad aparece en la generación
de dicho mlmero.) ¿Es TCP/ I P el protocolo IP más difícil de piratear? Sí, pero otras
computadoras interconectadas todavía pueden escuchar y, por tanto, es posible el
pirateo.
Acceso restringido
Puede incrementar la seguridad de los programas de su servidor y cliente
utilizando las siguientes sugerencias:
• Limitaciones en la conexión.
Restringir todo aquello que puede hacer un host
remoto mientras esté conectado. No permita comandos remotos sin las
autorizaciones apropiadas.
• Centrarse en ¡as tarjetas. Si tiene un router o gateway con más de una tarjeta
ethernet (o interfaz), especifique los servicios de cada dispositivo. Todos los
dispositivos pueden usar la mayoría de los servicios, a menos que se diga lo
contrario. De nuevo, si tiene más de una interfaz física, resulta muy raro que los
servicios necesiten todas las interfaces. Por ejemplo, es posible que quiera
configurar el Telnet en una tarjeta para la intranet y no en la tarjeta que permite
la conexión con Internet.
358/512
determinado (o incluso lo utiliza muy raramente), no lo active. Cuando lo
necesite, inicíelo pero, hasta ese momento, déjelo desactivado. (Esto incluye
sus propios servidores.)
Su servidor puede ejecutarse de forma más eficiente si desactiva todas las
utilidades y servicios innecesarios que ofrece. Examinando los registros del
servidor, puede determinar rápidamente todo aquello que resulta necesario e
innecesario.
Firewalls
Los firewalls ofrecen servicios o interfaces muy específicos entre los clientes
externos y los servidores internos. Los servicios son muy críticos para la seguridad
de la red interna. A menudo, los firewalls no están visibles al cliente y puede actuar,
de forma transparente, con ios servidores.
El papel principal a desempeñar por el firewall es el filtrado. Los algoritmos en el
firewall proporcionan dos formas de filtrado: pasivo y activo. El filtrado pasivo
examina sólo el direccionamiento de cada mensaje. Un buen firewall oculta la
dirección de cada servidor interno y obliga a realizar la traducción de todas las
direcciones. Adicionalmente, si se desea, el firewall puede reasignar puertos. El
filtrado pasivo actúa como un buffer o barrera inicial entre clientes y servidores.
El filtrado activo examina con detalle los paquetes e intenta determinar si
contienen comandos comprometedores u otras discrepancias. Este filtrado necesita
conocer los servicios disponibles en los servidores. For ejemplo, las conexiones
FTP requieren medidas de seguridad diferentes a las utilizadas en el caso de
HTTP,
De forma adicional, los servidores dentro del firewall pueden utilizar direcciones
IP no registradas. Por ejemplo, un servidor interno podría publicar utilizando estas
direcciones falsas mediante DHCP. A continuación, el firewall utiliza una máscara
que permite traducir estas direcciones (filtrado pasivo). Los clientes no podrán
iniciar las comunicaciones sin las direcciones reales.
NOTA
359/512
efectividad.
Las DMZ definen una forma sencilla y efectiva de retrasar la intrusión. Un ataque
puede romper un firewall sólo para descubrir otro muro. El espacio de red más
interno se correspondería con la intranet de la compañía, que contiene la
información crítica y privada. Nuestros antepasados ya probaron que esta filosofía
incrementó la seguridad a su favor, retrasando los asaltos, mejorando la detección
e incrementando la protección.
Algunos firewalls sencillos colocan los servidores sin conexión en la intranet
(véase la Figura 16.1 >. Es completamente seguro puesto que no existe una ruta a
los datos sensibles. Esta técnica presenta dos problemas fundamentales: ¿cómo
actualizar el servidor y cómo mantiene la compañía la información interna
sincronizada?
Una solución es transportar la información actualizada al servidor. Los
ingenieros llaman a esta posibilidad "sneakernet." Estos servidores limitan
realmente las posibilidades de acceso a las bases de datos, herramientas e
información dinámica que aparece normalmente dentro de la intranet. Las zonas
desmilitarizadas más avanzadas integran el acceso de la compañía a Internet, los
servicios a los clientes externos y la intranet.
DMZ colocan más de una barrera entTe Internet y la intranet (véase la Figura
16.2).
FIGURA 16.1 Un servicio básico de Internet coloca los servidores fuera del ámbito
de los firewall.
360/512
FIGURA 16.2 Una DMZ requiere conductos a través del firewall para pasar la
información.
Para obtener acceso al exterior de una DMZ, debe establecer reglas
especificando aquello que podrá entrar en la zona desmilitarizada. Los firewalls
proporcionan, en muv pocas ocasiones, muchos servicios. Realmente, son los
servidores que se encuentran detrás del firewall los que ofrecen estos servicios. No
obstante, si expone al exterior demasiadas cosas de su DMZ, olvídese de la DMZ y
salvaguarde su dinero. Las reglas de conducta se hacen (por necesidad} más
flexibles a medida que se acerca a la intranet. La siguiente lista ofrece algunas de
estas reglas habituales:
361/512
intranet), pero esto también supone un riesgo importante de seguridad puesto
que la base de datos contiene normalmente nombres de usuario y contraseñas
no cifradas. Cuando los servidores de la DMZ necesiten acceder a la base de
datos, puede crear un generador de mensajes punto a punto que genere y lleve
a cabo las peticiones de regreso a la intranet. Este generador de mensajes
debería estar formado por programas compilados reduciendo la posibilidad de
pirateo.
Espionaje físico
Una medida física obvia es una conexión dedicada. Desafortunadamente y una
vez que se entiende el funcionamiento, esto no resulta tan seguro. La tecnología
utilizada para el espionaje no intrusivo en conexiones dedicadas es bastante
sencilla. Sin embargo, es un buen comienzo. El medio físico ofrece formas distintas
de ocultar (o publicar) sus datos. Cuanto más esotérica es la conexión, mayor será
el coste. Esta sección describe las cuatro formas de transmisión física básicas:
eléctrica-óptica y conductor-no conductor.
Los canales eléctricos basados en conductor (paT trenzado o coaxial) son,
quizá, los más sencillos de intervenir. Los cables flexibles son muy propensos a
pruebas detalladas en el conductor principal. Además, las propiedades de
electromagnetismo permiten a cualquiera realizar escuchas no detectadas. El canal
inflexible es más seguro y presenta menos problemas, pero resulta más caro y más
complicado.
Las transmisiones eléctricas (como la radio o las microondas) y ópticas (como
los infrarrojos o el láser dirigido) constituyen la forma más sencilla de intervención.
Simplemente necesita disponer de una antena en el campo de transmisión. Las
microondas constituyen un medio de transmisión dirigida similar al láser. Por otro
362/512
lado, las bajas frecuencias y los infrarrojos tienen tendencia a la difusión. Estas
formas de transmisión no pueden protegerse físicamente y tienen muy poca
seguridad.
Los canales ópticos son los más seguros. Sin embargo, hace muy pocos años,
los investigadores descubrieron que si doblaban el conducto flexible un ángulo
crítico, entonces la señal de luz podía romperse. De hecho, utilizaron esto para
probar que, incluso, la fibra óptica era susceptible de espionaje. En ese momento,
todavía no podían introducir una señal. Si los equipos de investigación no han
encontrado una solución para introducir una señal óptica, puede que sea
demasiado pronto. De nuevo, para resolver este problema, puede utilizar canales
de fibra más rígidos que no se doblen en los ángulos críticos.
Resumiendo, un canal rígido reduce la posibilidad de espionaje. El canal óptico
tiene la seguridad añadida de ser impenetrable para el espionaje electromagnético.
Además, permiten transportar señales con anchos banda muy altos.
Espionaje de mensajes
La protección de los mensajes es menos obvia. Los firewalls pasan y filtran la
información expuesta en Internet. Utilizan los puertos y servicios específicos que
usa el cliente. La modificación de algunas de las configuraciones o interfaces
esperadas puede incrementar su seguridad.
A pesar de no animar la tendencia del Código abierto (Open Source), el uso de
protocolos privatizados puede desalentar la intrusión (incrementando, por otro lado,
la atención). Por ejemplo, en lugar de usar HTTP como protocolo, invente una
versión nueva y especializada. Esta aproximación tiene inconvenientes
significativos: debe escribir la interfaz del cliente v distribuirla a sus clientes.
Otro método es limitar ventanas de disponibilidad y envío de información. Por
ejemplo, durante la Segunda Guerra Mundial, los Aliados y Americanos
conversaban sobre sus flotas aéreas durante horas en su propio lenguaje. Durante
intervalos específicos, el mensaje cambiaría a un estado o instrucción táctica o
estratégica.
Utilizando este ejemplo, podría escribir un mecanismo de transferencia
financiera que se active durante intervalos específicos o, incluso, tenerlo abierto
todo el tiempo con información relevante durante las horas de inactividad.
El incremento de la aleatoriedad es la clave de la seguridad en los mensajes.
Por ejemplo, suponga que tiene un cajero automático conectado a Internet (una
idea horripilante, por cierto). Necesita enviar y recibir peticiones de transacciones al
servidor. Una forma de utilizar la aleatoriedad sería enviando y recibiendo
continuamente falsas transacciones. A continuación, a intervalos específicos, tanto
el host como la máquina reconocerían que una transacción es real. (Por supuesto,
esto es todavía demasiado peligroso para la situación actual.)
Otra forma de aleatoriedad es el cifrado de datos. Una buena seguridad de los
datos depende fundamentalmente de la cantidad de aleatoriedad que introduce el
cifrado de datos. Cuanto más aleatorios sean los datos, más difícil será
descifrarlos. El conjunto de claves constituye el rango de posibles resultados. La
363/512
clave perfecta de cifrado no tiene límite de tamaño. Desafortunadamente, a medida
que aumenta la clave, aumenta exponencialmente el tiempo de computación. Por
tanto, una buena clave debe tener una longitud suficiente reduciendo la posibilidad
de descubrimientos desde el ámbito computacional. Ésta es la razón de utilizar, por
parte de todo el mundo, del cifrado d e 128-bits.
CIFRADO A TRAVÉS DE LOS FILTROS DEL FIREWALL
No puede considerar la idea de juntar algunas tecnologías con el objetivo de desarrollar
una defensa más poderosa frente a los ataques. Por ejemplo, el cifrado frustra el filtrado
activo de un firewall. Además, el filtrado activo analiza dentro de los paquetes todo aquello
que pueda resultar sospechoso. Desafortunadamente, si los contenidos están cifrados,
sólo el cliente y el servidor pueden conocer dichos contenidos (ojalá). Si tiene activado el
filtrado, con los mensajes cifrados sólo puede esperar un filtrado pasivo.
El cifrado de 128-bits tiene un rango lo suficientemente amplio, de forma que si
computadora intenta localizar las claves en serie cada nanoscgundo (1x10-9
segundos), aproximadamente podría tardar 1Í120 años en encontrar las citadas
claves. Realmente, un tiempo demasiado largo. Desde el punto de vista de la
aleatoriedad, el cifrado de 12S-bits tiene un rango suficientemente amplio y se
considera la clave perfecta, al menos por ahora.
El cifrado presenta dos formas: una-vía (con pérdida) y dos-vías (sin pérdida).
Un cifrado de una vía pierde información sobre los datos originales. Se trata del
cifrado habitual de las contraseñas de UNIX. El cifrado de una-vía no tiene que
recuperar ningún dato; por ejemplo, las contraseñas que escribe un usuario sólo
tienen que coincidir con la forma cifrada.
Las peticiones enviadas a un servidor requieren obtener los datos. El cifrado de
dos-vías reconstituye los datos permitiendo recuperar la información mediante la
utilización de una clave de descifrado. El servidor y el cliente deben conocer el
algoritmo de cifrado y las claves de cifrado-descifrado.
Cifrado de mensajes
El cifrado se conoce desde hace mucho tiempo. El dispositivo de cifrado más
conocido fue la Máquina de Enigmas Alemana. De hecho, algunos de los
algoritmos actuales se basan en este dispositivo de la Segunda Guerra Mundial. La
idea principal del cifrado es asegurar que sólo los receptores entenderán el
mensaje. Los algoritmos de cifrado modernos (encriptación) son tan habituales
como las computadoras.
Cuando dos hosts se comunican, los diseñadores suponen que otros hosts no
pueden espiar los mensajes. Un dispositivo de escucha de red puede abrir y
examinar todos los protocolos presentados en este libro. Por supuesto para
piratear cualquier mensaje, el dispositivo de escucha debe estar físicamente en la
misma red. Del mismo modo, este dispositivo tiene que leer una gran cantidad de
información no relevante antes de obtener los mensajes realmente importantes.
Esta sección describe las diferentes partes del cifrado e introduce un proceso de
negociación habitual en las computadoras.
364/512
¿Cuáles son los tipos de cifrado disponibles?
Las computadoras interconectadas utilizan dos cifrados diferentes: clave-pública
y cía ve-simétrica. El cifrado de clave pública utiliza dos claves, una de cifrado y
otra de descifrado. A menudo, un servidor utiliza este algoritmo para iniciar el
proceso de cifrado o, simplemente, para realizar transacciones.
El cifrado de clave simétrica utiliza la misma clave para cifrar y descifrar. El
descifrado simplemente deshace lo realizado por el cifrado (algo similar a tensar un
muelle). Dada la naturaleza del cifrado de clave simétrica, los dos hosts debe
mantener la clave en secreto. Los algoritmos son muy rápidos y relativamente
sencillos. Del mismo modo, puede utilizar cualquier número aleatorio para la clave.
Por otro lado, el cifrado de clave pública incorpora dos claves: una clave de
cifrado (pública) y otra de descifrado (privada). Los servidores comparten la clave
de cifrado con cualquier cliente de la red. (Esta clave no es crítica, realmente son
sólo los datos que traduce el algoritmo.) El servidor interno mantiene la clave de
descifrado para descifrar el mensaje de entrada.
Los cifrados de clave pública tienen algunas limitaciones. Primero, las claves
deben estar relacionadas. El host no puede generar, de forma aleatoria, la clave de
cifrado sin calcular también la clave de descifrado. Esto restringe, de forma
significativa, el número de claves disponibles. Por otro lado, sólo puede tener un
conjunto pequeño de pares de claves disponibles con respecto al conjunto
completo de números enteros dado que el número debe estar relacionado con el
cifrado y descifrado. Por ejemplo, con una clave de 128-bits, sólo puede tener 232
posibles pares de claves. Segundo, debido a que tiene un número limitado de
pares de claves (no dispone del rango completo de números), estos cifrados son
menos seguros que una clave privada. Por ejemplo, un cifrado público de 128-bits
puede sólo alcanzar la potencia de un cifrado simétrico de 64 o 32-bits.
365/512
Otro problema es el tiempo de computación. A pesar de que los cifrados
simétricos son relativamente rápidos, todas las comunicaciones iniciales deben
utilizar la clave pública y esto supone una bajada importante del rendimiento. La
clave pública no es muy adecuada para grandes bloques de datos. Por tanto, la
complejidad que incorpora la utilización del cifrado y las claves tiene que formar
parte de los protocolos.
El último problema es más raro. El cifrado actualmente invita a realizar aquello
que precisamente intenta evitar la piratería y el espionaje. Aparentemente, Internet
incorpora un sector de piratas que desean aparecer en los titulares de todas las
noticias.
Uso de OpenSSL
Puede utilizar una API SSL funcional, aunque no muy bien documentada,
denominada OpenSSL. Está disponible en www.openssl.org y admite diferentes
plataformas, incluido Linux, por supuesto. Para utilizar la API, necesita seguir unos
pasos de configuración, compilación e instalación.
No se suministra compilada y, por tanto, algunos de los pasos de instalación no
se integran bien con Mandrake o Red Hat Linux. (Otras distribuciones pueden tener
problemas similares y es posible que quiera tomar nota.). Los siguientes pasos le
ayudarán instalar la herramienta para su uso:
1. Descargue el paquete completo, ábralo en un directorio seguro (no en el
directorio root) y desplácese al directorio creado.
2. Ejecute el archivo config (,/config). Si lo solicita, debe especificar el tipo de
sistema operativo de forma explícita con ./config linux-elf.
3. Ejecute make para compilar los archivos fuentes-
5. Entre en el sistema como root. Ejecute make install para mover los archivos
necesarios a sus directorios correspondientes (/usr/local/ssl/).
366/512
6. Cree las referencias a bibliotecas:
ln -s /usr/local/ssl/lio/Ubssl.a /usr/lirj/
ln -s /usr/local/ssl/liD/libcrypto.a /usr/lib/
ln -s /usr/local/ssl/include/openssl/ /usr/include
8. Incorpore MANPATH /usr/local/ssl/man a su archivo /etc/man.config. (Puede
que quiera ejecutar makewhatis para realizar búsquedas por asunto.)
9. Incorpore en su ruta de acceso /usr/local/ssl/bin.
367/512
ctx = SSL_CTX_new(method); /* Crear el nuevo contexto */
Puede mostrar el error con el siguiente código siempre y cuando los valores de
salida de las llamadas a la API sean NULL o cero:
ERR_print_errors_fp(stderr); /* imprime los errores en stderr */
El siguiente paso es crear un socket normal.
/******************************************************/
/*** Connecta el socket del cliente al servidor SSL ***/
/******************************************************/
struct sockaddr_in addr;
struct hostent *host = gethostbyname(hostname);
int sd = socket(PF_INET, SOCKSTREAM, 0); /* crear el socket */
bzero(&addr, sizeof(addr)):
addr.sin_family = AF_INET;
addr.sin_port = htons(port); /* puerto del servidor */
addr.sinaddr.s_addr = *(long*)(host->h_addr); /* IP del servidor */
connect(sd, &addr, sizeof (addr)); /* connexión con el servidor */
Después de la conexión de los sockets entre cliente y servidor, necesita crear
una instancia SSL y asociarla a la conexión:
/*************************************************************/
/*** Establicer el protocolo SSL y crear enlace de cifrado ***/
/*************************************************************/
SSL *ssl = SSL_new(ctx); /* crear nuevo estado de conexión SSL */
SSL_set_fd(ssl, sd); /* adjuntar el descriptor del socket */
if ( SSL_connect(ssl) == 1 ) /* realizar la conexión */
ERR_print_errors_fp(stderr); /* informe de errores */
A partir de este momento, tiene una conexión SSL con cifrado completo
(dependiendo del proceso negociado entre el cliente y el servidor). Puede obtener
la suite de cifrado de la siguiente forma:
char* cipher_name = SSL_get_cipher(ssl);
Además, puede leer los certificados con las siguientes llamadas:
/*************************/
/*** Leer certificados ***/
/*************************/
char line[1024];
X509 *x509 = X509_get_subject_name(cert); /* Obtener asunto */
368/512
X509_NAME_oneline(x509, line, sizeof(line)); /* Convertirlo */
printf("Subject: %s\n", line);
x509 = x509_get_issuer_name(cert); /* obtener emisor de certificado "/
X509_NAME_oneline(x509, line, sizeof(line)); /* convertirlo */
printf("Issuer: %s\n", line);
Por último, los programas pueden enviar y recibir datos utilizando llamadas
similares a sendO y recv{). No obstante, estas llamadas presentan algunas
diferencias. Primero, no aparece el parámetro flags; segundo, si se produce algún
error, la función devuelve -1. La definición formal de sendO y recv() eslablece que
si se produce algún error, entonces el valor es negativo.
/*** Enviar y recibir mensajes **/
int bytes;
bytes = SSL_read(ssl, buf, sizeof(buf)); /* obtener/descifrar */
bytes = SSL_write(ssl, msg, strlen(msg)); /* cifrar/enviar */
La API tiene más funciones disponibles para controlar el flujo, modificar el
estado SSL y contiguración.
369/512
/***********************************************************/
/*** Cargar archivos de clave privada y certificado ***/
/************************************************************/
/* establecer el certificado local de CertFile */
SSL_CTX_use_certificate_file(ctx, CertFile, SSL_FILETYPE_PEM);
/* establecer la clave privada KeyFile */
SSL_CTX_use_PrivateKey_file(ctx, KeyFile, SSL_FILETYPE_PEM);
/* verificar la clave privada */
if ( ISSL_CTX_check_private_key(ctx) )
fprintf(stderr, "Key & sertificate don't match");
Además, los clientes tienen la opción de cargar los archivos de certificado,
aunque no es necesario. Puede decidir que su cliente tenga certificados si la
seguridad de los datos lo requiere.
NOTA
Normalmente, cuando navega por Internet, puede adquirir un certificado de
varios sitios, como VeriSign. No obstante, si está realizando simplemente una
prueba, puede crear sus propios certificados utilizando las herramientas de
OpenSSL. (No utilice el certificado suministrado por OpenSSL.) La herramienta
Perl CA.pl de creación de certificados aparece en el directorio
/usr/local/ssl/misc. Si no le pregunta una serie de cuestiones, entonces no ha
configurad o correctamente su ruta de acceso para que apunte a
/usr/local/ssl/bin.
El socket del servidor es esencialmente el mismo que un socket normal de un
servidor:
/********************************************/
/*** Establecer el puerto del servidor ***/
/********************************************/
struct sockaddr_in addr;
int sd, client;
sd = socket{PF_lNET, SOCKSTREAM, ffl); /* Crear el socket * /
bzero(Saddr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sinport = htons(port);
addr.sin_addr.s_addr = INADDR_ANY; /* permitir cualquier puerto */
bind(sd, &addr, sizeof(addr); /* enlazar a un puerto */
370/512
listen(sd, 10); /* preparar el socket para la escucha */
client = accept(server, &addr, &len); /* aceptar conexión */
Como en el caso del cliente, el servidor debe crear un estado de sesión SSL y
asociarlo con la conexión del cliente.
/********************************************************************************/
/* Crear estado de sesión SSL en función del contexto y SSL_accept */
/*******************************************************************************/
ssl = SSl_new(ctx); /* obtener nuevo estado SSL con el contexto */
SSL_set_fd(ssl, client); /* asociar el socket con el estado SSL */
if ( SSL_accept(ssl) == FAIL ) /* aceptar el protocolo SSL */
ERR_print_errors_fp(stderr);
else
{ int bytes;
bytes = SSL_read(ssl, b u f , sizeof( b u f ));/* obtener petición */
SSL_write(ssl, reply, strlen(reply)); /" enviar réplica */
}
Los pasos son realmente muy sencillos. Pruebe en el sitio Web y conéctese con
Netscape. Debido a que Netscape no reconoce su certificado personalizado, el
programa presentará un mensaje de advertencia junto con diferentes cuadros de
diálogo que permiten registrar temporalmente el certificado.
371/512
implementación de la API. En este momento, no existe ninguna API estándar para
SSL. Sin embargo, puede obtener una referencia de uso de SSL en C con una
versión Open Source denominada OpenSSL. OpenSSL es una implementación
muy rica con más de 20Ü llamadas API.
Puede crear un recurso seguro, fiable y efectivo para Internet siempre y cuando
desarrolle una buena planificación y previsión mediante el uso de herramientas
estándar tales como OpenSSL.
El siguiente capítulo le introduce en el otro extremo de la compartición de
información con multidifusión y difusión. Se trata de intentos de seguridad para
garantizar la privacidad. La multidifusión y difusión intentan compartir tanto como
sea posible y de la forma más eficiente posible.
372/512
Capitulo 17
En este capítulo
Difusión de mensajes a un dominio
Multidifusión de mensajes a un grupo
Resumen: compartición eficiente de los mensajes
Una red es un muy buen medio para enviar un mensaje de un lugar a olro. Ésta
dispone de un amplio rango de tecnologías v anchos de banda que le permiten
intentar cosas diferentes. Una ventaja que no puede considerar como algo positivo
es el hecho de que todas las computadoras suelen estar conectadas a un
backbone de algún tipo. Ahora, ¿cómo podríamos hacer que una computadora
conectada al mismo backbone nos aportara alguna ventaja? Lo normal es que el
trafico sea grande, por lo que lo mejor sería una conexión dedicada de bosta host.
Algunos servicios pueden dirigirse hacia varios destinos de forma simultánea.
En lugar de enviar el mensaje a cada host, podría enviar un mensaje y hacer que
todos los hosts simplemente lo tomen. Los aspectos positivos de esto son obvios:
un tráfico menos redundante.
El Protocolo Internet ofrece dos formas de enviar mensajes: la difusión y la
multidifusión. Este capítulo discute los distintos tipos de distribución de mensajes y
la forma de implementarlos.
373/512
Repaso de la estructura IP
La subred es muy importante para el envió y recepción de mensajes de difusión.
Como se describió en el Capítulo 2, "Elocuencia del lenguaje de red TCP/IP",
cuando define una interfaz con ifconf ig, también define una dirección de difusión y
una máscara de red. La dirección de difusión es una dirección especial que el host
escucha para obtener mensajes.
La subred suele estar relacionada con la máscara de red y la dirección de
difusión. Por ejemplo, si tiene unos 250 hosts en su subred 198.2.56.XXX, su
máscara de red y difusión deberían ser 255.255.255.0 y 198.2.56.255. De esta
forma, si desea enviar un mensaje a alguien de su subred, debería utilizar la
dirección 198.2.56.255 (Como se indicó en el Capitulo 2, puede ser muy creativo
con las máscaras de red. Recuerde que el último bit significativo constituye el final
de la máscara.)
Realmente una subred es un wrapper de la implementación hardware. El trabajo
real tiene lugar en los niveles más bajos del hardware y en el núcleo. Para
entender lo que está ocurriendo, necesita comenzar por la conexión física.
Más allá de la conexión física, el envío de un mensaje de difusión es más
sencillo que la recepción. Para enviar un mensaje, sólo necesita la dirección IP
(como se describió en el Capítulo 2, la red convierte esa dirección en la dirección
Ethernet MAC de destino). Para obtener un mensaje, la tarjeta de red tiene que
estar escuchando la dirección MAC correspondiente. El problema es que los
mensajes de difusión no pueden considerar una dirección MAC distinta para cada
host De hecho, como se indicó en el Capítulo 2, dos hosts de la misma subred no
tienen por qué tener direcciones MAC similares. De esta forma, la difusión no
puede depender de ningún MAC especializado para transmitir los mensajes de
difusión.
En su lugar, cuando un programa envía un mensaje de difusión, el núcleo asigna
automáticamente una dirección MAC a todos (FF:FF:FF:FF:FF:FF). Este MAC es un
indicador para que todas las NIC (tarjetas de red) lo tomen (incluso en el caso de
que ningún programa esté esperando un mensaje de difusión).
La NIC abre las puertas y permite que el mensaje se copie en sus buffers
internos. Cuando termina, lo notifica al núcleo con una interrupción. El núcleo
recupera el paquete y observa la IP de destino. Si la dirección coincide con la
dirección de difusión, pasa el paquete a las colas del subsistema de red.
El subsistema de red (normalmente UDP) examina el mensaje y, sí encuentra un
socket de difusión con un número de puerto coincidente, traslada el paquete al
canal de E/S del socket. Si no, descarta el paquete. Puede que desee especificar
un número de puerto para indicar al núcleo que filtre los mensajes no deseados. En
otro caso, puede provocar una tormenta de respuestas indeseadas.
374/512
datagramas.
/***************************************************/
/*** Crea un socket datagrama de difusión ***/
/***************************************************/
const int on= 1 ;
s d = socket(PF_INET, SOCKJ.GRAM, 0);
if( setsockopt(sd, SOLSOCKET, SC_BROADCAST, &on, sizeof(on)) != 0 )
panic("error al definir la difusión");
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if ( bindfsd, &addr, sizeof(addr)) != 0 )
panic("error al enlazar");
addr.sinport = htons(atoi(strings[2]));
if ( inet_aton(strings[ 1 ], &addr.sin_addr) == 0)
panic("error en inet_aton (%s)", strings[1]);
wait(0);
375/512
Limitaciones de la difusión
Los mensajes de difusión tienen algunos problemas. Aunque tienen la
posibilidad de alcanzar muchas computadoras a la vez con un único mensaje, sólo
puede acceder a aquellas que se encuentren dentro de su subred. Aún así, puede
incrementar su rango de influencia modificando su dirección de difusión.
Desafortunadamente, no puede utilizar la difusión en una WAN {Red de área
amplia) puesto que la dirección 255.255.255.255 no puede ser válida en Internet.
(Puede incluso encontrarse con que la mayoría de los routers no permiten difundir
mensajes a cualquier sitio. De esta forma, incluso si tiene la dirección de difusión
correcta, digamos 198.2.255.255, el router de 198.2.1.0 puede ignorar el paquete.)
Adicionalmente, toda la subred escucha su mensaje. Debido a la forma en que
el hardware implementa la difusión, todas las tarjetas de red toman los mensajes
de difusión. Lo que puede colocar mucha carga innecesaria en los hosts que no
desean el mensaje.
Otro problema es el protocolo. IPv4 sólo soporta la difusión sobre datagramas
(se excluye TCP). Si quiere fiabilidad y generación de flujo, debe implementar el
suyo propio. Los datagramas prestan un buen servicio en muchas circunstancias y,
con un poco de refuerzo, puede proporcionar una buena interfaz a la que puedan
conectarse los clientes.
• Soporte para IPvó. IPvó mermó el soporte para difusión, pero ha adoptado y
extendido la multidifusión.
376/512
ACTIVACION DEL SOPORTE PARA MULTIDIFUSION
La mayoría de las distribuciones incluyen núcleos que tienen activa la multidifusión. (En
/proc/net/dev_mcast puede comprobar su núcleo de ejecución actual.) Sin embargo,
puede que no esté habilitado el enrutamiento de multidifusión, o si está utilizando un
núcleo descargado, puede que no tenga la opción definida. Si desea utilizar la
multidifusión o administra un router para otra gente que desea utilizar la multidifusión,
puede que necesite reconfigurar y recompilar el núcleo. Las configuraciones se
encuentran en la sección Networking (Red) de la configuración.
Grupo 0 224.0.0.0-224.0.0.255
377/512
Tiene el mismo formato que el campo sin_addr de la estructura sockaddr_in. El
campo imr_interface permite elegir una interfaz para el host. es similar a un bind(),
que permite especificar la interfaz del Jiosf (o dejar la selección abierta con un valor
INADDR_ANY). Sin embargo, puede que no funcione de la forma esperada.
ESTABLECIMIENTO DE UNA INTERFAZ DE
MULTIDIFUSIÓN POR DEFECTO
Cuando asigna el valor INADDR_ANY al campo imr_interface, el núcleo selecciona la
interfaz. Al menos con el núcleo de Linux 2.2.XX, esto no significa que "escuche todas las
interfaces". De esta forma, si tiene múltiples interfaces, puede que tenga que unirse a
todas las interfaces sobre las que quiera escuchar.
El siguiente fragmento de código muestra la forma de unirse a un grupo usandc
la estructura ¡p_mreq. Éste define el campo imr_interface como INADDR_ANY a
modo de demostración. No lo utilice a menos que tenga sólo una interfaz en su
host, los resultados pueden ser imprevisibles (véase el recuadro anterior).
/**************************************************************/
/*** Establece una unión a un grupo de multidifusión ***/
/**************************************************************/
const char *GroupID = "224.0.0.10";
struct ip_mreq mreq;
if ( inet_aton(GroupID, &mreq.imr_multiaddr) == 0 )
panic("dirección (%s) errónea", GroupID);
mreq.imr_interface.s_addr = INADDR_ANY;
if ( setsockopt(sd, SOL_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof (mreq)) != 0 )
panic("Error en la unión de multidifusión");
El número de grupos a los que se puede unir depende del hardware y de lo;
límites del núcleo. Muchos sistemas operativos UNIX limitan las conexiones a M: por
host. Eso sí, no por programa. Véase la sección "Cómo la red proporciona la
multidifusión", más adelante en este capítulo, para más información.
Cuando esté preparado para escuchar el puerto, puede dar de baja su afiliación:
378/512
/********************************************************/
/*** Se da de baja en un grupo de multidifusión ***/
/********************************************************/
if(setsockopt(sd,SOL_IP,IP_DR0P_MEMBERSHIP,Smreq,sizeof(mreq))!=0)
panic("Error al darse de baja del grupo de multidifusión");
También puede simplemente finalizar el programa, siendo el propio programa el
que se encarga de limpiar las conexiones.
LIMPIEZA DE LA MULTIDIFUSION
Puede que desee darse de baja del grupo de multidifusión de forma manual. Ésta es una
de las pocas ocasiones en las que el proceso de limpieza automático no es una buena
idea. Si el núcleo tiene algún problema, no aparecen errores cuando simplemente termina
el programa. Además, podria estar recibiendo mensajes después de que su programa
haya terminado. Esto suele dejar los puertos no disponibles o desconfigurado., para las
instancias nuevas del programa. El cierre manual permite al programa ver los errores en
la conexión, ayudando a la depuración.
La unión a un grupo de multidifusión es muy sencilla. De hecho, la creación de
un grupo de multidifusión local sólo requiere la selección de una dirección IP no
utilizada. Para las direcciones globales es distinto. Si desea publicar un servicio de
multidifusión en Internet, puede que tenga que solicitar una dirección (y un puerto)
a la IAB.
379/512
Cómo la red proporciona la multidifusión
La forma en que los sistemas operativos (como Linux) implementan la
multidifusión puede conducir a un bombardeo de mensajes. El conocimiento de su
fundo namiento puede ayudarle con las consideraciones de rendimiento y con la
forma de filtrar mensajes rápidamente.
Muchas NIC soportan una forma de solapamiento MAC (necesitando una
identificación adicional a corto plazo). Todas las NIC tienen asignadas direcciones
MAC. A diferencia de la difusión, que utiliza una dirección de difusión (FF:FF:FF:
FF:FF:FF), la multidifusión crea una dirección MAC temporal utilizando la dirección
de multidifusión como clave. El prefijo estándar para los MAC de multidifusión es
01:00:5E, y los tres últimos bytes (realmente 23 bits) proceden de la dirección de
multidifusión. Por ejemplo, una dirección de multidifusión 224 .138. 63.10 (E0:8A:
3F:0A) se transforma en una dirección MAC 01:00:5E:0A:3F:0A (observe que se
descarta el 8 de 8A).
El sistema operativo toma esta dirección y programa la JSJ1C para que la
acepte. La NIC acepta esta programación de tres formas:
• Clave hash (filtrado incompleto). La NIC divide el MAC en una array hash de 64
a 512 bits. Cuando un mensaje pasa a través de la red, la NIC divide esa
dirección MAC. Si el bit correspondiente está en el array, la NTC acepta el
mensaje. Este enfoque puede aceptar mensajes no deseados por el host. Por
tanto, el siguiente nivel es el responsable del filtrado de los mensajes no
deseados.
380/512
Puede que tenga que realizar otras tareas para la salida de los mensajes. Esta
sección discute estos aspectos y la forma de resolverlos.
Lo que deben saber los routers
Internet intenta obtener un mensaje de un origen a un destino utilizando dos
direcciones: la dirección IP y el MAC. El MAC no suele conocerse antes del envío
del mensaje, y la dirección IP presenta un enrutamiento incorporado. Pero los
routers esperan sólo un destino para un mensaje determinado (la inclusión de dos
destinos para una única dirección suele constituir un error).
La multidifusión requiere un par de características de red poco usuales. Estas
características inusuales influyen en la configuración de los routers de soporte. En
primer lugar, los routers deben aceptar los MAC generados. Esto realmente supone
un cambio en las responsabilidades del router y puede requerir configuraciones de
routers adicionales.
Con la creación de un protocolo de multidifusión, los estándares de Internet
propusieron el 1GMP (Internet Group Management Protoco}, Protocolo de
administración de grupos de Internet). En adelante, Protocolo IGMP [RFC1112,
RFC22361 para pasarlo a las divulgaciones de los grupos de multidifusión. El
protocolo IGMP porta los identifica dores y MAC del grupo al que los Jiosfs de los
subdominios desean unirse o descartar. El mensaje de unión sólo se pasa hasta
donde permite el ámbito. Aunque ningún ftosf que se encuentre detrás de un router
se una al grupo, el router tiene que continuar realizando un seguimiento de los
grupos disponibles. (Si el router se viene abajo, no se pierde la información.
Cuando el host interno se une a un grupo, el router comienza a reconstruir la lista
de grupos.)
Otra carga asociada a los routers define la exclusividad del protocolo nuevo.
Aunque todos los suscriptores del grupo vean cada mensaje, puede que no vean
exactamente el mismo mensaje (véase la sección "Limitaciones de la
multidifusión"). El router debe pasar todos los mensajes de difusión y multidifusión
a todos los subdominios a los que se encuentre unido. Esto puede requerir una
copia implícita o explícita del mensaje.
Un router copia un mensaje a dos o más conexiones diferentes. Los routers
suelen estar conectados a varias redes y se encargan de encaminar el tráfico entre
ellas. Una copia explícita coloca el paquete en más de una de esas redes.
La copia implícita se debe a la naturaleza de la red. Todos pueden ver todo lo
demás (en el mismo subdominio) puesto que están físicamente conectados al
mismo cable. Una copia implícita ocurre cuando más de un hosf captura un
mensaje cuando lo ve en la red.
Tunneling a través de firewalls (Mbone)
Algunos routers o firewalls no necesitan el soporte de implementación para
grupos de multidifusión, de forma que un host del subdominio no debería estar
capacitado para ver los mensajes procedentes del exterior. Un router que sólo
soporta mensajes directos (o de unidifusión) es un router de unidifusión.
381/512
Poco antes de 1992, los programadores pretendían sacar partido a las
características de multidifusión más novedosas. Desafortunadamente, la red
estaba llena de routers de unidifusión. La respuesta era encapsular el protocolo
nuevo en un wrapper IP y enviarlo directamente a un receptor que debería
desempaquetarlo y pasarlo. Este backbone de red virtual para multidifusión
(Mbone) puede utilizarse también para firewalls.
¿Cómo funciona esto? El Mbone utiliza un router de multidifusión (mrouted) para
aceptar y pasar mensajes a través del dominio extemo a otro servidor mrouted. El
servidor mrouted acepta cada paquete de mensajes completo y lo coloca en otro
paquete con su propia cabecera. El resultado es un mensaje con dos cabeceras IP.
En el otro extremo, el servidor mrouted receptor desempaqueta el mensaje y
coloca el resultado en su subdominio.
Limitaciones de la multidifusión
La multidifusión tiene muchos aspectos en común con la difusión, pero también
tiene sus propias características distintivas. Algunas de ellas son el soporte
hardware, los cuellos de botella de rendimiento-ancho de banda, la responsabilidad
y la exclusividad.
382/512
Responsabilidad para todos los mensajes
El último aspecto es la responsabilidad; cualquiera puede enviar un mensaje
tanto si es propietario o forma parte de un grupo de multidifusión como si no es así.
Esto puede resultar en la interferencia de los que deseen perjudicar la efectividad
de Internet.
383/512
Capitulo 18
En este capítulo
¿Cuándo se deben usar sockets raw?
¿Cuáles son las limitaciones?
Cómo poner los sockets raw a funcionar
¿Cómo opera pine,?
¿Cómo opera traceroute?
Resumen: toma de decisiones raw
Explicación de ICMP
Con el uso de sockets raw se abren los protocolos de gestión de mensajes (de
error). Los protocolos de alto nivel, como TCP y UDP, cierran la posibilidad de
envío de paquetes ICMP. Además, la API de IP no ofrece un SOCK_M5G (similar a
50CK_5TREAM o SOCK_DGRAM). Para enviar paquetes del estilo SOCK_M5G,
tendría que crear un paquete raw, crear y rellenar la cabecera ICMP, y enviarlo.
384/512
Los comandos ICMP ofrecen varios servicios como se detalla en el Apéndice A,
"Tablas de datos". Algunos de los comandos útiles incluyen eco (utilizado en ping),
marca de tiempo, consulta de router, y máscara de dirección. Por supuesto, estas
funciones son todas de bajo nivel. ICMP está diseñado sólo para administrar las
tareas más simples de envío de mensajes.
• Sin puertos. Se pierden los puertos (conexiones de red virtuales). Esto puede
ser realmente un problema. Puesto que al no tener puertos, el núcleo pasa un
385/512
paquete raw a todos los sockets raw iguales. En otras palabras, el núcleo no
puede determinar el destino real si más de un socket raw tiene el mismo
número de protocolo. El ejemplo MyPing, incluido en este capítulo mas
adelante, demuestra este problema.
• No funcionan los TCP y UDP raw. No se puede esperar que un TCP o UDP ra w
funcione. Se puede establecer el parámetro de protocolo a cualquier cosa que
se desee, incluso a TCP (6) o UDP (17), pero todos los sistemas operativos no
distinguen el socket recibido. En otras palabras, se puede crear un socket raw
UDP, construirlo e iniciar la cabecera UDP, y enviar el mensaje, pero no se
espera ninguna respuesta.
386/512
struct protoent* proto;
int sd;
proto = getprotobyname("ICMP");
sd = socket (PF_SOCKET, SDCK_HAW, proto ->p_proto);
Como se mencionó anteriormente, ICMP no tiene su propia constante para el
segundo parámetro de socket(), así que utilice SOCK_RAW con el valor de
protocolo apropiado.
387/512
unsigned int sum=0;
for ( sum = 0; len > 1; len -= 2 ) /* Sumar todas las "/
sum += *buf++; /" palabras de 16Q. */
íf { len == 1 ) /* Si existe un byte de acarreo, */
sum += *(unsigned char*)buf; /* añadir a la suma. "/
sum = (sum >> 16) + (sum & 0xFFFF); /* Añadir el acarreo */
sum += (sum >> 16); /* (otra vez). */
result = -sum; /* Tomar el complemento a uno. */
return result; /* Devolver un valor de 16 bits. */
}
Tráfico de terceros
La manipulación directa de la cabecera IP permite poner en práctica algunas
habilidades muy inusuales. Una es el tráfico de terceros de bajo nivel. Suponga
que tiene una red de tres servidores—el origen, el destino y un intermediario. El
intermediario acepta mensajes del origen, realiza algo de procesamiento y
388/512
entonces pasa el resultado al destino. Esto- se llama tráfico de terceros. En
muchos ejemplos, el paquete enviado al intermediario incluiría el origen v destino
final (junto con su propia dirección).
Ahora tiene que interactuar con un sistema heredado que no entiende que la
respuesta debe enviarse a un hustdistinto aparte del origen. Muchos protocolos
distintos a TCP responden al campo IP origen de la cabecera IP. Al manipular la
cabecera IP puede establecer el campo IP origen a una dirección de terceros en
vez de la del origen.
EL TRÁFICO DE TERCEROS PUEDE PONER A PRUEBA LA
MORALIDAD
La posibilidad de configurar el campo IP origen como se desee es muy poderosa e
inusual y raramente necesario. La mayoría de las veces, los programadores que usan
esta característica están intentando realmente engañar a la red, el engaño de los hosts
dentro del mensaje final vino de algún otro lugar. Quienes intentan el spoofing de red se
ponen en riesgo de quebrantamiento de las leyes locales, nacionales e internacionales.
Primero, los ISP pueden (y deberían) detectar fácilmente el spoofing de
paquetes a través del rastreo de los mensajes y la verificación de los campos IP
origen. Segundo, Linux no permite que los paquetes engañados sean enviados
porque rellena el campo origen IP. Y tercero, los ISP desechan normalmente estos
paquetes de "origen encaminada".
El receptor MyPing
El receptor es simple. Después de la creación del socket raw, repetidamente
llama a recvfrom(), como se muestra en el Listado 18.2.
Listado 18.2 Bucle del receptor MyPing
/***************************************************************************/
/* Receptor MyPing - Obtiene el mensaje pendiente y los muestra.*/
/*** De MyPing.c en el sitio web. ***/
/***************************************************************************/
for (;;)
{ int bytes, len=sizeof(addr);
389/512
bzero(buf, sizeof(buf)); /* Vacia el buffer y coge el msg. */
bytes = recvfrom(sd, buf, sizeof(buf), 0, &addr, &len);
if ( bytes > 0 ) /* Si no hay error, */
display(buf, bytes); /* comprobar el ID y mostrar. */
else
perror("recvfrom");
}
El emisor MyPing
El emisor tiene un poco más de trabajo que hacer. Junto con la preparación y el
envío del mensaje, tiene que aceptar cualquier mensaje espúreo que el núcleo
elige colocar en su cola (véase el Listado 18.3).
Listado 18.3 Bucle del emisor MyPing
/****************************************************************************/
/* Emisor MyPing - Obtener y lanzar cualquier mensaje pendiente, */
/*** componer y enviar el mensaje, y hacer una pausa. ***/
/*** De MyPing.c en el sitio web. ***/
/***************************************************************************/
for (;;)
{ int len=sizeof(r_addr);
/* Obtener cualquier mensaje que el núcleo puede haber enviado.*/*/
if ( recvfrom(sd, Spckt, sizeof(pckt), 0, firaddr,
&len) > 0 )
printf("***Got message!"**\nM);
/*---Iniciar el paquete saliente---*/*/
bzero(&pckt, sizeof(pckt)); /" Contenido a cero. */
390/512
pckt.hdr.type = ICMP_ECHO; /" Pedir eco. */
pckt.hdr.un.echo.id = pid; /* Establecer ID. */
for ( i = 0; i < sizeof(pckt.msg)-1; i++ )
pckt.msg[i] = i+'0'; /" Rellenar el buffer. */
pckt.msg[i] = 0; /* Realizar la compatibilidad con C-string. */
pckt.hdr.un.echo.sequence = cnt++; /* Establecer el contador. */
pckt. hdr. checksum = /* Calcular la suma de comprobación. */
checksur(&pckt, sizeof (pckt));
if ( sendto(sd, &pckt, sizeof(pckt), 0, addr, /* ¡ENVIAR! */
sizeof (*addr)) <= 0 )
perror("sendto");
sleep(1); /* Esperar un segundo. */
}
El restablecimiento del mensaje a los mismos valores no es completamente
necesario pero es preventivo. Cuando el destino responde, el emisor obtendrá una
copia del mensaje. Si no consigue el mensaje (y cualquier otro mensaje ICMP), el
núcleo acaba apartando cada vez más memoria para los mensajes no deseados.
Para que el algoritmo funcione correctamente, el programa tiene que cambiar
algunas de las configuracione5 por omisión del socket. Primero, muchos programas
ping establecen el TTL (tiempo de vida) al valor de salto más alto (255). Adicional-
mente, el socfcef del emisor debe tener habilitada la opción de no bloqueo al llamar
a recvfrom{) para que no espere.
El programa MyPing es un punto de partida para la mayoría de los gestores de
mensajes de paquetes al estilo de ICMP. Después de que se acostumbre a este
algoritmo, otros programas socket raívson similares a la programación de
datagramas.
Otro tipo de programa de socket raw de estilo ICMP es traceroute.
Probablemente ha usado traceroute para entender cómo es el camino de un host
visitante o de un mensaje basura. El programa se fundamenta en el programa
MyPing porque utiliza mensajes ICMP.
El algoritmo básico es enviar un mensaje ping al destino con un TTL insuficiente.
El router intercepta el mensaje caducado y envía un error de regreso. Se repite con
un TTL mayor hasta que el mensaje llegue al destino. El resultado es realmente
muy ingenioso: el rourer entre el origen y el destino descubre que el campo TTL ha
caducado, así que envía un error (mensaje ICMP) de vuelta al origen. El router
coloca su dirección en el mensaje de error. Sólo un socket ICMP raw funciona aquí
porque los mensajes de error no se pueden obtener de los routers.
El bucle es similar al proceso del emisor de MyPing. El programa necesita un
poco de programación adicional para interpretar las respuestas:
391/512
TTL = 0;
do
{ int len=sizeof(r_addr);
struct iphdr *ip;
TTL++;
392/512
Resumen: toma de decisiones raw
El socket raw ofrece la capacidad de funcionar con los protocolos de gestión de
mensajes de error (ICMP) que utiliza el subsistema IP. La ventaja principal de los
sockets raw es su gran velocidad, ya que sólo es una capa de abstracción sobre la
trama de red física.
De los programas socket raw anteriores, los más comunes son ping v
traceroute. Cuando se entiende cómo funcionan éstos y los riesgos que conlleva la
gestión de mensajes datagrama, se puede elegir el mejor método que reúna los
requisitos del programa.
393/512
Capitulo 19
IPv6: la próxima generación
de lP
En este capítulo
Internet contiene actualmente la mavor parte del tráfico de red del mundo. Tiene
una gran cantidad de potencia y flexibilidad. Basa su potencia y flexibilidad en el
paquete IP. Este paquete facilita la obtención de mensajes de aquí a allá. Sin
embargo tiene ciertas limitaciones, limitaciones basadas principalmente en el
crecimiento y extensibilidad.
En este capítulo se profundiza en la próxima generación de IP: IPng ("IP Next
Generation") o IPvó. Durante todo este libro, se ha trabajado con IPv4, la mayoría
de ese conocimiento es aplicable a IPvó. Sólo se necesita conocer unos pocos
consejos y trucos adicionales para asegurar una larga vida a los proyectos
creados.
394/512
El número de direcciones que se asignan y utilizan tiene alarmado al Comité de
arquitectura de Internet (IAB). El comité ha descubierto que menos del 1% de los
números asignados están realmente asociados con un nodo en la red. El resultado:
un suministro decreciente de direcciones disponibles.
IAB espera que el número de computadoras conectadas a Internet se
incremente en unas cien veces más dentro los próximos años. Simplemente IPv4
no tiene suficientes direcciones disponibles para responsabilizarse de la demanda.
FIGURA 19.1
395/512
La dirección IPv6 está dividida dentro de cinco partes principales. Estas partes
identifican la naturaleza de la dirección, el enrutamiento y el host.
La Figura 19.1 tiene cinco partes: ID de asignación, ID TLA, ID NLA, ID SLA e ID
de interfaz. La Tabla 19.1 define la finalidad de cada uno de ellos.
Tabla 19.1 Partes de la dirección IPv6
Campo Descripción
ID de asignación Esta es la señalización de 3 bits (001) que indica que ésta es una
dirección de Internet pública. (3 bits.)
ID TLA Agregación de alto nivel. Representa el nivel más alto del Internet
de todo el mundo. (13 bits )
Uno de los problemas que las redes tenían que resolver era el de "todos
conocen a todos", problema tratado anteriormente en el Capítulo 2. Este problema
fuerza a que todos los routers conozcan todas las direcciones disponibles para que
encaminen correctamente un mensaje. Esta es la razón de por qué las máquinas
no podían utilizar la dirección MAC.
IPv4 resolvió ese problema utilizando clases incorporada;, en la dirección IP de
red. IPv6 utiliza una forma más blanda. Tiene que resolver todavía direcciones en
las regiones TLA, NLA y SLA, pero no tienen que resolverlos a la vez. En vez de
eso, los routers trasladan el mensaje de un grupo al siguiente, basados sólo en una
parte de la dirección.
396/512
como corresponde. Pero, después de que se despoje de la envoltura IP, aparecerá
como un mensaje TCP o UDP. Si se programa con cuidado las aplicaciones, se
puede actualizar de IPv4 a IPv6 muy fácilmente.
Esencialmente, IPv4 es un subconjunto de IPvó. IPvó hereda todas las
características buenas de IPv4 y desecha las anticuadas. Las direcciones de IPv4
son reasignadas a una dirección IPv6. Para asignar la dirección, todos los bits
superiores se establecen a cero y los últimos 48 bits son OxFFFF más la dirección
IPv4. Por ejemplo, la transformación de 128.10.48.6 a una dirección IPv6, sería
::FFFF:128.10.48.6 o ::FFFF:800A:300b~ (de nuevoves la abreviatura para todos
ceros).
Por supuesto, la única limitación es que no puede funcionar al revés: una
aplicación IPv4 no puede aceptar directamente un mensaje IPv6. (Algunos
sistemas permiten el secciona miento de dirección, la cual trasforma la dirección de
128 bits dentro de una dirección temporal de 32 bits. Cuando la aplicación IPv4
acepta el mensaje IPvó, sólo ve la dirección seccionada. Cuando la aplicación
responde con la dirección temporal, el núcleo transforma el mensaje de regreso a
una dirección de 128 bits.)
397/512
un problema, puede utilizarlo para regresar al estado anterior que no tiene
problemas.
398/512
sd = socket(PF_INET8, SOCKSTREAM, 0); /* TCP6 */
/*---O esta opción---*/*/
sd = socket(PF_INET6, SOCK_DGRAM, 0); /* UDP6 */
/*---O esta opción.---*/*/
Sd = socket(PF_INET6, SOCK_RAW, 0); /* Raw-6 O ICMP6 */
En cada uno de estos ejemplos, puede observar que todo permanece igual excepto el tipo
de socket. Este es el objetivo de la interfaz socket() común. Los siguientes pasos cambian
muy poco:
addr.sin6_farriily = AF_INET6;
addr.sin6_port = htons(MY_PORT);
if ( inet_pton(AF_INET6, "2FFF::80:9AC0:351", &addr.sin6_addr) ==0 )
perror("inet_pton failed");
sockaddr_in6 tiene otros campos que el programa puede ignorar. Si establece esos
campos a cero (usando bzero() o memset()) todo debería funcionar bien. Asimismo, no
tiene nada más que hacer en el programa.
El fragmento de código anterior utiliza una llamada nueva, inet_pton(). Esta y su
llamada compañera, inet_ntop(), son nuevas y ayudan con las diversas formas de cálculo
de direcciones. La n, por supuesto, representa la red, y la p representa la presentación. Esta
llamada soporta muchos formatos de dirección, incluidos IPv4, IPvó, Rose, IPX y radio HAM.
La versión actual que la biblioteca GNU ofrece no está documentada y sólo soporta INET e
INET6.
El prototipo completo se declara como sigue: #include <arpa/inet.h>
int inet_pton(int domain, const char* prsnt, void* buf); char *inet_ntop(int domain, void* buf,
char* prsnt, int l e n ) ;
L a llamada inet_pton() transforma la dirección alfanumérica de prsnt al formato binario
ordenado de bytes de red y coloca el resultado en buf. La llamada inet_ntop() realiza lo
inverso. Los parámetros domain y len definen la red (AFJNET o AFJNET6) y la longitud del
array prsnt, respectivamente.
399/512
/********************************/
/ * * * Definición del paquete IPV6 . * * * /
/********************************/
unión IPv6 Address
{
unsigned char uS[16]; unsigned short int U16[8]; unsigned long int u32[4]; unsigned long
long int u64[2];
};
struct IPvGJHeader {
unsigned int version:4; /* La versión IP (6). */
unsigned int priority:4;
unsigned int flow_label:24;
unsigned int payload_len: 16; /* Bytes que siguen a la cabecera. */
unsigned int next_header:8; /* Protocolo (6 para TCP). */
unsigned int hop_limit:8; /* Lo mismo que TTL. */
unión IPv6_Address source;
unión IPv6_Address dest;
};
Sólo tres de los campos requieren una explicación. El campo prioríty es un
campo experimental que establece la prioridad del paquete. Si la red se
congestiona, los routers pueden mantener o desechar los paquetes de prioridad
baja. Por omisión, todos los paquetes tienen cero en el campo priority.
El campo flow_label es experimental también y funciona con priority. El flujo es
una secuencia de paquetes que se trasladan desde un origen a un destino a través
de una serie de routers. flow_label ayuda a los routers a determinar cualquier
gestión especial. Un mensaje tiene un único flowjabel; después de que sea
seleccionado, el valor del flujo no cambia para el resto de paquetes en el flujo.
Puede establecer priority y flow_label cuando enlaza la dirección del socket usando
el campo opcional sin6_flowinfo en sockaddr_in6.
El último campo, payload Jen, puede ser cero para indicar un paquete enorme.
Una sola red gigabit (o incluso 100 Mb) enviando paquetes pequeños (menos de
100 KB) es un desperdicio de ancho de banda. Establezca este campo a cero e
incluya un registro adicional entre la cabecera y la carga útil. Lo siguiente es la
definición de como aparecería en el orden de bytes de red:
400/512
/*********************************************************************/
/*** Definición de opción do paquete gigante (muy grande).***/
/*********************************************************************/
struct Jumbo_Payload
{
unsigned char option; /* Iguales a 194. */
unsigned char length; /* Iguales a 4 (bytes). */
unsigned long bytes; / * Cantidad de carga útil. * /
} ;
Con esta opción, puede enviar un paquete de hasta 4 GB. No es una opción
mala si se usa en un periodo corto de tiempo.
401/512
transitoria. Los otros tres bits están actualmente reservados. El otro subcampo
igualmente tiene cuatro bits e indica el ámbito de la dirección (una dirección local o
global). Cuanto más alto el número, más extenso el ámbito. Por otra parte, IPv4
utiliza TTL para determinar el ámbito (números bajos caducarían antes de alcanzar
las direcciones globales). Además, como se dijo en el Capítulo 17, !Pv4 tiene
reservados ciertos aloques de direcciones a ámbitos globales, lugares o locales. La
Tabla 19.2 define los diferentes ámbitos.
Tabla 19.2 Subcampo de ámbito del IPv6 multidifusión.
Ámbito Rango Descripción
1 Nodo Local en el mismo host (como 127.0.0.1),
2 Enlace Mensajes que permanecen dentro del grupo del router. Los
routers nunca permiten que estos mensajes pasen a través
de ellos.
{
struct in6_addr ipv6mr_nultiaddr;/* Dirección multidifusión IPV6. */
unsigned int ipv6mr_interface; /* Número de interfaz. */
};
402/512
interfaz (eth0), etc.
Para habilitar la generación de multidifusión IPvó, utilice el siguiente trozo de
código:
/******************************************************/
/*** Asociación a un grupo multidifusión IPv6. ***/
/******************************************************/
const char "GroupIQ = "FF02::4590:3A0";
struct ipv6_mreq mreq;
if ( inet_pton(GroupID, &mreq.ipv6mr_multiaddr) == 0 )
panic("address (%s) bad", GroupID);
mreq.ipv6mr_interface = 0; /" Cualquier interfaz. "/
if ( setsockopt(sd, SOL_IPV6, IPV6_ADD_MEMBERSHIP, &inreq, sizeof
(mreq))!= 0)
panic("Join multicast failed");
if ( setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0 )
panic("Can't reuse address/ports");
Como se puede ver, el código es muy similar al código de generación
multidifusión de IPv4.
403/512
cuantos bytes— el tamaño de paquete máximo de IPv4 es de 65.000 bytes y el
tamaño máximo de un paquete IPvó que no es gigante es de 65.535+40 bytes.
IPv6 ha desechado el soporte de generación de ditusión de IPV4. En realidad, no
es un problema. La generación de multidifusión tiene mayor flexibilidad y control
que la generación de difusión, y la generación de difusión no se utiliza realmente
en los proyectos modernos.
Por último, IPv6 no incluye un campo mayor de suma de comprobación. En
simplificación de la cabecera IP, han desechado algunos de los campos de datos.
Sin embargo, esto puede causar problemas con interfaces viejas que no incluyen
una suma de comprobación o CRC en la trama física. Las interfaces hardware más
nuevas hacen toda Ja integridad de los datos por el sistema operativo. Si tiene
hardware antiguo, puede comprobar si lo incorpora.
404/512
La red IPvó abarca a la red IPv4, así que todas las direcciones 1PV4 funcionan
todavía. Muchos sistemas operativos que soportan IPv6 pueden utilizar
probablemente una pila dual para soportar y traducir paquetes entre las dos redes.
El esquema de direccionamiento y la traducción dentro de estas pilas duales
permiten algunas colaboraciones implícitas—requiriendo poca reprogramación para
soportarlo.
El núcleo Linux, como 2.2.0, soporta IPv6, pero no todas las distribuciones del
sistema operativo instalan un núcleo compilado con esta característica habilitada.
De hecho, si la distribución no incluye un núcleo compilado con IPvó, la distribución
no tiene probablemente las herramientas actualizadas tampoco. En este capítulo
se describe cómo conseguir funcionar IPv6 en el host de Linux.
Adicionalmente, en este capítulo se demostró por ejemplo cómo traducir
programas ÍPv4. También se introdujeron dos nuevas herramientas para ayudar en
la transformación de la dirección. El soporte de IPv6 en los programas incrementa
la probabilidad de utilizar ese trabajo en el futuro.
405/512
Parte V
Apéndices
En esta parte
A Tablas de datos
B API de red
C Subconjunto API del núcleo
D Clases d e objetos
406/512
Apéndice A
Tablas de datos
En este apéndice
Dominios: primer parámetro de socket()
Tipos: segundo parámetro de socket()
Definiciones de protocolo
Asignaciones estándar de puertos de Internet (100 primeros puertos)
Códigos de estado HTTP 1.1
Opciones de socket (get/setsockopt())
Definiciones de señales
Códigos ICMP
Asignación de multidifusión IPv4
Asignación de direcciones IPv6 propuesta
Códigos ICMPv6
Campo de ámbito de multidifusión IPv6
Campo flag de multidifusión Ipv6
407/512
<bits/socket.h>.
Tabla A.1 Valores de familia de protocolos para el parámetro de dominio de socket()
Tipo Descripción y ejemplo Datos asociados
408/512
sipx_node[IPXNODELEN];
_u8 sipx_type;
/* padding */
unsigned char sipx zero;
};
409/512
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
#define s6_addr64 in6_u.u6_addr64
};
Struct sockaddr_in6 {
unsigned short int sin6_family;
_u16 sin6_port;
_u32 sin6_flowinfo;
struct in6_addr sinfi_addr;
};
PF_ROSE Amateur Radio X.25 typedef struct {
PLP char rose_addr[5];
#include <linux/rose.h> } rose address;
struct sockaddr_rose {
sa_family_t srose_family;
rose_address srose_addr;
ax25_address srose_call;
int srose_ndigis;
ax25_address srosedigi;
struct full_sockaddr_rose {
sa_family_t srose_family;
rose_address srose_addr;
ax25_address srose_call;
unsigned int srose_ndigis;
ax25 address
srose_digis [rose_max_digis];
};
PF_DECnet (Reservada para el proyecto DECnet) #define NB_NAME_LEN 20
410/512
PF_KEY API de administración de claves
PF_KEY
411/512
unsigned char type;
struct ec_addr addr;
unsigned long cookie;
};
412/512
SOCK_PACKET (Capa Física). Coloca el socket en modo promiscuo (si está
disponible), en el que recibirá todos los paquetes de la red. Se trata de
una herramienta específica de Linux. Sólo accesos al punto root.
(Desaprobado—utilice PF_PACKET en su lugar.)
Definiciones de protocolo
El Listado A.1 es un extracto del archivo /etc/protocols [RFC2292] de la
distribución. Contiene los estándares de protocolo usados habitualmente en el
paquete de red. No es aconsejable revisar este archivo.
Listado A.1 Archivo /etc/protocols
413/512
Asignaciones estándar de puertos de Internet
(100 primeros puertos)
El Listado A.2 muestra los puertos estándar (hasta el puerto N.'-' 100) definidos
en el archivo / etc/services. Puede cambiar muchas de estas asignaciones para
adecuarlas a sus necesidades, pero asegúrese de poner al corriente a los clientes,
si decide hacerlo.
Listado A.2 Archivo /etc/services
tcpmux 1/tcp # TCP port service multiplever
rtmp 1/ddp # Oouting Taüle Haintenance Protocol
npp 2/ddp # Name Binding Protocol
echo 4/ddp # AppleTalk Echo Protocol
zip 6/ddp # Zone Information Protocol
echo 7/tcp
echo 7/udp
discard 9/tcp sink null
discard 9/udp sink null
systat 1 1 /tcp users
daytime 1 3 /tcp
daytime 13/udp
net stat 15/tcp
qotd 17/tcp quote
msp 18/tcp # message send protocol
rnsp 18/udp # message send protocol
chargen 19/tcp ttytst source
chargen 19/udp ttytst source
ftp data 20/tcp
ftp 21/tcp
fsp 21/udp fspd
ssh 22/tcp # SSH Remote Login Protocol
ssh 22/udp # SSH nemote Login Protocol
telnet 23/tcp
#private 24
smtp 25/tcp mail
#unassigned 26
414/512
time 37/tcp timserver
time 37/udp timserver
rlp 39/udp resource # resource location
nameserver 42/tcp ñame # IEN 116
whois 43/tcp nicname
re-mail-ck 50/tcp # Remote Mail Checking Protocol
re-mail-ck 50/udp # Remote Mail Checking Protocol
domain 53/tcp nameserver # name-domain server
domain 53/udp nameserver
mtp 57/tcp # deprecated
bootps 67/tcp # BOOTP server
bootps 67/odp
bootpc 68/tcp # BOOTP client
bootpc 68/cdp
tftp 69/udp
gopher 70/tcp # Internet Gopher
gopher 70/udp
rje 77/tcp netrjs
finger 79/tcp
www 80/tcp http # worldWideWeb HTTP
www 80/udp # HyperText Transfer Protocol
link 87/tcp ttylink
kerberos 88/tcp kerberos5 krd5 # Kerberos v5
kerberos 88/udp kerberos5 krb5 # Kerberos v5
supdup 95/tcp
linuxconf 98/tcp
# reserved 100
415/512
Tabla A.3 Códigos HTTP
Valor Nombre de Código y descripción específica
Clase de Clase
código
1xx Informational 100 Continue
101 Swilching Protocols
2 xx Successful 200 OK
201 Created
202 Accepted
204 No Contení
401 Unauthorized
403 Forbidden
416/512
409 Cortflict
410Gone
417/512
SOL_SOCKET SO_LINGER Retrasar hasta Y Y YLinger struct
enviar datos linger
418/512
SOL_SOCK SO_TYPE Tipo de socket Y Y Inleger int
ET
419/512
S IPOPTI Opciones IP Y Y Y O int[ ]
OL ONS pci
JP on
es
S IP_PKT Habilitar obtener Y Y Y B int
OL INFO ool
JP ea
n
info. de paquete
420/512
Tabla A.6 Opciones socket de nivel IPv6
421/512
S0LJ IPV6_MULTI Especificar 7 T lnteger int
PV6 CAST_ HOPS número de saltos
multidifusión
7 7 7
S0LJ IPV6_MLIL Especificar Direc struct in6 addr
PV6 TICAST_IF interfaz ción
multidifusión IPvó
de salida
7 7 ■
S0LJ IPV6_MUL Habilitar > lnteg int
PV6 TICAST_ loopback er
LOOP multidifusión
7 7 7
S0LJ IPV6_NEX. Habilitar Boole int
PV6 THOP especificar an
siguiente
salto
7 7 7
S0LJ IPV6_PKTI Recibir lnteg int
PV6 NFO información er
de paquete
7 7 7
SOLJ IPV6_PKT Especificar Opci im D
PV6 OPTIONS opciones de ones
paquete
7 7 7
S0LJ IPV6_ROU Habilitar Boole int
PV6 TER_ALERT alertas de an
router
7 7 7
SCLJ IPV6_RXS Recibir Boole int
PV6 RCRT router de an
origen
7 7 7
S0LJ IPV6_UNIC Especificar lnteg int
PV6 AST_HOPS límite de er
saltos
Tabla A.7 Opciones socket de nivel TCP
Nivel Opción Descripción * R W Valor Tipo
SOL.TCP TCP_tC££PALIVE N lnteger ¡m
ife actividad
fíela rdo
-
(reemplazado por
vsc/i cal!)
Je retransmisión
422/512
de segmentn (butler
Je transmitió ni
MaKlc
(reemplazado por
*VSCtl CilIFí
■.cemento- parcial-
mente completos
actividades
actividades
misiones de SYN
HN-WAIT-2
huérfano
lleguen datos
423/512
SOL TCP TCP. WIN DOW Unir ventana Y Y Y mt
CLAMP anunciada
Definiciones de señales
Kn la Tabla A.8 se recogen las señales estándar y su significado.
Tabla A.8 Códigos de señales estándar de Linux
Acción Comentario
SIGHUP Cuelgue detectado en el terminal de iroiitro!. o cest del
proceso de control.
424/512
SIGURG 16,2.1,21 Condición urgente on sot ket Í4 2 ISSD)
>,22 K/S posible ahora (4 2 BSD1.
SIGPOLL Sinónimo de SIGIO (System V]
Códigos ICMP
La Tabla A.9 muestra los distintos tipos de paquetes ICMP ¡RFC7921 y su
significado.
Tabla A.9 Descripciones de códigos CMP
Tipo Código Descripción
ü u Respuesta de eco.
3 Puerto no ale.ilzable.
425/512
12 Flost no alcanzable para TOS.
426/512
Tabla A.9 Descripciones de códigos CMP (continuación)
Tipo Código Descripción
14 Violación de prioridad de host.
5 Redirigir.
8 0 l'eiicíón de eco.
9 0 Anuncio de router
IÜ U Solicitud de router.
11 Tiempo excedido.
12 Problema de parámetro.
Códigos ICMPv6
La Tabla A.12 muestra el nuevo ICMPv6 IRFC2463] para IPv6.
Tabla A. Descripciones del código ICMPv6
12
Tipo Código Descripción
1 Destino no alcanzable.
4 Puerto no alcanzable.
4 Problema de parámetro.
2 Opción no reconocida.
0 (no definido)
Bit # Descripción
0 Transitoriedad.
0 = dirección bien conocida.
1 = dirección transitoria.
I (reservado).
2 (reservado).
3 (reservado).
Apéndice B
API de red
En este apéndice
Conexión a la red
Comunicación por un canal
Terminación de conexiones
Conversiones de datos de red
Herramientas de direccionamiento de red 489 Controles socket
Conexión a la red
Los API de Sockets proporcionan herramientas que facilitan la creación de
sockets y la conexión a otros hosts. En este apartado se describe los API más
importantes para la conexión y creación de sockets.
socket()
socketO crea un canal bidireccional con el que, generalmente, se establece una
conexión con la red. Este canal puede utilizarse con llamadas específicas de red o
con E/S de archivo generales.
Prototipo
#include <resolv.h> tfinclude <sys/socket.h>
Sinclude <sys/types.h>int socket(int domain, int type, int protocol);
Valor devuelto
Si tiene éxito, la llamada devuelve un descriptor de socket válido.En caso
contrario, el resultado es un valor menor que cero. Verfique el contenido de errno
para obtener más información acerca del error.
Parámetros
domain Selecciona el protocolo de red para el socket (véase el
Apéndice A, 'Tablas de datos").
type Selecciona la capa de red (véase el Apéndice A).
protocol Normalmente, cero (véase el Apéndice A).
Posibles errores
EPROTONOSUPPORT El protocolo type o el protocol especificado no está
Ejemplos
/*-* Crear un socket TCP ***/ int srJ:
sd = socket(PFINET, SOCK_STREAM, 0);
Crear un socket ICMP "*/ int sd;
srJ = Sccket(PF_INET, SOCK_RAW, titons(IPPROTO_ICMP)) ;
bind()
bind() define un puerto ■> un nombre para un socket. Si desea disponer de un
puerto consistente para una próxima conexión, deberá asociar el socket a un
puerto, bn la mayoría de los casos, el núcleo realiza una llamada automática a
bind() para el socket, si dicha llamada no se realiz.a explícitamente. La asignación
de puerto generada por el núcleo puede es diferente en cada ejecución.
Prototipo
Sinclude <sys/socket.n> «include <resolv.h>
int t u n d í int sockfd, struct sockaddr* addr, int addrlen);
Valor devuelto
Cero, si todo va bien. Si se produce un error, puede conocer la causa del mismo
a partir del contenido de errno.
Parámetros
sockfd Descriptor del socket a. asociar.
Addr Asignación o no mbre del puerto.
Addrlen Longitud de addr, ya que su tamaño puede variar.
Posibles errores
EBADF EINVAL
sockfd no es u n descriptor válido.
El socket está ya asociado a una dirección. Esto es algo EACCES que puede
cambiar en el futuro; véase .../linux/unix/ socket.c para más detalles.
La dirección está protegida v el usuario no es un super usuario.
ENOTSOCK
El argumento es un descriptor de archivo, en lugar de un socket.
Ejemplo
/*** Asociar el puerto #99&9 al socket desde una dirección de Internet ***/ int sockfd;
struct sockaddr_in addr;
sockfd = socket(PF_INET, SOCKETSTREAM, 0); bzero(&addr, sizeof(addr¡); addr. sin_f
amily = AF_I.NET;
addr.sin_port = htons(9999); /* u otro puerto "/ /" para asociar cualquier interfaz de red */
addr.sin_addr.s_addr = INAQDRANY;
/* o Píen, para asociar una interfaz especifica, utilice esto: V /* inet_aton( "128.1 .1 .1",
S,addr.sin_addr|; "/ if ( bind(sockfd, Saddr, sizeof(addr)) i= 0 ) perror("bind"];
listen() convierte el socket en un socket de escucha. Esta opción está disponible
tan sólo para protocolos SOCK_5TREAM. La llamada crea una cola con las
conexiones entrantes.
listen()
Prototipo
«incluüe <sys/socket.h>
tfinclude <resclv.h>
int listendnt sockfd, int queue_len);
Valor devuelto
Cero, si todo va bien. Si se produce un error, puede conocer la causa del mismo
a partir del contenido de errno.
Parámetros
sockfd
Socket SOCK_STREAM que ha sido asociado aun puerto.
queuejen
Número máximo de conexiones pendientes.
osibles errores
EBADF
ENOTSOCK
EOPNOTSUPP
El argumento sockfd no es un descriptor válido.
El argumento sockfd no es un socket.
El socket no es de un tipo que soporte la operación listenQ. Si facilita un socicer
distinto de SOCK_STREAM, obtendrá este error.
Ejemplo
**" Convertir un socket en un socket de escucha con 10 slots *"'/ .nt sockfd;
iockfd = socket(PFJNET, SOCK_STREAM, C); ''•■■configurar dirección con binü()---*/
Listen(sockfd, 10); /* crear una cola de 10 pendientes */
accept()
Espera una conexión. Al llegar la conexión, devuelve un descriptor de socket
nuevo (independiente de sockfd) para dicha conexión específica. Esta llamada está
disponible sólo para sockets 50CK_STREAM.
Prototipo
íinclude <sys/5DCket.h>
tfinclude <resolv.h>
int accept(int sockfd, struct
Valor devuelto
Si >=0 Si < 0 sockaddr 'addr, int *addr_len¡;
Un descriptor de socket nuevo. Error; errno contiene los detalles.
Parámetros
sockfd Descriptor de socket de escucha y asociado.
ción en esta zona. Aunque debe coincidir con la familia de sockfd (AFJNET), no
se da por supuesto.
addrjen Se pasa esta referencia de longitud para que la llamada
pueda informar de la cantidad exacta del bloque de datos addr utilizada. Esto
significa que deberá reiniciar el valor en cada llamada.
Posibles errores
EBADF ENOTSOCK
El descriptor del socket no es válido.
El descriptor apunta a un archivo, no a un socket.
El socket referenciado no es del tipo SOCK_STREAM.
El parámetro addr no es una parte escribible del espacio de direcciones de
usuario.
El socket está marcado como de no bloqueo y no hay conexiones presentes a
aceptar.
Firewall establece prohibir la conexión.
Memoria libre insuficiente.
EOPNOTSUPP
EFAULT
EAGAIN
EPERM
ENOBUFS, ENOMEM
Ejemplos
/""* Aceptar una conexión, ignorando el origen ***/
int sockfd = socket(PFINET, SOCKSTREAM, 0);
/•-•-asociar dirección al socket con bind()—*/
/"- -convertirlo en un socket de escucha con listen(|---*/
for ( ; ; )
{ int client;
client - accept(sockfd, 0, B);
/"■■-interactuar con el cliente---*/
close(client);
/"** Aceptar una conexión, capturando el origen en un registro ***/ int sockfd =
socket(PF_INET, SOCKSTREAM, 0|; /'-■-asociar dirección al socket con bind()---*7 /•■■-
convertirlo en un socket de escucha con listen(|-■-* / for (;¡)
¡ struct sockadórin addr;
int client, addr^len - addr;
client = accept(sockfo, Saddr, Saddrlen);
printf ("Connected: %s:Ssd\n" , inet_ntoa(addr. sin_addr) ,
ntohs(addr.sinport)); /'---interactuar con el cliente */ close(client);
}
connectO
Conecta con un peer o un servidor. Puede utilizar esta función para los
protocolos SOCK_DGRAM o SOCK.STRE AM. Para UDP, simplemente recuerde el
puerto al cual está conectado. Permite utilizar sendO y recv(). Para TCP
(SOCK_5TREAM), esta llamada inicia un intercambio de señales de tres vías para
las comuniaciones en serie.
Prototipo
#include <sys/socket.h> #include <cesolv.fi >
Valor devuelto
Cero (0), si todo va bien. Si se produce un error, puede conocer la causa del
mismo a partir del contenido de errno.
Parámetros
sockfd
addr addr len
Nuevo socket creado. Opcionalmente, puede hacer una llamada bind() previa
para asignar el puerto local. Si no lo hace, el núcleo asignará el siguiente sfof de
puerto disponible.
Dirección y puerto de la dirección de destino. Longitud del bloque de datos addr.
Posibles errores
EBADF EFAULT
ENOTSOCK EISCONN
ECONNREFUSED
ETIMEDOUT
ENETUNREACH
EADDINUSE
EINPROGRESS
Descriptor incorrecto.
La dirección de la estructura del socket queda fuera del espacio de direcciones
de usuario. Eslo se debe a una referencia incorrecta a la estructura addr.
El descriptor no corresponde a un .sodceí.
El socket ya está conectado. No es posible volver a conectar un socket
conectado. En lugar de ello, deberá cerrar el socket y crear uno nuevo.
Conexión rehusada por el servidor.
Limite de tiempo excedido al intentar ln conexión.
No se ha podido acceder a la red.
La dirección ya está en uso.
El socket es de no bloqueo, y la conexión no puede completarse
inmediatamente. Se puede usar selectO ° pollO
para completar la conexión, seleccionando el socket para escritura. Una vez que
selectO indique la capacidad para escribir, utilice getsockopt() para leer la opción
SO__ERROR al nivel SOL^SOCKET para determinar si la conexión ha tenido éxito
(SO_ERROR cero) o no (SO ERROR es uno de los códigos de error previamente
mencionados, que explica la ra?ón del fallo).
EALREADY El socket es de no bloqueo, y aún no se ha completado
Ejemplo
Conectar a un servidor TCP ***/ int sockfd;
struct sockaddr_in addr; sockfd = socket(PF_INET, SOCKSTREAM, 0); bzero(&addr,
sizeof(addr)); addr.sinfamily = AF_INET; addr.sin_port = 13; /* hora actual */
inet_atoi("127.0.0.1 ", Saddr.sinaddr); if ( connect(sockfd, Saddr, sizeof(addr)) l- 0 )
perror("connect");
socketpairO
Crea un par de sockets vinculados a modo de canal, pero utilizando un
subsistema de socket. Es casi idéntica a la llamada de sistema pipe(), pero ofrece
la funcionalidad adicional del API de Socket. socketpairO soporta sólo sockets
PF_UNIX o PF_LOCAL. No es necesario bindO este socket a un nombre de archivo
del sistema de archivos.
Prototipo
«include <sys/socket.h> flinclude <resolv.h>
int socketpair(int domain, int type, int protocol, int sockfds[2]);
Valor devuelto
Cero (0), si todo va bien. Si se produce un error, puede conocer la causa del mo
a partir del contenido de errno.
mis-
Parámetros
domain type
protocol sockfds[2]
Posibles errores
EMFILE Hay demasiados descriptores en uso por este proceso.
Ejemplo
'*** Crear un par Pe sockets ***/ .nt sockfd[2]; ;truct sockaddrux addr; _f
( socketpair(PF_LOCAL, S0CK_STREAM, perror("socketpair");
1, sockfd) != a |
Prototipo
Winclude <sys/socket.h> «include <resolv.h>
int seud(if>t socMfd, void "Duffer, int msg_len, int optionsl;
Valor devuelto
Al igual que writeO, esta llamada devuelve el número de bytes escritos. El
número de bytes puede ser menor que msg Jen. 5i la llamada no consigue escribir
todos los bvtes requeridos, puede utilizar un bucle para realizar escrituras
sucesivas. Si el rebultado es negativo, la llamada almacena los detalles del error en
errno.
Parámetros
sockfd
buffer
msgjen
options
Canal del socket. Éste puede ser un socket SOCK J5RAM o SOCK_STREAM
conectado.
Datos a enviar.
Número de bytes a enviar.
Conjunto de tlags para habilitar la manipulación especial de mensajes:
• MSG_OOB. Enviar el mensaje fuera de banda (urgente).
Posibles errores
EBADF
ENOTSOCK
EFAULT
EMSGS1ZE
Se ha especificado un descriptor no valido.
El argumento sockfd no es un socket.
Se ha especificado un espacio de direcciones de usuario no válido para buffer.
La llamada no se ha podido completar debido a que el socket requiere que el
mensaje sea enviado atómicamente, y el tamaño del mensaje hace esto imposible.
EAGAIN
El socket está marcado como de no bloqueo y la operación requerida debería
bloquear.
El sistema no ha podido reservar un bloque de memoria interno. La operación
podría completarse con éxito cuando los buffers estén disponibles.
Ha tenido lugar una señal.
No hay memoria disponible.
Se ha pasado un argumento no válido.
Se ha apagado el extremo local en un socket conectado. En este caso, el
proceso recibe también un SIGPIPE, a menos que esté activado
MSG_NOSIGNAL.
ENOBUFS
EINTR
ENOMEM
EINVAL
EPIPE
Ejemplo
/*"" Enviar un mensaje (TCP, UDP) a un destino conectado ***/ int sockfd;
int bytes, bytes_wrote=0;
/*--- Crear socket, conectar a servidor/peer ---*/ while ( (bytes = send(sockfd, buffer,
msg_len, ü j ) > 0 )
Prototipo
Sinclude <sys/socket.h> #include <resolv.h>
int sendto(int sockfd, voic* msg, int len, int options, struct sockaddr «addr, int addr_len);
sendtoO
Valor devuelto
Devuelve el número de bytes enviados, o bien -1, si tiene lugar algún tipo de
error.
Parámetros
sockfd
Descriptor del socket.
Datos a enviaT.
Número de bytes a enviar.
Flags de control de mensaje (como en send()).
Dirección de destino.
Tamaño del cuerpo de datos de destino.
msg len
options
addr
addrjen
Posibles errores
(Igual que send().)
Ejemplo
/*** Enviar un mensaje (TCP, UDP) a un destino NO conectado ***/ int sockfd;
struct sockaddr_in addr;
sendmsgO
Ensambla un mensaje a partir de varios bloques de datos. Esta rutina toma los
datos de la estructura iovec y crea un mensaje individual. Si msg_name señala a
una sockaddr real, la rutina envía el mensaje sin conexión. Si señala a NULL, la
rutina supone que el socket está contectado.
Prototipo
#mclude <sys/socket .h>
tfinclude <resolv.h>
#include <sys/uio.ti>
int sendmsg(int sockfd, const struct msghdr "msg, ^ unsigned int options];
Valor devuelto
Esta llamada devuelve el número total de bytes enviados, o -1, si tiene lugar un error
(verificar errno para más información).
Parámetros
sockfd msg
options
Descriptor del socket abierto.
struct msghdr {
_ptr_t msg_name; /* Dest address */
socklen_t msg_namelen; /* Address length */
struct iovec *msg_iov; /* Buffers vector */
size_t msg_iovlen; /* Vector length */
_ptr_t msg_control; /* Ancillary data */
size_t msg_controllen; /* Ancillary data len */
int msg_flags; /* Received msg flags */
};
Los datos auxiliares permiten que el programa transfiera datos como descriptores de
archivos.
Flags de control de mensajes (como en sendQ).
Posibles errores
(Igual que sendQ.)
Ejemplo
int i, sd, len, bytes; char buffer[MSGS][iaa]
struct iovec io[MSGS]; struct msghdr msg; struct sockaddr_in addr;
sd = socket(PF_INET, SOCK_DGP,AM, 0]; bzero(8addr, sizeof(addr)); addr.sin_family =
AF_INET; addr.sin_port = htons(8080); inet_aton(&addr.sin_addr, "127.0.0.1"); bzero(&msg,
sizeof(msf)); msg. rnsg_naine = Saddr; msg.msg_namelen = sizeof (addr);
for ( i = 0; i < MSGS; i++ )
{
io[i].iovbase = buffer[i];
spnntf (buffer[i] , "Buffer #%d: this is a test\n", i); io[i].iov_len = strlen(buffer[i]);
}
sendfile()
Medio rápido de transmitir un archivo a través de un socket. La llamada lee los
datos de injfd y los escribe en out_fd. Esta llamada no cambia el puntero del
archivo en dejfd, pero sí el de out_fd. La llamada comienza leyendo a partir de
-offset paTa contar los bytes. Tras esto, «offset señala el byte siguiente al último
leído. Si necesita añadir información de cabecera, consulte la opción TCP_CORK
en TCP(4) para mejorar el rendimiento.
Prototipo
#include <umstd>
int sendfilefint out_fd, int in_fd, off_t -offset, size_t count);
Valor devuelto
Si tiene éxito, la llamada devuelve el número total de bytes copiados. Si tiene
lugar un error, la llamda devuelve -1 e inserta en errno el código del error.
Parámetros
out_fd in_fd
offset
count
Descriptor de destino (puntero de archivo modificado).
Descriptor de origen (puntero de archivo no modificado).
Puntero para una variable que contiene el desplazamiento inicial.
Número de bytes a enviar.
Posibles errores
EBADF
EINVAL
ENOMEM
EIO
Ejemplo
No se ha abierto el archivo de entrada para lectura, o no se ha abierto el archivo
de salida para escritura.
Descriptor no válido o bloqueado.
Memoria insuficiente para leer ¡n_fd.
Error no especificado al leer ¡n_fd.
Sinclude <unistd.ri>
struct stat fdstat;
int client = accept(sd, Qt, 0);
int fd = open( "filenaitie.gif", C_RDONLY);
fstat(fd, &fdstat);
sendfile(client, fd, 0, fdstat.st_size); close(fd); cióse(client);
recv()
Espera y acepta un mensaje de un peer, cliente o servidor conectado. La
llamada de sistema se comporta de forma parecida a read(), pero aporta flags de
control. UDP puede usar esta llamada si está conectado a un peer.
Prototipo
#include <sys/socket.h> #include <resolv.h>
int recv(int sockfd, void* buf, int maxbuf, int options);
Valor devuelto
Número de bytes leídos, o -1, si tiene lugar un error.
Parámetros
sockfd buf
maxbuf options
Descriptor del socket abierto.
Array de bytes para aceptar el mensaje entrante.
Tamaño del array.
Conjunto de flags que pueden operarse aritméticamente con OR:
• M5G_OOB. Este f¡ag solicita la recepción de datos fuera de banda, que no
serían redibidos en el flujo de datos normal. Algunos protocolos colocan datos
expedidos al comienzo de la cola normal de datos, por lo que este flag no podría
utilizarse con dichos protocolos.
• MSG_PEEK. Este flag hace que la operación de recepción devuelva datos del
comienzo de la cola recibida, sin eliminar dichos datos de la cola. Por tanto, una
llamada de recepción subsiguiente devolvería los mismos datos.
• MSG_WAITALL Este flag pide que la operación quede bloqueada hasta que
complete la petición. Sin embargo, la llamada puede devolver menos datos de los
solicitados si se captura una señal, ocurre un error o una desconexión, o el
siguiente dato a recibir es de un tipo distinto al devuelto.
• MSG_ERRQUEUE. Recibe paquetes de la cola de errores.
Posibles errores
EBADF ENOTCONN
ENOTSOCK EAGAIN
EINTR
EFAULT
EINVAL
El argumento s no es un descriptor válido.
El socket está asociado a una conexión orientada al protocolo y no ha sido
conectado (véanse connectO y acceptO).
El argumento sockfd no hace referencia a un socket.
El socket está marcado como de no bloqueo, y la operación de recepción podría
bloquear, o bien se ha establecido un límite de tiempo para la recepción, el cual ha
expirado antes de que los datos hayan sido recibidos.
Una señal ha interrumpido la operación de recepción antes de que hubiera datos
disponibles.
El puntero del buffer de recepción señala fuera del espacio de direcciones del
proceso.
Se ha pasado un argumento no válido.
Ejemplo
/*** Hecv un mensaje (TCP, UDP) de un destino conectado *'*/ int sockfd;
int bytes, bytes_wrote=0;
/*--■ Crear socket, conectar a servidor/peer ---*/ if ( (bytes = recv(sockfd, buffer, msg__len,
0)) < 0 ] perrorf"send");
/*** Recv un mensaje URGENTE ( T C P ) de un destino conectado --"/ /*** Este código
suele ser un controlador de señales SIGURG ***/ int sockfd;
int bytes, bytes_wrote=0;
/*--- Crear socket, conectar a servidor ---*/ if ( (bytes = recv(sockfd, buffer, msg_len, 0)) < 0
) perror("Urgent message"];
recvfromO
Espera y recibe un mensaje de un peer no conectado (UDP y sockets raw).
Tenga presente que T/TCP nunca utiliza esta llamada. En su lugar, el receptor
utiliza acceptO-
Prototipo
Sinclude <sys/socket.h> #include <resolv.h>
int recvfrom(int sockfd, vOLd *buf, int Duf_ien, int options, struct sockaddr 'addr, int
'addr_len];
Valor devuelto
Si tiene éxito, el número de bytes leídos. Si tiene lugar un error, el valor devuelto
es - \, y errno contiene el código del error.
Parámetros
sockfd buf
bufjen
options addr addr len
Descriptor del socket abierto.
Arravde bytes para recibir los datos.
Tamaño máximo del buffer (mensaje truncado y descartado si el buffer es
demasiado pequeño).
Opciones de control del canal (igual que recv()).
Puerto y dirección del peer emisor.
Tamaño máximo de addr (dirección truncada, si dicho tamaño es demasiado
pequeño). Una vez devuelto, este valor cambia para ajustarse al número de bytes
utilizados.
Posibles errores
(Igual que retvQ.)
Ejemplo
struct sockaddr^in addr;
int addr_len=sizeof(addr], bytes_read;
char buf[1024);
int sockfd = socket(PF_INET, SOCKDGHAM, 0); /"'Asociar socket a puerto especifico*""/
bytes_read = recvfrom(sockfd, buf, sizeof(buf), 0, Saddr, &add_len); if ( bytes_read < 0 |
perror("recvfron failed");
recvmsg()
Recibe varios mensajes simultáneos procedentes de una misma fuente. Esta
llamada se utiliza habítualmente con sockets SOCK_DGRAM (por la misma razón
que sendmsgO). La operación carece de flexibilidad para aceptar mensajes de
diferentes fuentes.
Prototipo
flinclucie <sys/socket . h> #include <resolv.h> #include <sys/uio.h>
int recvmsgfint sockfd, struct msghdr 'msg, unsigned int options);
Valor devuelto
Número total de bytes recibidos, si no hay error; en caso contrario, el valor
devuelto es -1.
Parámetros
sockfd Descriptor del socket que espera el mensaje,
msg Datos recibidos.
options Opciones de control del canal (igual que recv()).
Posibles errores
(Igual que recv())
Ejemplo
char buffer[MSGSl[10001; struct sockaddr_in addr; struct íovec io[MSGS]; struct msghdr
msg;
bzero(&addr, sizeof(addr|\; msg.msg_name = Saddr; msg.msg_namelen = sizeof(addr);
for ( i = 0; i < MSGS; i++ ) 1
io| i) .íovjjase = bufferiil;
io[i].iou_len = sizeof(buffer[i));
}
Terminación de conexiones
El último paso que lleva a cabo un programa sólido tras comunicarse con el host
externo, consiste en cerrar la conexión. En este apartado se describen las A P I
para terminación de sockets.
shutdownO
Cierra direcciones de flujo de datos o rutas específicas. Las conexiones socket
son bidireccionales, por omisión. Si desea limitar el flujo para que sea de sólo
lectura o de solo escritura, shutdown se encarga de cerrar el otro extremo del
canal.
Prototipo
tfinclude <sys/socket.h>
int shutdown(int sockfd, int how);
Valor devuelto
Cero, si todo va bien. Si tiene lugar un error, puede localizar la causa del mismo
en errno.
Parámetros
sockfd how
Descriptor del socketabierto.
Flag que indica la parte (o totalidad) del canal a ceTrar:
Posibles errores
EBADF
ENOTSOCK
ENOTCONN
sockfd no es un descriptor válido.
Ejemplo
int sockfd;
struct sockaddr_in addr;
sockfd = socKet(PF_INET, SDCK_STBEAH, 0);
bzero(&addr, sizeof(addr));
r.sin_fainily = AF_INET; ir.5in_port = htons(OEST_PORT); !t_aton(DEST_ADDR,
Saddr.sinaddr); inect(sockfd, Saddr, sizeof(addr));
( shutdown(sockfd, SHUT_WR) != 0 ) PANIC("Can't make socket input-only");
tonsO o htonlO
Convierte datos binarios de orden de bytes de host en datos binarios de orden ;
bytes de red. En un procesador little-endian, la llamada intercambia los bytes. n host
big-endian no hace si no devolver el valor.
rototipo
Lnclude < neti.net/in.h>
isigned short int htons(unsigned short int hostshort); isigned long int
htonljunsigned long int host_lonf);
'alor devuelto
(ninguno)
'a rá metros
host_short Valor de host de 16 bits,
hostjong Valor de host de 32 bits.
'osibles errores
(ninguno)
ejemplo
Asignar #1023 al socket * * * /
Struct sockaddr_in addr; addr.sin_port = htons(1023);
/*** Asignar 128.1.32.10 a la dirección de destino ***/ Struct sockaddr_in addr;
addr.sin_addr.s_addr = hton(0x8001200A);
ntohs() o ntohI()
Convierte el orden de bites de red al orden de bytes de host.
Prototipo
#include <netinet/in.h>
unsigned short int ntohs(unsigned short int network_snort);
unsigned long int ntohl(unsigned long int network_lonf);
Valor devuelto
El valor convertido (16 ó 32 bits).
Parámetros
network_short Valor de 16 bits a convertir,
networkjong Valor de 32 bits a convertir.
Posibles errores
(ninguno)
Ejemplo
struct sockaddr_in addr;
int client, addrlen=sizeof(addr);
client = accept(sockfd, Saddr, Saddrlen);
if ( client > 0 )
printf("Connected %lX;%d\n", ntohl(addr.sin_addr),
ntohs (addr. sin__port)) ;
inet_addr()
Herramienta de conversión inapropiada para la conversión de direcciones
numéricas punto decimal al formato binario de orden de bytes de red. Si falla, el
valor devuelto (-1 ó 255.255.255.255) seguirá siendo una dirección válida. La
herramienta ¡net_aton() aporta una gestión de errores mejorada.
Prototipo
#include <netinet/in.h>
Valor devuelto
No cero Si todo va bien, el valor es la dirección IP convertida.
INADDFLNONE(-1) Paramero no válido. (Éste es el defecto de la llamada-
no hay valor negativo, y 255.255.255.255 es la dirección
de difusión general. 1
Parámetros
ip_address Formato de notación punto decimal legible (por ejemplo,
128.187.34.2)
Posibles errores
(errno no configurado)
Ejemplo
if ( (addr.sj.n_addr.s_addr = inet_addr(" 128.187.34.2")) == -1 )
perror("Couldn't convert address");
inet_aton()
Convierte una dirección IP legible a partir de la notación punto decimal al
formato binario ordenado de bytes de red. Esta llamada sustituye a inet_addr().
Prototipo
#include <netinet/irt.n>
int inet_aton(const char* ip_addr, struct in_addr *addr);
Valor devuelto
Distinto de cero, si todo va bien. Si tiene lugar un error, la llamada devuelve un
cero.
Parámetros
ip_addr Cadena ASCII de la dirección IP (por ejemplo, 187.34.2.1).
Posibles errores
(errno no configurado)
Ejemplo
Struct sockaddr_in addr;
if ( inet_aton("187.43.32.1", Saddr.sinaddr) == 0 )
perrorf"inet_aton(] failed");
inet_ntoa()
Esta llamada convierte el formato binario ordenado de bytes de red al formato
legible decimal. Tenga en cuenta que esta llamada utiliza una zona de memoria
estática; por tanto, llamadas subsiguientes reemplazarían los resultados
precedentes.
Prototipo
#include <netinet/in.h>
const char* inet_ntoa(struct in_addr -addr);
Valor devuelto
Dirección en una cadena devuelta.
Parámetros
addr Dirección binaria (normalmente, el campo de dirección de struct
sockaddrjn).
Posibles errores
(errno no configurado)
Ejemplo
Clientfd = acceptfserverfd, Saddr, Saddrsize);
if ( clientfd > 0 )
printf["Connected %s:td\n",
inet_ntoa(addr.sin_addr|, ntohs(addr.sinport));
inet_pton()
Convierte direccoines IPv4 o IPV6 legibles de la notación con punto o dos
puntos a formato binario ordenado de bytes de red.
Prototipo
#include <netinet/in.h>
int inet_pton(int domain, const char* prsnt, void *addr);
Valor devuelto
Distinto de cero si todo va bien. Si tiene lugar un error, la llamada devuelve cero.
Parámetros
domai Tipo de red (AFJNET o AFJNET6).
n
Posibles errores
(errno no configurado)
Ejemplo
struct sockaddrin addr;
if ( inet_pton(AF_INET6, "187.43.32.1", &addr.sin6_addr) == 0 )
perrorf"inetpton() failed");
inet_ntop()
Esta llamada convierte el formato binario ordenado de bytes de red al formato
legible decimal. A diferencia de inet_ntoa, aquí es necesario proporcionar una
cadena de anotación. Esta función soporta tanto AFJNET como AFJNET6.
Prototipo
#include <arpa/inet.h>
char* inet_ntop(int domain, struct in_addr *addr, char* str, int len);
Valor devuelto
La llamada devuelve el paramero str.
Parámetros
domain Tipo de red (AFJNET o AFJNET6).
Posibles errores
(errno no configurado)
Ejemplo
char str[l00];
clientfd = accept(serverfd, Saddr, &addr_size);
if | clientfd > 0 )
printf("Connected %s:%d\n",
inet_ntop(AF INET, addr.sinaddr, str, sizeof(str|),
ntohs(addr.sin_port));
Herramientas de direccionamiento
de red
Al igual que las herramientas de dalos y direccionamiento, API ofrece acceso a
los servicios de denominación. Estos servicios incluyen servicios de nombres de
dominio (DNS), protocolos, etc. En este apartado se describen las [amadas que
perimten convertir nombres a un formato legible.
getpeername()
Esta rutina obtiene la dirección acotada o el nombre del peer conectado al otro
extremo del canal del socket sockfd. La llamada coloca el resulado en buf. buf Jen es
el número de bytes disponibles en buf. Si es demasiado pequeño, la información
quedará truncada. Ésta es la misma información que se obtiene con la llamada
acceptQ.
Prototipo
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *adrir_len);
Valor devuelto
Cero, si todo va bien. Si tiene lugar un error, podrá localizar la causa del mismo
en errno.
Parámetros
sockfd Canal del socket conectado.
Posibles errores
EBADF El argumento sockfd no es un descriptor válido.
Ejemplo
struct sockaddr_in aCdr; int add_len = sizeof(addr);
if ( getpeername(client, Saddr, &addr_len) != 0 )
perror("getpeernameO failed");
printf ("Peer ; \s'.%d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
gethostname()
Obtiene el nombre del host local. La llamada coloca el resultado en el paramero
name, con un máximo de len bytes.
Prototipo
#include <unistd.h>
int gethostnaine(char *name, size_t len);
Valor devuelto
Cero, si todo va bien. Si tiene lugar un error, podrá localizar la causa en errno.
Parámetros
name Buffer para almacenar el nombre del host local.
Posibles errores
EINVAL len es negativo o, para gethostname() en Linux/i386, len es
menor que el tamaño real.
Ejemplo
char name[50];
if ( getpeername(name, sizeof(ñame)| != 0 )
perrorf"getpeernamef) -failed");
printf("My host is: %s\n", name);
gethostbyname()
Busca y traduce el nombre de host a una dirección IP. El nombre puede ser un
nombre de íiosf o una dirección. Si se trata de una dirección, la llamada no realiza
búsqueda; en lugar de ello, devuelve la dirección en los campos h_name y
h_addr_list[0] de la estructura hostent.
Si el nombre de host termina con un punto, la llamada tratará a dicho nombre
como absoluto, sin abreviaturas. En otro caso, la llamada buscará los nombres de
subredes locales. Si se ha definido HOSTALIASES en el entorno propio, la llamada
buscará el archivo al que apunta HOSTALIASES.
Prototipo
#include <netdb.h>
Valor devuelto
La llamada devuelve un puntero struct hostent; si falla, el valor devuelto es NULL. La
estructura lista todos los nombres y direcciones almacenados en el host. La macro h_addr
proporciona compatibilidad en descenso.
#define h_addr h_addr_list|0]
struct hostent {
char *h_name; /* official ñame of host V
char **h_aliases; /* alias list */
int haddrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses; 0th is the primary */
};
Parámetros
name Nombre de host a buscar o dirección IP.
Posibles errores
ENOTFOUND Host especificado desconocido.
Ejemplo
int i;
struct hostent *host;
host = gethostbyname("sunsite.une.edu");
if ( host != NULL )
{
getprotobyname()
Esta función lee el archivo /etc/protocol file para obtener el protocolo que
coincide con pname. Esta llamada se utiliza para traducir nombres, como HTTP,
ETO y Telnet a sus números de puerto predeterminados.
Prototipo
#include <netdb.h>
struct protoent *getprotobyname(const char *pname);
Valor devuelto
Si tiene éxito, la llamada devuelve un puntero para protoent (definido más
adelante en este apéndice). En caso contrario, devuelve NULL.
struct protoent {
char *p_name; /* official protocol ñame */
char **p_aliases; /* alias list "/
int p_proto; /* protocol number */
};
Parámetros
pname Nombre del protocolo. Puede ser cualquiera de los nombres o
alias de protocolo reconocidos.
Posibles errores
(errno no configurado)
Ejemplo
#include <netdb.h>
…
int i;
struct protoent *proto = getprotobyname("http");
if ( proto != NULL )
{
printf("Official ñame: %s\n", proto->name);
printf("Port#; %d\n", proto->p_proto);
for ( i = 0; proto->p_aliases[i] != 0; i++ )
printf("Alias[%d]: %s\n", i+1, proto->p_aliases[i]);
}
else
perror("http");
Controles de socket
Mientras el socket está abierto, existen varias formas de configurar su
comportamiento. En este apartado se describen las llamadas API.
setsockopt()
Cambia el comportamiento del socket sd. Cada opción posee un valor (algunas
opciones son de sólo lectura o escritura). Por medio de optval y opílense puede
configurar cada opción individual. Si desea ver la lista completa de todas las
opciones, consulte el Apéndice A.
Prototipo
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sd, int level, int optname, const void "optval, socklen_t optlen);
Valor devuelto
Cero, si todo va bien. Si tiene lugar un error, podrá localizar la causa en in errno.
Parámetros
sd Socket A modificar.
Posibles errores
EBADF El argumento sd no es un descriptor válido.
Ejemplo
const int TTL=128;
/ * —Cambiar el tiempo de existencia a 128 saltos-- */
if ( setsockopt(Sd, SOLJP, SO_TTL, &TTL, sizeof(TTL)) != 0 )
perror("setsockopt() failed");
getsockopt()
Obtiene la configuración del socket.
Prototipo
#include <sys/types.h>
#include <sys/socket.h»
int getsockopt(int sd, int level, int optname, void "optval, socklen_t "optlen);
Valor devuelto
Cero, si todo va bien. Si tiene lugar un error, podrá localizar la causa en errno.
Parámetros
sd Socket a leer.
optlen Longitud del valor en bytes. Este campo se pasa solo como
referencia.
Posibles errores
EBADF El argumento sd no es un descriptor válido.
Ejemplo
int error, size=sizeof(error);
if ( getsockopt(sd, SOL_SOCKET, SO_ERROR, &error, Ssize) != 0 )
perrorf"getsockopt() failed");
printf("socket error=\d\n", error);
Apéndice C
Subconjunto API del núcleo
En este apéndice
Tareas
Threads
Bloqueo
Señales
Archivos y otros
Tareas
Las tareas incluyen tanto procesos como threads. Los threads (pThreads) se
definen en el siguiente apartado; en éste se describen los procesos y las tareas de
bajo nivel (clones).
fork()
Para esta llamada se crea un nuevo proceso (tarera independiente). Esta
llamada crea un proceso hijo (secundario) que se ejecuta con el proceso padre
(principal). Se debe poner cuidado en captuar el proceso hijo y dirigirlo a la tarea
asignada a éste; si no se hace así, el proceso hijo ejecutará todas las instrucciones
del proceso princ-pial (se ejecute rten ambos >.
Prototipo
#include <unistd.h>
pid_t fork(void);
Valor devuelto
0 Corresponde a la tarea bija.
>0 Corresponde a la tarea padre.
<0 La tarea padre no ha podido crear una tarea secundaria nueva; comprobar
errno.
Parámetros
(ninguno)
Posibles errores
EAGAIN fork() no ha podido asignar la cantidad de memoria suficiente
para copiar las tablas de páginas de la tarea padre y asignar una
estructura de tarea para el proceso hijo.
Ejemplo
int PID;
if ( (PID = fork(|) == 0 )
{ /*--- CHILD ---*/
/*** Ejecutar la asignación hija ***/
exit();
}
else if ( PID > 0 )
{ /*--- PARENT ---*/
int status;
/**** Realizar el trabajo prinicpal ****/
wait(status); /* puede hacerse en control de señal SIGCHLD */
}
else /*--- ERROR ---*/
perror("fork() failed");
_clone()
Se trata de una llamada del sistema de bajo nivel para la creación de tareas. Es
posible controlar directamente los elementos compartidos entre las tareas padre e
hija. No está pensada para programadores aficionados, pues los resultados
podrían ser impredecibles. (Véase el Capítulo 7, "División de la carga: multitarea",
para una descripción completa de esta llamada.)
Prototipo
#include <sched.h>
int _clone(int (*fn)(void* arg), void* stacktop, int flags, void* arg);
Valor devuelto
process ID Si es negativo, errno contiene el código correspondiente al error.
Parámetros
fn Inicio de la tarea hija. Crea una función (o procedimiento) que
acepta un argumento de parámetro void*. Cuando la rutina
intenta volver, el sistema operativo termina la tarea de forma
automática.
Stacktop Se debe crear una pila para la tarea hija. Este parámetro
señala a la parte superior de la pila (dirección más alta del bloque
de datos). Como se suministra la pila, el tamaño de ésta es fijo y
no puede aumentar como lo haría una pila normal.
Posibles errores
EAGAIN __clone() no puede asignar memoria suficiente para copiar las
tablas de páginas de la tarea padre y asignar una estructura de
tarea para la hija.
Ejemplo
#define STACKSIZE 1024
void Child(void* arg)
{
/*---responsabilidad de la hija---*/
exit(0);
}
...
int main(void)
{ int cchild;
char *stack=malloc(STACKSIZE);
if ( (cctiild = _clone(&Child, stack+STACKSIZE 1 , SIGCHLD, 0) == 0)
exec()
Ejecuta un programa externo (ya sea un script binario o ejecutable con #!
<interpreter> [arg] en la primera línea). Esta llamada reemplaza la tarea que esté
actualmente en ejecución con el texto del programa externo. El nuevo programa
mantiene los archivos abiertos y PID del origen de la llamada.
Las llamadas execl(), exedp(), execle(), execv(), y execvp() son
presentaciones de execve()-
Prototipo
#include <unistd.h>
int execve(const char* path, char* const argv[], char* const envp[|);
int execl(const char* path, const char* arg, ...);
int execlplconst char* file, const char" arg, ...);
int execle(const char* path, const char" arg, char* const envp|]);
int execv(const char* path, char" const argv[]);
int execvplconst char* file, char* const argv[]);
Valor devuelto
Esta llamada no devuelve nada si tiene éxito. Si fracasa, el valor devuelto es -1.
Parámetros
file Programa a ejecutar. La llamada busca el nombre en esta
variable utilizando la PATH definida.
Posibles errores
EACCES El archivo o un intérprete de script no es un archivo
ordinario, o se ha denegado la ejecución de permisos
para el archivo o un intérprete de script, o el sistema de
archivos tiene montado noexec.
Ejemplo
execl("/bin/ls", "/bin/ls", "•al", "/home", "/boot", 0);
perror("execl() failed"); /* IF innecesario: si hay éxito, no vuelve */
char *args[ ]={,lls", "-al", "/home", "/boot", 0};
execvp(args[0], args);
perrorf"execvp() failed");
sched_yield()
Renuncia al control de la CPU sin bloqueo. Esta rutina indica al planificador que
la tarea actualmente en ejecución renuncia al resto de su franja temporal asignada.
La llamada devuelve la siguiente asignación de franja temporal.
Prototipo
#include <sched.h>
int sched yield(void) ;
Valor devuelto
Cero, si todo va bien y se transfiere e control; -1, en otro caso.
Parámetros
(ninguno)
Posibles errores
(no definidos)
Ejemplo
#include <sched.h>
sched_yield();
wait(), waitPID()
Espera el reconocimiento de la terminación de un proceso hijo. Es importante
evitar que el proeso zombi se retrase en la tabla de procesos con el fin de liberar
recursos importantes. La llamada wait() espera la terminación de cualquier
proceso, en tanto que la llamada waitPID() permite especificar un proceso o grupo
de procesos concreto. Se pueden utilizar las siguientes macros para obtener
información del estado:
• WIFEXITED(status) es distinto de cero si el proceso hijo ha salida
normalmente.
• WEXITSTATUS(status) evalúa los ocho últimos bits significativos del código
devuleto por el proceso hijo terminado, que podría haber sido confiurado como
argumento para una llamada exit(), o como argumento pam una instrucción de
vuelta en un programa padre. Esta macro puede ser evaluada soameníe si
WIFEXITED devuelve un valor distinto de cero.
« WIFSIGNALED(status) devuelve true si el proceso hijo ha salido como
consecuencia de no haber capturado una señal.
• WTERMSIG(status) devuelve el número de la señal que ha causado la
terminación del proceso hijo. Esta macro puede ser evaluada solamente si
WIF5IGNALED devuelve un valor distinto de cero.
• WIFSTOPPED(status) devuelve true si el proceso hijo que ha causado la
vuelta se encuentra actualmente detenido; esto es posible únicamente si la llamada
se ha hecho usando WUNTRACED.
• WSTOPSIG(status) devuelve el número de la señal que ha causado la
detención del proceso hijo. Esta macro puede evaluarse sólo si WIFSTOPPED
devuelve un valor distinto de cero.
Prototipo
#include <sys/types.h>
#include <sys/wait.h>
PID_t wait(int *status];
PID_t waitpid(PID_t PID, int "status, int options);
Valor devuelto
Ambas llamadas devuelven el PID del proceso hijo que ha terminado.
Parámetros
status Devuelve el estado de finalización del proceso secundario. Si es
distinto de cero o NULL, este parámetro recoge el código de
terminación del hijo y el valor de exit().
Posibles errores
ECHILD Si el proceso especificado en PID no existe o no es hijo del
proceso de llamada. (Esto puede ocurrir para un proceso hijo
propio, si la acción de SIGCHLD está con-fgurada como SIGJGN.)
Threads
Los threads son otro tipo de tarea. En este apartado se definen algunas
llamadas de la biblioteca p Threads.
pthread_create()
Esta llamada crea un proceso de núcleo ligero (thread). El thread se inicia en la
función a la que señala start_fn usando arg como parámetro de la función. Cuando
la función vuelve, el thread terminates. La función debe devolver un valor void* v,
pero, aunque no sea así, el thread termina y el resultado es NULL.
Prototipo
tfinclude <pthread.h>
int pthread_create(pthread_t *tchild, pthread_attr_t *attr, void (*start_fn)(void *), void *arg);
Valor devuelto
Un valor positivo, si tiene éxito. Si la llamada que crea el thread encuentra
cualquier tipo de error, devuelve un valor negativo y guarda el código del error en
errno.
Parámetros
thread Control del thread (pasado sólo como referencia). Si tiene éxito, la
llamada coloca el control del thread en este parámetro.
Posibles errores
EAGAIN Recursos del sistema insuficientes para crear un proceso para
el nuevo thread.
Ejemplo
void* child(void "arg)
{
/**** ¡Hacer algo! ****/
pthread_exit(arg); /* terminar y devolver arg */
}
int main()
{ pthread_t tchild;
if ( pthreadcreate(&tchild, 0, child, 0) < 0 )
perror("Can't créate thread!");
/'*** ¡Hacer algo! ****/
if ( pthread_join(tchild, 0) != 0 )
perror("Join failed");
}
pthread_join()
Es parecido a la llamada del sistema wait(); espera y aceptar el valor devuelto
por el thread hijo.
Prototipo
#include <pthread.h>
int pthread_join(pthread_t tchild, void **retval);
Valor devuelto
Un valor positivo, si todo va bien. Si la llamada que crea el thread encuentra
algún error, devuelve un valor negativo y almacena el código del error en errno.
Parámetros
thread Control del thread a esperar.
Posibles errores
ESRCH No se ha podido encontrar un thread correspondiente al
especificado por tchild.
Ejemplo
(Véase pthread_create().)
pthread_exit()
Termina explícitamente el thread actual, devolviendo retval. También se puede
usar una simple instrucción de vuelta.
Prototipo
#include <pthread.h>
void pthread_exit(void 'retval);
Valor devuelto
(ninguno)
Parámetro
retval Valor voíd* a devolver. Asegúrese de que este valor sea memoria sin
pila.
Posibles errores
(ninguno)
Ejemplo
(Véase pthread_create().)
thread_detach()
Separa un thread tchild del padre (principal). Normalmente, tendrá que adjunto
esperar a todos los procesos y threads. Esta llamada permite crear varios reads e
ignorarlos. Es equivalente a configurar el alributo del thread en el mo-ento de su
creación.
Prototipo
#include <pthread.h>
int pthread_detach(thread_t tchild);
Valor devuelto
Cero, si todo va bien. Si la llamada que crea el thread encuentra cualquier error,
devuelve un valor negativo y guarda el código del error en errno,
Parámetro
tchild Thread hijo a separar.
Posibles errores
ESRCH No ha sido posible hallar un thread correspondiente al
especificado en tchild.
Ejemplo
void child(void *arg)
/**** iHacer algo! ****/
pthread_exit(arg); /" terminar y devolver arg */
int main()
{ pthread_t tchild;
if ( pthread_create(fitchild, 0, child, 0) < 0 )
perror(“Can't create thread!");
else
pthread_detach(tchild);
/**** ¡Hacer algo! ****/
}
Bloqueo
La ventaja principal que se obtiene al usar threads es la compartición de la
memoria de datos. Debido a que es posible que varios threads puedan tratar de
revisar la memoria al mismo tiempo, es necesario bloquear ésta para conseguir un
acceso exclusivo. En este aparado se describen las llamadas pThread que pueden
utilizarse (incluso con clones) para bloquear memoria.
pthread_mutex_init(), pthread_mutex_destroy()
Estas llamadas crean y destruyen variables de mutex de semáforo. Tal vez no
necesite el iniciador, ya que las variables definidas son más fáciles y rápidas de
utilizar. La llamada destroy libera normalmente cualquier recurso. Sin embargo, la
implementación de Linux no utiliza recursos asignados, por lo que la llamada no
hace más que verificar si el recurso está desbloqueado.
Prototipo
#include <pthread.h>
/*---Configuración mutex p-edef mida---*/
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread inutex t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t 'mutex,
const pthrcad mutcxattr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Valor devuelto
Siempre cero.
Parámetros
mutex Mutex a crear o destruir.
Posibles errores
(ninguno)
pthread_mutex_lock(), pthread_mutex_trylock()
Bloquea o intenta bloquear un semáforo para introducir una sección crítica. El
parámetro es simplemente una variable que actúa como tique de reserva. Si otro
thread intenta bloquear una posición reservada, entrará en estado de bloqueo
hasta que el thread propietario de la reserva libere el semáforo.
Prototipo
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *rnutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
Valor devuelto
La llamada devuelve cero si todo va bien, y un valor distinto de cero, si hay
algún error. El código exacto del error se encuentra en errno.
Parámetro
mutex Variable de semáforo.
Posibles errores
EINVAL El mutex no se ha iniciado correctamente.
Ejemplo
pthread_mutex_t mutex = fastmutex;
if ( pthread_mutex_lock(&mutex) == 0 ) {
/*** trabajar con datos críticos ***/
pthread(mutex_unlock(&mutex);
}
pthread_mutex_unlock()
Desbloquea un semáforo mutex.
Prototipo
#include <pthread.h>
int pthread_mutex_unlock (pthread_mutex_t *mutex);
Valor devuelto
La llamada devuelve cero si todo va bien, y un valor no nulo si hay errores. El código
exacto del error se halla en errno.
Parámetro
mutex Variable de semáforo.
Posibles errores
EINVAL El mutex no ha sido iniciado correctamente.
EPERM El thread que llama no es propietario del mutex (sólo en mufex de
verificación de errores).
Ejemplo
(véase pthread_mutex_lock().)
Señales
Cuando se trabaja con tarcas, el programa puede obtener señales (o
notificaciones asincronas). Este apartado describe las llamadas del sistema que
permiten capturar y procesar dichas señales.
sígnal()
Registra la rutina sig_fn para responder a la señal signum. El comportamiento
predeterminado es un disparo individual; el controlador de señales regresa a la
configuración por omisión una vez obtenida la primera señal. Utilice sigartion() en su
lugar si desea un mayor control sobre el comportamiento.
Prototipo
#include <signal.h>
void (*signal(int signum, void (*sig_fn)(int signum)))(int signum);
- or -
typedef void (*TSigFn)(int signum);
TSigFn signal(int signum, TSigFn sig_fn);
Valor devuelto
Un valor positivo, si todo va bien. Si la llamada que crea el thread encuentra
ilgún error, devuelve un valor negativo y guarda el código del error en errno.
Parámetros
signum Número de la señal a capturar.
Posible error
(errno no configurado)
Ejemplo
void sig_handler(int signum)
...
switcn ( signum )
{
case SIGFPE:
...
}
}
if ( signal(SIGFPE, sig_handler) == 0 )
perror("signal() failed");
sigaction()
De forma parecida a signal(), sigaction() establece la recepción de determinadas
señales. Sin embargo, a diferencia de signaesta llamada ofrece un mayor control
sobre la notificación de las señales. Es algo más complicada de usar.
Prototipo
#include <signal.h>
int sigaction(int signum, const struct sigaction "sigact, struct sigaction *oldsigact);
Valor devuelto
Cero, si todo va bien; distinto de cero, en otro caso.
Parámetros
signum Señal a capturar.
Posibles errores
EINVAL Se ha especificado una señal no válida. También se genera si se
hace un intento de cambiar la acción de SIGKILL o SIGSTOP
que no puede atraparse.
sigprocmask()
Especifica que señales se pueden interrumpir a) recibir una señal.
Prototipo
#include <signal.h>
int sigprocmask(int how, const sigset_t *sigset, sigset_t *oldsigset);
Valor devuelto
Distinto de cero, si hay error; cero, en otro caso.
Parámetros
how Así son tratadas las señales interrumpidas mientras se recibe
una señal:
• SIG_BLOCK. El conjunto de señales bloqueadas es la unión
del conjunto actual y el argumento sigset.
• SlG_UNBLOCK. Las señales de sigset son eliminadas del
conjunto actual de señales bloqueadas. No está permitido
desbloquear una señal que no está bloqueada.
• SIG_SETMASK. El conjunto de señales bloqueadas se
contigua con arreglo al argumento sigset.
Posibles errores
EFAULT El parámetro sigset o oldsigset señala a una parte de la
memoria que no forma parte del espacio de direcciones del
proceso.
Archivos y otros
En este apartado se describen algunas bibliotecas y llamadas de! sistema
relacionadas con la administración de archivos.
bzero(), memset()
bzero() inicia a cero el bloque especificado. Esta llamada está en
desuso, y se ha visto reemplazada por memset().
Prototipo
#include <string.h>
void bzero(void "mem, int bytes);
void* memset(void "mem, int val, size_t bytes);
Valor devuelto
bzero() no devuelve ningún valor.
memset() devuelve la referencia mem.
Parámetros
mem Segmento de memoria a iniciar,
Posibles errores
(ninguno)
Ejemplo
bzero(&addr, sizeof(addr));
mernset(&addr, 0, sizeof (addr) );
fcntI()
Manipula el archivo o controlador del socket.
Prototipo
#include <unistd.n>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *flock);
Valor devuelto
Si hay error, se devuelve -1 y se configura errno con arreglo a éste. Si todo va
bien, el valor devuelto depende del tipo de operación:
F_DUPFD Nuevo descriptor.
Parámetros
fd Descriptor a manipular.
Posibles errores
EACCES Operación prohibida por bloqueos mantenidos por otros
procesos.
Ejemplo
include <unistd.h>
include <fnctl.h>
...
printf("PID which owns SIGIO: %d", fnctl(fd, F_GETOWN));
#include <umstd.h>
#include <fnctl.h>
...
if (fnctl(fd, F_SETSIG, SIGKILL) != 0 )
perror("Can't set signal");
#include <unistd.h>
#include <fnctl.h>
...
if ( (fd_copy = fcntl(fd, F_DUPFD)) < 0 )
perror("Can't dup fd");
pipe()
Crea un canal que apunta a él mismo. En cada descriptor de archivo presente
en fd[l coinciden la entrada (fd[0]) y la salida (fd[1]). Si se escribe en fd[1], se
podrán leer los datos en fd[0]. Se utiliza sobre todo con fork().
Prototipo
#include <unistd.h>
int pipe(fd[2]);
Valor devuelto
Cero, si todo va bien; -1, si hay error.
Parámetro
fd Array de dos enteros para recibir los valores del descriptor de
archivo nuevo.
Posibles errores
EMFILE Ya hay demasiados descriptores de archivo en uso por el
proceso actual.
Ejemplo
int fd[2];
pipe(fd); /* crear canal */
poll()
De forma parecida a select(), esta llamada espera cambios en alguno de los
canales de E/S. En lugar de usar macros para administrar y controlar la lista del
descriptor, el programador utiliza entradas de estructura.
Prototipo
#include <sys/poll.h>
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
Valor devuelto
Si es menor que cero, es por que Ka ocurrido un error; un valor cero devuelto
indica una expiración en el límite de tiempo de la llamada. En otro caso, la llamada
devuelve el número de registros del descriptor que han cambiado.
Parámetros
ufds He aquí un array de estructuras pollfd. Cada registro corresponde a un
descriptor de archivo diferente.
struct pollfd {
int fd; /* descriptor de archivo */
short events; /* eventos pedidos */
short revents; /* eventos devueltos */
);
El campo fd es el descriptor de archivo a
verificar. Los campos events y revents
indican los eventos a verificar y los eventos
que ocurrirán, respectivamente. Estos son
los valores de bit disponibles:
POLLHUP. Cuelgue.
Posibles errores
E N O M E M No hay espacio para ubicar tablas del descriptor de archivos.
Ejemplo
int fd_count=0;
struct pollfd fds[MAXFDs];
fds[fd_count].fd = socket(PF_INET, SOCK_STREAM,0);
/*** socket bind() y listen() ***/
fds[fd_count++}.events = POLLIN;
for (;;)
{
if ( poll(fds, fd_count, TIMEOUT_MS) > B )
{
int i;
if ( (fds[ 0 ] . revents & POLLIN ) != 0 )
{
Read()
Lee buf_en bytes del descriptor de archivo fd en el buffer. Puede usar esta
llamada del sistema tanto para sockets como para archivos, pero la llamada no
proporciona tanto nivel de control como la llamada del sistema recv().
Prototipo
include <unistd.h>
int read(int fd, char *buffer, size_t buflen);
Valor devuelto
Número de bytes leídos realmente.
Parámetros
fd Descriptor de archivo (o socket).
Posibles errores
EINTR La llamada ha sido interrumpida por una señal antes de leeT
ningún dato.
Ejemplo
int sockfd;
int bytesread;
char buffer[1024];
/*--- crear socket & conectar a servidor ---*/
if ( (bytes_read = read(sockfd, buffer, sizeof(buffer))) < 0 )
perror("read" );
select()
Espera cualquier cambio en el estado de E/S en los conjuntos de descriptores
de archivo. Cuando cambia cualquiera de los conjuntos especificados, la llamada
vuelve. Hay cuatro macros que facilitan la construcción y administración de
conjuntos de descriptores de archivo:
• FDCLR Elimina un descriptor del conjunto.
• FD_SET Añade un descriptor al conjunto.
• FD_ISSET Comprueba si el descriptor especificado está listo para E/S.
• FD_ZERO nicia el conjunto a vacío.
Prototipo
#include <sys/time. h>
#include <sys/types.h>
#include <unistd.h>
int select(int hi_fd, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval 'timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
Valor devuelto
Número de descriptores cuyo estado ha cambiado. Si ocurre un errror, el valor
devuelto es negativo. Si expira el límite de tiempo, el valor devuelto es cero.
Parámetros
hi_fd Número de descriptor de archivo más alto + 1. Por ejemplo, si hay
cuatro archivos abiertos más el stdío, los descriptores podrían ser 0, 1,
2, 3, 5, 6, y 8. El más alto es el 8. Si incluye fd(8) en la instrucción de
selección, hi_fd sería igual a 9. Si el fd más alto fuese 5, este parámetro
sería igual a 6.
Posibles errores
EBADF Se ha dado un descriptor de archivo no válido en alguno de los
conjuntos.
EINVAL n es negativo.
Ejemplo
int i, ports[]= {9001, 9002, 9004, -1};
int sockfd, max=0;
d_set set;
struct sockaddrin addr;
struct timeval timeout={2,5O0000}; /* 2.5 sec. */
D_ZERO(&set);
dzero( &addr, sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
for ( i = 0; ports[i] > 0; i++ )
{
write()
Escribe msgjen bvtes en el descriptor del campo fd del buffer. También se puede
usar un descripor de socket, pero no se obtiene tanto control como la llamada del
sistema send().
Prototipo
#include <unistd.n>
int write(int fd, const void 'buffer, size_t msglen);
Valor devuelto
Número de bvtes escritos. El número de bytes puede ser menor que msgjen. Si
la llamada no tiene éxito al escribir los bytes requeridos, se puede usar un bucle
para realizar escrituras sucesivas. Si es negativo, la llamada guarda los detalles del error
en errno.
Parámetros
fd Descriptor de archivo (o de socket).
Posibles errores
EBADF fd no es un descriptor de archivo válido o no está abierto para escritura.
EINTR La llamada ha sido interrumpida por una señal antes de escribir ningún
dato.
Ejemplo
/*** Escribir un mensaje (TCP, UDP o Raw) ***/
int sockfd;
int bytes, bytes_wrote=0;
/*--- Crear socket, conectar al servidor ---*/
while ( (bytes = write(sockfd, buffer, insg_len)| > 0 )
if ( (bytes_wrote +- bytes) >= msg_len )
break;
if ( bytes < 0 )
perror("write");
close()
Cierra todos los descriptores (de archivo o de socket). Si el socket está
conectado a un servidor o cliente, requiere un close(). El canal permanece en
realidad activo después del cierre, hasta que se vacía o expira el límite de tiempo.
Cada proceso posee un límite en cuanto al número de descriptores abiertos que
puede tener. getdtablesizeQ devuelve 1024 en Linux 2.2.14, en tanto que el archivo
/usr/include/linux/limits.h define este límite con NR_OPEN. Además, los tres primeros
descriptores predeterminados son stdin (0), stdout (1), y stderr (2).
Prototipo
#include <unistd.h>
int close(int fd);
Valor devuelto
Cero, si todo va bien. Si ocurre un error, puede hallar la causa del mismo en errno.
Parámetro
fd Descriptor de socket o archivo.
Posible error
EBADF fd no es un descriptor de archivo abierto válido.
Ejemplo
int sockfd;
sockfd = socket(PF_INET, SOCK_RAW, htons(99));
if ( sockfd < 0 )
PANIC("Fallo al crear socket raw");
...
if ( close(sockfd) != 0 )
PANIC("Fallo al cerrar socket r a w " ) ;
Apéndice D
Clases de objetos
En este apéndice
Excepciones de C++
Clases de soporte C++
Clases de mensajería C++
Clases de sockets C++
Excepciones de lava
Clases con soporte Java
Clases Java de E/S
Clases de sockets Java
Este apéndice contiene las clases definidas en el API Java y la bibliteca C++
personalizada descrita en este libro v en el sitio web asociado a éste. Cada clase
posee una de estas tres designaciones: Cíate (clase normal que uno mismo puede
ejemplificar), Clase abstracta (clase que define una estructura para clases
derivadas), y Superclase (clase principal ejemplificable).
Excepciones de C++
En este apartado se describen las clases correspondientes a las clases de
excepciones definidas en este libro. La jerarquía responde a la filosofía de hacer el
menor trabajo posible, así como minimizar el riesgo poencial de crear clases
catastróficas.
Exception ← Range Exception
← FileException
← NetExceplion ← NetCorwersionException
← NetDNSEnception
← NetlOEnceplion
← NetConnectException
← NetConfigExcsption
FIGURA D.1
Jerarquía de clases de excepciones de C++.
Exception (Superclase)
Constructor:
Exception(SlmpleString s);
Descripción general: mensaje de excepción genérico con el tipo SimpleString.
Método:
const char* GetString() Recupera el mensaje de cadena.
Excepciones secundarias:
NetException (Clase)
Constructor:
NetException(SimpleString s);
Descripción general: excepción genérica de red.
Clase superior: Exception
Excepciones secundarias:
SimpleString (Clase)
Constructor:
SimpleString(const char* s);
SimpleString(const SimpleStringS s);
Descripción general: tipo de cadena ligero y muy sencillo.
Métodos:
Excepciones: (ninguna)
HostAddress (Clase)
Constructor:
HostAddress(const char* Name=0, ENetwork Network=elPv4);
HostAddress(HostAddress& Address);
Descripción general: clase para administrar la identificación de host.
Métodos:
void SetPort(int Port); Establece el número de puerto.
Excepciones:
Exception
NetConversionException
NetDNSException
Excepciones: (ninguna)
TextMessage (Clase)
Constructor: (ninguno)
Descripción general: patrón de mensaje para crear mensajes específicos para
enviar recibir.
Clase superior: Message
Métodos:
Excepciones: (ninguna)
Socket (Superclase)
Constructor:
Socket(void);
Socket(int sd);
Socket(ENetwork Network, EProtocol Protocol);
Socket(Socket& sock);
Descripción general: clase de socket genérica, no apta para ejemplificaron
directa.
Métodos:
void Bind(HostAddress& Addr); Adjunta socket a puerto-
interfaz.
void Closelnput(voíd) const; Cierra flujo de entrada.
void CloseOutput(void) const; Cierra flujo de salida.
int Send(Message& Msg, int Options=0) Envía mensaje a sitio
const; conectado.
int Send(HostAddress& Addr, Message&. Envía mensaje dirigido.
Msg, int Options=0) const;
int Receive(Message& Msg, int Options=0) Recibe mensaje de
const; conexión.
int Receive(HostAddress& Addr, Message& Recibe mensaje recibido.
Msg, int Options=0) const;
void PermitRoute(bool Setting); Permite múltiples
paquetes.
void KeepAlive(bool Setting); Mantiene conexión activa.
void ShareAddress{bool Setting); Comparte dirección de
puerto-interfaz.
int GetReceiveSize(void); Obtiene-establece tamaño
de buffer de envío.
void SetReceiveSize(int Bytes);
int GetSendSize(voÍd); Obtiene-establece tamaño
de butfer de envío.
void SetSendSize(ínt Bytes);
int GetMinRecetve(void); Obtiene-establece marca
de agua mínima para señal
void SetMinReceive(int Bytes);
de recepción SIGIO.
int GetMinSend(void); Obtiene-establece marca
de agua mínima para señal
void SetMinSend(int Bytes);
de envío SIGIO.
struct timeval GetReceiveTimeout(void); Obtiene-establece tiempo
antes de abortar una
void SetReceiveTimeout(struct timeval& val);
recepción.
struct timeval GetSendTimeout(void); Obtiene-establece tiempo
antes de abortar un envío.
void SetSendTimeout(struct timeval& val);
ENetwork GetType(void); Obtiene tipo de socket
(red).
virtual int GetTTL(void); Obtiene-establece tiempo
de existencia.
virtual void SetTTL(int Hops);
int GetError(void); Obtiene errores pendientes.
Excepciones:
Net Exception
FileException
NetConnectException
NetlOException
NetConfigException
SocketStream (Clase)
Constructor:
SocketStream(void);
SocketStreatn(int sd);
SocketStream(ENetwork Network);
SocketStream(SocketStream& sock);
Descripción general: socket de flujo (SOCK_STREAM).
Clase superior: Socket
Métodos:
Excepcion:
NetConfigException
SocketServer (Clase)
Constructor:
SocketServer(int port, ENetwork Network=elPv4, ¡nt QLen=15);
SocketServer(HostAddress& Addr, int QLen=15);
Descripción general; servidor TCP.
Clase superior: SocketStream
Métodos:
Excepciones:
Exception
NetConnectException
SocketClient (Clase)
Constructor:
SocketClient(ENetwork Network=elPv4);
SocketClient(HostAddressS Host, ENetwork Network=elPv4); //auto-connect
Descripción general: cliente TCP.
Clase superior: SocketStream
Método:
void Connect(HostAddress& Addr); Conecta con host en Addr.
Excepción:
NetConnectException
Datagram (Clase)
Constructor:
Datagram(HostAddress& Me, ENetwork Network=elPv4,
EProtocol Protocol=eDatagram);
Datagram(ENetwork Network=elPv4, Eprotocol Protocol=eDatagram);
Descripción general: socket de datagrama genérico (UDP).
Clase superior: Socket
Métodos:
Excepción:
NetConfigException
Broadcast (Clase)
Constructor:
Broadcast(HostAddress& Me);
Descripción general: socket de difusión para subredes.
Clase superior: Datagram
Métodos: (ninguno)
Excepción:
NetConfigException
MessageGroup (Clase)
Constructor:
MessageGroup(HostAddress& Me, ENetwork Network=elPv4);
Descripción general: socket multidifusión.
Clase superior: Datagram
Métodos:
Excepciones:
NetConfigException
NetConnectException
RangeException
Excepciones de Java
En este apartado se describen todas las excepciones significativas que puede
generar un programa Java al trabajar con sockets.
IOException ← ProlocoIException
← UnknownHostException
← UnknownService Exception
← SocketException ← BindException
← Connect Exception
← NoRouteToHostException
FIGURA D.3
Jerarquía de clases de excepciones Java.
java.io.lOException (Clase)
Constructor:
IOException(); lOException(String msg);
Descripción general: excepciones genéricas de entrada y salida.
Clase superior: Exception
Excepciones secundarias:
java.net.ProtocolException Error de protocolo en Socket
java.net.SocketException (Clase)
Constructor:
SocketException();
SocketException(String msg);
Descripción general: excepción al tratar de usar bind(), connect(), listen(), o
accept(). Usado por ServerSocket, ClientSocket, y MessageGroup.
Clase superior: lOException
Excepciones secundarias:
java.net.BindException No se ha podido asociar a dirección/ puerto (a
menudo por estar en uso por otro proceso).
java.net.DatagramPacket (Clase)
Constructor:
DatagramPacket(byte[] buf, int len);
DatagramPacket(byte[] buf, int len, InetAddress addr, int port);
DatagramPacket(byte[] buf, int Offset, int len);
DatagramPacket(byte[] buf, int Offset, int len, InetAddress addr, int port);
Descripción general: portadores de mensajes básicos para recibir y enviar
mensajes.
Métodos:
InetAddress getAddress(); Obtiene o establece la dirección de origen
void setAddress(lnet Address addr); o destino del paquete.
byte[] getData(); Obtiene o establece los datos del
void setData(byte[] buf); mensaje.
Excepciones: (ninguna)
java.net.InetAddress (Clase)
Constructor: (ninguno)
Descripción general: socket de dirección de Internet. Esta clase no posee
constructor. En lugar de ello utiliza alguno de los métodos estáticos.
Métodos estáticos:
InetAddress getByName(String Devuelve una dirección de Internet para
host); el host.
Métodos:
String getHostAddress(); Obtiene la dirección numérica.
Excepción:
Unknown Host Exception
int read(byte[] arr, int offset, int Lee un array de length bytes en arr,
length); comenzando en offset.
Excepciones:
lOException
Java.io.ByteArraylnputStream (Clase)
Constructor;
ByteArraylnputStream(byte[] buf);
ByteArraylnputStream(byte[] buf, int offset, int length);
Descripción general: permite crear un flujo de entrada virtual a partir de un
array e bytes. A veces se obtiene un bloque de datos (como los de
DatagramSocket); ita clase realiza la tarea de hacer circular esa información.
java.io.ObjectlnputStream (Clase)
Constructor:
ObjectlnputStream(lnputStream o);
Descripción general: con esta clase se leen objetos transmitidos o
almacenados. Estos objetos se crean con un InputStream (disponible en la clase
Socket).
Clase superior: InputStream
Métodos:
int available(); Devuelve el número de bytes que se
pueden leer sin bloqueo.
Excepciones:
lOException
ClassNotFoundException
NotActiveException
OptionalData Exception
InvalidObjectException
SecurityException
StreamCorruptedException
nt write(byte[] arr, int offset, Escribe un array de len bytes (arr) comenzando ent
int len); offset.
Excepción:
lOException
Java.io.ByteArrayOutputStream (Clase)
Constructor:
ByteArrayOutputStream();
ByteArrayOutputStream(int size);
Descripción general: permite convertir datos en un array de bytes. Hay clases
como DatagramSocket que operan sólo con bloques de datos; esta clase realiza la
tarea se hacer circular la información.
Clase superior: OutputStream
Métodos:
void reset(); Limpia los buffers y vacía el array.
int write(byte[] arr, int offset, Escribe un array de len bytes (arr)
int len); comenzando en offset.
Excepciones: (ninguna)
java.io.ObjectOutputStream (Clase)
Constructor:
ObjectOutputStream(OutputStream o);
Descripción general: con esta clase de transmiten o almacenan objetos. Los
objetos se crean con un OutputStream (disponible en la clase Socket).
Clase superior: OutputStream
Métodos:
void close(); Cierra este canal.
Excepciones:
lOException
SecurityException
Java.io.BufferedReader (Clase)
Constructor:
BufferedReader(Reader i);
BufferedReader(Reader i, int size);
Descripción general: mantiene buffers para mejorar el rendimiento. Realiza
algo de traducción de tipos para el reconocimiento de líneas, size especifica el
tamaño de los buffers de entrada.
Clase superior: Reader
Métodos:
void close(); Cierra el canal.
ínt read(byte[] arr, int offset, int Lee un array de bytes en arr comenzando
length); en offset, en length bytes.
Excepción:
lOException
Java.io.PrintWriter (Clase)
Constructor:
intWriter(Writer o);
intWriter(Writer o, boolean autoFlush);
intWriter(OutputStream o);
intWriter(OutputStream o, boolean autoFlush);
Descripción general: realiza cierta traducción de tipos de datos a texto legible.
El flag autoFlush obliga a efectuar limpieza cuando el programa llama a príntl()-
Clase superior: Writer
Métodos:
boolean checkError(); Limpia el flujo y comprueba errores.
int write(char[] arr, int offset, Escribe un array de caracteres (arr) comenzando en
int len); offset, en len bytes.
Excepciones:
lOException
SecurityException
Java.net.Socket (Clase)
Constructor:
SockettString host, int port);
Socket(lnet Address addr, int port);
Socket(String host, int port, InetAddress lAddr, int IPort);
Socket(InetAddress addr, int port, InetAddress lAddr, int IPort);
Excepciones:
lOException
SocketException
java.net.ServerSocket (Clase)
Constructor:
ServerSocket(int port);
ServerSocket(int port, int backLog);
ServerSocket (int port, int backLog, InetAddress bindAddr);
Descripción general: este socket TCP especializado crea un socket de servidor
de escucha.
Clase superior: Object
Static Method:
setSocketFactory(SocketlmplFactory fac); Establece la fábrida de
implementación del Socket.
Métodos:
Socket accept(); Acepta una conexión de cliente y devuelve un
Socket.
Excepciones;
lOException socketException
Java.net.DatagramSocket (Clase)
Constructor:
DatagramSocket();
DatagramSocket(int port);
DatagramSocket(int port, InetAddress bíndAddr);
Descripción general: éste es el socket genérico de datagrama (UDP) para
pasar mensajes,
Clase superior: Object
Métodos:
void close(); Cierra el socket.
Excepciones:
lOException
SocketException
Java.net.MulticastSocket (Clase)
Constructor:
MulticastSocket();
MulticastSocket(int port);
Descripción general: éste es el socket de datagrama genérico (UDP) para
mensajes no conectados.
Clase superior: DatagramSocket
Métodos:
Excepciones;
lOException
SocketException