0% encontró este documento útil (0 votos)
481 vistas512 páginas

Programacion de Socket Linux

Este documento proporciona una introducción a la programación de sockets en Linux desde la perspectiva del cliente y del servidor. Explica conceptos clave como direccionamiento IP, tipos de sockets, modelos de comunicación, manejo de E/S y rendimiento, y propone un enfoque orientado a objetos. El documento está dividido en tres partes que cubren programación de red básica, control de carga en servidores y uso de la API de sockets de Java.
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como ODT, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
481 vistas512 páginas

Programacion de Socket Linux

Este documento proporciona una introducción a la programación de sockets en Linux desde la perspectiva del cliente y del servidor. Explica conceptos clave como direccionamiento IP, tipos de sockets, modelos de comunicación, manejo de E/S y rendimiento, y propone un enfoque orientado a objetos. El documento está dividido en tres partes que cubren programación de red básica, control de carga en servidores y uso de la API de sockets de Java.
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como ODT, PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 512

Programación

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

Capítulo 1. Recetario del cliente de red


Un mundo conectado mediante sockets
Generalidades del direccionamiento TCP/IP
Escucha del servidor: el algoritmo básico del cliente
La llamada del sistema socket procedimientos y advertencias
Realización de la llamada: conexión al servidor
Obtención de la respuesta del servidor
Cierre de la conexión
Resumen: ¿qué ocurre entre bastidores?

Capítulo 2. Elocuencia del lenguaje de red TCP/IP


Generalidades de la numeración IP
Identificación de la computadora
Organización del ID Internet
Máscaras de subredes
Routers y resolución de direcciones
Direcciones desaprovechadas y especiales
Números de puertos de host IP
Ordenación de bytes de red
Uso de herramientas de transformación de Internet
Aplicación de las herramientas y extensión del cliente
Diferentes clases de sockaddr
Canales con nombre de UNIX

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

Capítulo 4. Envío de mensajes entre peers


¿Qué son los sockets basados en la conexión?
Canales abiertos entre programas
Comunicaciones fiables
Conexiones de protocolo inferior
Ejemplo: conexión al demonio HTTP
Protocolo HTTP simplificado
Obtención de una página HTTP
¿Qué son los sockets sin conexión?
Configurando la dirección del socket
Algunos programas sólo necesitan el mensaje

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

Capítulo 5. Explicación del modelo de capas de red


Solución del desafío de red
Cuestiones de hardware de red
Cuestiones de transmisión de red
Interacción de la red con el sistema operativo
Interacción de la red con el programa
Modelo de red de interconexión de sistemas abiertos (OSI)
Capa 1: física
Capa 2: enlace de datos
Capa 3: red
Capa 4: transporte
Capa 5: sesión
Capa 6: presentación
Capa 7: aplicación
Paquete de protocolos de Internet
Capa 1: capa de acceso de red
Capa 2: capa de funcionamiento en Internet (IP)

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

Capítulo 6. Generalidades sobre el servidor.


Asignación del socket. el flujo de programa general del servidor
Un servidor de eco sencillo
Asociar puertos a un socket
Creación de una cola de espera de sockets
Aceptar conexiones de clientes
Comunicación con el cliente
Reglas generales sobre la definición de protocolos
¿Qué programa habla primero?
¿Qué programa dirige la conversación?
¿Qué nivel de certificación necesita?
¿Qué tipos de datos utilizar?
¿Cómo debe manejar los datos binarios?
¿Cómo saber cuando se produce un interbloqueo?
¿Necesita sincronización horaria?
¿Cómo y cuándo reiniciar la conexión?
¿Cuándo ha finalizado?
Un ejemplo extenso: un servidor de directorio HTTP
Resumen: los elementos básicos de un servidor

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

Capítulo 8. Cómo decidir cuándo esperar E/S


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
Un lector de sondeos
Escritor de sondeo
Sondeo de las conexiones
E/S asíncrona

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

Capítulo 9. Cómo romper las barreras del rendimiento


Creación de servlets antes de la llegada del cliente
Colocación de un tope en el número de conexiones cliente
Preduplicación de sus servidores
Ajuste a diferentes niveles de carga
Ampliación del control con un select inteligente
El asalto al planiricador
Sobrecarga de select()
Llegar a un compromiso con un select inteligente
Problemas de implementación
Redistribución de la carga
Investigación a fondo del control de sockets
Opciones generales
Opciones de socket específicas de IP
Opciones de socket específicas de IPv6
Opciones de socket específicas de TCP
Recuperación del descriptor de socket
Envío antes de la recepción: mensajes entrelazados
Apunte sobre los problemas de E/S de archivos
Utilización de E/S sobre la base de la demanda para recuperar tiempo de
CPU
Aumento de la velocidad de send()
Descarga de recv()
Envío de mensajes de prioridad alta

7/512
Resumen: discusión de las características de rendimiento

Capítulo 10. Diseño de Socket Linux robustos


Utilización de herramientas de conversión
Controle los valores de retorno
Captura de señales
SIGTIPE
SICURC
SIGCHLD
SIGHUP
SICIO
SICALARM
Administración de recursos
Administración de archivos
La pila de la memoria
Memoria de datos estáticos
CPU, memoria compartida, y procesos
Servidores críticos
¿Qué se califica como un servidor crítico?
Interrupciones y eventos de comunicación
Cuestiones sobre la recuperación de la sesión
Técnicas de recuperación de la sesión
Cuestiones sobre la concurrencia cliente-servidor
Interbloqueo de la red
Inanición de la red
Ataques de denegación de servicio
Resumen: servidores sólidos como una roca

8/512
PARTE III
Examen objetivo de los sockets

Capítulo 11. Cómo ahorrar tiempo con objetos


La evolución de la ingeniería del software
Programación funcional paso a paso
Cómo ocultar detalles de implementación con la programación modular
Los detalles no son necesarios: programación abstracta
Cómo conseguir un pensamiento más natural mediante la programación
orientada a objetos
Cómo llegar a la programación Nirvana
Reusabilidad del trabajo
Consolidación de la reusabilidad con la reubicabilidad (pluggability)
Presentación de los fundamentos de los objetos
Encapsulación de la implementación
Herencia de métodos
Abstracción de datos
Polimorfismo de métodos
Características de los objetos
La clase o el objeto
Atributos
Propiedades
Métodos
Derechos de acceso
Relaciones
Extensión de los objetos
Plantillas
Persistencia
Generación de flujos
Sobrecarga

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

Capítulo 12. Uso de la API de red de Java


Exploración de diferentes sockets Java
Programación de clientes y servidores
Implementación de mensajeros
Envío a múltiples destinos
Conexión a través de E/S
Clasificación de las clases de E/S
Conversión entre clases de E/S
Configuración del socket Java
Configuraciones compartidas por sockets Java
Configuraciones Java específicas de la multidifusión
Multitarea de los programas
Uso de threads en una clase
Cómo añadir threads a una clase
Sincronización de métodos
Limitaciones de la implementación
Resumen: programación de redes de tipo Java

Capítulo 13. Diseño y uso de una estructura de socket en C++


¿Por qué utilizar C++ para la programación basada en sockets?

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

Capítulo 14. Limitaciones de los objetos


Recordatorio sobre objetos
Comience con buen pie
Un mezclador no es un objeto
Separación entre análisis y diseño
El nivel de detalle apropiado
La explosión de la herencia
Reutilización-Desaprovechamiento
Uso correcto de la directiva friend de C++
El operador sobrecargado
Los objetos no lo resuelven todo
Las novedades llegan y evolucionan
Infección de la herencia

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

Capítulo 15. Encapsulado de red con Llamadas de procedimiento


remoto (RCP)
Repaso del modelo OSI
Comparación de programación de red y procedimental
Limitaciones del lenguaje
Mantenimiento de sesiones conectadas
Suministro de métodos middleware
Stubbing en las llamadas de red
Adición de la implementación de llamadas de servicio
Implementación del nivel de presentación
Creación de RPC con rpcgen
Lenguaje de interfaces de rpcgen
Creación de llamadas con estado con conexiones abiertas
Diagnosis del problema del estado
Recuerde dónde está
Seguimiento de una ruta específica

12/512
Recuperación desde un estado erróneo
Resumen: creación de una caja de herramientas para RPC

Capítulo 16. Cómo añadir seguridad a los programas de red y SSL


Asignación de permisos para trabajar
Niveles de identificación
Formas de intercambio
El problema de Internet
Todo está visible
Formas de ataque/intrusión
Pirateo del TCP/IP
Cómo garantizar la seguridad en un nodo de red
Acceso restringido
Firewalls
Zonas desmilitarizadas (DMZ)
Cómo garantizar la seguridad del canal
Cifrado de mensajes
¿Cuáles son los tipos de cifrado disponibles?
Algoritmos de cifrado públicos
Problemas con el cifrado
Seguridad a nivel de sockets (SSL)
Uso de OpenSSL
Creación de un cliente SSL
Creación de un servidor SSL
Resumen: el servidor seguro

Capítulo 17. Cómo compartir mensajes con multidifusión, difusión y Mbone


Difusión de mensajes a un dominio
Repaso de la estructura IP
Programación para activar la difusión

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

Capítulo 18. La potencia de los sockets raw


¿Cuándo se deben usar sockets raw?
Explicación de ICMP
Cómo controlar la cabecera IP
Aceleración a través de la red física
¿Cuáles son las limitaciones?
Cómo poner los sockets raw a funcionar
Selección del protocolo correcto
Creación de un paquete ICMP
Cómo calcular una suma de comprobación
Cómo controlar la cabecera IP
Tráfico de terceros
¿Cómo opera ping?
El receptor MyPing
El emisor MyPing
¿Cómo opera traceroute?
Resumen: toma de decisiones raw

Capítulo 19. IPv6: la próxima generación de IP


Problemas actuales de direccionamiento
Resolución de la reducción del espacio de direcciones TPv4

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

Apéndice A. Tablas de datos


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

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

Apéndice C. Subconjunto API del núcleo


Tareas
Threads
Bloqueo
Señales
Archivos y otros

Apéndice D. Clases de objetos


Excepciones de C++
Exception (Superclase)
NetException (Clase)
Clases de soporte C++
SimpleString (Clase)
HostAddress (Clase)
Clases de mensajería C++
Message (Clase abstracta)
TextMessage (Clase)
Clases de sockets C++
Socket (Superclase)
SocketStream (Clase)
SocketServer (Clase)
SocketClient (Clase)

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.

Organización del libro


Este libro entra en muchos aspectos particulares de la programación en red.
Está organizado en cinco partes, cada una de las cuales se basa en el contenido
de la parte anterior:

• Parte I: Programación de red desde la perspectiva del cliente.


En esta parte se introducen los sockets y se definen los términos básicos. Se
describen los distintos tipos de sockets, los esquemas de dirección y la teoría
de redes.

• Parte II: La perspectiva del servidor y el control de carga.

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 III: Examen objetivo de los sockets.


C no es el único lenguaje de programación que proporciona acceso a sockets.
En esta parte se presentan algunas técnicas orientadas a objetos y se
describen las ventajas y limitaciones de la tecnología de objetos, en general.

• Parte IV: Sockets avanzados: más prestaciones


En esta parte se presentan una gran cantidad de técnicas de programación de
redes avanzadas, que incluyen la seguridad, la difusión y la multidifusión, IPv6
y sockets raw.

• 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.

Estilos del lector profesional


Este libro está pensado para el profesional de la programación. Normalmente, el
profesional necesita alguna de estas tres cosas: aprender una tecnología nueva,
solucionar un problema concreto, o buscar algún ejemplo o definición. Los
capítulos y apéndices intentan responder a cada uno de estos criterios.

• Lectura lineal de principio a fin. Esta opción es la idónea cuando se desea


aprender tema a tema, en sucesión. Cada apartado del capítulo se basa en el
contenido de capítulos precedentes. El texto está organizado en términos de
complejidad creciente. La programación de sockets puede resultar bastante
intuitiva al principio. Sin embargo, al avanzar en este tema van surgiendo
cuestiones relativas a la temporalización, excepciones, rendimiento, etc, que
pueden echar por tierra el mejor de los diseños. La lectura lineal facilita la
compresión ordenada de los conceptos necesarios para realizar las tareas
como es debido.

• Hojear. Hojeando el libro se puede obtener la información necesaria sin entrar


en muchos detalles. Se supone que el profesional conoce bien el tema y desea
ir directamente al punto de interés. De forma parecida a navegar por la Web, el
hojeador va pasando de unos temas a otros hasta que localiza exactamente la

20/512
información que anda buscando.

• Consulta. El experto suele necesitar información (tablas, fragmentos de


programa, API concretas) rápidamente. La información ha de ser en este caso
lo más sucinta posible y fácil de localizar.
El libro contiene toda la información fundamental necesaria para abordar las
tareas más habituales en este terreno. Además, hay muchos capítulos en los que
existen porciones de texto apartadas del contenido normal del tema. Estos
fragmentos contiene información adicional, detalles, notas para expertos y
opiniones.

La audiencia y la interacción esperada


El libro contiene muchos ejemplos de código que ilustran los temas tratados.
Lea los apartados y pruebe los programas. Para ello necesitará conocer cómo
llevar a cabo lo siguiente:
1. Crear un archivo fuente C o Java (en cualquier editor).

2. Compilarlo usando un compilador C o Java.

3. Ejecutar el programa.

Algunos ejemplos requieren, además, modificar el núcleo (difusión o


multidifusión). La mayoría de las distribuciones de Linux instalan y ejecutan la red
aunque no exista una conexión física a la misma. Para poder ejecutar todos los
programas que aparecen en el libro necesita:

• Programas y compilar código C, C++ y Java.


• Tener todos los compiladores instalados.
• Saber configurar el núcleo para redes, difusión y multidifusión.
• Compilar e instalar el núcleo nuevo.
Reservas y limitaciones
Aunque todos los programas que figuran en el texto han sido probados, es
posible que alguno no funcione tal cual se presenta aquí, debido a que la
configuración del sistema cambia con el tiempo. En la mayoría de los casos, si algo
no funciona, se debe a que ha cambiado la distribución.
Además, hay expertos que han verificado los programas en otros sistemas
operativos UNIX. Esto puede representar una ironía con respecto a la máxima de
portabilidad. Sin embargo, si existe una discrepancia entre Linux y otras
implementaciones, en el libro se incluyen siempre los listados para Linux (y se
incluye. además, una nota descriptiva de este hecho). El libro está centrado
principalmente en Linux.

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.

Convenios usados en el libro


A lo largo del libro se han utilizado los siguientes convenios tipográficos;

• Las líneas de código, comandos, instrucciones, variables y cualquier texto que


se ha de teclear o que aparece en pantalla, se muestra en un tipo de letra de
espacio fijo o sin adornos. El tipo de letra sin adornos se utiliza también para
indicar las entradas del usuario.

• Los marcadores de posición en las descripciones sintácticas aparecen en un


tipo de letra sin adornos y cursiva. Ha de reemplazar los marcadores de
posición por nombres de archivo, parámetros o el elemento que éstos
representen.

• Se ha usado el atributo de cursiva para resaltar términos técnicos cuando éstos


son definidos.

• El icono → delante de una línea de código representa la continuación de la


línea previa. A veces una línea de código es demasiado larga para caber en
una línea de texto en la página. Si ve este símbolo delante de una línea de
código, recuerde que forma parte de la línea anterior.

• El texto incluye también referencias a documentos relativos a estándares de


Internet, denominados Request For Comment (RFC). Estas citas aparecen
entre corchetes, con el número RFC correspondiente, como [RFC875].

22/512
Parte I

Programación de red desde la


perspectiva del cliente

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

Recetario del cliente de red

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.

Un mundo conectado mediante sockets


Hace varios años, el funcionamiento en red implicaba a una línea serie dedicada
de una computadora a otra. Ninguna otra computadora podía compartir el mismo
circuito, y UNIX utilizó UUCP (copia de UNIX a UNIX) para mover archivos entre
sistemas. Como la tecnología de transmisión en la línea mejoró, se hizo posible el
concepto de compartición de la línea de transmisión. Esto implicaba que cada
computadora necesitaba identificarse de forma única y había que realizar turnos en
la transmisión. Existen varios métodos diferentes para compartir la red en el
tiempo, y muchos de ellos funcionan bastante bien. A veces, las computadoras
transmiten simultáneamente, causando una colisión de paquete.
El hardware y los controladores de bajo nivel gestionan las cuestiones de
colisiones y retransmisiones, ahora un objeto de programación del pasado. Esto
libera al diseño de fijar su atención en la transmisión y recepción de mensajes. La
API de Socket (Interfaz de programación de la aplicación) suministra a los
diseñadores el medio para recibir o enviar mensajes.
La programación de sockets se diferencia de la aplicación o herramienta de
trabajo normal, en que trabajamos con programas y sistemas que funcionan
concurrentemente. Esto implica que necesita conocer la sincronización,
temporización y administración de recursos,
Los sockets enlazan tareas asíncronas con un único canal bidireccional. Esto
podría conducir a problemas como el interbloqueo y la inanición. Con conocimiento
y planificación, puede evitar muchos de estos problemas. Puede obtener
información de cómo gestionar cuestiones de mutitarea en el Capítulo 7, "División
de la carga: multitarea", y construcción de sockets robustos en el Capítulo 10,
"Diseño de sockets Linux robustos".
Normalmente, un servidor sobrecargado reduce su velocidad de respuesta a lo
recibido de Internet. La administración de recursos y la temporización reduce la
carga del servidor, incrementando el rendimiento de la red. Puede encontrar
muchas ideas para mejorar el rendimiento en la Parte II, "La perspectiva del
servidor y el control de carga".
Internet fue diseñado para funcionar con la conmutación de paquetes. Todos los

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.

Generalidades del direccionamiento TCP/IP


Las redes soportan diferentes tipos de protocolos. Los programadores han
utilizado algunos protocolos para controlar temas específicos como radio-
microondas; otros intentan resolver los problemas de Habilidad de la red. TCP/IP
(Protocolo para el control de la transmisión-Protocolo de Internet) fijó la atención en
el paquete y en el potencial de pérdida en los canales de comunicación. Siempre
que falle un segmento de red, el protocolo intenta encontrar una ruta nueva.
El rastreo de paquetes, detección de pérdida y retransmisión son algoritmos
difíciles, porque la temporización no es el único indicador. Afortunadamente, la
experiencia industrial ha probado los algoritmos utilizados en el protocolo.
Normalmente, se pueden ignorar estas cuestiones durante el diseño, porque las
soluciones están ocultas dentro del protocolo.
TCP/IP está compuesto por capas: los protocolos de nivel alto ofrecen mas
habilidad pero menos flexibilidad y los niveles inferiores ofrecen más flexibilidad
pero sacrifican la fiabilidad. Con los diferentes niveles de flexibilidad y fiabilidad, la
API de Socket ofrece todo !o que las interfaces necesitan. Esto es una desviación
de la estrategia del UNIX estándar en la que cada nivel tiene su propio conjunto de
llamadas.
El archivo de E/S estándar utiliza también una estrategia por capas. Las
computadoras conectadas a través de TCP/IP utilizan sockets, en su mayoría, para
comunicarse con el resto. Esto puede parecer extraño, considerando que las
distintas capas de protocolos disponibles para un programa han sido diseñados
para que open() (el cual produce un descriptor de archivo) y fopen() (el cual
produce una referencia de archivo) sean diferentes y casi incompatibles. Todas las
capas de protocolos están disponibles a través de una: socket(). Esta llamada
única resume todos los detalles de implementación de redes diferentes (TCP/IP,
IPX, Rose).
Fundamentalmente, cada paquete condene los datos, la dirección de origen y la
de destino. Cada capa del protocolo añade su propia firma y datos adicionales
(envoltura) a la transmisión del paquete. Cuando se transmite, la envoltura ayuda al
receptor a reenviar el mensaje hacia la capa apropiada que aguarda su lectura.
Cada computadora conectada a Internet tiene una dirección IP (Protocolo de
Internet), un único número de 32 bits. Sin la unicidad, no hay forma de conocer el
destino correcto de los paquetes.
TCP/IP lleva el direccionamiento un paso más allá con el concepto de puertos.

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.

Escucha del servidor: el algoritmo básico del


cliente
La conexión cliente-socket más simple es la que abre una conexión a un
servidor, envía una consulta y acepta la respuesta. Algunos de los servicios
estándar incluso no esperan consultas. Un ejemplo es el servicio timeof-day
hallado en el puerto 13. Desafortunadamente, muchas distribuciones de Linux no
tienen ese servicio abierto sin que se revise el archivo /etc/inetd.conf. Si tiene
acceso a una máquina BSD, HP-UX o Solaris, puede intentar conectarse a ese
puerto.
Existen varios servicios disponibles para probar con seguridad. Puede intentar
en la máquina la ejecución del comando Telnet para conectarse al servicio FTP
puerto (21):
% telnet 127.0.0.1 21
Después de la conexión, el programa obtiene el mensaje de bienvenida del
servidor. No funciona muy bien el hacer Telnet para conectarse al servidor FTP,
pero puede ver la interacción básica. El ejemplo simple de cliente del Listado 1.1 se
conecta al servidor, lee la bienvenida y luego se desconecta.

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.

La llamada del sistema socket: procedimientos y


advertencias
La única herramienta que hace eficaz al receptor de mensajes y comienza el
proceso completo de envío y recepción de mensajes de otra computadora es la
llamada del sistema socket(). Esta llamada es la interfaz común entre todos los
protocolos disponibles en un sistema operativo Linux/UNIX. Al igual que la llamada
del sistema open() crea un descriptor para acceder a los archivos y dispositivos en
nuestro sistema, socket() crea un descriptor para acceder a las computadoras de la
red. Esta llamada necesita información que determine a qué capa quiere acceder.
La sintaxis es como sigue:
#include <sys/socket.h>
#include <resolv.h>
int socket(int domain, int type, int protocol);
La llamada al sistema socket() acepta varios valores distintos. Para obtener una
lista completa, véase el Apéndice A, "Tablas de datos". Por ahora, encontrará
algunos en la Tabla 1.1.
Tabla 1.1 Valores de parámetros seleccionados de la llamada de sistema socket()
Parámetro Valor Descripción
domain PF_INET Protocolos de Internet IPv4; pila TCP/IP.
PF_LOCAL Canales con nombre locales al estilo BSD. Normalmente
utilizado en el registro del sistema o en una cola de
impresión.

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:

• EPROTONOSUPPORT. El tipo de protocolo o el protocolo especificado no se


soporta dentro de este dominio. Esto ocurre cuando el dominio no soporta el
protocolo que se demanda. Excepto para SOCK_RAW, muchos tipos de
dominio sólo soportan el valor cero en el protocolo.

• EACCES. El permiso para crear un socket del tipo especificado o protocolo se


deniega. Nuestro programa es posible que no tenga los privilegios adecuados

31/512
para crear un socket. SOCK_RAW y PF_PACKET necesitan los privilegios de
root.

• EINVAL. Se desconoce el protocolo o no se dispone de la familia de protocolo.


Esto ocurre cuando un valor en cualquier campo dominio o tipo es inválido.
Para obtener un listado completo de valores válidos, véase el Apéndice A.
Por supuesto, necesita conocer los archivos de cabecera más importantes a
incluir. Para Linux, son estos:
#include <sys/socket.h> /* Define funciones estándar. */
#include <sys/types.h> /* Tipos de datos del sistema estándar. */
#include <resolv.h> /*Define los tipos de datos que se necesitan */
El archivo sys/socket.h tiene las definiciones de función que se necesitan para
la API de Socket (incluyendo, por supuesto, la función socket(). sys/types.h
incorpora muchos de los tipos de datos que se utilizan con los sockets.
USO DE RESOLV.H FRENTE A SYS/TYPES.H
En este libro se utiliza el archivo resolv.h para la definición de los tipos de datos. Se debe
observar que otras distribuciones de Linux o versiones UNIX pueden utilizar sys/types.h,
el archivo de inclusión más estandarizado. Durante la escritura de este libro, los ejemplos
se comprobaron sobre Mandrake 6.0-7.0, el cual utiliza archivos de inclusión muy
peculiares. (Parece que estas versiones de distribución tienen el archivo sys/types.h
erróneo el cual no incluye el archivo netinet/in.h que se necesita para los tipos de
dirección.)
La llamada del sistema socket() sólo crea las colas para el envío y recepción de
datos, de forma contraria a la llamada del sistema para la apertura de archivos, la
cual abre el archivo y lee el primer bloque. Sólo cuando el programa ejecuta una
llamada del sistema bind(), el sistema operativo conecta la cola a la red.
Se utilizará de nuevo el ejemplo del teléfono, el socket es el auricular sin
teléfono o conexión de red. Si se ejecuta bind(), connect() o alguna E/S se
conecta el auricular al teléfono y éste a la red. (Si el programa no realiza
explícitamente la llamada bind(), el sistema operativo realiza implícitamente la
llamada por él. Para obtener más información consulte el Capítulo 4, "Envío de
mensajes entre peers".)

Realización de la llamada: conexión al servidor


Después de la creación del socket, se puede obtener el primer "¿hola?" a través
de la conexión al servidor. La llamada del sistema connect() es similar en varios
aspectos a la llamada de alguien por teléfono:

• Se identifica el destino utilizando un número de teléfono. Cuando se llama a un


número de teléfono, se identifica a un teléfono específico ubicado en cualquier
lugar del mundo de la red telefónica. La dirección IP identifica de la misma
forma a la computadora. Al igual que los números de teléfono tienen un formato
específico, se requiere que la conexión posea un formato específico para definir
a qué computadora se conecta y cómo conectarse.

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.

• El camino de regreso hasta su auricular está oculto dentro del sistema


telefónico. La red telefónica tiene varios caminos compartidos, al igual que una
red de computadoras. Es importante tener un camino de regreso para obtener
los mensajes de vuelta a su auricular. El equipo de destino o servidor obtiene la
dirección y el puerto de su programa para poder responder utilizando un camino
similar.
El número telefónico tiene que estar publicado para que otros le llamen. Si su
programa acepta llamadas, debe especificar un único canal (o puerto) y publicarlo
a sus clientes.
La llamada del sistema connect() se define como sigue:
#include <sys/socket.h>
#include <resolv.h>
int connect(int sd, struct sockaddr *server, int addr_len);
El primer parámetro (sd) es el descriptor de socket que se creó con la llamada
socket(). El último parámetro es la longitud de la estructura sockaddr. El segundo
parámetro apunta hacia diferentes tipos y tamaños de estructuras. Esto es
importante, porque es lo que hace diferentes las llamadas socket() de las llamadas
de E/S de archivos.
Recuerde que la llamada del sistema socket() soporta al menos dos dominios
diferentes (PF_INET y PF_IPX). Cada dominio de red (PF_*) tiene su propia
estructura para describir la dirección. Todos ellos tienen un padre común—el
mismo que se usa en la definición connect()—struct sockaddr. Consulte el
Apéndice A para obtener un listado completo de todas las declaraciones de
estructura.
ABSTRACCION DE DATOS SOCKADDR
La interfaz sockaddr utiliza abstracción de datos. La abstracción de datos simplifica las
interfaces asegurando que mientras los tipos de datos pueden cambiar, los algoritmos
permanecen igual. Por ejemplo, una pila puede contener diferentes tipos de datos, pero la
función de la pila sigue siendo la misma: meter, sacar y así sucesivamente. Para utilizar
una interfaz abstracta, el primer campo de la estructura sockaddr debe tener el mismo
significado. Todas las estructuras tienen un campo común: ..._family. El tipo del campo es
un entero de 16 bits sin signo. El valor de este campo determina qué clase de dominio de
red utilizar.

RELACION DEL TIPO SOCKET CON EL CAMPO


DE FAMILIA SOCKADDR
El tipo de dominio que se estableció en la llamada del sistema socket() debe ser el mismo
valor que el primer campo en la familia sockaddr. Por ejemplo, si el programa creó un
socket PF_INET6, el campo de la estructura debe ser AF_INET6 para que el programa

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_

sin_port El número de puerto del servidor Red 13 (información


horaria)
sin_addr La dirección IP numérica del servidor Red 127.0.0.1
(localhost)

El programa rellena cada campo antes de la invocación a la llamada del sistema


connect()- Linux tiene enmascarada levemente la llamada del sistema, así que no
es necesario transformar sockaddr_in en sockaddr. Por transportabilidad, puede
seguir las convenciones todavía y aplicar las transformaciones.

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.

Obtención de la respuesta del servidor


El socket se abre y el canal se establece. Ahora, se puede obtener el primer
"hola". Algunos servidores inician la conversación como la persona responde al
teléfono. Una vez que la conexión está abierta, se puede utilizar la biblioteca
estándar de llamadas de E/S de bajo nivel para la comunicación. He aquí la
llamada del sistema read():
#include <unistd.h>
ssize_t read(int td, void *buf, size_t count);
Probablemente se encuentra familiarizado con esta llamada. Aparte de su
capacidad especial para utilizar el descriptor socket (sd) en lugar del descriptor de
archivo (fd), todo lo demás es aproximadamente lo mismo que la lectura de un
archivo. Puede incluso utilizar la llamada del sistema read() como en el siguiente
fragmento de código:
...
int sd, bytes_read;
sd = socket(PF_INET, SOCK_STREAM, 0); /* Crear el socket. */
/*** Conectar al host ****/
bytes_read = read(sd, buffer, MAXBUF); /* Leer el mensaje. */
if ( bytes_read < 0 )
/* Informar del error de la conexión; rutina de salida. */
...
De hecho, se puede transformar el descriptor de socket a un FILE* para E/S de
nivel bajo. Por ejemplo, para utilizar fscanf (), puede seguir el ejemplo de abajo.
(Las líneas en negrita indican los cambios del listado previo.)
char Name[NAME], Address[ADDRESS], Phone[PHONE];
FILE *sp;
int sd;
sd = socket(PF_INET, SOCK_STREAM, 0); /* Crear el socket. */
/*** conectar al host ****/
if ( (sp = fdopen(sd, "r")) == NULL ) /* Transformar a FILE*. */

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:

• EAGAIN. Una E/S no bloqueada está seleccionada, y no existen datos


disponibles. Este error indica al programa que intente la llamada de nuevo.

• EBADF. fd no es un descriptor de archivo válido o no está abierto para lectura.


Esto puede ocurrir si la llamada del socket no tuvo éxito o el programa cerró el
canal de entrada (creándolo de sólo escritura).

• EINVAL. fd se ha conectado a un objeto que es inadecuado para la lectura.


La llamada del sistema read() no ofrece un control especial sobre la forma en
que utiliza el socket. Linux ofrece otra llamada del sistema estándar, recv(). Se
puede utilizar directamente recv() con el descriptor de socket para obtener la

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".

• MSG_OOB. Se procesan los datos fuera de banda. Se usa en los mensajes de


alta prioridad. Algunos protocolos permiten elegir entre una prioridad normal o
alta cuando se envía un mensaje. Se establece esta señalización para indicar al
administrador de cola que busque y devuelva los mensajes fuera de banda en
vez de los datos normales. Se puede ver el Capítulo 10 para obtener mayor
información.

• MSG_PEEK. Se lee de forma no destructiva. Se utiliza para indicar al


administrador de cola que lea de la cola de mensajes sin mover el puntero de
índice de lectura. (En otras palabras, una lectura posterior produce al menos los
mismos datos cada vez. Mire la caja de texto en la próxima página.)

• MSG_WAITALL. Se utiliza para que no devuelva el mensaje hasta que el buffer


suministrado esté completo. Algunas veces se obtiene un buffer semilleno,
porque el resto de los datos están en tránsito. Si conoce cuanta información
está enviando el servidor y no desea recomponerla, utilice esta señalización
para terminar de rellenar el buffer (o esperar indefinidamente).

• MSG_DONTWAIT. Se utiliza para que el mensaje no se bloquee si la cola está


vacía. Es similar al establecimiento de la característica de no bloqueo en el
socket, único requisito de esta opción en esta llamada del sistema recv()
exclusivamente. Normalmente, si no existen datos de mensaje disponibles, el
proceso espera (se bloquea) hasta que lleguen algunos datos. Si se utiliza esta
señalización y la cola no tiene datos disponibles a la hora de la llamada, la
llamada del sistema devuelve un código de error EWOULDBLOCK
inmediatamente. (Linux no soporta actualmente esta opción en la llamada

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:

• ENOTCONN. sd no está conectado. El descriptor de socket proporcionado no


está conectado a otro equipo homólogo o al servidor.

• ENOTSOCK. sd no es un socket. El descriptor de socket proporcionado no


tiene la firma que indica que viene de una llamada del sistema socket().
Se debe observar que si se utiliza la llamada del sistema read() con un

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:

• EBADF. fd no es un descriptor de archivo válido.


SIEMPRE HAY QUE CERRAR LOS DESCRIPTORES DE SOCKET
Siempre se deben cerrar manualmente los descriptores, particularmente los sockets. Por
omisión, el sistema operativo cierra todos los descriptores y vacía los buffers. Si el
descriptor hace referencia a un archivo, el proceso funciona sin afectar a otros sistemas.
Los sockets, por el contrario, pueden tardar más de lo necesario, bloqueando recursos y
haciendo la conexión difícil a otros clientes.
La llamada del sistema shutdown() da mayor control del cierre de canales,
porque se puede cerrar cualquier canal entrante o saliente. Esta llamada es
especialmente útil cuando se utilizan sockets para reemplazar a stdin o stdout.
CONFUSIÓN EN LOS COMANDOS DE CIERRE
La llamada del sistema shutdown() es distinta al comando shutdown (sección 8 de los
manuales en línea de UNIX) para cerrar el sistema operativo.
Con la llamada del sistema shutdown(), se puede cerrar cualquier dirección de
flujo de datos del canal de comunicación, estableciendo el camino de sólo lectura o
sólo escritura:
#include <sys/socket.h>
int shutdown(int s, int how);

El parámetro how puede tener tres valores:

Valor Función

0 (cero) Sólo escritura (pensar en "O" de "output").

40/512
1 (uno) Sólo lectura (pensar en "I" de "input").

2 Cerrar la entrada y la salida.

Resumen: ¿qué ocurre entre bastidores?


Varias cosas ocurren entre bastidores cuando el programa abre un socket y se
conecta a un servidor TCP. Lo único que realiza la llamada socket es crear una
cola de mensajes. Realmente, ocurren más cosas cuando el programa se conecta.
(Se puede obtener un listado del programa completo en el CD adjunto.) La Tabla
1.2 muestra lo que ocurre en la parte del cliente y del servidor.
Es bastante para una llamada connect() simple. Este proceso se puede
complicar mucho más, especialmente entre los equipos de encaminamiento (como
la administración de encaminamiento, verificación de paquetes, fragmentación y
defragmentación, conversión de protocolos, tunneHng, etc.). La API de Socket
simplifica considerablemente la comunicación de red.
Tabla 1.2 Pasos para la creación y conexión de un socket
Acciones del cliente Acciones del servidor
1. Llama a socket(): crea una cola y (Esperando una conexión)
establece las señalizaciones para los
protocolos de la comunicación.
2. Llama a connect(): el sistema (Esperando)
operativo asigna un número de puerto
temporal si el socket no tiene un número
de puerto asignado a través de bin().
3. Envía un mensaje al servidor
solicitando una conexión, indicando qué
numero de puerto está usando.
(Esperando al servidor) 4. Coloca la petición de conexión en la
cola de escucha.
(Esperando) 5. Lee la cola de conexión, acepta la
conexión y crea un canal de socket
individual.
(Esperando) 6. Algunas veces, crea una tarea o
thread único para interactuar con el
programa.
(Esperando) 7. Envía de regreso una confirmación
de que la conexión es aceptada. Envía
un mensaje al puerto o espera una
consulta del programa. El servidor

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

Elocuencia del lenguaje de red


TCP/IP

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.

Organización del ID Internet


El ID Internet utiliza un esquema de direccionamiento de lo más general a lo
más específico. Cada ID es un número de cuatro bytes, como el ID ethernet. El
primer número, leyendo de izquierda a derecha, es la clase de red. Observe la
Figura 2.1.
La dirección IP actúa como un mapa de carreteras para los routers. Cada parte
de la dirección ofrece una información más específica sobre dónde se encuentra el
destino. Comienza con la clase de red y termina con el número de host. Puede
compararlo fácilmente a la dirección de una carta: la primera línea es el
destinatario, luego la calle o apartado de correos. El detalle se reduce en la
dirección hasta el estado o país.

FIGURA 2.1 La dirección IP tiene varios componentes que identifican la dimensión


de la red.
El direccionamiento de Internet tiene cinco clases básicas de direcciones. Cada
clase es un conjunto de asignaciones de red con distinta dimensión. El plan de
direccionamiento de Internet se encuentra organizado para que las empresas
puedan comprar segmentos del espacio de direcciones. Las clases están
etiquetadas de la A a la E:

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.

D 224.0.0.0 a 228-2 ó 268.435.454 nodos.

239.255.255.255 Estas direcciones no se encuentran asignadas pero se reservan


para direcciones de multidifusión.

E 240.0.0.0 a 228-2 ó 268,435,454 nodos.

255.255.255.255 Éstas están reservadas para un uso futuro. Se debe observar


que 255.255.255.255 es una dirección IP de difusión general.
Nunca es una dirección válida.

Las clases de red se encuentran numeradas de forma peculiar, porque el


direccionamiento utiliza los primeros bits de la dirección para determinar la clase.
Por ejemplo, la red de Clase A tiene un cero en el primer bit de la dirección. De
igual modo, la Clase B tiene un uno en el primer bit y un cero en el segundo:
Clase A: 0 (0000,0000) a 126 (0111,1110).
Clase B: 128 (1000,0000) a 191 (1011,1111).
Clase C: 192 (1100,0000) a 223 (1101,1111).
Clase D: 224 (1110,0000) a 239 (1110,1111).
Clase E: 240 (1111,0000) a 255 (1111,1111).
Con el uso de esta estrategia, los routers pueden rápidamente determinar (con
cuatro bits) cómo encaminar el mensaje. Hoy en día, se encamina mejor utilizando
el encaminamiento de dominio de Internet sin clase (CIDR). Estas definiciones

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.

Routers y resolución de direcciones


Las redes de área local (LAN) pueden llegar a estar muy sobrecargadas por
todos los mensajes entre los hosts. Cada computadora en Internet podría potencial
mente escuchar cada mensaje. Pero con todo el tráfico, las redes son muy
confusas, los paquetes colisionan con otros (en transmisiones simultáneas), y el
rendimiento cae cercano a cero.
Las LAN utilizan el direccionamiento y las submascaras para limitar el tráfico
dentro de grupos (o clusters) de computadoras. Más allá del paso de mensajes a
las interfaces apropiadas, los routers actúan como puertas de control de
información. El tráfico local permanece dentro del cluster, y el tráfico externo pasa
de un lado a otro.
Los hosts utilizan la máscara de subred para determinar si tiene que pasar un
mensaje al router. Todos los mensajes destinados al exterior pasan a través del
router, y los mensajes locales permanecen dentro de la subred. Este proceso
reduce la congestión en el backbone de la red.
Los routers también dirigen los mensajes hacia el destino utilizando sus tablas
de rutas. Cada router indica a otros routers de la red cuales son sus rutas de red
para que le envíen los mensajes que pueda encaminar correctamente. Las redes
de área ancha (WAN) utilizan esta característica para enviar mensajes.
Cada router a. lo largo del camino entre el origen y el destino mira en la
dirección IP, comparando las redes con las tablas de rutas. Los routers trasladan el
mensaje cada vez más hacia un destino específico. El primer salto intenta
resolverlo en el cluster, el segundo traslada el mensaje para resolverlo en la
subred, así sucesivamente, hasta que se resuelve la clase.
Tan pronto como el router encuentra una correspondencia (general o
específica), traslada el mensaje en esa dirección, utilizando la dirección MAC para
enviar el mensaje al siguiente router. Eventualmente el cluster correspondido
obtiene el mensaje, y el ARP del router reemplaza su MAC con la MAC del destino.
Un adaptador de red ethernet acepta únicamente mensajes con su ID. En
cuanto el host se inicia y estabiliza, comunica a todos los de la red su ID ethernet y
su dirección IP. Ésta es la función de ARP, como mencionamos anteriormente.

Direcciones desaprovechadas y especiales


Como mencionamos antes, existen algunas direcciones reservadas. La subred
activa de un máscara de subred tiene dos direcciones reservadas: todo a cero
(dirección de red) y todo a uno (dirección de difusión). Significa que cuando cuenta
las direcciones que tiene en su disposición, debe restar 2 a ese número. Considere
esto: si crea 100 subredes, pierde efectivamente 200 direcciones.
Esta pérdida es justo al principio. Dos grandes bloques de direcciones están
reservadas para uso interno: del 0.0.0.0 al 0.255.255.255 y del 127.0.0.0 al

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".

Números de puertos de host I P


Todos los mensajes llegan a una o más direcciones reconocidas. Si un
programa acepta estos mensajes, puede recibir información destinada para otros
programas en ejecución. El sistema operativo tiene poca consideración hacia
dónde se dirige el mensaje, con tal de que se entregue. La pila TCP/IP añade el
concepto de puerto. Un sistema puede tener varios puertos disponibles, todos
asociados con la misma dirección.
La pila TCP/IP añade los puertos para abstraer la red y facilitar la programación.
Estos no son puertos físicos reales. Son canales que el subsistema de red utiliza
para redireccionar la información al programa apropiado. Todos los programas de
red no reciben todos los mensajes que llegan; sino que sólo reciben los mensajes
de su puerto.
En la red basada en Internet, cada paquete peculiar de datos IP tiene la
dirección del host y el número de puerto. Cuando un paquete llega de la red, un
campo de 16 bits en la cabecera del paquete indica el número de puerto de
destino. El sistema operativo lee este campo y coloca el paquete nuevo en la cola
de puertos. Desde allí, el programa lee el socket (con la llamada del sistema read()
o recv()). Asimismo, cuando el programa transmite un mensaje (mediante la
llamada del sistema write() o send()), el sistema operativo coloca los datos en la
cola saliente del puerto.
Por omisión, sólo un programa posee un puerto. De hecho, si se intenta ejecutar
dos programas que asignan el mismo número de puerto en la misma computadora,
la llamada del sistema devuelve un código de error EINVAL. Se puede compartir un
puerto utilizando SO_REUSEADDR (véase el Capítulo 9, "Cómo romper las
barreras del rendimiento").

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.

9 discard Como /dev/null.

13 daytime Fecha y hora del sistema.

20 ftp-data Puerto de datos FTP.

21 ftp Conexión FTP principal.

23 telnet Conexión Telnet.

25 Smtp, mail Correo UNIX.

37 time, timeserver Servidor de hora.

42 namesserver Resolución de nombres (DNS).

70 gopher Información con menús de texto.

79 finger Usuarios actuales.

80 www, http Servidor web.

Se puede encontrar una lista más completa en el Apéndice A.


Probablemente reconozca algunos de estos servicios. El formato del archivo es
legible: número de puerto, nombre de servicio, alias y descripción. Puede interac-
tuar con muchos de ellos usando Telnet o el programa mostrado anteriormente.
Tenga presente que, aunque /etc/services puede incluir un servidor, la
computadora puede que no tenga el servidor asociado ejecutándose (por ejemplo,
la distribución Mandrake no habilita los servicios de hora). Todos los puertos
incluidos en el archivo /etc/services están reservados, por lo que su uso puede
causar conflictos.
El programa basado en sockets utiliza algunos puertos locales para toda la
comunicación: esto es responsabilidad elemental de bind()- Aunque no se utilice la
llamada del sistema bind(), va que el programa utiliza el socket para cualquier E/S,

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.

Ordenación de bytes de red


Muchos tipos distintos de computadoras pueden residir en una red, pero es
posible que no utilicen el mismo procesador. Todos los procesadores no almacenan
sus números binarios de la misma forma. Las computadoras usan dos tipos de
almacenamiento de números binarios básicos: big-endian y little-endian.
Simplemente, los números big-endian se leen de izquierda a derecha; los números
little-endian se leen, de derecha a izquierda. Por ejemplo, considere el número
214.259.635. En hexadecimal, el número se lee como #0CC557B3. Un procesador
big-endian almacena este valor como sigue:
Dirección: 00 01 02 03 04
Datos: 0C C5 57 B3 ...
Se debe observar que el byte más significativo (0C) está listado primero. El
procesador little-endian lo almacena al revés:
Dirección: 00 01 02 03 04
Datos: B3 57 C5 0C ...
Se debe observar que el byte menos significativo (B3) está listado primero.

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.

Uso de herramientas de transformación de Internet


Hace muchos años, se eligió que el protocolo de red entendiera big-endian. Esto
es bueno para procesadores big-endian, pero ¿qué hay de los little-endian? Se
disponen de varias herramientas que ayudan a realizar la transformación. Los
programas de red utilizan estas herramientas todo el tiempo para rellenar la
estructura struct sockaddr_*, sin preocuparse del endianness del proceso. Ahora,
otra dificultad: todos los campos de la estructura no están ordenados en bytes de
red. Algunos son ordenados en bytes de host. Considere el extracto de código del
Capítulo 1, "Recetario del cliente de red":
/****************************************************************/
/*** Ejemplo de herramientas de transformación: ***/
/*** cómo rellenar sockaddr_in. ***/
/****************************************************************/
struct sockaddr_in dest;
char *dest_addr = "127.0.0.1";
...
dest.sin_family = AF_INET;
dest.sin_port = htons(13); /* Puerto nº>13 (servidor de hora). */
if ( inet_aton(dest_addr, &dest.sin_addr) == 1 ) {
...
El código rellena tres campos: sin_family, sin_port y sin_addr. sin_family es un
campo ordenado en bytes de host, así que no necesita transformación. Los otros
dos están ordenados en bytes de red. El campo sin_port de dos bytes transforma el
puerto 13 utilizando htons(). Existen varias herramientas transformadoras como a
continuación:

53/512
Llamada Significado Descripción

htons De host a red (short) Convierte 16 bits binarios a big-endian.

htonl De host a red (long) Convierte 32 bits binarios a big-endian.


ntohs De red a host (short) Convierte 16 bits binarios a formato de host.
ntohl De red a host (long) Convierte 32 bits binarios a formato de host.

En el Apéndice B, "API de red", se muestran estas funciones con mayor detalle.


Es de interés, que la utilización de estas herramientas no consume ningún tiempo
de CPU si la computadora es big-endian (orden en bytes de red).
La siguiente llamada del ejemplo, inet_aton(), transforma la dirección IP ASCII
(usando notación punto) al binario equivalente. También se convierte a la
ordenación de red (no se necesita llamar a htonl() en el resultado). A continuación
se muestran otras llamadas:
Llamada Descripción

inet_aton() Transforma la notación punto (###.###.###.###) al binario


ordenado en red. Devuelve cero si falla y distinto de cero si la
dirección es válida.
inet_addr() Obsoleta (la misma que inet_aton). No gestiona los errores
correctamente. Si ocurre un error, devuelve -1
(255.255.255.255—la dirección de difusión general).
inet_ntoa() Transforma un binario IP ordenado en red a un ASCII en
notación punto decimal.
gethostbyname() Pregunta al servidor de nombres para transformar el nombre
(como por ejemplo www.linux.org) a una o más direcciones IP.
getservbyname() Obtiene el puerto y protocolo asociado a un servicio del
archivo /etc/services.
Las bibliotecas ofrecen muchas más herramientas de función que éstas. En este
libro se trata sólo unas pocas que se consideran útiles. Para obtener más
información de estas llamadas de función, se puede consultar el Apéndice B.
ALCANCE DE LA TRANSPORTABILIDAD
Para quienes piensan que probablemente no tengan que preocuparse sobre estas
herramientas de transformación, deben considerar esto: la idea global del desarrollo del
software Linux es realizarlo compatible con todos los sistemas, ¿es correcto? Al usar esas
llamadas no se hace daño a nada a pesar de la plataforma. Generalmente, es una buena
práctica de programación programar como si fuera a ser trasladado a otro procesador.
Si está probando a transformar de un formato a otro, puede encontrar ¡a función
transformadora adecuada en alguna biblioteca. Ésa es la belleza del uso de la
tecnología establecida como la compatibilidad de POSIX. Por supuesto, eso
significa que existen muchas herramientas para examinar, lo cual ayuda a tener las
herramientas agrupadas por su función. (Ésa es la razón por la cual este libro

54/512
incluye varias páginas de manual aplicables en los apéndices.)

Aplicación de las herramientas y extensión del


cliente
El próximo paso es extender la funcionalidad del lector de hora con la capacidad
de enviar un mensaje y conseguir la respuesta. De nuevo, un descriptor de socket
se puede intercambiar con un descriptor de archivo. Al igual puede utilizar read(),
puede utilizar write():
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
He aquí algunos errores comunes que se pueden encontrar:

• EHADH. fd no es un descriptor de archivo válido o no se puede abrir para


escritura. Por ejemplo, el programa cerró va el descriptor de socket, o el socket
nunca fue abierto correctamente (comprobar el valor devuelto de la llamada
socket()).

• EINVAL. fd está asociado a un objeto que es inadecuado para la escritura. Esto


puede ocurrir si el programa había cerrado previamente el canal de escritura.

• EFAULT. Se especificó una dirección de espacio de usuario no válida en un


parámetro. El puntero buf no apunta a un espacio válido. Cuando la llamada
intentó acceder a la región de memoria, obtuvo una infracción de
segmentación.

• 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;

sd = socket (AF_INET, SOCKSTREAM, 0);


... /*** Conectar al host. ***/
while ( bytes_written < len ) /* Repetir hasta que todos */
{ /*...los bytes del mensaje son enviados.*/
retval = write(sd, buffer +bytesjwritten, len);

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);

... /*** Conectar al host. ***/


sp = fdopen(sd, "w"); /* Crear FILE* desde el socket. */
if ( sp == NULL )
perror("FlLE" conversión failed");
fprintf(sp, "%s, %s, %s \n",Name, Address, Phone);
Se debe observar en el primer ejemplo, que el programa tiene que realizar un
bucle con write() para obtener todos los bytes externos. Aunque esto es un flujo de
socket, no puede garantizar que el programa envíe todos los bytes en seguida. El
segundo ejemplo no tiene esta limitación, porque FILE* tiene su propio subsistema
de gestión de buffers de datos. Cuando se escribe en un búfer FILE*, el subsistema
obliga a esperar hasta que se envían todos los bytes.
Hay, por supuesto, un socket dedicado para escribir la llamada: send(). Como la
llamada recv(), da al programador más control sobre la ejecución de la transmisión.
La declaración del prototipo es:
#include <sys/socket.h>
#include <resolv.h>
int send(int sd, const void *msg, int len, unsigned int flags);

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.

• MSG_DONTROUTE. No permite el encaminamiento del paquete. Esto

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.

• MSG_DONTWAIT. No espera a que send() finalice. Esta opción permite al


programa proceder como si send() estuviera hecho (o delegado). Si se utiliza,
el sistema operativo emite una señal SIGIO, para indicar una operación write()
completa. Si la operación se bloquea porque la cola de send() está llena, la
llamada devuelve un error y establece EAGAIN en errno.

• MSG_NOSIGNAL. No emite ninguna señal SIGPIPE. Si el otro extremo cierra


pronto, y si el programa local envía otro mensaje, se puede obtener una señal
SIGPIPE. Si no se está preparado para esta señal, el programa detiene su
ejecución.
El uso de la llamada del sistema send() es similar al uso de recv(). Si quiere
agregar las señalizaciones, utilice el operador aritmético O:
/*******************************************************/

/*** Realizar una escritura normal con un canal de socket.***/


/*******************************************************/

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:

• EBADF. Se especificó un descriptor inválido. La causa más probable es que el


programa no comprobó el valor devuelto de la llamada socket().

• ENOTSOCK. El argumento no es un socket. A lo mejor se mezclan los


descriptores de socket y de archivo.

• EMSGSIZE. El socket pidió al núcleo enviar atómicamente el mensaje, y el


tamaño del mensaje a ser enviado, hizo esto imposible. Los mensajes de
difusión no pueden ser fragmentados, o el programa establece la opción de
socket a "no fragmentar".

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 .

• EPIPE. El extremo local ha sido cerrado en un socket orientado a la conexión.


En este caso, el proceso recibe también un SIGPIPE, a menos que MSG_
NOSIGNAL esté establecido. (Idéntico a EPIPE en la llamada del sistema
write())
Utilice la llamada del sistema send() para investigar un servidor. Por ejemplo, si
quiere utilizar el programa finger en un servidor, abra un canal al puerto (79) del
servidor, envíe el nombre de usuario y lea la respuesta. Este algoritmo es
normalmente lo que hace el cliente.
No necesita ninguna opción especial para realizar esta tarca. Sin embargo,
puede desear leer tanta información como el servidor le envíe. Algunas veces la
respuesta puede ser más grande que el tamaño del buffer. El siguiente trozo de
listado de programa muestra la implementación del algoritmo. Puede conseguir el
listado de programa completo en el CD-ROM que acompaña a este libro.
/*******************************************************************/
/*** Extensión del comprobador de puertos, añade la capacidad ***/
/*** de acceder a cualquier puerto y enviar un mensaje. ***/
/*******************************************************************/
int main(int count, char *strings[])
{ int sockfd;
struct sockaddr_in dest;
char buffer[MAXBUF];
/*--- Crear un socket y asignar un número de puerto. ---*/*/
sockfd = socket(AF_INET, SOCKSTREAM, 0) ;
bzero(&dest, sizeof(dest));
dest.sin_f amily = AF_INET;
dest.sin_port = htons(atoi(strings(2)));
inet_addr(strings[1 ], &dest.sin_addr.s_addr);
/*--- Conectar a un servidor y enviar la petición. ---*/*/
if ( connect{sockfd, &dest, sizeof(dest)) != 0 )
PANIC("connect() failed");
printf(buffer, "%s\n", strings[3]);
send(sockfd, buffer, strlen(buffer), 0);
/*--- Vaciar el buffer y leer la respuesta CORTA. ---*/*/
bzero(buffer, MAXBUF);

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.

Diferentes clases de sockaddr


Las redes soportan varios tipos de protocolos distintos. Cada protocolo tiene
funciones y características específicas. Para la red, sólo son paquetes. Los tipos de
paquete incluyen:

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.

Canales con nombre de UNIX


Considere por unos instantes cómo funciona syslog. El problema que surge es
el siguiente: ¿cómo se podrían coordinar varias aplicaciones que son susceptibles
de emitir errores o mensajes en diferentes momentos? Syslog es una herramienta
que acepta mensajes como éstos. La API de Socket tiene ya incorporado este tipo
de coordinación.
Los sockets con nombre permiten que varios programas locales se envíen
mensajes (o paquetes). Ellos tienen nombre, porque crean realmente un archivo en
el sistema de archivos. La comunicación es sólo local; nada pasa a través de la
red, y un cliente de red no puede conectarse a él.
Funciona como un socket normal: puede crear una conexión de flujo o de data-
grama. Como se dijo anteriormente, la única diferencia es el sockaddr. He aquí
cómo configurar el canal con nombre:
/****************************************************/
/*** Ejemplo de socket con nombre de Unix. ***/
/****************************************************/
#include <sys/un.h>
int sockfd;
struct sockaddr_un addr;
sockfd = socket (PF_LOCAL, SOCKSTREAM, 0);

bzero(&addr, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy (addr.sun_path, "/tmp/mysocket"); /* Asignar nombre. */

if ( bind(sockfd, &addr, sizeof(addr)) != 0 )


perror("bind() failed");

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.

Resumen: aplicación de herramientas y


numeración IP
La API de Socket es una herramienta de interacción muy flexible para la
programación en red. Ésta soporta varios protocolos distintos, permitiendo conectar
e interconectar con otros protocolos de funcionamiento en red.
El protocolo de Internet utiliza direccionamiento que incorpora en los mensajes
de encaminamiento y los grupos de clustering de las computadoras. Esto hace a
cada mensaje autónomo del gestor del origen: un mensaje dejado en la red puede
conseguir el destino a través del encaminamiento y las tablas ARP. Sin embargo,
debido a la flexibilidad del direccionamiento y la asignación escasa, la asignación
de direccionamiento está perdiendo bloques de direcciones válidas.
Parte de la pila TCP/IP resume la red con puertos. La mayoría de conexiones
utilizan puertos para comunicarse con programas específicos en otras
computadoras de red como si poseyeran la conexión de red. Esta característica
facilita la programación y reduce la inundación de mensajes que el programa
podría recibir realmente.
La red utiliza tipos y ordenaciones de bytes (endianness) específicos para pasar
los mensajes de una parte a otra, sockaddr define el protocolo, dirección y puerto
de la conexión. Debido a todos estos formatos de datos distintos, la API de Socket
ofrece muchas herramientas de transformación para el direccionamiento {por
ejemplo, inet_addr(), inet_aton(), inet_ntoa()) y para el endianness (htons(),
ntohs(), htonl()).
TCP y UDP ofrecen al programa niveles de interacción distintos. En el próximo
capítulo se definen los distintos tipos y capacidades de cada protocolo dentro de IP.

61/512
Capitulo III

Tipos de paquetes de
Internet
En este capítulo

El paquete de red fundamental


Análisis de varios paquetes
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

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.

El paquete de red fundamental


Si se pudiera realmente observar los bits que viajan de una computadora a otra,
¿qué se vería? Cada protocolo es muy distinto, pero todos comparten una
característica necesaria en común: todos transportan el mensaje del programa.
Algunos protocolos incluyen la dirección origen, mientras otros requieren la del
destino. Se puede pensar que no requerir un destino es un poco usual, pero
algunos protocolos (como UUCP) utilizan la conexión como la dirección de destino.
El protocolo de Internet (IP) IRFC791I requiere que un paquete tenga tres
elementos básicos: origen, destino y datos. (La carga útil de datos incluye su
tamaño.) Estos elementos ofrecen un nivel de autonomía al paquete. No importa
dónde está el paquete, se puede identificar de dónde viene, dónde va y cómo es de
grande.
La autonomía del paquete es una característica de Internet. Mientras el paquete
esté vivo (los datos son oportunos y relevantes), los routers trasladan los datos a
su destino cuando el paquete está activado sobre la red.
ALIASlNG DE PAQUETE
La autonomía que tiene el paquete también conlleva una parte negativa. Mientras un
paquete ofrece la forma de llegar a cualquier sitio desde cualquier parte, un programador
malicioso puede fácilmente engañar a la red. La red no requiere que la dirección del host
origen esté validada. Hacer aliasing o spoofing (enmascaramiento de la verdadera
identidad asumiendo una distinta) de la dirección hardware es difícil, pero los programas
pueden enmascarar otros ID. Se debe observar que núcleos de Linux recientes no
permiten el spoofing.
Cómo se trató en el Capítulo 2, "Elocuencia del lenguaje de red TCP/IP", el
paquete de red está en el orden de bytes de red (o big-endian). Recordando esto,
observe la definición de la estructura del paquete de red en el Listado 3.1. La
Figura 3.2 muestra el esquema físico de la cabecera IP.

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.

Campo frag_offset y flags dont_frag y more_frags


Estos flags controlan cómo fragmentar los paquetes o si se fragmentan. Cuando
un paquete extenso atraviesa la red y se encuentra con un segmento de red
reducido (uno que no pueda soportar el tamaño de trama del paquete), el router
puede intentar dividir el paquete en trozos más pequeños (fragmentación). Un
paquete fragmentado permanece fragmentado hasta que llega al destino. Puesto
que cada fragmento tiene su propia cabecera IP, la sobrecarga fija disminuye el

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.

Campo time_to_live (TTL)


Este campo contaba originalmente el número de segundos que un paquete
podía perdurar en la red durante su tránsito. Más tarde, el significado cambió al
número de saltos de router. Un salto es la transición a través de un host o router
(nodo) donde el nodo traslada activamente un paquete de una red a otra.
Este campo de 8 bits permite hasta 255 saltos de router antes de ser
descartado. Cuando un router o host de reenvío obtiene el paquete, resta este
campo en uno. Si el campo es igual a cero antes de la llegada al destino, el nodo
descarta el paquete y envía un error al origen. El campo TTL evita que existan
paquetes dando vueltas indefinidamente en la red.
Se puede utilizar la opción de socket IP„TTL para establecer este valor (véase el
Capítulo 9). Alternativamente, se puede establecer la opción directamente si se
elige el tratamiento de cabecera IP directo (IP_HDRINCL).

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.

Análisis de varios paquetes


IP ofrece varios protocolos de paquetes que se clasifican desde muy rápidos a
muy fiables. Todos ellos se apoyan en la capa más baja—el paquete básico IP. Sin
embargo, cada capa se ha desarrollado para resolver problemas específicos. Para
seleccionar el tipo de paquete apropiado, se debe conocer lo que se transmite.
Los tipos de paquetes que probablemente sean más de interés son: TCP, UDP,
ICMP y raw. El conocimiento de las ventajas y desventajas de cada tipo le puede
ayudar a elegir el más apropiado para la aplicación. Cada tipo de paquete tiene
distintas ventajas, como se encuentras resumidas en la Tabla 3.2.
Tabla 3.2 Ventajas de cada tipo de paquete

68/512
Raw ICMP UDP TCP
Sobrecarga fija (bytes) 20-60 20-60+[4] 20-60+[8] 20-60+[20-60]

Tamaño del mensaje (bytes) 65,535 65,535 65,535 (ilimitado)

Fiabilidad Baja Baja Baja Alta

Tipo de mensaje Datagrama Datagrama Datagrama Flujo

Rendimiento Alto Alto Medio Bajo

Integridad de los datos Baja Baja Media Alta

Fragmentación Sí Sí Sí Baja

En esta tabla, se observa que cada tipo de paquete contiene contraposiciones.


Una fiabilidad de valor baja sólo significa que no se puede confiar en el protocolo
para conseguir la fiabilidad. A pesar de que las diferencias pueden ser extremas, se
recuerda que son meramente comparaciones.

Cuestiones relacionadas con los paquetes


Cada protocolo controla ciertos aspectos en la transmisión. Los siguientes
apartados definen cada aspecto y categoría de la Tabla 3.2. Puede ayudarle a
entender por qué ciertos protocolos implementan algunas características y omiten
otras.

Sobrecarga fija de protocolo


En la sobrecarga fija de protocolo se incluyen dos cosas: el tamaño de cabecera
en bytes y la cantidad de interacción que el protocolo requiere. Una sobrecarga alta
fija de paquete puede reducir el rendimiento, porque la red tiene que gastar más
tiempo en trasladar cabeceras y menos tiempo de lectura de datos.
Un protocolo robusto en sincronización e intercambio de señales incrementa la
interacción de sobrecarga fija. Esto es más costoso en la red WAN debido a la
propagación de retardos. La Tabla 3.2 no incluye esta medida.

Tamaño de mensaje de protocolo


Para calcular el rendimiento de la red, necesita conocer el tamaño de paquete y
la sobrecarga fija del protocolo. El tamaño de transmisión le da el tamaño máximo
de un mensaje enviado. Todos menos TCP utilizan un único mensaje, esta
limitación es debida normalmente a las limitaciones del paquete IP (65.535 bytes).
La cantidad de datos que el programa transmite por paquete es el tamaño de
transmisión menos las cabeceras.

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.

Tipo de mensaje de protocolo


Algunos mensajes son autocontrolados e independientes de otros mensajes.
Fotos, documentos, mensajes de e-mail, etc. son algunos ejemplos que se pueden
adaptar al tamaño del paquete. Otros se parecen más a un flujo de corriente, como
sesiones Telnet, canales abiertos de HTTP [RFC2616], documentos, fotos o
archivos grandes. El tipo de mensaje define qué estilo se adapta mejor a cada
protocolo.
PROTOCOLO HTTP
HTTP 1.0 podría efectivamente utilizar UDP para la transferencia de mensajes en vez de
TCP. El cliente envía simplemente la consulta de un documento específico, y el servidor
responde con el archivo. Efectivamente, no se efectúa ninguna conversación entre el
cliente y el servidor.

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.

Integridad de los datos de protocolo


La tecnología del funcionamiento en red tiene actualmente bastantes medidas
de seguridad para la integridad de los datos. Algunas interfaces de red incluyen
una suma de comprobación o verificación por redundancia cíclica (CRC) en cada
mensaje de bajo nivel. También incluyen tecnología hardware especial que puede
filtrar el ruido y obtener el mensaje auténtico. Adicionalmente, cada protocolo
incluye medidas para detectar errores en los datos. Estos errores pueden o no
pueden ser importantes para el programador.
La importancia de la integridad de los datos depende de los propios datos; es

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.

• Crítico. Datos importantes y responsables. Datos que si están fuera de


secuencia o defectuosos pueden causar daño a la propiedad o seguridad. Por
ejemplo, transacciones financieras, tarjetas de crédito, números PIN, firmas
digitales, dinero electrónico, secretos comerciales, actualizaciones de escáner
de virus y actualizaciones de productos.

• Importante. Datos que necesitan un funcionamiento correcto. Cualquier pérdida


puede causar un mal funcionamiento. Por ejemplo, conexiones XI1, descargas
de FTP, páginas web, direcciones de servidor o router, y conexiones Telnet.

• Informativo.Datos que pueden tener menos del 100% de fiabilidad para un


funcionamiento correcto. Por ejemplo, e-mail, flujo de noticias, publicidad y
páginas web.

• Temporal. Datos que están asociados a la fecha y hora. A menos que el


programa utilice esta información dentro de un tiempo específico, se reduce su
importancia. Por ejemplo, datos climáticos, datos de supervisión y datos
horarios.

• Desechable. Datos que pueden degenerarse sin perder su utilidad. Son


normalmente vídeo y sonido. Por ejemplo, películas, archivos de sonido, fotos y
spam {por supuesto).
Previamente a la elección del tipo de paquete o protocolo, se tienen que
clasificar los datos de acuerdo a esta lista. También se tiene que incluir las
restricciones adicionales (o externas) del programa. Estas también pueden ser
restricciones administrativas.

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).

Sobrecarga fija (bytes) De 20 a 60.

Fiabilidad Baja (la red puede descartar o reconfigurar los paquetes).

Tipo de mensaje Datagrama.

Rendimiento Alto (baja sobrecarga fija del sistema).

Integridad de los datos Baja (el sistema no valida el mensaje).

Fragmentación Sí.

Linux ofrece la opción de funcionar con capas distintas en la pila IP (para


obtener una definición completa de la pila IP y las capas se puede consultar el
Capítulo 5, "Explicación del modelo de capas de red"). El mensaje TCP/IP más
básico es el mensaje IP raw. No tiene información aparte de la más básica.
Puede utilizar el paquete IP para crear la capa más básica y así crear los
propios protocolos a medida. Para acceder al paquete IP seleccione SOCK_RAW
en la llamada del sistema socket(). Por seguridad, debe tener privilegios de root
para ejecutar un programa socket raw.
El socket raw permite trabajar con las tripas del paquete IP. Puede configurar el
socket para que funcione con dos niveles de detalle: tratamiento de cabecera y
datos o sólo de datos. El tratamiento de los datos es parecido a la transmisión de
datos UPD sin soporte de puertos. El tratamiento de la cabecera permite establecer
directamente los campos cabecera.
Con el uso de este mensaje se obtiene ventajas y desventajas. Al ser un
mensaje datagrama, no se ofrece garantía de la llegada o de la integridad de los
datos. Sin embargo, se puede enviar o recibir mensajes casi a la velocidad de la
red. Para consultar más información de la administración de paquetes raw se
puede consultar el Capítulo 18, "La potencia de los sockets raw".

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.

Fiabilidad Baja (el mismo que 11' raw).

Tipo de mensaje Datagrama.

Rendimiento Alto (el mismo que IP raw).

Integridad de los datos Baja (el mismo que IP raw).

Fragmentación Sí (pero es improbable).

Si implanta ICMP en el programa puede reutilizar el socket para enviar mensajes


a iiosís distintos sin volver a abrir el socket. Los mensajes se pueden enviar usando
la llamada del sistema sendmsgO o sendto() (como se describirá en el próximo
capítulo). Estas llamadas necesitan una dirección de destino. Con un socket
individual, se pueden enviar mensajes a tantos puntos como se desee.
Las ventajas y desventajas de un paquete ICMP son esencialmente las mismas
que en IP raw (y otros datagramas), Sin embargo, el paquete incluye una suma de
comprobación para la validación de los datos. También, la probabilidad de que la
red pueda fragmentar un paquete ICMP es muy pequeña. Esto se debe a la
naturaleza de los mensajes ICMP: se usan para indicar los estados, los errores y el
control. El mensaje no será muy grande, así que nunca necesitará la
recomposición.
Aunque pueda usar ICMP en los mensajes propios, se utiliza habitualmente para
mensajes de error y de control. Todos los errores del funcionamiento en red viajan
por la red dentro de un mensaje ICMP. El paquete tiene una cabecera que incluye
los códigos de error, y la parte de los datos puede contener un mensaje más
específico describiendo el error.
Como parte del protocolo IP, ICMP obtiene una cabecera IP y añade su propia
cabecera. El Listado 3.2 muestra una definición de la estructura.

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. */
};

FIGURA 3.3 Esquema ICMP.


El tipo y el código definen qué error se produjo, msg puede ser cualquier
información adicional para buscar detalladamente lo que salió mal. Para obtener un
listado completo de tipos y códigos, véase el Apéndice A.

Protocolo de datagrama de usuario (UDP)


El Protocolo de datagrama de usuario (UDP) se utiliza principalmente para las
comunicaciones sin conexión (mensajes independientes). Éste puede enviar
mensajes a diferentes destinos sin volver a crear los sockets y actualmente es el
protocolo sin conexión más común. Los atributos de UDP están incluidos en la
Tabla 3.5.

74/512
Tabla 3.5 Atributos UDP
Tamaño del mensaje (bytes) 65,535 (65.507 máxima carga útil de datos).

Sobrecarga fija (bytes) De 28 a 68.

Fiabilidad Baja.

Tipo de mensaje De un solo uso.

Rendimiento Medio.

Integridad de los datos 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:

• Dividirpaquetes grandes. Se toma cada mensaje y se divide en porciones


asignándole un número (por ejemplo, 2 de 5). El equipo del otro extremo
recompone el mensaje. Se recuerda que más sobrecarga fija y menos envío
de datos reducen el rendimiento.

• Seguir la pista de cada paquete. Se asigna un número único a cada paquete.


Se fuerza al equipo homólogo a mandar un acuse de recibo de cada paquete,
porque sin acuse de recibo, el programa reenvía el último paquete. Si el
equipo homólogo no obtiene el paquete esperado, solicita un reenvío con el

75/512
último número de mensaje o envía un mensaje de reanudación.

• Añadir una suma de comprobación o CRC. Se verifican los datos de cada


paquete con una suma de datos. Un CRC es más fiable que una suma de
comprobación, pero la suma de comprobación es más fácil de calcular. Si el
equipo homólogo descubre que los datos están corruptos, le indica al
programa que reenvíe el mensaje.

• Usar tiempos de espera. Se puede asumir que un tiempo de espera expirado


implica que ha fallado. El origen puede retransmitir el mensaje, y el receptor
puede enviar un recordatorio al emisor.
Los tipos de datos Critico e Importante requieren la fiabilidad de TCP o una
mejor. El tipo Fallo intolerable requiere mucho más de lo que ofrecen cualquiera de
estos protocolo. Estos pasos esquematizados imitan la fiabilidad de TCP.
UDP confía en las características y los servicios de IP. Cada paquete de
datagrama UDP recibe una cabecera UDP e IP. En el listado 3.3 se define la
estructura UDP.
Listado 3.3 Definición de la estructura U D P
/***********************************************************/
/*** Definición de la estructura UDP (datagrama). ***/
/*** (Definición formal en netinet/udp.h.) ***/
/***********************************************************/
typedef unsigned char ui8;
typedef unsigned short int ui16;
struct UDP_header {
ui16 src_port; /* Número de puerto del origen. */
ui16 dst_port; /* Número de puerto del destino. */
ui16 length; /* Tamaño del mensaje. */
ui16 checksum; /* Suma de comprobación del mensaje. */
uchar data[]; /* Datos del mensaje. */
};

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).

Sobrecarga fija (bytes) De 40 a 120.

Fiabilidad Alta (El recibo de los datos comprobado).

Tipo de mensaje Flujo.

Rendimiento Bajo (comparado con otros protocolos).

Integridad de los datos Alta (incluye suma de comprobación).

Fragmentación Improbable.

Para conseguir una mayor fiabilidad se requiere la garantía de que el destino


obtenga el mensaje exacto que el emisor envió. UDP tiene velocidad pero no tiene
la fiabilidad que necesitan muchos programas. TCP resuelve el problema de la
fiabilidad.
La red, sin embargo, tiene varios problemas fundamentales que la hacen poco
fiable. Estos problemas no son una limitación. De hecho, son inherentes al diseño
de la red. Para conseguir fiabilidad, mensajes con capacidad de flujo a través de la
web entrelazada, TCP/IP tiene que incorporar muchas de las ideas sugeridas en el
apartado de UDP. Internet tiene tres obstáculos: conexiones dinámicas, pérdida de
datos y caminos reducidos, como se tratarán en los siguientes apartados.

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.

Definición de la cabecera TCP


TCP tuvo que añadir bastante información a su cabecera para soportar todas las
características que ofrece. El tamaño, en bytes, de la cabecera TCP es unas tres
veces el de la cabecera UDP. En el Listado 3.4 se observa la definición de la
estructura TCP.
Listado 3.4 Definición de la estructura TCP
/***************************************************************/
/*** Definición de la estructura TCP (socket de flujo). ***/
/*** (Definición formal en netinet/tcp. h). ***/

/*************************************************/
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. * /

u i n t d a t a _ o f f :4; / * Offset de los datos. * /


u i n t _r e s : 6 ; / * (Reservado.) * /
uint urg_flag:1; / * Urgente, mensaje fuera de banda. * /
uint ack_flag:1; / * Campo de acuse de recibo válido. * /
uint psh_flag:1; / * Colocar el mensaje a procesar inmediatamente. * /
uint rst_flag:1; / * Reiniciar la conexión debido a errores. * /
uint syn_flag:1; / * Abrir una conexión virtual (canal). * /
uint fin_flag:1; / * Conexión cerrada. * /
ui16 window; / * C u á n t o s b y t e s s e p e r m i t e n r e c i b i r. * /
ui16 checksum; / * Suma de comprobación del mensaje. * /
ui16 urg_pos; / * Último byte de un mensaje urgente. * /
ui8 options|]; /* Opciones TCP. */
ui8 _padding[]; /* (Necesitado para alinear data[]). */
uchar data[]; /* Datos del mensaje. */
} ;

FIGURA 3.5 Esquema TCP.


La cabecera puede tener un tamaño variable, así el campo data_off apunta

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.

Observación de las interacciones TCP


Cuando se abre una conexión de generación de flujo, el programa y el servidor
intercambian una serie de mensajes, los cuales aparecen relacionados y descritos
en la Tabla 3.7.
Tabla 3.7 Intercambio de señales de tres direcciones
Envíos del cliente Envíos del servidor Descripción
SYN=1 (syn_flag) Consulta una conexión virtual (canal).
ACK=0 (ack_flag) Establece el número de secuencia.
SYN=1 (syn_flag) Permite y acusa recibo de una conexión
virtual.
ACK=1 (ack_flag)

SYN=0 (syn_flag)

ACK=1 (ack_flag) Establece una conexión virtual.

Esto se llama intercambio de señales de tres direcciones. Durante las


transferencias, el cliente y el servidor especifican el tamaño de buffer de sus
buffers de recepción (ventanas).
Por otra parte, el cierre de una conexión no es una tarea tan simple como puede
parecer en un principio, debido a que puede darse el caso de que existan datos en
tránsito. Cuando el cliente cierra una conexión puede ocurrir la interacción
mostrada en la Tabla 3.8.

81/512
Tabla 3.8 Cierre de una conexión TCP
Cliente Servidor Descripción

FIN=1 (fin_flag) Transmite datos El cliente solicita cerrar.


Recibe datos
ACK=1 Transmite más El servidor vacía los canales.
Recibe más
ACK=1 FIN=1 Cierre aceptado. El servidor cierra y
espera un ACK del cliente.
ACK=1 El cliente cierra su lado.

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.

Cómo encajan los protocolos IP


Mientras tiene lugar la interacción con la red, tal vez se pregunte cómo encajan
entre sí todo este conjunto de protocolos. En algunos casos, puede parecer que no
existe una adaptación plena. Algunos de ellos utilizan ciertas características de
otros, pero realmente no funcionan tan estrechamente juntos que parezcan
inseparables.
Los protocolos IP raw, ICMP, UDP y TCP desempeñan papeles específicos.
Puede utilizar estos protocolos para adaptar sus necesidades cuando diseña la
aplicación de red. Por supuesto, mientras TCP tiene más habilidad y características
que los otros protocolos, no puede reemplazar ICMP con TCP. Debido a que los
subsistemas Linux requieren características distintas de TCP/IP, cada tipo de
paquete es importante para que el sistema funcione correctamente.
Los paquetes ICMP, UDP y TCP físicamente confían en el paquete IP raw. Sus
cabeceras y datos residen en la sección de datos IP, siguiendo a la cabecera IP.

Cómo escudriñar la red con Tcpdump


La observación de paquetes en una red en funcionamiento muestra de forma
efectiva qué hace con los mensajes el núcleo y cómo el subsistema de red
resuelve consultas de dirección. Desde el paquete IP raw a TCP, tcpdump es una
herramienta que visualiza los datos de la red. Por seguridad, se necesita acceso de
root para ejecutar tcpdump.
Por omisión, tcpdump utiliza el modo promiscuo así que puede ver todo en la
red. El modo promiscuo manipula la interfaz hardware directamente para aceptar
todos los mensajes.

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.

Escritura de un escudriñador de red a


medida
¿Cómo trabaja tcpdump? Puede leer el extenso programa Open Source, o
escribir su propio escudriñador de red (una especie de tcpdump). La única cosa
que necesita conocer es cómo capturar cualquier mensaje para un host. La
herramienta que en este apartado se describe le ayuda a escribir un escudriñador
de red que desensamble paquetes deseados para un host. No soporta el modo
promiscuo, sin embargo.
Por seguridad (como tcpdump), necesita ser root para ejecutar un escudriñador
de red. El escudriñador captura todos los mensajes destinados a la computadora.
Para obtener todos los mensajes, utilice las siguientes llamadas:
sd = socket(PF_INET, SOCK_PACKET, filter);
bytes_read = recvfrom(sd, buffer, sizeof(buffer), 0, 0, 0);
Observe el tipo de socket nuevo: SOCK_PACKET. Es un socket a nivel hardware
de sólo lectura.
Puede utilizar varios filtros para SOCK_PACKET. Los filtros le indican a la capa IP
qué clase de paquete desea capturar. A continuación se muestra varios de estos
filtros:

• ETH_P_802_3 Tramas 802.3.

• ETH_P_AX25 Tramas AX.25.

• ETH_P_ALL Todas las tramas (¡cuidado!).

• ETH_P_802_2 Tramas 802.2.


El filtro a utilizar es ETH_P_ALL. Como indica la nota, cuidado con este filtro,
porque cuando lo selecciona, se lo da todo. El resultado de la llamada es:
sd = socket(PF_INET, SOCK_PACKET, ETH_P_ALL);

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.

Resumen: elección de los mejores paquetes


para el envío de mensajes
Puede utilizar tcpdump y el escudriñador para visualizar las clases distintas de
paquetes que la computadora envía y recibe. Estos paquetes, en el caso de IP,
pueden ser paquetes IP raw, paquetes ICMP del gestor de mensajes de error,
datagramas UDP o mensajes de flujo TCP. Cada tipo de paquete completa un
papel específico mientras deja suficiente espacio para la expansión.
Cada paquete tiene su propia cabecera. ICMP, UDP y TCP adjuntan sus
cabeceras a la cabecera IP. El rango total del tamaño dedicado a estas cabeceras
es de 20 a 120 bytes. El equilibrio entre las cabeceras y los datos verdaderos
afectan al rendimiento de la red.
TCP tiene el menor rendimiento debido a la proporción entre cabecera y datos.
Al ofrecer la comunicación más fiable entre dos computadoras, es el protocolo más
usado en Internet. TCP ofrece una interfaz de generación de flujo que le permite
utilizar funciones de E/S de la biblioteca superior, tal como fprintf() y fgets().
Puede utilizar UDP para enviar mensajes individuales a hosts distintos sin
conectar de nuevo. Eso virtualiza la red usando puertos, al hacerlo parece que la
conexión tiene acceso exclusivo a la red. UDP ofrece rendimiento bueno pero una
transmisión poco fiable.
El protocolo que más se utiliza es TCP, debido a su fiabilidad. UDP lo sigue
detrás a lo lejos. La red de hoy ha dado un paso adelante desde el experimento de
utilizar las interfaces y paquetes de muy bajo nivel hacía el envío de mensajes. El
rendimiento no es tan importante como la fiabilidad, pero los usuarios continúan
observando el asunto del rendimiento.

85/512
Capitulo IV

Envío de mensajes entre


peers
En este capítulo
¿Qué son los sockets basados en la conexión?
Ejemplo: conexión al demonio HTTP
¿Qué son los sockets sin conexión?
Envío de un mensaje directo
Garantía de llegada de un mensaje UDP
Tareas enrevesadas: una introducción a la multitarea
Resumen: modelos conectados frente a modelos sin conexión

Se puede pensar en el pase de mensajes desde dos ángulos distintos: continuo


y sin interrupción (TCP) o paquetes discontinuos de información (UDP). Mientras el
flujo de datos continuo es como una transmisión telefónica, el paquete discontinuo
es como una carta en un sobre con dirección.
El flujo continuo requiere que se tenga una conexión establecida con el destino.
Esto asegura que la información no se pierde durante el intercambio y que está
ordenada cuando llega. Los mensajes discontinuos permiten que se realice una
conexión para facilitar simplemente la programación. Sin una conexión, el
programa tiene que colocar una dirección en cada mensaje.
En este capítulo se trata las interfaces sin conexión y las basadas en la
conexión, Las comparaciones y ejemplos le pueden ayudar a elegir el tipo más
apropiado para las aplicaciones.

¿Qué son los sockets basados en la


conexión?
Linux (y todos los sistemas operativos tipo UNIX) ofrece esencialmente tres

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.

Canales abiertos entre programas


Los sockets TCP ofrecen un canal bidireccional abierto entre dos programas.
Como un micrófono del auricular, el canal envía y recibe información de flujo sin
cortes. Los programas pueden enviar simultáneamente información a otros sin
tener que recomponer el diálogo del mensaje individual,
TCP también recuerda a quién se está hablando. Cada mensaje en niveles
inferiores del protocolo IP tiene que suministrar la dirección de destino con cada
mensaje. Es como marcar el número de un amigo cada vez que se desea volver a
hablar.
Cuando se conecta al otro programa (utilizando la llamada del sistema
connect()), el socket entonces se acuerda de la dirección de destino y el puerto.
También, puede utilizar las llamadas de biblioteca de alto nivel diseñadas para la
E/S de flujo, como fprintf() y fgets(). Lo que simplifica enormemente la
programación.
Como diseñador del programa, el protocolo TCP le ayuda a evitar los problemas
de pérdida de datos que pueden ocurrir con otros protocolos. Esta simplicidad 1c
permite fijar la atención en el programa.

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).

• ¿La forma de los datos son consultas independientes? El programa puede


aceptar la respuesta en cualquier orden cuando el mensaje es una consulta
independiente. Por ejemplo, el orden de ejecución no es importante en el caso
de consultas desligadas: un programa puede consultar un archivo y entonces
continúa para consultar el estado del equipo homólogo. Sin embargo, si
estuviera enviando puntos de coordenadas para un jugador en un juego de red
— esto es, una consulta interdependiente—el orden de las posiciones es muy
importante. (Sí=UDP; no=TCP.)

• ¿Si reordena aleatoriamente los mensajes, responde casualmente el programa con


el mismo resultado? Ésta es la verdadera prueba litmus de los canales basados
en mensajes. Los flujos no pueden tolerar la reordenación de los paquetes.
TCP cuidadosamente reordena los mensajes dentro de un mensaje continúo.
(Sí=UDP; no=TCP.)

• ¿Describe la conversación como una conexión de canal o un mensajero FedEx?


Por un lado, el flujo de la información entre el programa y el equipo homólogo
puede compararse al agua en una tubería. Esta información tiene un orden. Por
otro lado, un paquete individual de información se puede comparar a un bulto.
La información está desordenada y en paquetes. Por ejemplo, puede recibir

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.)

• Si desconecta y vuelve a conectar entre cada mensaje, ¿el servidor o equipo


homólogo tienen que guardar el rastro de dónde estuvo o el estado de la
transacción? Algunas transacciones pueden ser simplemente "dame esto", y la
respuesta es "aquí". El servidor no recuerda nada de transacción en
transacción. Otras transaciones le fuerzan a pasar a través de estados
distintos. Una sesión Telnet tiene dos estados: conectado y no conectado.
(No=UDP; sí=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.)

• ¿Puede la red perder un mensaje sin afectar a la computación de destino? El


emisor puede que necesite tratar los datos perdidos, pero con tal de que el
destino no dependa de cada llegada segura del mensaje, puede utilizar el
protocolo sin flujo UDP. Por ejemplo, puede no ser demasiado importante perder
una consulta desordenada, pero otra cuestión bien distinta es perder consultas
ordenadas. Por ejemplo, los precios de las existencias en un mercado
requieren un flujo cuidadoso de información, pero las estadísticas atmosféricas
pueden tener unas pocas pérdidas sin lastimar el pronóstico. (Sí=UDP;
no=TCP.)
Después de responder a estas cuestiones, si detecta que sólo una respuesta es
TCP, debe de utilizar TCP o reforzar el protocolo UDP. Estas reglas no son duras y
rápidas; puede elegir aceptar las cuestiones con el UDP menos fiable con el fin de
obtener el rendimiento deseado.

Conexiones de protocolo inferior


Es posible utilizar la llamada del sistema connect() en un socket UDP. Puede
ser atractivo cuando no necesita E/S de alto nivel, ya que UDP ofrece una ayuda
de rendimiento con los mensajes autocontenidos. Sin embargo, la conexión UDP
funciona algo diferente a cómo TCP gestiona las conexiones.
En el capítulo 3 se describió el proceso de intercambio de señales de tres
direcciones. Este intercambio de señales establece el nivel de comunicación para
que los datos fluyan entre los programas. Ya que UDP no soporta flujos, la llamada
del sistema connect() sólo simplifica el pase de mensajes.
La llamada del sistema connect() en una conexión UDP registra simplemente el
destino de cualquier mensaje enviado. Puede utilizar read() y write() como en una
conexión TCP, pero no tendrá garantías de fiabilidad y ordenación. El algoritmo
presentado en el Listado 4.1 es muy similar al de programación 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.

Ejemplo: conexión al demonio HTTP


El protocolo al que los usuarios más se conectan es HTTP. Describe un interfaz
muy simple que incluye una consulta. El servidor, a su vez, interpreta la consulta y
responde con un mensaje que el cliente puede entender. Ese mensaje puede ser
cualquier documento dentro de un formato simple de cabecera e-mail.
La simplicidad de la interfaz le evidencia la cantidad de transacciones que
existen en la red. Muchas interacciones cliente-servidor están basadas en
transacciones individuales. Toda la sobrecarga fija de la interacción y
comprobación de fiabilidad de TCP se puede desperdiciar en una transacción
individual. A pesar de eso, éste es el estándar aceptado.

Protocolo HTTP simplificado


Par enviar una consulta a un servidor HTTP, sólo necesita conocer un único
comando. Esta es un representación demasiado simplificada del protocolo HTTP.
En el Capítulo 6, "Generalidades sobre el servidor", se trata el protocolo en mayor
profundidad:
GET <query> HTTP/1.0<cr><cr>
El aspecto normal de la consulta es como un camino de directorio, pero éste
puede tomar parámetros y variables. Cuando introduce la URL http://www.kernel.
org/mirrors/ en el navegador, el navegador abre un socket a www.kernel.org y envía
el siguiente mensaje:
GET /mirrors/ HTTP/1.0<cr><cr>
También puede enviar información sobre la clase de datos que puede recibir.
Esto ayuda a la interpretación de los datos. Los dos últimos <cr> son líneas nuevas
que indican el final del mensaje. El cliente puede enviar cualquier número de
parámetros y configuraciones, pero debe finalizar el mensaje con dos líneas en
blanco.
La conexión es con flujo, así que no conoce si el mensaje tiene éxito al final. Lo
que puede conducir a esperar indefinidamente algo que nunca llega. Las dos líneas
en blanco le indican al servidor que el cliente a terminado de hablar.

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");

/* Inicializa la estructura de puerto/dirección del servidor. */*/


bzero(&dest, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = serv->s_port; /* Servidor HTTP. */
if ( inet_addr(Strings[1], &dest.sin_addr.s_addr) 0 )
PANIC(Strings[1]);
/*---Conectar al servidor.---*/*/
if ( connect(sd, &dest, sizeof(dest)) != 0 )
PANIC("Connect");
/* Componer la consulta y enviarla. */*/
sprintf(buffer, "GET % s HTTP/1.0\n\n", Strings(2]);
send(sd, buffer, strlen(buffer), 0);
/* Mientras existan datos, leerlos y mostrarlos. */*/
do
{
bytes_read = recv(sd, buffer, sizeof(buffer)-1, 0);
buffer[bytes] = 0;
if ( bytes_read > 0 )

92/512
printf("%s", buffer);
}
while ( bytes_read > 0 );

El programa del Listado 4.2 abre la conexión, envía la consulta de la linea de


comando y muestra todo lo que obtiene hasta que el servidor cierra la conexión.
Versiones posteriores del protocolo HTTP (1.1 y el HTTP-NG propuesto) incluyen la
capacidad de dejar el canal abierto y aprovecharse de la conexión TCP. De nuevo,
esta característica enfatiza la necesidad de indicar al receptor cuando el emisor ha
terminado de emitir. Puede leer mucho más sobre las nuevas versiones de HTTP
en las RFC que se encuentran en www.w3c.org.

¿Qué son los sockets sin conexión?


Todas las comunicaciones no requieren tener un canal bidireccional abierto
entre los peers. Si el teléfono es un ejemplo de conexión de generación de flujo, el
sistema postal representa mejor un sistema basado en el mensaje (o sin conexión).
Como el sistema postal, UDP compone los mensajes, asigna una dirección y lo
envía sin ningún seguimiento del mensaje de cómo viaja al destino. (Sólo reiterar:
la falta de credibilidad de un datagrama significa que no ofrece garantías de
reparto. Eso no significa que no funcione.)

Configurando la dirección del socket


La instalación completa de una distribución Linux incluye normalmente
herramientas que le permiten enviar y recibir notas de otra estación de trabajo en la
red. Estas herramientas requieren una única dirección de máquina y tal vez un
nombre de usuario. Puede implementar este tipo de herramientas usando un
socket UDP. Para hacer esto, necesita observar si el usuario actual es el receptor
del mensaje.
Estas herramientas de tipo mensajero, utilizan las dos partes de los datos
(nombre de host y de usuario) para identificar la unidad del receptor. Al no existir
datos en flujo, la implementación puede usar una interfaz sin conexión.
La interfaz sin conexión no realiza la llamada del sistema connectO, pero no se
puede llamar a send() o recv() sin tener una conexión. De hecho, el sistema
operativo ofrece dos llamadas del sistema de bajo nivel que incluyen la dirección
de destino: recvfrom() y rendto(). Estas llamadas actúan de forma similar a recv() y
send(), pero se suministra el destino como parte de la llamada:
#include <sys/socket.h>
#include <resolv.h>
int sendto(int sd, char* buffer, int msg_len, int options,
struct sockaddr *addr, int addr_len);
int recvfrom(int sd, char* buffer, int maxsize, int options,
struct sockaddr *addr, int *addr_ien);

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);

/*---Enlazar a un puerto particular.---*/*/


while (1)
{ int bytes, addr_len=sizeof(addr);
bytes = recvfrom(sd, buffer, sizeof(buffer), 0, &addr,
&addr_len);
fprintf(log, "Got message from %s:%d (%ú bytes)\n",
inet_ntoa(addr.sin_addr), ntohs(addr.sin_port),bytes);
/*** Petición proceso.****/
sendto(sd, reply, len, 0, addr, addr_len);
}
En este ejemplo, se crea el socket y se enlaza al puerto. El bucle while espera al
mensaje de entrada, registra la conexión, procesa la consulta y responde con el
resultado. El bucle reinicia el valor de addrjen así que el tamaño de addr no parece
reducirse con cada llamada del sistema. Se puede utilizar esta información en addr
como la dirección devuelta por la respuesta.
Si utiliza la llamada del sistema recvfromO, debe suministrar un valor addr y
addrjen; no puede establecerlos a NULL (0). Puesto que UDP es sin conexión,
necesita conocer el origen de la consulta. La forma más fácil de hacer eso es
guardar intacto addr mientras se procesa la consulta.

Algunos programas sólo necesitan el mensaje


El protocolo sin conexión simplifica el intercambio de señales. TCP requiere un
intercambio de señales de tres direcciones: el cliente envía una consulta en la
conexión, el servidor acepta la consulta y envía su propia consulta de conexión, y
el cliente acepta la consulta de conexión del servidor.
UDP, sin conexión, no tiene efectivamente intercambio de señales; el mensaje
es él mismo la única colaboración remotamente compartida. Significa que el
protocolo no comprueba la conexión. Casualmente, si el equipo homólogo no
puede encontrar u ocurre algún error de transmisión, se puede obtener un mensaje
de error de red. Sin embargo, ese mensaje puede llegar mucho tiempo después de
que la red detecte el error (de uno a cinco minutos).
El protocolo sin intercambio de señales UDP reduce la sobrecarga fija del
intercambio de señales de TCP. Para soportar esto, puede observar uno o dos

95/512
paquetes de configuración en total. Bastante bueno para comunicaciones de alta
velocidad.

Transaction TCP (T/TCP): un TCP sin conexión


El intercambio de señales de tres direcciones TCP, comparado con el protocolo
UDP, requiere hasta 10 paquetes de configuración. Este arranque es muy lento
comparado con el generador de peticiones que sólo necesita una única
transacción. Durante el proceso de arranque, los dos extremos de la conexión
verifican cada servicio y la fiabilidad del canal del otro. Similarmente, el proceso de
cerrado requiere un intercambio de señal adicional (véase el Capítulo 3).
TCPv3 [RFC1644] añade una característica nueva ofreciendo algunas de las
velocidades de conexión de UDP, mientras se reduce la fiabilidad de TCP.
Transaction TCP (T/TCP) hace la conexión, transmisión y cierre en una única
llamada del sistema sendto(). ¿Cómo lo hace?
TCP establece la conexión con el uso del intercambio de señales de tres
direcciones, y cierra o apaga la conexión utilizando una señal de intercambio
particular. Estos pasos son necesarios para asegurar que el cliente y servidor
consigan todos los datos enviados en el orden correcto. Para encontrar ese
objetivo, el paquete TCP tiene que utilizar flags para indicar cuándo establecer la
conexión (SYN), acusar recibo de datos (ACK) y cerrar el canal (FIN).
En el Capítulo 3 se describe el formato del paquete TCP. La cabecera del
paquete incluye varios campos que pueden parecer estar en exclusión mutua. Por
ejemplo, tiene flags separados para SYN, ACK y FIN. ¿Por qué quiere desperdiciar
el espacio preciado de un paquete cuando un solo bit de cada campo está
probablemente activo a la vez?
T/TCP utiliza estos campos simultáneamente. Cuando el cliente T/TCP se
conecta, envía el mensaje al servidor mientras demanda la conexión (SYN).
También establece el flag FIN para cerrar la conexión tan pronto como el servidor
termine la transacción. Véase la Figura 4.1.
El servidor responde con su propia petición de conexión, una petición de cierre,
y se termina con un acuse de recibo del cliente. (El acuse de recibo de la petición
de conexión está implícita.) El paquete incluye los datos que el servidor generó.
Finalmente, con el acuse de recibo del cliente, el servidor finaliza.
T/TCP puede ser muy rápido. Pero existe un problema: toda la información debe
ser transmitida con MSS (máximo tamaño del segmento) de TCP, lo cual sólo es
540 bytes. Sin embargo, puede cambiar esa configuración hasta 64KB. Además, el
programa no se limita a enviar un mensaje; puede enviar varios mensajes.
El programa servidor no tiene que hacer nada, sólo tiene que trabajar
soportando T/TCP. El algoritmo para una conexión normal TCP sirve para T/TCP
también, puesto que todos los soportes del lado del servidor están programados en
el subsistema de red. Todo el trabajo está en el lado del cliente. Puede utilizar el
siguiente algoritmo en el cliente:

96/512
/***********************************/
/*** Algoritmo básico T/TCP. ***/
/**********************************/
int flag=1;
int sd;
sd = socket (PF_INET, SOCK_STREAM, 0);

if ( setsockopt (sd, IPPROTO TCP, TCP_NOPUSH, &flag, sizeof(flag)) != 0 )


PANIC("TCP_NOPUSH not supported");
/**** Configurar addr para el destino. ****/
if ( sendto(sd, buffer, bytes, MSG_FIN, &addr, sizeof (addr)) < 0 )
PANIC("sendto");
...

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.

Envío de un mensaje directo


Hasta ahora se ha trabajado con un canal conectado para enviar y recibir
mensajes y se ha comparado con el uso de un teléfono. Un mensaje dirigido no
conlleva conexión, en tanto que no existe intercambio de señales. Al crear un
mensaje dirigido (no un canal) a un peer se requiere un emisor y un receptor. El
emisor transmite el mensaje, y el receptor acepta el mensaje. Eso es muy simple.
Cada mensaje necesita un destino (el núcleo coloca automáticamente la
dirección de retorno en el paquete). send() y recv() asumen que tienen un canal
conectado, lo cual define automáticamente el destino. En vez de eso, deben utilizar
las llamadas del sistema recvfrom() y sendto()/ porque ambas suministran el
direccionamiento que cada mensaje requiere.
Anteriormente al envío del mensaje, se construye la dirección de destino, la cual
incluye la dirección dei host y el número de puerto. Como se describió en el
capítulo anterior, si el programa omite la selección del puerto, el sistema operativo
asigna aleatoriamente uno al socket. No se trata de una buena idea para
programas fijos. La llamada debe conocer el puerto de destino para direccionar
correctamente el mensaje.
COMPROBACION DEL SOPORTE DE SOCKADDR EN LINUX
El núcleo del Linux soporta varios tipos distintos de protocolos de direccionamiento, pero
no todos los protocolos se incluyen directamente en los núcleos de las distribuciones. Por
ejemplo, los núcleos de las distribuciones omiten a menudo los protocolos de radio
aficionado. Si se encuentra dudoso de qué protocolos asociados soporta el núcleo
compilado, ejecute simplemente el programa. Las llamadas del sistema bind(), sendto(),
connect() y recvfrom() visualizan en pantalla un error si reciben un sockaddr que no
entienden. Se puede encontrar una lista de los sockaddr soportados en el Apéndice A,
"Tablas de datos".
Los emisores de TCP/UDP deben conocer en qué puerto escucha el receptor
para que el sistema operativo encamine el paquete al programa correcto. Este
puerto es normalmente un número de puerto acordado entre el emisor y el

98/512
receptor. Por ejemplo el archivo /etc/services incluye los números de puerto
publicados que ofrecen los servicios estándar.

Asociación del puerto al socket


Puede requerir un número de puerto específico desde el sistema operativo
utilizando la llamada del sistema bind(). Puede haber visto esta llamada antes, y
lamentablemente muchas páginas de manual UNIX la definen como "nombre de
socket". Esta definición se refiere a sockets PF_LOCAL o PF_UNIX que utiliza el
sistema de archivos. Una descripción mejor es "asociar un número de puerto u otra
interfaz de publicación al socket”.
ASOCIACIÓN DE PUERTO Y NOMBRE
La asignación del puerto y el nombre al socket varían drásticamente entre cada miembro
de la familia sockaddr, Algunos, como AF_LOCAL y AF_AX25, son nombres
alfanuméricos; otros, como AF_INET y AF_IPX, utilizan puertos. Los números de puerto
deben ser únicos, por lo general dos sockets TCP o UDP no pueden tener los mismos
números de puerto (al menos con AF_INET). Sin embargo, un socket UDP y un socket
TCP puede tener compartido el mismo número de puerto. Así es cómo algunos servicios
(en /etc/services) le ofrecen ambas conexiones.
La definición prototipo de la llamada del sistema bind() es:
#include <sys/socket.h>
#include <resolv.h>
int bind(int sd, struct sockaddr *addr, int addr_size);
En este apartado se introduce de forma superficial a bind(); para una descripción
más profunda, se puede consultar el Capítulo 6. El uso es similar a la llamada del
sistema connect() donde se necesita iniciar el primer parámetro addr:
/*****************************/
/*** Ejemplo de bind(). ***/
/*****************************/
struct sockaddr addr;
int sd;
sd = socket(PF_INET, SOCK_STREAM, 0);
bzero(&addr, sizeof(addr)};
addr.sinfamily = AF_INET;
addr.sin_port = htons(MY_PORT); /* Petición de un puerto especifico. */
addr.sin_addr_s_addr = INADDR_ANY; /* Cualquier interfaz IP. */
if ( bind(sd, &addr, sizeof(addr)) ! = 0 )
perror("bind");
...

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.

Cómo enviar el mensaje


El envío y recepción de mensajes UDP se parece un poco al juego de coger en
la niebla. Los jugadores se turnan para lanzar y coger, aunque ellos no pueden
verse uno al otro. El emisor es el primero en lanzar el balón. El receptor debe tener
una localización y un número de puerto conocido, establecido con la llamada del
sistema bind(). El emisor no necesita establecer su número de puerto, porque cada
mensaje incluye la dirección de retorno. Para un jugador, el potencial está al lazar
el balón a un espacio vacío,
El emisor realiza poca iniciación y, a menudo, sólo una petición de mensaje.
Puede colocar esto en un bucle para lanzar mensajes de una parte a otra. El
código para el emisor está el Listado 4.4 y en connectionless-sender.c del sitio
web.
Listado 4.4 Ejemplo de un emisor de datagramas
/*****************************************************/
/*** Ejemplo de emisor. ***/
/*** (Extracto de connectionless-sender.c.) ***/
/*****************************************************/
struct sockaddr addr;
int sd, bytes, reply_len, addr_len=sizeof(addr);

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.)

Cómo coger el mensaje


De forma distinta al emisor, el programa receptor necesita establecer el número
de puerto (utilizando bind()). Después de la creación del socket, se debe publicar
el número de puerto acordado, para que el equipo homólogo pueda comunicarse
con él. El código del Listado 4.5 muestra un ejemplo de un receptor. Puede
encontrar el programa completo en el archivo connectionless-receíver.c en el sitio
web.

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.

Garantía de llegada de un mensaje UDP


El descarte de un mensaje que está sólo medio leído puede no parecer una
buena idea. Cuando se usa el protocolo UDP, existen varias cuestiones que se
necesitan conocer. Como se mencionó anteriormente, necesita conocer cómo es el
mensaje de grande. UDP es un protocolo poco fiable, lo cual significa que no se
puede garantizar que el destino consiga el mensaje.
Puede creer que en todos los casos necesita fiabilidad, pero no todos los
sistemas lo necesitan. Por ejemplo, algunos mensajes son muy críticos en el
tiempo. Tan pronto como el tiempo se expira, el significado del mensaje se reduce
a nada.
Si desea reforzar UDP (sin alcanzar a TCP al 100%), debe seguir varios pasos.
De hecho, puede sobrepasar fácilmente la fiabilidad de TCP.

Cómo fortalecer la fiabilidad de UDP


La idea general de TCP es ofrecer una interfaz con capacidad de flujo entre el
cliente y el servidor. TCP introduce sobrecarga fija que reduce el rendimiento
sensiblemente en toda la comunicación. A pesar de eso y de forma distinta a UDP,
puede garantizar la llegada de cada mensaje. Si desea esa garantía en UDP, tiene
que añadir su propio código.
Para et debate, cada mensaje tiene varios paquetes UDP. Al asegurar que el
destino obtiene el mensaje se introducen dos problemas interesantes:

• ¿Cómo conoce el receptor que supuestamente llega un paquete?

• ¿Cómo conoce el emisor si el receptor consiguió el paquete?

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 receptor puede que nunca envíe un acuse de recibo.

• 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.

Verificación de la integridad de los datos


Los paquetes se trasladan del origen al destino a través de las redes y los
routers. En cualquier momento, la red puede corromper el mensaje (y con
frecuencia en una red congestionada). Una colisión fuerza al host del origen a
retransmitir. En el ejempío anterior, donde el paquete 3 acaba perdiéndose, lo más
probable es que haya sido el host el que envió el paquete. Sin embargo, la red
puede fácilmente haberlo destruido.
UDP y TCP utilizan una suma de comprobación en sus datos. La capa IP verifica
su propia cabecera e ignora los datos del mensaje. Las sumas de comprobación
son útiles para la detección de errores menores, pero es posible que los errores se
cancelen unos a otros. Algunas interfaces de red incluyen una verificación por
redundancia cíclica (CRC) o códigos de corrección de error (ECO en el hardware
para el descubrimiento y reparación de los datos corruptos.
Una vez más, el paquete 3 podría llegar realmente, pero los contenidos podrían
estar corruptos mientras a pesar de todo pasan las sumas de comprobación de IP y
UDP.
Podría incorporar su propia suma de comprobación, CRC, hash o ECC en los
paquetes y mensajes. Los algoritmos para éstos están disponibles en Internet de la
comunidad Open Source. Cuando elige el algoritmo de validación, debe equilibrarlo

105/512
con estos elementos:

• La cantidad de datos que se transmiten. Al incrementar el tamaño de cabecera


se puede reducir significativamente el rendimiento de red.

• La posibilidad de reparación de los datos. Algunos datos se pueden reparar


fácilmente debido a la redundancia o a que son poco críticos.

• Los valores contra la posición de los datos. Las sumas de comprobación


computan rápidamente los valores de los datos, pero pierden el significado del
orden de los bytes. CRC requiere más computación pero puede identificar
fácilmente los errores de bit hasta el tamaño del CRC. Por ejemplo, un CRC de
32 bits puede detectar con fiabilidad errores de bit de hasta 32 bits de tamaño.
Más allá de los 32 bits, pierde fiabilidad.
La fiabilidad de los datos es importante para muchas aplicaciones. La
comprobación, validación y secuencia ayudan a garantizar la llegada de un
mensaje de forma segura y ordenada. Debe asegurarse que el emisor y receptor
se comunican para ayudar a pulir el flujo de datos.

Fallos imprevistos del flujo de datos


La comunicación entre los peers no es siempre clara. Cada uno puede asumir
que el otro debe hablar primero (interbloqueo de red). Otro problema ocurre cuando
se pone en espera indefinidamente: nunca puede estar seguro de si la persona no
le ha puesto en espera para tomarse un descanso el resto del día.
La inanición ocurre cuando un generador de peticiones espera indefinidamente
una respuesta. La causa puede incluir un red mala, un equipo lento, un equipo
reiniciado, etc. En muchos casos, puede resolver un receptor "bloqueado por
inanición" programando un evento, el cual es simplemente un mensaje para
recordar al emisor que el programa receptor está en espera todavía.
El evento debería esperar un período de tiempo antes de volverse impaciente.
Podría incluir el último ID de secuencia enviado e incluso una marca de tiempo en
el mensaje. Después de no recibir nada del emisor, el receptor puede reiniciar la
conexión. Este proceso no requiere que el emisor entienda los mensajes de evento
y de reinicio de la conexión.
Un interbloqueo de red puede ocurrir cuando uno de los mensajes se pierde
durante un intercambio de paquetes. El emisor espera una respuesta, mientras que
el receptor espera aún el mensaje perdido. Un mensaje de evento podría ayudar
también a romper el interbloqueo. En el ejemplo de intercambio de paquetes, el
receptor incita al emisor con el último ID de secuencia que obtuvo. El programa, a
su vez, retransmite el mensaje perdido.

Tareas enrevesadas: una introducción a la


multitarea
Regresando a los programas del receptor y emisor, ¿por qué utilizar dos

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.

Resumen: modelos conectados frente a


modelos sin conexión
El pase de mensajes entre los equipos homólogos o clientes y los servidores
involucra una comunicación: sin conexión o basada en la conexión. Con los
mensajes basados en la conexión, se puede abrir simplemente un canal, conectar
a un equipo homólogo y proseguir con el envío y recepción de información. La
conexión guarda el camino bacía el destino por el programador, así que no se tiene
que volver a plantear. TCP y UDP soportan la conexión; sin embargo sólo TCP
extiende la conexión con fiabilidad y ordenación. La conexión de UDP ofrece una
mera abreviatura, así que se pueden utilizar servicios de E/S de más alto nivel
como sendto() y recvfrom().
El pase de mensajes sin conexión es similar al envió postal de una carta: se
necesita direccionar cada mensaje antes del envío. Para hacer esto, se deben
utilizar llamadas distintas a send() y recv() que son las que utiliza la comunicación
basada en la conexión. Éstas son sendto() y recvfrom(). Estas llamadas de]

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

Explicación del modelo de


capas de red
En este capítulo
Solución del desafío de red
Modelo de red de interconexión de sistemas abiertos (OSI)
Paquete de protocolos de Internet
Diferencias fundamentales entre OSI e IP
¿Que da servicio a qué?
Resumen: de la teoría a la práctica

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.

Solución del desafío de red


La programación de red utiliza muchas partes de la ciencia de la computación e
ingeniería del software que se complica bastante si funcionan en un nivel bajo . De
forma similar a la construcción de una casa, si fija la atención en la viruta de
madera, puede que nunca complete la casa. Pero con la combinación de las
herramientas apropiadas y los materiales, puede fijar más su atención en el
producto final y sus características potenciales.
Los desafíos que introduce la programación de red hace que esta tecnología
sea muy interesante. Sin embargo, programar la red desde la base hacia arriba,
sólo lo hace un gurú (o maestro de todos los protocolos de computadora). Los
desafíos de la programación de red incluyen el hardware, la transmisión, las
interacciones con el sistema operativo y las interacciones con el programa.

Cuestiones de hardware de red


La tecnología ofrece muchas opciones para la confección de una red. Las
opciones incluyen medios cableados contra no cableados (algunos conductos
físicos transportan la señal), transmisiones eléctricas contra no eléctricas, caminos
directos contra indirectos y distancia corta contra larga. La Tabla 5.1 muestra
algunas formas de conexiones de red.
Tabla 5.1 Distintos medios de red tienen características y limitaciones diferentes
Medios Cableado Eléctrico Directo Distancia Rendimiento
máximo

Cable coaxial Sí Sí Conectado <2Km 10 Mbps

Par trenzado Sí Sí Conectado <150m 100 Mbps

Fibra óptica Sí No Conectado (Ilimitado) 100 Gbps


inalámbrico: HF No Sí. Difusión >1000Km <10 Kbps
Inalámbrico: VHF/ No Sí Difusión, línea <30Km <40 Kbps
UHF de foco

Inalámbrico: No Sí Sí, línea de <30Km <1 Mbps


microondas
foco
Satélite No Sí Sí (Ilimitado) <10 Mbps
Infrarrojos No No Sí, difusión <10m <1 Mbps
Láser No No Sí Muy larga <100 Gbps

Afortunadamente, el núcleo oculta todas estas tecnologías de la interacción


directa del programador. Se puede uno imaginar la dificultad que existe en conocer
qué clase de medio y transmisión utiliza la computadora. Todas estas tecnologías

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.

Cuestiones de transmisión de red


Las cuestiones relativas al hardware fijan la atención en la tecnología del canal
físico de comunicación. La próxima cuestión del funcionamiento en red es el
traslado del paquete en torno a la red. El paquete afronta muchos peligros que
pueden corromperlo o perderlo en su totalidad. A menudo, el emisor y el receptor
obtienen notificación little-to-no de un fallo de paquete. Algunas veces el receptor
para el temporizador porque no ha recibido un mensaje del emisor cuando, en
realidad, el camino entre ellos puede estar roto.

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.

Interacción de la red con el sistema operativo


La red debe ser capaz de interactuar con el sistema operativo por varias
razones. Para realizar su trabajo, el sistema operativo gestiona los recursos que el
subsistema de red necesita, como interrupciones, puertos y memoria. También,
aunque el subsistema de red administra los detalles de la red, tiene que residir en
el núcleo para obtener el mejor rendimiento.
La interfaz entre la red y el núcleo es compleja—especialmente si el núcleo no
es reentrante. Afortunadamente, el núcleo de Linux ha superado el obstáculo del
reingreso y puede gestionar las cuestiones asociadas.
Cuando un mensaje llega, el hardware envía una interrupción a la CPU para que
recoja el mensaje. Si el núcleo (donde reside el gestor de interrupciones) está
ligeramente retrasado, otro mensaje puede invadir el primer mensaje. Esto es
especialmente cierto cuando la tarjeta de red está configurada en el modo
promiscuo (aceptación de todos los mensajes que escucha). El modo promiscuo se
trató anteriormente en el Capítulo 3, "Distintos tipos de paquetes de Internet".
Excepto para las redes simuladas como PPP, las CPU raramente trabajan
directamente con las interfaces de red hardware. La CPU utiliza un coprocesador
en la tarjeta de interfaz de red. La CPU carga los datos dentro de la interfaz de red
y le da instrucciones para transmitir. Cuando lo hace, la tarjeta provoca una
interrupción a la CPU. Lo que indica al núcleo que la tarjeta de red está otra vez
preparada para aceptar otro mensaje. Por otra parte, PPP necesita mucho más
interacción de CPU, porque la interfaz es mucho más lenta, no es tan agobiante
como podría ser.
Una cola de mensajes mantiene los mensajes de entrada y salida. Cuando el
núcleo obtiene una notificación indicando que la tarjeta está preparada para otra
trama, toma directamente el mensaje de la cola de mensajes. Si el programa envía
un mensaje cuando la interfaz no está preparada, el núcleo coloca la petición o el
mensaje en la cola para un procesamiento posterior. La red es un recurso limitado;
dos programas no pueden acceder a ella a la vez. Como resultado, el sistema
operativo tiene que poner en cola y procesar las consultas de una en una.

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.

Interacción de la red con el programa


El área principal de preocupación es cómo interactúa la red con el programa.
Puesto que la programación de red tiene demasiadas dificultades y advertencias,
se considerarán los detalles específicos de cada situación por separado y luego se
determinará cómo se interrelacionan. En este apartado se tratan algunas de estas
cuestiones.
El primer tema es la recepción y tráfico con errores y excepciones. El sistema
operativo y el programa deben ser capaces de recibir y gestionar los errores o
excepciones de la red. Algunos errores y excepciones son asíncronos a la
ejecución del programa. C y Java pueden interceptarlos, pero algunos lenguajes no
pueden (por ejemplo, Pascal). Así que, cuando vaya a considerar cómo escribir la
aplicación, recuerde que algunos de los lenguajes no se prestan bien a las
demandas de la programación de red.
El segundo tema es la fiabilidad del paquete y de los datos. Algunos programas
necesitan alta fiabilidad en los datos; otros no. Se debe analizar qué tipo de datos
se tiene y cuánta fiabilidad se necesita. Se puede encontrar una lista de tipos de
datos y su importancia crítica en el Capítulo 2, "Elocuencia del lenguaje de red
TCP/IP".
El tercer tema es la sincronización y la concurrencia. El programa de red
interactúa con otro programa en ejecución concurrente. Debe coordinar los
programas para evitar el interbloqueo y los problemas de inanición (tratados en
detalle en el apartado "Cuestiones de concurrencia cliente-servidor" del Capítulo
10, "Diseño de socket Linux robustos").
Por último, se examinarán las conexiones virtuales frente a las conexiones
reales. La red ofrece al programa una forma más simple de interacción haciendo
parecer que el programa posee la conexión de red. Estas conexiones virtuales
(puertos) entregan sólo aquellos mensajes que, supuestamente, son vistos por la
conexión.
Mientras que los temas de interacción con el programa son complejos, hacen la
programación de red desafiante y divertida. Esta lista de cuestiones no es de gran
tamaño. Sin embargo, la API de Socket oculta muchos retos de programación de

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.

Modelo de red de interconexión de sistemas


abiertos (OSI)
El modelo de red más conocido—OSI—utiliza una estrategia basada en siete
capas. Cada capa del modelo abstrae la interfaz entre el hardware y el programa.
Este modelo extiende la interfaz incluso dentro del espacio de usuario.
Los programas y las aplicaciones en la estación de trabajo ofrecen junto con las
herramientas necesarias realizar el trabajo. El espacio de usuario define el área de
trabajo del cliente. Fije la atención en este área, porque un diseño bueno realiza las
interacciones de usuario con el programa intuitivas y sin costuras.
El modelo de red OSI ofrece una interfaz común para que en cuanto un usuario
entienda el procedimiento, pueda utilizar los mismos métodos en otras
implementaciones OSI. El modelo OSI oculta y abstrae intencionadamente los
detalles de implementación del hardware específico. Esto da al usuario la misma
sensación de independencia de la plataforma.
La interfaz del modelo OSI intenta responder a cada uno de los retos específicos
de la programación en red. Sin embargo, a la vez ofrece interfaces (o enganches)
dentro de los niveles inferiores para poner en marcha la potencia de los
programadores. El modelo tiene siete capas, empezando con la capa hardware,
como muestra la Figura 5.1. Cada capa de abajo arriba abstrae la implementación
de red hasta el usuario y el programa.

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.

Capa 2: enlace de datos


La capa de enlace de datos se apoya en la capa física. El papel principal de la
capa de enlace de datos es ofrecer una transferencia de datos de host a hosí.
Empaqueta los mensajes dentro de tramas para el hardware. Comprueba e intenta
corregir los errores de transmisión. Si el hardware no implementa sumas de
comprobación o CRC, la capa de enlace de datos realiza los cálculos ella misma,
La capa de enlace de datos interactúa entre el núcleo y el hardware de red.
Normalmente, la capa de enlace de datos es un controlador de red que se ubica en
el núcleo. El controlador utiliza una interfaz uniforme para que el núcleo pueda
funcionar a ciegas con tecnologías distintas.
Esta capa prepara las tramas de datos para la transmisión y desenvuelve los
mensajes recibidos. El controlador espera una interrupción que indique que un
mensaje se envió o se recibió. Cuando obtiene la notificación del mensaje enviado,
el controlador de enlace de datos carga la siguiente trama a enviar.
El enlace de datos funciona estrechamente con el subsistema de gestión de
buffers del núcleo. La mayor parte del tiempo, necesita sólo de 10 a 60 KB de
memoria RAM. La configuración del núcleo de Linux permite definir el tamaño de
trama y del gestor de buffers por encima de 1 MB para transferencias muy rápidas
(100 MBps o superiores), si se tiene suficiente memoria. Esto es útil para las
conexiones de alta velocidad, que pueden recibir tramas grandes o tener un
rendimiento alto. Para utilizar esta característica, es probable que tenga que
recompilar el núcleo, porque muchas distribuciones no lo habilitan.
Los puentes de red se ubican en la capa de enlace de datos. La opción de
puente del núcleo de Linux es todavía una característica experimental en el
momento de la escritura de este libro. Si quiere experimentar con ella, es probable
que tenga que compilar el núcleo para incluir los controladores.
La capa de red se apoya encima de la capa de enlace de datos y ofrece
resolución de direcciones y encaminamiento.

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.

Paquete de protocolos de Internet


Linux (como la mayoría del resto de sistemas operativos UNIX) no utiliza
directamente el modelo de red OSI. Sin embargo, el modelo es un punto de partida
para el entendimiento de la pila IP. Linux usa el paquete de protocolos de Internet
para administrar su interfaz de red nativa.
NOTA
El protocolo IP nació del ARPAnet (Red de la agencia de proyectos de investigación
avanzada) fundada por DARPA (Agencia de proyectos de investigación avanzada para la
defensa) en 1972. BBN Corporation implemento el primer trazo de la red. Más tarde,
UNIX, un sistema operativo libre ofrecido por Bell Labs, adoptó el modelo también. Las
universidades involucradas utilizaron UNIX para comprobar un concepto de enlace entre
computadoras juntas a lo largo y ancho de Estados Unidos. La API de Sockets comenzó
con BSD 4.2 en 1983.
El paquete de protocolos de Internet tiene cuatro capas que se corresponden
estrechamente con el modelo OSI. La capa más alta se denomina capa de
aplicación. La capa de aplicación del paquete de protocolos IP abarca las capas 5,

119/512
6 y 7 de OSI (véase la Figura 5.2). Compare los distintos modelos en la Figura 5.1.

Capa 1: capa de acceso de red


La capa primera de red de IP es la que coincide más estrechamente con las
capas de enlace de datos y física de OSI. Puesto que el hardware y los
controladores están también enlazados juntos estrechamente y el soporte del
hardware varía ampliamente, no se puede aislar uno del otro.
Las características son similares: los controladores funcionan estrechamente
con las interfaces hardware para ofrecer un conjunto común de funcionalidad al
núcleo.
El núcleo, en cambio, da al controlador acceso directo a los puertos e
interrupciones. Si una interfaz está desprovista de cualquier característica que
necesita el núcleo, los controladores sustituyen la carencia.

FIGURA 5.2 El DoD o Pila de protocolo Internet se compara estrechamente


con el modelo OSI. La API de Socket termina en la capa de transporte (4) de
OSI. El protocolo deja las capas restantes a los programas de alto nivel como
Telnet, FTP y Lynx.
Linux añade otra característica que complica esta capa: el intercambio en
caliente de adaptadores de red. Esto se puede encontrar en el soporte de PCMCIA.
Cuando inserta una tarjeta en el PC, el gestor de la tarjeta PC la reconoce, asigna
los puertos c interrupciones apropiados, inserta los módulos del núcleo definidos y
configura la red. Asimismo, cuando retira la tarjeta, el gestor deshabilita la red y
elimina el módulo del núcleo.

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.

Capa 2: extensiones de la gestión de mensajes de


error-control (ICMP)
ICMP (Protocolo de mensajes de control en Internet) añade la gestión de
mensajes de error omitida en la capa de red OSI. ICMP ofrece los protocolos de
mensajes para la administración de los errores y excepciones en el
encaminamiento y reenvío de mensajes. Las excepciones normales son network
not reachable y host not found.
Este protocolo funciona con otros sistemas para realizar varias funciones. Por
ejemplo, funciona con la tablas de rutas (ARP) cuando los hosts son inalcanzables.
Adicionalmente, funciona con TCP para acelerar o reducir la comunicación y
cambiar la ventana deslizante. TPv6 ha añadido incluso control para la
administración de grupos de multidifusión.
Lo más interesante de ICMP es que lo hace todo sin requerir ningún tipo de
ayuda. Ni UDP, ni TCP lo utilizan. Ellos aceptan sus mensajes de su propio
procesamiento.

Capa 3: de host a host (UDP)


Contrariamente a la opinión popular, UDP (Protocolo de datagrama de usuario)
no se corresponde perfectamente con la capa de transporte OSI (véase la Figura
5.3). La capa de transporte garantiza la entrega, la ordenación de paquetes, los
paquetes sin errores y funciones de flujo. UDP no; observe la Tabla 5.2.
Tabla 5.2 Capa de transporte contra UDP
Capa de transporte UDP
Datos fiables Datos fiables (sumas de comprobación).

Entrega fiable Entrega no garantizada.

Tamaño de ventana negociado Ventana fija (determinado por el programa).

Orientado al registro Orientado al paquete.

Construido sobre la capa de Construido sobre IP.


funcionamiento en red
UDP añade una característica principal de la capa de transporte: redes virtuales

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.

FIGURA 5.3 La pila IP fija su atención más en la funcionalidad que en el


encapsulado. Al revisar el diagrama para reflejar mejor el alcance de una capa
a otra se observa que UDP se hunde un poco dentro de la capa de red (3) de
OSI.

Capa 3: flujos de host ( T C P )


La capa TCP presenta una correspondencia muy clara con la capa de transporte
OSI. Ambos ofrecen la fiabilidad, la generación de flujo, ordenación y
administración de error necesitados para la construcción de sesiones. Es
importante destacar que TCP no se apoya en UDP. Tiene su propia cabecera y su
propio camino en el subsistema de red. Véase la Tabla 5.3 para obtener la
comparación uno al lado del otro.

122/512
Tabla 5.3 Capa de transporte contra TCP
Capa de transporte TCP
Datos fiables Datos fiables (sumas de comprobación).

Entrega fiable Entrega garantizada.

Tamaño de ventana negociada Ventana deslizante.

Orientado al registro Orientado al flujo.

Construido sobre la capa de Construido sobre IP.


funcionamiento en red

Capa 4: capa de aplicación


Se sitúa en dónde el modelo TCP/IP termina. Conforme al modelo, la capa de
aplicación abarca las capas de sesión (5), presentación (6) y aplicación (7) de OSI,
El modelo TCP/IP tiene unas pocas aplicaciones que rellenan esos papeles.
Algunos ejemplos de esta capa son lo navegadores web, gateways, Telnet y FTP
{Protocolo de transferencia de archivos). Las llamadas de procedimiento remoto
(RPC) se alinean mejor con la capa de presentación. El sistema de archivos de red
(NFS) depende de RPC y parece apoyarse en la capa de aplicación. Todavía, estas
aplicaciones tienen los límites difuminados así que su asignación a cualquier capa
es difícil.
La capa de aplicación se apoya en la capa de flujos de host (UDP y TCP). Sin
embargo, puede aceptar mensajes directamente de ICMP.

Diferencias fundamentales entre OSI e IP


Los modelos OSI e IP utilizan la gestión en capas para definir los papeles de
cada pila de protocolos. Ellos también se desplazan desde el código dependiente
del hardware hasta la aplicación abstracta. Pero puede haber observado algunas
diferencias.
OSI dispone en capas los datos al igual que los protocolos. Recuerde la Figura
5.1, cada capa de la pila tiene su propia información de cabecera. Como los datos
se desplazan hacia abajo de la pila a la capa física, cada capa encapsula el
mensaje dentro de su propio paquete.
Por ejemplo, si envía un mensaje de capa de sesión, la capa física ve una lista
de cabeceras en orden inverso: enlace de datos, red, transporte y sesión. El
mensaje sigue él sólo todas estas cabeceras hasta el final de los datos. Cada capa
utiliza este proceso, incluida la capa física. Casualmente, puede tener hasta siete
cabeceras en un paquete individual.
El receptor invierte el proceso. Usa los datos encapsulados para determinar si
pasar el mensaje arriba a la siguiente capa. En el ejemplo previo, el mensaje llega
a la capa física. Cada capa desenvuelve su propia cabecera. Si observa otra
cabecera, pasa el mensaje arriba a la siguiente capa. Ocasionalmente, el mensaje

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.

¿Qué da servicio a qué?


El próximo paso es saber cómo utilizar el modelo de red DoD. Si desea acceder a todas las
capas distintas de la pila IP, utilice la llamada del sistema socketO.
Tabla 5.4 Resumen de capas de DoD
Capa DoD Acceso de usuario/programador
4-Aplicación FTP, Gopher, Lynx, Netscape, IRC
3-De host a host (TCP) socket(PF_INET, SOCK_STREAM, 0);
3-De host a host (UDP) socket(PF_INET, SOCK_DGRAM, 0);
2-Internetwork (ICMP) socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);
2-Internetwork (IP) socket(PF_INET, 50CK_RAW, protocol);
1-Acceso de red socket(PF_INET, SOCK_PACKET, filter);

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.

Resumen: de la teoría a la práctica


En este capítulo se examina la interfaz entre los distintos elementos de red que necesitan
funcionar en harmonía. El desafío de red incluye la gestión de la interfaz del hardware de la
computadora, cuestiones de conexión de red, gestión de enlaces de sistema operativo y
gestión de la interfaz de aplicación. Cada uno tiene problemas específicos que introducen la
forma de dar al programa acceso a otras computadoras remotas. El funcionamiento en red
controla estos problemas utilizando con cuidado modelos diseñados para ayudar de una forma
consistente al programa.
Dos modelos de funcionamiento en red, OSI e IP (o DoD), separan las soluciones dentro

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

La perspectiva del sevidor y


el control de carga
En esta parte
6 Generalidades sobre el servidor
7 División de la carga: multitarea
8 Cómo decidir cuándo esperar E/S
9 Cómo romper las barreras del rendimiento
10 Diseño de Sockets Linux robustos

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

Los programas de red siempre tienen un emisor y un receptor. Generalmente, el


emisor es un cliente que se conecta a un servicio ofrecido por una computadora
conectada a la red. La parte I de este libro, "Programación de red desde la
perspectiva del cliente", cubre en detalle el punto de vista del cliente -cómo
conectarse a un servidor, cómo crear comunicaciones sin conexión, y cómo usar
las herramientas de soporte de IP. Este capítulo nos presenta la otra parte: el
receptor o la perspectiva del proceso del servidor.
La perspectiva del servidor completa la interacción cliente/servidor. Para ilustrar
la relación entre el cliente y el servidor, debe comparar la red a un sistema
telefónico y el servidor al número telefónico central de una gran compañía que
encamina las llamadas a sus empleados. El cliente se conecta a uno de los
empleados a través del número central y una extensión. A partir de este ejemplo
puede ver como un servidor en una red acepta conexiones desde un cliente. El
número principal es la dirección del host y la extensión es el puerto del servicio.
Los servidores solicitan un número de puerto específico —el puerto que el
cliente conoce. Comparable a la publicación de un número de teléfono, si el
servidor no consigue el número que él quería, el cliente no puede llamarlo.
Este capítulo cambia la perspectiva desde la petición de servicios, tal y como se
discutió en la Parte 1, hacia la oferta de los mismos. Le llevará paso a paso a
través del proceso de escritura y creación de un servidor. Al finalizar, un servidor de

127/512
directorios HTTP pequeño le mostrará como conectarse a un cliente y cómo
empaquetar mensajes HTML.

Asignación del socket el flujo de programa


general del servidor
El proceso para la creación de un servidor siempre comienza con la creación del
socket Así como el cliente necesitaba llamadas específicas en determinados
momentos, el servidor trabaja de un modo similar pero añade unas pocas llamadas
extras al sistema. El servidor utiliza la llamada del sistema socket(), pero debe
hacer un trabajo extra que era opcional para el cliente, como puede verse ilustrado
en la Figura 6.1.
El programa cliente que escribió en los primeros capítulos seguía el diagrama de
cliente. El orden de las llamadas que realizaba el cliente es: socket(), connect(),
read(), write(), y close(). La llamada del sistema bind() no era necesaria porque el
sistema operativo realizó esa función en su lugar. El número de puerto no se
necesitaba porque el programa llamaba al servidor. El cliente siempre realiza una
conexión activa porque la persigue enérgicamente.
Los servidores, por otro lado, necesitan proporcionar un número de puerto
específico y consistente a los programas cliente si Ies va a prestar servicio. El
diagrama de un senador muestra unas pocas diferencias respecto al del cliente. El
programa servidor que escriba deberá utilizar las llamadas de sistema socket(),
bind(), listen(), accept(), y close(). Y mientras el programa cliente es una
conexión activa, el servidor es una conexión pasiva. Las llamadas de sistema
listen() y accept() crean una conexión sólo cuando el cliente pide una conexión
(similar a la acción de responder al timbre de un teléfono).

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.

Asociar puertos a un socket


El socket basado en la conexión comienza, como siempre, con una llamada a
socket(). Normalmente el servidor utiliza la capa segura de protocolo TCP, de modo
que puede utilizar el flag SOCK_STREAM. Pero debe ir un paso más allá -los
clientes necesitan saber a qué puerto deben conectarse.
La llamada del sistema bind() pregunta al sistema si su programa puede
apropiarse de un número de puerto particular. Así como una compañía de ventas
pide y publica un número de teléfono central para sus clientes, la llamada b¡nd()
selecciona un número de puerto. Si su servidor no especifica un número de puerto,
el sistema operativo asigna el siguiente disponible de entre su conjunto de
números. Este puerto puede ser diferente cada vez que ejecuta el programa si el
sistema operativo es el encargado de asignarlo.
También puede averiguar si el servidor ya está en ejecución si solicita -pero no
obtiene- un número de puerto específico. El sistema operativo asigna el puerto a un
sólo proceso.
La llamada del sistema b¡nd() tiene el siguiente aspecto:
#include <sys/socket.h>
#include <resolv.rt>
int bind(int sd, struct sockaddr *addr, int addrsize);
La variable sd es el descriptor del socket que creó primero. Definida ya en el
Capítulo 1, la variable addr apunta a un nombre de socket (el número de puerto) de
la familia de registros sockaddr (definida en el Capítulo 2, "Elocuencia del lenguaje
de red TCP/IP"). El último parámetro que se le proporciona, addrsize, es la longitud
del registro sockaddr.
El programa necesita facilitar el tamaño de registro del sistema operativo a
causa del concepto global de la API sockets: una interfaz pero muchas
arquitecturas. El sistema operativo soporta muchos protocolos, cada uno utilizando
su propia estructura de asignación de nombres, y cada estructura con un tamaño
diferente.
Para utilizar bind(), necesita crear y rellenar addr (véase el Listado 6.1).
Listado 6.1 Utilización de bind() en un servidor TCP
/*************************************************/
/*** Ejemplo de socket INET: rellena la estructura sockaddr_in ***/
/*** variable addr. ***/
/*************************************************/

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:

• EBADF. El descriptor del socket no es válido. Esto puede ocurrir si la llamada


del sistema socket() falló y no comprobó el valor de retorno.

• 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.

Creación de una cola de espera de sockets


La interfaz del socket le proporciona a su programa un modo de conectarse con
otros programas en la red. La conexión es exclusiva: una vez que un programa se
conecta, ningún otro programa puede conectarse. Una manera de evitar esto es la
creación de una cola de espera.
Puede habilitar la cola de espera del socket con la llamada del sistema listen().
Volviendo al primer ejemplo de este capítulo, si la compañía sólo tuviera un
teléfono, no podría manejar más de una llamada cada vez; así que no sería extraño
recibir una señal de ocupado en la línea. La solución es añadir el servicio de
llamadas en espera.
La llamada del sistema listen() es similar a una cola de llamadas en espera.
Cuando el servidor llama a listen(), ésta designa el número de huecos de la cola.
La llamada también convierte el socket en un socket sólo de recepción. Esta
conversión es importante para que más tarde pueda aceptar la llamada del sistema
accept().
#include <sys/socket. h>
#include <resolve.h>
int listen(int sd, int numslots);
De nuevo, el parámetro sd indica el descriptor del socket creado en la llamada
socket(). El parámetro numslots adjudica el número de conexiones en espera. El
segmento de código del Listado 6.3 muestra una utilización típica.
Listado 6.3 Ejemplo de listen()
/************************************************************************/
/*** Ejemplo de Listen-Conversión: convierte el socket en un ***/
/*** socket en espera para conexiones cliente. ***/
/***********************************************************************/
int sd;
sd = sock e t (PF_INET, SOCK_STREAM, 0 ) ; /*** Asocia el puerto ***/
if ( listen(sd, 2 0 ) ! = 0 ) /* convierte a un socket en espera */
perror("Listen"); /* ...con 2 0 huecos para espera */
Normalmente los servidores establecen la profundidad de la cola entre 5 y 20
conexiones pendientes. Una cola de profundidad mayor de 20 es normalmente
malgastada en entornos de producción normales (multitarea). Sin la multitarea,
tendrá que incrementarla a un número cercano al tiempo de espera que establezca

133/512
(tal y como 60 para 60 segundos).
Los siguientes son los errores devueltos comúnmente por una llamada del
sistema listen();

• EBADF El descriptor del socket no es válido (véase la descripción de la


llamada del sistema bind()).

• EOPNOTSUPP El protocolo del socket no soporta llamadas en espera. Por


ejemplo, TCP (SOCK.STREAM) soporta la cola de espera, pero UDP
(SOCK_DGRAM) no lo hace.
El siguiente paso después de convertir el socket en un socket en espera es
aguardar y aceptar conexiones, tal y como abordaremos en la siguiente sección.

Aceptar conexiones de clientes


En este punto, el programa ha creado el socket, asociado un número de puerto,
y estableciendo una cola de conexiones. Ahora puede aceptar conexiones. La
llamada del sistema accept() convierte el descriptor del socket en un despachador
para todas las conexiones. Aquí es donde las cosas se vuelven un poco más
extrañas, y se rompen las reglas iniciales. Cuando convierte el socket en un socket
en espera, deja de ser un canal bidireccional para datos. Su programa realmente
no puede ni tan siquiera leer datos; el socketen espera le permite tan sólo aceptar
conexiones. La llamada del sistema accept() espera (o queda en estado de
bloqueo) hasta que llega una conexión.
Cuando el cliente realiza una conexión, el socket en espera crea un nuevo canal
bidireccional para ese cliente utilizando el mismo puerto. La llamada del sistema
accept() crea implícitamente un nuevo descriptor de socket para el programa. En
efecto, cada nueva conexión crea una nueva línea dedicada entre el cliente y el
servidor. A partir de este punto, interactuará con el cliente a través del nuevo canal.
Adicionalmente, puede determinar quién se está conectando a su servidor
puesto que un par de los parámetros de la llamada obtienen la información de
conexión del cliente. El Capítulo 4 presentaba una llamada del sistema similar,
recvfrom(), que obtenía los datos y la dirección de retorno. Esta información sobre
la dirección es útil para seguir la pista de las computadoras que se conectan a su
servidor.
#include <sys/socket.h>
#include <resolve.h>
int accept(int sd, sockaddr *addr, int *addrsize);
Como siempre, el parámetro sd es el descriptor del socket. Al igual que con la
llamada del sistema recvfrom(), los dos últimos parámetros proporcionan a la
llamada un lugar para escribir la dirección del cliente y su número de puerto. A
diferencia de la llamada del sistema recvfrom(), los dos últimos parámetros de
accept() son opcionales. Si no quiere obtener la dirección del llamador y su
número de puerto, simplemente establezca estos parámetros a cero.

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 */
...

Comunicación con el cliente


Observe que el listado cierra clientsd. El descriptor del socket clientsd se
diferencia de sd, el descriptor de socket principal. Es importante cerrar clientsd
porque cada conexión genera un nuevo descriptor. No cerrar cada conexión puede
consumir a la larga las reservas de descriptores de archivos.
REUTILIZACION DE LA VARIABLE ADDR
Puede reutilizar la variable local addr de la llamada del sistema bind()- Después de haber
completado la llamada bind() en el puerto del servidor, el servidor no necesitará más esa
información. Es seguro entonces utilizar esa variable para otros propósitos.
Recuerde que la mayoría de los parámetros están en orden de bytes de red.

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:

• EBADF. El descriptor del socket no es válido (véase la descripción de bind() en


el Capítulo 4).

• EOPNOTSUPP. El socket debe ser un SOCK_STREAM para esta llamada.


• EAGAIN. El socket se ha configurado en modo de no bloqueo y la cola de
escucha no tiene conexiones pendientes. Una llamada a accept() con la cola
vacía bloquea la tarea a menos que el socket se convierta en un socket que no
bloquea.
El servidor de eco simplemente devuelve lo que recibe hasta que obtiene un
mensaje bye (véase el Listado 6.6).

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);
}
...

Observe que la condición final es la búsqueda de un "bye\r", no "bye\n".


Dependiendo de como se realice el procesamiento de la entrada (el Telnet no
realiza ninguno), el servidor puede obtener uno u otro. Para lograr una mayor
robustez, con sidere la posibilidad de probar ambos. Pruebe este programa con
Telnet como cliente. El programa completo, llamado simple-server.c, está
disponible en el sitio web.

Reglas generales sobre la definición de


protocolos
Cuando se comunica con otras computadoras, su programa necesita acordar un
procedimiento de interacción. Dos acciones muy importantes son "¿quién habla
primero?" y "¿cuándo hemos terminado?" Este acuerdo define un protocolo de
transacción.
El cliente y el servidor utilizan estos procedimientos para asegurarse que no
transmiten al mismo tiempo o esperan el uno al otro a ciegas para transmitir
primero. Durante la creación de servidores, considere las cuestiones de las
siguientes sub-secciones.

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?

¿Qué programa dirige la conversación?


Los clientes frecuentemente dirigen la interacción con el servidor. El cliente se
conecta al servidor y envía peticiones. El servidor, a su vez, procesa las peticiones
y envía una respuesta.
Pero algunas veces puede tener que cambiar los papeles. Por ejemplo, un
cliente pide alguna información sobre bases de datos del servidor. Después de
transmitir los datos, un cliente diferente actualiza algunos campos que solicitó el
primer cliente. Si el primer cliente trabaja exclusivamente con el primer conjunto de
datos, podría tomar decisiones erróneas.
En ese tipo de situaciones, el servidor debería enviar las actualizaciones no
solicitadas al cliente, y el cliente necesita aceptarlas. Considere cómo el cliente
interactúa ciin el servidor y cómo los equipos homólogos interactúan entre sí, fíjese
en las excepciones y defina modos de tratarlas.

¿Qué nivel de certificación necesita?


Para escribir un sistema extremadamente seguro, es esencial conocer con quién
está hablando el servidor. A su vez, el servidor debe conocer o certificar al usuario,
o como mínimo a la máquina.
El proceso de certificación puede incluir nombres de usuarios y contraseñas.
También puede utilizar un emisor de certificados de confianza para las máquinas
de los usuarios. El Capítulo 16, "Cómo añadir seguridad a los programas de red y
SSL," aborda la capa de sockets seguros (Secure Socket Layer, SSL) y nos
presenta un par de posibles soluciones para este problema.
Por otro lado, puede no necesitar certificación, pero desear en su lugar algún
tipo de registro. ¿Con qué frecuencia su servidor soporta conexiones masivas?
¿Necesita hacer más robustas ciertas partes del servidor para satisfacer las
necesidades del usuario? ¿Cómo puede discretamente informarse sobre su
cliente?
Es importante que considere cada una de estas cuestiones al programar su
servidor. En la Web puede encontrar varios ejemplos de como conseguir satisfacer
las necesidades-tendencias de los usuarios así como obtener conexiones masivas.

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.

¿Qué tipos de datos utilizar?


La mayoría de los servidores con los que interactúa utilizan el código ASCII para
¡a correspondencia, mientras que la mayoría de las páginas web están en formato
texto/HTML. Es buena idea considerar las cuestiones "¿es la forma de datos más
eficiente para utilizar?" y "¿soporta el cliente la compresión de datos?" A medida
que considere su servidor y los clientes de sus usuarios, reflexione sobre cómo
disminuir los retardos del servidor, de la red, y del cliente.
La compresión de datos puede tener algunas ventajas reales. El ASCII
comprimido puede producir una reducción entre el 50% y el 80% en el tamaño de
los datos. Incluso los datos comprimidos pueden ser código ASCII sin demasiado
inflación, de modo que la red no perderá ninguno de los bits más significativos. Los
siguientes apartados ofrecen más detalles acerca de cómo manejar los datos.

¿Cómo debe manejar los datos binarios?


Los datos binarios -especialmente los comprimidos- son más eficientes que el
código ASCII. Problema: algunas redes sólo soportan bytes de 7 bits. Éste es un
problema real para los datos binarios de 8 bits. Estas redes son usualmente
dinosaurios que cuestan mucho dinero reemplazar. Afortunadamente, los routers
conectados a estas redes frecuentemente reconocen la incompatibilidad y
convierten los datos en un sentido u otro según se necesite. Esta conversión
requiere tiempo de computación y por tanto retarda la propagación. El darle un
formato nuevo a los datos afecta al host receptor, porque el host tiene que
reconstruir los datos fracturados.
Además, ¿comienza con datos no binarios y luego cambia a binarios? Si es así,
o bien su cliente o bien su servidor necesitan decirle al otro cuando cambiar. Del
mismo modo, puede querer alternar de nuevo en algún momento determinado.

¿Cómo saber cuándo se produce un interbloqueo?


Puede encontrarse con oportunidades en las que el servidor y el cliente están
esperándose el uno al otro (interbloqueo). Mientras esperan, están inmovilizando
recursos valiosos y pueden estar agotando la paciencia de sus usuarios.
Intente identificar las situaciones que causan un interbloqueo antes de
determinar si su problema particular es un interbloqueo. Si tiene un interbloqueo, la
única solución es cortar la conexión y probar de nuevo. Cuando tiene que
conectarse de nuevo, pierde el contexto de la conexión (en qué lugar de la
interacción se encuentra), y puede perder datos.
El cliente o el servidor pueden encontrarse con inanición (espera indefinida por
recursos). La inanición sucede generalmente cuando un cliente o servidor están
realizando otras funciones aparte de administrar la conexión. Al igual que con el
interbloqueo, la solución estándar es esperar un tiempo y volver a conectarse.

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.

¿Cómo y cuándo reiniciar la conexión?


En algunos puntos de la interacción, el servidor y el cliente pueden necesitar
arrancar de nuevo sin perder la conexión. La pérdida de la conexión puede
significar una pérdida de información, por lo que es obligado restablecer la
conexión.
El protocolo TCP/IP ofrece un mensaje de prioridad que puede utilizar como
mensaje de reinicio. Para más información sobre los mensajes de prioridad y datos
fuera de banda, vea el Capítulo 9, "Cómo romper las barreras del rendimiento."
Este mensaje es sólo un comienzo; tanto su servidor como su cliente deben retorn;
a algún punto de inicio. Eso es un desafío para la programación estructurada.
La reconexión es un modo directo de empezar de nuevo. El mensaje urgen!
puede decirle tanto al cliente como al servidor que cierre la conexión. Entonces, <
cliente puede volver a conectarse para restablecer la comunicación. Sin embargo,
1 reconexión puede provocar una pérdida de información no deseada. Por tanto, u
reinicio puede forzar tanto al cliente como al servidor de regreso a un punto conoc
do. El valor crucial de sus datos juega un papel muy importante en la recuperació
de una conexión.

¿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.

Un ejemplo extenso: un servidor de directorio


HTTP
El servidor de eco es un gran punto de inicio para varios servidores diferentes.
Un servidor común que está en la mente de mucha gente es un servidor HTTP. En
estos momentos, un servidor HTTP completo está un poco fuera del ámbito de este
texto, pero puede crear uno mucho más pequeño que responda a las peticiones de
cualquier navegador.
Esta sección describe un servidor pequeño que genera código HTML
dinámicamente en lugar de obtener un archivo desde el sistema de archivos. De
alguna manera, esto permite trabajar de un modo más fácil y agradable con el
programa. Puede encontrar el listado completo del programa en el sitio web, con el
nombre html-ls-server.c.
Tal y como describíamos en la última sección, HTTP 1.0 tiene un protocolo
específico que define cómo interactúan el cliente y el servidor. El Listado 6.7
muestra la diferencia con el ejemplo del servidor de eco del Listado 6.6.
Listado 6.7 Ejemplo de un receptor HTTP simple
/**********************************/
/*** Receptor HTTP simple. ***/
/**********************************/
...
while (1)
{ int client;
int size = sizeof(addr);
client = accept(sd, &addr, Ssize);
if { client > 0 )
{ char buffer[1024];
/*--- Mensaje para el cliente - - - ' * / * /
char "reply = "<html><body>Hello!</body></html>/n";
bzero(buffer, sizeof(buffer); /* limpia el buffer */
recv(client, buffer, sizeof(buffer), 0); /* obtiene msg */
send(client, reply, strlen(reply), 0);/* envia respuesta */

141/512
/*--- muestra mensaje 'helio' del cliente en servidor ---*//
fprintf(stderr, "%s", buffer);
close(client);
}

else
perror("Accept");
}

Cada vez que el cliente se conecta, envía un mensaje similar al siguiente:


GET /dir/document HTTP/ 1 . 0
(algunas definiciones de protocolo)
La primera línea es la petición. Todos los mensajes siguientes le dicen al
servidor lo que el cliente HTTP puede aceptar c interpretar, y pueden incluir
información de configuración para ayudar al servidor a determinar cómo interactuar
con el cliente. La línea más importante para analizar es el comando GET. GET tiene
dos parámetros —la petición y el protocolo implícito. Su servidor puede dividir la
petición para conseguir la ruta y el documento solicitados. (Por supuesto, la
petición puede ser más complicada que un nombre de archivo plenamente
cualificado.)
El protocolo HTTP 1.0 permite espacios en blanco en la ruta, por lo que durante
la exploración de la línea se necesita obtener todo lo que hay entre la / inicial y el
HTTP/. La ruta puede incluir letras en notación hexadecimal, pero el flujo de
programa básico del servidor de ejemplo las ignora.
Adicionalmente, el protocolo utiliza algunos de los protocolos de encabezado
MIME cuando el servidor responde. El cliente espera que el documento tenga
ciertos campos y un estado:
HTTP/ 1 . 1 2 0 0 0 K
Content-Type: text/ html
...
(empty line)
(empty line)
<html>
<head>
...
La primera línea es el estado. Si se ha preguntado alguna vez de dónde
proviene el error 404 (petición no encontrada), la primera línea de la respuesta le
dice a su cliente cómo de acertada fue la consulta. Acuda al Apéndice A, "Tablas de
datos," para obtener una lista completa de los códigos de estado del protocolo
HTTP 1.1.

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() */
}

...

Puede mejorar el listado del programa ordenando los nombres de archivos,


reconociendo los tipos de archivos, incluyendo los códigos de error de HTTP 1.1, y
así consecutivamente. El flujo de programa existente sólo trabaja con directorios. El
servidor coloca un enlace FTP para cualquier archivo ordinario. De nuevo, puede
obtener el programa completo en el sitio web.

Resumen: los elementos básicos de un servidor


En este capítulo se explica cómo se ven las cosas desde la perspectiva del
servidor. Esto le ayuda a ver qué porcentaje de la red está bien acoplado, al tiempo
que mejora sus habilidades de programación en red. Con esto en mente, puede
ofrecer a sus clientes muchos servicios mientras controla y centraliza los datos y
las operaciones críticas. Dispone de una amplia variedad de habilidades en la
distribución de la carga, en la consecución de más clientes, y en el incremento de
la manejabilidad de los datos.

Las aplicaciones de servidores de red uhlízan tres llamadas nuevas —bind(),


listen(), y accept()— aparte de aquellas que los clientes típicos utilizan. Las
utiliza para seleccionar el número del puerto (bind()), definir la cola de la conexión
(listen()), y aceptar conexiones nuevas (accept()). La llamada accept() crea un
socket nuevo de conexión para cada conexión nueva, abriendo su programa para
permitir múltiples conexiones simultaneas a través de un solo puerto.
La definición del servidor requiere que considere los protocolos de
transacciones. Estos protocolos describen cómo el cliente y el servidor interactúan
y cómo deberían comportarse. Paso a paso, analiza la interacción cliente-servidor y
el mejor modo para alcanzar cada petición de un modo correcto y eficiente.

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,

Definición de multitarea: procesos frente a


threads
Con la multitarea, puede acceder a la potencia total de Linux y otras copias de
UNIX porque puede utilizarla para asignar el tiempo de CPU en fracciones
independientes de tiempo (timeslicing) y espacio (recursos del sistema). Los
programas pueden conseguir más y trabajar más eficientemente si están diseñados
apropiadamente para la multitarea.
Las tareas son unidades sueltas de ejecución en su sistema. Cada elemento
activo con el que trabaje es una tarea. La multitarea de computadoras se divide en
dos campos —procesos y threads (o procesos de poco peso). Estos dos términos
son actualmente dos extremos a lo largo del amplio espectro de la compartición de
datos. Para comprender cómo trabaja la multitarea, necesita conocer como el
sistema operativo separa las tareas individuales entre sí.
Cada tarea agrupa información en diferentes particiones de la memoria
(páginas). El sistema operativo asigna a cada tarea una tabla de páginas (un
conjunto de páginas en el que cada página realiza una función diferente). La tarea
utiliza estas páginas a través del subsistema de memoria virtual (VM) del
microprocesador. La VM es una tabla que traduce las direcciones efectivas del
programa a ubicaciones físicas. Cuando el sistema operativo inicia un cambio de
tarea, graba información sobre la tarea (el contexto) y carga en VM la tabla de
páginas de la siguiente tarea.
OBJETIVO DE LA MEMORIA VIRTUAL
La tabla de páginas de ta memoria virtual incluye más información además de la mera
traducción. También incluye referencias a derechos de acceso (lectura-escritura-
ejecución). El sistema operativo también señaliza aquellas páginas que son
intercambiadas al almacenamiento secundario. De ese modo, cuando el programa
accede a la página, el microprocesador emite una falta de página (la página está perdida).
El gestor de fallos de página carga entonces la página perdida desde el almacenamiento
secundario.

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.)

Cuándo se debe utilizar la multitarea


¿Cuándo debe utilizar la multitarea? Generalmente, un usuario siempre debe
tener el control sobre el programa. Algunas veces el programa tiene que esperar a
que se completen otras operaciones, por lo que debe utilizar la multitarea para
interactuar con el usuario mientras espera al resto de operaciones, Del mismo
modo que Netscape le permite acceder a sus menús mientras está descargando
una página web, el programa padre que diseñe debería delegar todas las
operaciones de E/S en red a tareas hijo. De hecho, dado que servidores diferentes
tienen tiempos de respuesta muy variables, el fraccionamiento de varios threads
para cargar la información de ellos puede hacer un uso más eficiente de su canal
de red.
Aquí tenemos una buena guía no oficial para saber cuándo utilizar la multitarea:
mientras está esperando una E/S, el programa podría simultáneamente

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.

Diferencias en las tareas


Las diferencias entre los procesos y los threads son sutiles. Cuando trabaje con
ellos, tenga en mente las siguientes características y estilos.
Procesos Threads

A partir de una llamada El llamador proporciona el nombre de la


realizada con éxito, hay dos función a ejecutar como hijo.
procesos en ejecución en línea.
El hijo debe finalizarse El hijo puede finalizarse explícita o
explícitamente con una llamada implícitamente con pthread_exit(void* arg) o una
del sistema exit(). instrucción de retorno.
No hay datos compartidos. La única
información pasada al hijo es la imagen de los
datos del padre antes de la llamada del sistema.
El hijo comparte los datos del padre, acepta un
parámetro, y/o devuelve un valor.
El hijo está siempre asociado El hijo se puede separar del padre. Por tanto, el
con un padre. Cuantío finaliza, hijo puede finalizar sin intervención paterna. (A
el padre debe eliminar al hijo. menos que el hijo esté separado, el padre
también debe eliminar el thread.)
Puesto que los datos de cada Todos los datos por los que se compite deben
proceso son independientes ser identificados y bloqueados para que no se
unos de otros, no hay produzca ninguna corrupción en los mismos.
contención de los recursos.
Sistema de archivos Sistemas de archivos enlazados. El hijo ve
independiente. cualquier cambio que el padre realiza sobre el
directorio de trabajo actual (chdir), el sistema de
ficheros raíz (chroot), o los permisos por defecto

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.

¿Cómo creo un proceso?


La multitarea se consigue más frecuentemente con procesos. No comparten
nada, crean una nueva instancia de su programa, y heredan copias de los canales
de E/S del padre abiertos. Para generar un proceso, utilice la llamada del sistema
fork():
#include <unistd.h>
pid_t fork(void);
La llamada del sistema fork() tiene una interfaz muy simple y modesta: se la
llama, y de repente se tienen dos procesos idénticos ejecutándose a la vez. La
llamada del sistema fork() tiene tres rangos de valores:
• Cero. El programa en cuestión es el hijo. Si la llamada devuelve un cero, el
fork() tuvo éxito y la tarea actualmente en ejecución es la tarea hijo. Para obtener
el identificador de proceso del hijo (PIE)), utilice la llamada del sistema getpid().
• Positivo. El programa en cuestión es el padre. Si la llamada devuelve un valor
positivo, es señal de que el fork() ha tenido éxito de nuevo y la tarea actualmente
en ejecución es la tarea padre. El valor devuelto es el P1D del nuevo hijo.
• Negativo. Ha ocurrido un error; la llamada no tuvo éxito. Compruebe errno o
utilice perror() para determinar la naturaleza del error.
La mayoría de los programas colocan la llamada del sistema fork() en una
sentencia condicional (cómo un if). La sentencia condicional ayuda a separar la
tarea padre de la hija. Los Listados 7.1 y 7.2 muestran dos ejemplos típicos que
utilizan esta llamada.

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 )
{

/* el padre pone orden */


/* fijese en NO WAIT() -- debe *//
/* utilizar señales (véase más adelante) */
}

else if ( pchild < 0 )


{ /* Algún tipo de error */
perror("Can’t process job request");
}
}
El Listado 7.1 crea un hijo en un entorno seguro. El padre realiza algún trabajo y
a continuación espera a que el hijo finalice. Alternativamente, puede utilizar un
algoritmo, tal y como el visto en el Listado 7.2, que convierta al padre en un delega-
dor de trabajos. Cuando alguna entidad exterior solicita una operación, el padre
crea un hijo para realizarla. La mayoría de los servidores utilizan lo visto en el
Listado 7.2.
Puede querer utilizar la multitarea para conseguir realizar diferentes trabajos a la
vez. Los trabajos pueden tener el mismo algoritmo, pero utilizan datos diferentes.
Sin este concepto, los programas pueden malgastar tiempo de computación en
esfuerzos duplicados. La diferenciación separa las tareas de forma que no
dupliquen esfuerzos. Aunque no hay ningún error sintáctico en el código fuente del
Listado 7.3, éste anula el objetivo de la multitarea porque no realiza ninguna
diferenciación.
Listado 7.3 Ejemplo de bifurcación sin diferenciación
/*****************************************************************************/
/*** Este es un ejemplo de "ejecución conjunta". Si se llama ***/
/*** a fork() si ninguna diferenciación, las dos tareas realizan el */
/*** trabajo de la otra de forma duplicada, malgastando por tanto ***/
/*** tiempo de CPU. ***/
/*- - -hacer algo- --*/*/
fork();
/* continuar---*/*/
Este libro alude a la multitarea sin diferenciación como ejecución conjunta o

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.

¿Cómo creo un thread?


Los threads le ofrecen la potencia de recursos estrechamente compartidos entre
padres, hijos, y hermanos. Utilice el frireac/írig-para delegar en tareas de datos
para un objetivo común. Por ejemplo, un thread obtiene una imagen, mientras que
otro la convierte para su visualización. En contraste, los procesos le ayudan
cuando no necesita interacciones tan estrechamente ligadas.
Una implementación específica del threading de la que puede haber oído hablar
es Pthreads. Pthreads es un conjunto de bibliotecas de llamadas que ofrecen
threading en un modo conforme a POSIX 1c. Los programas que escriba utilizando
Pthreads es muy probable que sean compatibles con otros sistemas operativos
conformes con POSIX. La llamada de la biblioteca para la creación de nuevos
Pthreads es pthread_create():
#include <pthread.h>
int pthread_create(pthread_t* child, pthread_attr_t* attr, void*
(*fn)(void*), void* arg);
LA DIFERENCIA ENTRE LAS LLAMADAS DE BIBLIOTECA Y LAS
LLAMADAS DEL SISTEMA
La diferencia entre una llamada de biblioteca y una llamada del sistema es la cantidad de
trabajo hecho en las bibliotecas frente al núcleo. Una llamada del sistema fork() es
realmente una interfaz a los servicios de sistema del núcleo. La llamada pthread_create()
está en una biblioteca diferente que la traduce a la llamada del sistema _clone(). En este
caso, para compilar programas en C con threads, asegúrese de incluir -Ipthread como

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.

attr Un conjunto de atributos o modos de actuación para el nuevo thread. Puede


definir cómo trabajará e interactuará el thread con el padre en el momento de
la creación de la copia. Algunos de estos modos de actuación incluyen
prioridad, adjuntos, y planificación. Este parámetro puede ser nulo (NULL).

fn Un puntero a una función que albergará el thread. Cada thread se ejecuta


en una subrutina especifica de programa (a diferencia de fork())- Cuando el
thread abandona la subrutina, el sistema finaliza el thread. Este ayuda a
evitar el problema de convergencia de tareas {véase más arriba).

arg Un parámetro que ha de pasarse a la función. Puede utilizarlo para


configurar los valores iniciales del thread. Asegúrese de hacer disponible al
thread el bloque de datos al que apunta este parámetro. En otras palabras,
no haga referencia a una variable de pila. El parámetro también puede ser
nulo (NULL).

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;

/*---realiza otras iniciaciones ---*/*/


/* Crea un thread nuevo; informa de errores si falla */
if ( pthread_create(&tchild, NULL, &Child_Fn, Sarg) != 0 )
perror("Pthreads error"); /* error */

/*** Observe que los threads no tienen una sección del hijo ***/ç
/* Aún estamos implícitamente en el padre */

/* realiza otras tareas */


/* espera la finalización del hijo */
pthread_join(tchild, NULL);
return 0;
}

Cuando genera un proceso, utiliza la llamada del sistema fork(). La única


diferencia es la instrucción de programación condicional para separar los caminos
de ejecución. Con una generación con Pthreads, le proporciona atributos (cómo
debe actuar el thread), una rutina hijo (dónde ha de ejecutarse el hijo), y un
parámetro (qué datos específicos con los cuales llamar al hijo).
Los procesos requieren una llamada específica a exit() para evitar la ejecución
conjunta, la cual colocará al final de la ejecución del hijo. Con Pthreads, no se tiene
que preocupar sobre este tema. Una instrucción de retomo (o incluso fracasar sin
una instrucción de retorno) finaliza implícitamente el thread hijo.

La llamada del sistema _clone(): la llamada de los


valientes
Linux le ofrece una llamada del sistema de bajo nivel que le da un mayor control

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
}

#define STACK 1024


int main(void)
{ int cchild;
char "stack = malloc(STACK);
if ( (cchild = _clone(&Child, stack+STACK-1, SIGCHLD, 0)
== 0 )
{/** En el hijo -- sección inasequible **//}
else if ( cchild > 0 )
wait ();
else
perror("Can't clone task"};
}

/*****************************************************************************/
/* 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.

Comunicación entre tareas


Se crean tareas únicas discriminándolas de modo que no realicen trabajo
duplicado. Los dos tipos de discriminación -caminos especificados y comunicación-
le ayudan a diseñar y delegar las responsabilidades de una manera lógica. Por
ejemplo, suponga que está jugando una mano de poker. El juego utiliza varias
fases y modos de operación para asegurarse que los jugadores juegan
correctamente. Puesto que cada uno tiene diferentes manos para jugar, estilos de
juego divergentes, y fondos variables con los que apostar, el juego se vuelve
interesante. Igualmente, se consiguen resultados óptimos cuando se tienen en
cuenta las distintas funciones de las tareas y las responsabilidades.

Iniciación: establecer el estado del hijo


En el poker, cada jugador adquiere un conjunto de fichas, y el repartidor
distribuye la mano. Los jugadores pueden tener técnicas diferentes para apostar un
importe en su mano; v el repartidor tiene la responsabilidad añadida de gestionar la
baraja. En programación, una forma de comunicación es simplemente el estado en
el cual la tarea comienza. Tanto los procesos como los threads usualmente siguen
un camino de ejecución diferente que el de los padres después que los padres
establezcan los datos compartidos.
Utilice los padres para establecer el entorno en el que trabajarán los hijos. Una
vez que los hijos obtengan todo lo que necesitan, pueden empezar su
procesamiento. Los padres pueden tener un cierto grado de control sobre el hijo,
pero —igual que el repartidor y los jugadores— éstas suelen ser habitualmente
directivas ("¡No puedes hacer esto!"), no procedimientos ("¡Vamos, adelante!
¡Apueste su casa!").
Cuando se crean threads, se sigue un procedimiento al igual que durante la
creación de procesos. Primero, se establece el entorno compartido para los
procesos y threads, y a continuación se crea la tarea. Adicionalmentc, los threads
pueden aceptar un parámetro void* (igual que una llamada a procedimiento) para
iniciación. El tipo de datos void* es un modo de crear tipos abstractos de datos
propio del C. Con él, puede pasar cualquier valor alrededor y es labor del receptor
interpretar el significado real. Este parámetro es similar al hecho de aceptar jugar la
mano en una partida de poker.
UTILIZACION DEL TIPO DE DATOS VOID
El pase de un tipo void* puede parecer muy potente, pero ha de ser cuidadoso en el
modo de crear el tipo. El dato tiene que ser o bien un bloque reservado (utili zando la
llamada de biblioteca mallocO). datos globales, o datos estáticos. En otras palabras, no
deben ser datos de pila (o auto). La razón es simple: mientras el programa realiza las
llamadas-devoluciones de procedimientos, la pila va a cambiar. Los datos pueden no
estar a la vista del hijo.

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.

Comunicación entre procesos (IPC)


Durante el juego de poker, los jugadores se envían mensajes entre si y al
repartidor. Algunos de estos mensajes son instructivos, algunos emocionales (como
podrá adivinar). Inmóviles, los jugadores dirigen el mensaje a un jugador particular
o al grupo completo.
Las tareas que diseñe y programe pueden utilizar comunicación entre procesos
(IPC). IPC normalmente se nos presenta en la forma de canales; su tarea puede
dirigir mensajes a otras tareas utilizando estos canales. Los canales son
unidireccionales, del mismo modo que la utilización de los canales de E/S stdin y
stdout. La llamada del sistema pipe() crea el siguiente canal:
#include <unistd.h>
int pipe(int fd[2]);
El parámetro fd es un array de dos enteros. Este array acepta las referencias de
los dos descriptores de archivos nuevos. Dos errores comunes son:
• EMFILE. Hay demasiado archivos ya abiertos.

• EFAULT. fd no está apuntando a un array de dos enteros.

El siguiente es un ejemplo de utilización de pipe:


/******************************************************************/
/*** Ejemplo de utilización de la llamada del sistema pipe()*/
/******************************************************************/
int fd[2]; /* crear el array para mantener los descriptores */

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 */
...

INDEXACIÓN DE DESCRIPTORES DE ARCHIVO


A cada canal de E/S se le asigna un número de descriptor de archivo. Éste es un índice
en una tabla de descriptores de archivo. Todos los procesos consiguen tres canales de un
modo predeterminado: stdin (0), stdout (1), y stderr (2). Cuando crea un canal, éste toma
los dos números siguientes disponibles (uno para la entrada y otro para la salida). Por
ejemplo, si una tarea no tiene otros archivos abiertos, la llamada a pipeQ genera pipe-
input (3) y pipe-output (4).
Establecer un canal con multitarea es más complicado y puede resultar un
quebradero de cabeza, pero una vez que lo haya hecho un par de veces, no
volverá a causarle más jaquecas.
Los Listados 7.8 y 7.9 contrastan los diferentes modos de creación de canales
para tareas padre en threads y procesos. Puesto que el programador tiene que
prestar especial atención a las tareas clonadas, los ejemplos no las incluyen.
Listado 7.8 Creación de canales en un proceso padre
/*************************/
/*** Proceso padre ***/
/************************/
int FDs[2]; /* crea la ubicación para el nuevo canal */
pipe(FDs); /* crea el canal: FDs[0]=entrada, FDs[1]=salida */
char buffer[1024];
/*---Crea el proceso utilizando fork() ---*/*/
close(FDs[0]); /* Hacerlo de sólo escritura */
...
write(FDs[1], buffer, buffer_len); /* envía mensaje al hijo */
...
/*---realiza procesamiento ---*/*/
wait();
close(FDs[1]);
Listado 7.9 Creación de canales en un thread padre
/*** Thread padre ***/
int FDs[2]; /* crea la ubicación para el nuevo canal */
pipe(FDs); /* crea el canal: FDs[0]=entrada, FDs[1]=salida */

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.

LA UTILIZACIÓN DE CANALES EXIGE EL CIERRE DE UN EXTREMO


Cuando el sistema crea un canal, conecta el canal de entrada al canal de salida: el canal,
en efecto, se enrolla sobre sí mismo -cualquier cosa que el programa escriba puede ser
leído por la misma tarea (como si se estuviera hablando a si misma). El mismo mensaje
se dirige hacia el hijo a pesar de todo, pero el padre ve su propió mensaje en la entrada.
La mayoría de las rutas de comunicación son en una dirección: el padre le dice al hijo qué
debería procesar, de modo que su proceso cierra los canales innecesarios. Sin embargo,

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.

Señalización de un cambio de rumbo:


replegarse
El juego del poker tiene unos pocos controles asincronos directos. Un jugador
puede replegarse (salir del juego actual) en cualquier momento. Si el juego hubiera
tenido lugar en el viejo oeste, un tramposo podría encontrarse con una finalización
repentina y asincrona. En cualquier caso, el repartidor levanta las cartas arrojadas.
De igual modo, después de iniciar un proceso, puede cambiar su rumbo o su tarea
padre puede ser notificada de la finalización con señales.
Cada tarea que programe debería manejar todas las señales útiles. Hay
aproximadamente 30 clases diferentes de señales (dos de las señales son
definidas por el usuario). Puede ignorar o no capturar la mayoría de las señales
porque la probabilidad de que ocurran es pequeña o porque capturarlas no sea
significativo. Muchas señales libres frecuentemente finalizan su programa, y
algunas de éstas son importantes para programas de multitarea. Por ejemplo, el
sistema notifica al padre cuando un hijo finaliza con una señal SIGCHLD.

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:

• SA_ONESHOT. Utiliza el modo de actuar de System V de resetear el gestor una


vez que una señal es capturada.

• SA_RESETHAND. Igual que SA_ONESHOT.


• A_RESTART. Reinicia algunas llamadas del sistema si una señal las interrumpe.
Utilice este flag para reiniciar llamadas del sistema tales como accept().

• SA_NOMASK. Permite a la misma señal interrumpir mientras se está manejando


una señal previa.

• SA_NODEFER. Igual que SA_NOMA5K.


• SA_NOCLDSTOP. No notifica al padre si un proceso hijo se para ( SIGSTOP,
SIGTSTP, SIGTTIN, o SIGTTOU). Este flag es de suma importancia en este
capítulo.
El Listado 7.12 le muestra un ejemplo de cómo capturar la señal SIGFPE
(excepción de punto flotante) e ignorar la señal S1GINT (interrupción del teclado).
Listado 7.12 Ejemplo de gestor de SIGFPE y SIGINT
/*******************************************************/

/*** Ejemplo de captura de SIGFPE e paso por alto de SIGINT ***/


/******************************************************/

#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---*/*/
}

PÉRDIDA DE SEÑALES EN LOS GESTORES DE SEÑALES


Si gasta demasiado tiempo en un gestor de señales, su programa puede perder señales
pendientes de ser atendidas. La cola de señales sólo admite una señal -si llegan dos
señales, se almacena sólo una. Por tanto, intente minimizar la cantidad de tiempo de CPU
gastada en el gestor de señales. Además, hacer el gestor corto y sucinto le asegurará que
su programa consiga más tiempo de CPU.
Los servidores y los clientes pueden recibir varias señales diferentes. Para
hacer el código más robusto, debe capturar todas las señales razonables y
pertinentes. (Algunas señales, tales como SIGFAULT, es mejor no capturarlas. Si
obtiene esta señal, hay algún error en su código o datos, y no podrá recuperarse.)
REDUCCIÓN DEL CÓDIGO CON GESTORES DE SEÑALES COMPARTIDOS
Puede mezclar sus gestores de señales en un sólo gestor. El sistema le pasa al gestor el
número de la señal. Una vez dentro del gestor, simplemente compruebe ese valor para
realizar las acciones apropiadas.
Puede enviar cualquier señal a una tarea a la que puede controlar. En otras
palabras, las señales siguen las mismas reglas de seguridad que el resto. Para
enviar una señal desde la línea de comandos, puede usar el comando kill. Así
mismo, puede llamar a la llamada del sistema kill() en un programa. El prototipo de
la llamada del sistema kill() es el siguiente:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t PID, int sig_num);
Puede obtener toda la información acerca de las extensas (y enrevesadas)
opciones disponibles para esta llamada leyendo las páginas del manual.
Esencialmente, el programo llama a killQ con el identificador del proceso y la señal
que quiere enviarle.

Resultados del hijo


Volviendo al ejemplo del poker, cuando finaliza la mano o cuando un jugador se
retira, el repartidor tiene que poner en orden y barajar la baraja. De hecho, algunos
jugadores pueden querer ver los resultados de la mano ganadora antes que el
ganador recoja la apuesta. De una manera similar, el padre ordena y comprueba
los resultados de cada uno de sus hijos.
El último método de comunicación son los resultados de la finalización. El padre
utiliza esta información para decidir si el hijo ha tenido éxito. Cuando un proceso
termina, siempre devuelve un entero con signo. El thread, por otro lado, puede
devolver un objeto abstracto utilizando un void*, un objeto abstracto tal y como el
parámetro del thread.
Actué con precaución cuando proporcione los resultados de este modo -deben

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.

Venciendo al reloj: condiciones de carrera y


exclusiones mutuas (mutex)
La potencia que los threads le ofrecen es muy seductora. Si los threads son
gestionados correctamente, sus programas se ejecutarán más rápidamente y
experimentarán pocas pérdidas de control. Sin embargo, esta característica tiene
un truco -la contienda por los recursos. Si dos threads revisan los mismos datos a
la vez, pueden corromper los resultados, Y tratar de depurar un recurso en
contienda puede requerir muchas horas de su tiempo. La siguiente subsección
discute sobre cuestiones de condiciones de carrera.

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 );
}

Para una mayor claridad, asegúrese de observar como el programa incrementa


las variables puntero después de hacer referencia a la cola. Estas parecen
correctas puesto que se ejecutan en paralelo línea por línea. Desgraciadamente,
rara vez ocurre así. Suponga que el Thread 1 se queda rezagado del Thread 2
unas pocas líneas. Es posible que el Thread 2 pueda ejecutar el test vacío justo
antes que el Thread 1 restaure la variable out. Esta instancia creará el problema de
un test no válido porque out nunca será igual a in.
Este problema tiene una tendencia diferente si los dos threads están
conmutando de uno a otro (multitarea en lugar de tareas paralelas). Un cambio de
tarea puede ocurrir justo después de la sentencia out++ en el Thread 1. El Thread 2
continúa entonces con datos potcncialmente erróneos en las variables out y empty
porque éstas no completaron sus tests.
Efectivamente, las dos rutinas están compitiendo por cuatro recursos de datos:
queue, in, out, y empty.

Exclusión mutua (mutex)


Puede manejar las secciones críticas impidiendo que otros procesos utilicen el
recurso (exclusión mutua o simplemente mutex). Esta señalización (permitir sólo
una tarea en cada instante) nos asegura que los threads no corrompan datos en
las secciones críticas. Un semáforo de exclusión mutua es un flag que controla el
acceso en serie y actúa como un semáforo de tráfico. Mientras el flag esté

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.

• Rápido (predeterminado)-( PTHREAD_MUTEX_INITIALIZER) Tan solo


comprueba el bloqueo. Si el mismo thread intenta bloquear una exclusión
mutua dos veces, se interbloquea.

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.

Problemas de exclusión mutua de Pthread


Tiene unas pocas limitaciones con Pthreads. Primero, la exclusión mutua de
Pthreads no tiene referencias a la sección crítica de la memoria. Tan sólo es un
flag. Volviendo a la analogía del semáforo de circulación, el recurso por el que se
compite es la intersección -no el semáforo. Éste funcionaría con o sin la
intersección. La función principal de la exclusión mutua es para que se utilice
exclusivamente para los datos por los que se compite. Por citar un caso, si no es
cuidadoso, podría programar dos threads para utilizar un sólo semáforo para datos
sin relación. Esto no es peligroso -no bloqueará el sistema, producirá un
interbloqueo o corromperá datos. Sin embargo, puede bloquear un thread sin
necesidad.
Segundo, podría tener varios threads trabajando en un gran bloque de datos,
como una tabla. Cada una de las celdas no tienen relación, y los threads están
trabajando en secciones diferentes. No quiere bloquear la tabla completa, solo
secciones de la misma (llamada zona de bloqueo). Las exclusiones mutua de
Pthreads le dan la capacidad de bloquear accesos concurrentes, pero no puede
determina dinámicamente donde está trabajando su thread.
Finalmente, también puede querer organizar por prioridades el acceso. No se
produce perjuicio en la lectura de algunos datos, y puede tener varios lectores a la
vez (conocido como bloqueo compartido). Pero ningún otro thread puede escribir.
De manera parecida, puede querer sólo un escritor; éste sólo trabaja si el dato por
el que se compite es actualizado atómicamente. Los Pthreads están limitados al
acceso exclusivo -independientemente de que lo tengan o no.
La versión actual de las exclusiones mutua de Pthreads no satisface todas sus
necesidades, puede construir sus propias extensiones encima de los mismos.

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.

2. Bloquea el recurso IRA. 2. Bloquea los recursos ahorrados.

3. Utilización del recurso IRA, cambio 3. Utilización de los recursos


de los recursos ahorrados. ahorrados, cambio del recurso IRA.
4. Desbloquea del recurso IRA. 4. Desbloquea del recurso IRA.

5. Desbloquea de los recursos 5. Desbloquea de los recursos


ahorrados. ahorrados.
El Thread #2 se interbloquea en el paso 2. Utilizando una política de asignación
de nombres más estandarizada, el interbloqueo es más apreciable:
Thread #1 Thread #2
1. Bloquea Funds_Mutex_1. 1. Bloquea Funds_Mutex_2.

2. Bloquea Funds_Mutex_2. 2. Bloquea Funds__Mutex_1.


3. Utilización de Funds_Mutex_2, 3. Utilización de Funds_Mutex_1,
cambio de Funds_Mutex_1. cambio de Funds_Mutex_2.
4. Desbloquea Funds_Mutex_2. 4. Desbloquea Funds_Mutex_2.

5. Desbloquea Funds_Mutex_1. 5. Desbloquea Funds_lv1utex_1.


El interbloqueo ocurre porque los dos threads quieren los recursos del otro. A
continuación vemos unas pocas reglas que pueden reducir el riesgo potencial de
un interbloqueo:

• Designación de grupos. Identifique los recursos o grupos interdependicntes y


dele a sus exclusiones mutuas un nombre similar (por ejemplo: Funds_Mutex_1
y Funds_Mutex_2).

177/512
• Bloqueo en orden. Bloquea los recursos numéricamente desde el menor hasta
el mayor.

• Desbloqueo en orden. Desbloquea los recursos numéricamente desde el mayor


hasta el menor.
Siguiendo estas políticas de interbloqueo, puede evitar la desagradable tarea de
depuración necesaria para desenredar un interbloqueo.

Control de hijos y eliminación de procesos


zombis
Ha creado una pareja de threads o procesos. Después de haber creado algunos
hijos, tiene algún control sobre ellos. La pregunta es, ¿cómo controla su comporta
miento? Como mencionamos anteriormente, puede comunicarse con ellos
utilizando señales, mensajes, y datos.

Preste más atención a los hijos: prioridad y


planificación
Puede disminuir la prioridad del hijo de forma que otras tareas consigan un
mayor tiempo de CPU. (Como debe saber, sólo una tarea con privilegios de root
puede incrementar la prioridad de una tarea.) Las llamadas del sistema que se
utilizan para cambiar la prioridad son getpriority() y setpriority().
A diferencia de los procesos que ofrecen poco control sobre los hijos, el
threading le permite cambiar su algoritmo de planificación o renegar del mismo.
Linux utiliza un algoritmo de planificación organizado en prioridades round robín
(como comentamos anteriormente en este capítulo). Pthreads le ofrece tres
algoritmos diferentes de planificación:
• Normal. Este es igual a los algoritmos de planificación de Linux,
(predeterminado)
• Round Robin. El planificador ignora la prioridad y cada thread consigue un
fragmento de tiempo hasta que finaliza. Debe utilizarse en entornos en tiempo real.
• FIFO. El planificador encola v ejecuta cada thread hasta su conclusión. De
nuevo, este tipo de planificación debe utilizarse sobre todo para entornos en tiempo
real.

Entierro de los procesos zombis: limpieza tras la


finalización
Quizá se escape a su observación que algunos de los ejemplos de la tabla de
procesos con los que ha trabajado pueden tener procesos zombis flotando
alrededor. Lo sentimos, esto no es un cuento escalofriante de Halloween, pero el
problema puede causar pánico a cualquier administrador de sistemas. Después de
introducirlos en su tabla de procesos, algunas veces no se irán sin reiniciar e]
sistema.

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.

• EINVAL. El thread tchild ya se encuentra en un estado de desconexión. Puede


utilizar pthread_create() de la siguiente manera:
/**********************************************************************/
/*** Ejemplo de separación de un thread de su padre. Esto ***/
/*** permite al padre proseguir sin comprobar la ***/
/*** finalización del hijo. ***/

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.

Ampliación de los clientes y servidores actuales


Puede aplicar los conceptos de este capítulo en los ejemplos de capítulos
previos y ampliarlos con procesos y threads. Puede añadir multitarea al servidor de
eco añadiendo las llamadas a fork() o pthread_create(). Puesto que ahora está
creando tareas nuevas, la llamada del sistema fork() le exige que capture y
despache las señales de finalización de los hijos. Para obtener el listado fuente
completo, vea echo-server.c en el sitio web.
Tras compilar y ejecutar este programa, puede conectarse a este servidor con el
comando telnet. De hecho, puede tener varias conexiones ejecutándose al mismo
tiempo. Es precisamente lo que un usuario espera -una aparente conexión
exclusiva al servidor. La creación de tareas y la delegación de responsabilidades le
ayuda a proporcionar este servicio.

Llamada de programas externos con el exec del


servidor
A nadie le gusta tener que volver a crear algo que ya funciona bien.
Probablemente prefiera utilizar el comando ps a construir esa información por sí
mismo en un programa. Además, la utilización de Perl para realizar algunas
operaciones es mucho más fácil que escribir un analizador sintáctico en C. Así que,
¿cómo utilizará estos comandos ya asentados?
La API de la llamada del sistema exec() amplía la llamada del sistema forkí)
permitiéndole llamar a programas externos e interactuar con ellos. Esto es esencial
mente equivalente a lo que las llamadas de CGI (Common Gateway Interface,

181/512
Interfaz de Gateway Común) son en la Web, como se muestra a continuación:

• Un host en la red ofrece un servidor de CGI que recibe un comando. Este


tiene parámetros de línea de comandos de la siguiente forma:
"http://www.server.com/cgi/ <command>?param1+param2+ ..."

• El cliente envía un mensaje, por ejemplo:


"http://www.server.com/cgi/ls?tmp+proc"

• El servidor crea un proceso hijo y redirige la Stdin, stdout, y stderr al canal


del cliente.

• A continuación el servidor llama al programa utilizando la llamada del sistema


exec()
Observe la potencia de la multitarea con el simple hecho de que la ejecución de
programas externos no se puede realizar efectivamente sin la multitarea. La
llamada del sistema exec() tiene cinco vertientes:
• execI() Lista de parámetros variable, el primero de los cuales es el comando
con la ruta completa. Todos los parámetros siguientes son los argumentos de la
línea de comandos, comenzando con el argumento en la posición cero (arg[0]). La
lista finaliza con un cero o NULL.
/* Por ejemplo: */

if { execl("/bin/ls", "/bin/ls", "-aF", "/etc", NULL) != 0 )

perror("execl failed");
exit(-1);
/* el "if" es redundante -- exec no devuelve un valor si no falla *//

LOS DOS PRIMEROS PARÁMETROS DE EXECL()


La llamada del sistema execl() puede parecer redundante; ¿Por qué el primer y segundo
parámetro son el mismo comando? El primer parámetro es el nombre actual del comando
(a ser ejecutado), y el segundo es el parámetro en la posición cero de la línea de
comandos (igual que arg[0] en el main() de un programa en C). No olvide incluir el
nombre -algunos comandos externos comprueban el nombre para entender qué
actuación deben realizar. Además, no olvide el último NULL o cero; éste indica el final de
la lista de parámetros.
• execp(). Similar a execl(), pero el comando busca la ruta de ejecución.
/* Por ejemplo: encuentra el programa 'ls' en una ruta de ejecución */
if ( execl("ls", "ls", "-aF", "/etc", NULL) ¡= 0 )
perrorf"exeep failed");
exit(-l);

• execle(). Similar a execl(), pero el último parámetro es un array de cadenas

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);

• execvpf(). Como execv(), pero el comando busca la ruta de ejecución.


/* Por ejemplo: */
char *args[]={"ls", "-aF", " / etc", NULL};
if ( execle(args[0], args) != 0 )
perror("execle failed");
exit(-1);
Si tiene éxito, la llamada del sistema exec() no vuelve a su programa. El sistema
operativo sustituye el contexto de la tarea con el programa externo, de modo que ni
siquiera necesita la instrucción if; cualquier instrucción ejecutada después de una
llamada a exec() significa que ésta ha fallado.
Para utilizar esta llamada del sistema en un servidor de socket, sustituya el
bucle principal de espera con lo siguiente:
/*---------------------------------------------------------------*/

/* Fragmento de código para aceptar conexiones de clientes */


/* y llamar a un programa externo ("ls -al /etc"). */
/*------------------------------------------------------------------------------*/
...
while (1)
{ int client, addr_size = sizeof(addr);
client = accept(sd, Saddr, &addr_size);

183/512
printf("Connected: %s:%d\n", inet_ntoa(addr.sin_addr),
ntohs<addr.sin_port));
if ( fork() )
close(client);
else
{

close(sd); /* El cliente no necesita acceder al socket */


d u p 2 (client, 0 ) ; /* Sustituir stdin */
d u p 2 (client, 1 ) ; /* Sustituir stdout */
d u p 2 (client, 2 ) ; /* Sustituir stderr */
execl("/bin/ls", "/bin/ls", "-al", "/etc", 0 ) ;
perror{"Exec failed!"); /* algo fue mal */
}
}
Consulte ls-server.c en el sitio web para ver el listado completo.
DIFERENCIAS ENTRE FORK() Y VFORK()
En otras versiones de UNIX, querrá utilizar la llamada del sistema vfork(). Puesto que los
procesos no comparten sus datos, los hijos deben obtener una imagen del espacio de
datos de los padres. Si tan sólo quiere llamar a exec() después de fork(), esa copia es un
despilfarro de tiempo y espacio. Para resolver este problema, algunas versiones de UNIX
nos presentan la llamada del sistema vfork() para suprimir la copia. Sin embargo, Linux
utiliza un procedimiento de copia-sobre-la-escritura (copy-on-write) (copiar las páginas de
los padres sólo si el hijo o padre las modifica). El intérprete de comandos del hijo (exec-
child) no modifica las páginas de datos, así que el sistema no copia ninguna página de
datos. Por tanto, Linux no necesita la llamada del sistema vfork() porque la ha asignado a
fork().
Las dos diferencias principales entre una creación normal de la pareja padre-hijo
son la redirección de la E / S y la llamada exec*(). Para suministrar al programa
externo toda la E / S que necesita para trabajar correctamente, necesita
redireccionar todos los canales estándar, incluyendo stderr.
Puede utilizar la llamada del sistema exec() tanto en threads como en procesos.
Con gran cantidad de herramientas disponibles, esto llega a ser muy útil. Los
threads tienen una amonestación; puesto que no pueden redirigir la E / S estándar
sin afectar a los threads de cualquier otro equipo homólogo, los threads que llaman
a exec() comparten la misma E / S que los threads homólogos.
UTILIZACIÓN DE UNA LLAMADA EXEC() EN UN THREAD
He aquí un problema interesante: ¿qué ocurre si un thread llama a exec()? El programa
externo no podría reemplazar al Mamador sin afectar a los equipos homólogos. El núcleo
de Linux utiliza la política de que la llamada no compartirá los datos, pero los canales de
archivos existentes permanecerán compartidos. De esta manera, cuando el programa

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().

Resumen: distribución de la carga del proceso


La multitarea le da a sus programas el empujón extra que necesitan para
mejorar la accesibilidad y la flexibilidad. Puede utilizar threads y procesos para
aumentar la velocidad de sus clientes e impulsar sus servidores. Con la gestión de
tareas y las comunicaciones, puede facilitar la programación y mejorar la fiabilidad.
En este capítulo se ha presentado el tópico de la multitarea y los diferentes tipos
de tareas —procesos y threads. También aprendió cómo crearlas con herramientas
de alto nivel tales como Pthreads o fork() o con la herramienta de bajo nivel
_clone().
Después de haber creado la tarea, puede controlar las interacciones de los
threads con la sincronización. Las sincronización adicionalmente le ayuda a
serializar (o coordinar) el acceso a los datos por los que se compite. La
señalización nos garantiza la integridad de los datos.
Toda tarea puede aceptar mensajes a través de los canales u otros dispositivos
de E/S así como señales para manipular eventos asincronos. Los canales le
permiten dirigir información a los hijos, descargando a los padres. También le
permiten redirigir la E/S estándar para una interacción más simple. Las señales
capturan eventos que pueden ayudarle a que sus programas sean más robustos.
En el siguiente capítulo se extienden los conceptos de E/S y se ofrecen
alternativas a la multitarea.

185/512
Capitulo viii

Cómo decidir cuándo esperar


E/S

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.

Bloqueo de la E/S: ¿por qué?


El entorno multitarea le autoriza a realizar gran cantidad de cosas a la vez. De
hecho, cuando es diseñado correctamente, el resultado es 100% escalable a multi-
procesamiento simétrico. Esta facultad tiene algunas limitaciones y reglas. Si no
cumple con esas reglas, el sistema lo padece.
Una regla a considerar es que toda tarea tiene que ceder tiempo a otras tareas.
El sistema sigue esta regla en todo lo que hace. Por ejemplo, cada tarea consigue
una porción de tiempo de la CPU. Si no hay nada que hacer, la tarea cede su
porción. Pero ¿cómo sabe la tarea si hay algo que hacer? ¿No está siempre
haciendo algo?
Bueno, sí y no. La razón primordial para no tener nada que hacer es estar
esperando la finalización de alguna operación de E/S. Tan sólo como baremo,
considere la velocidad del procesador contra la velocidad del disco o de la red.
Incluso con colecciones de discos muy rápidos, puede tener una tasa de
transferencia de 160MB/s y un tiempo de búsqueda/latencia de 5ms. Si una tarea
está ejecutándose en un Pentium III a 500MHz, cada instrucción se lleva un pulso
de reloj. Puede prever una media de 2-4ns por instrucción. En la cantidad de
tiempo que lleva finalizar una búsqueda, su programa podría haber ejecutado
alrededor de 1.250.000 instrucciones de ensamblador (opcodes).
RETARDOS PROVOCADOS POR ENERGYSTAR
La mayoría de los sistemas nuevos de computadoras de hoy en día cumplen los
estándares Green y EnergyStar. Un requisito es que la unidad de disco se apague cuando
no se utilice durante un determinado periodo de tiempo. La unidad necesitará alrededor

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.

¿Cuándo debo bloquear?


Puede dejar que un programa se bloquee (pare el procesamiento y espere)
cuando un recurso de E/S se toma su tiempo para finalizar. Cada vez que su
programa realice una llamada del sistema, el sistema operativo puede obligar a su
tarea a esperar a que finalice la transacción. En la mayoría de los casos, la tarea
espera a la E/S para enviar o recibir información. El bloqueo tiene lugar bajo las
siguientes circunstancias:

• Lectura. Un bloqueo de lectura sucede cuando aún no ha llegado ningún dato.


Internamente, incluso un byte puede provocar que la llamada readO devuelva
un valor después de un periodo de tiempo específico.

• 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.

Alternativas al bloqueo de la E/S


¿Cuáles son algunas de las alternativas al bloqueo que puede utilizar? Puede
querer que su programa realice otras tareas mientras está esperando a la
finalización de una petición al sistema. El programa podría:
• Comprobar la integridad de sus datos.
• Iniciar y rastrear otras peticiones.
• Esperar a varias conexiones de socket.
• Procesar algunos cálculos con uso intensivo de CPU.
Puede ser bastante seductor evitar el bloqueo de la E/S, considerando cuánto
puede llevar a cabo mientras espera una petición. Pero es difícil crear el entorno
para que no se produzcan bloqueos a menos que lo diseñe desde el principio. (La
mejora del soporte sin bloqueo es un desafío y puede implicar un montón de
trabajo.) Elija alguno de estos tres métodos para ayudar a evitar el bloqueo:
sondeo, tiempos de espera, y E/S asincrona.
E/S ASINCRONA CONTRA E/S CONTROLADA POR SEÑALES
Los algoritmos para E/S asincrona que se presentan en este capítulo son esencialmente
de E/S controlada por señales, permitiendo que las señales determinen cuando los
buffersde. E/S están listos para leer o escribir en ellos. La verdadera E/S asincrona, tal y
como se define en POSIX . 1 , nunca se bloquea. De hecho, al iniciar una lectura se
devolverá el control inmediatamente. Los buffers receptores se consideran inseguros
hasta que la lectura finaliza y el programa recibe una señal. Linux (al igual que otros
sistemas operativos) no cumple con lo indicado para la E/S asincrona en sockets de
POSIX.1. Por razonas de consistencia con la mayoría de la industria, este capítulo utiliza
el término "E/S controlada por señales."

Comparación de las diferentes interacciones de


programación de E/S
Para incrementar el rendimiento y la sensibilidad, quizá su herramienta más
importante sea la utilización creativa del subsistema de E/S. Sus programas están
sujetos a la obtención de información de fuentes externas. El control de cuándo y
cómo lo bloquea hace a su programa más sensible al usuario. Linux ofrece
fundamentalmente cuatro estilos diferentes de interacción de E/S. Es útil tener en
cuenta que cada uno de los cuatro estilos diferentes tiene puntos fuertes y débiles.
La Figura 8.1 muestra una tarea leyendo un paquete con los diferentes estilos de
interacción. El proceso comienza sin ningún dato en espera en los buffers del
núcleo.

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 ***/
}
...

Lo importante de este segmento de código es la sección de la computación.


Linux utiliza una planificación organizada en prioridades round-robin. Mientras una
tarea realiza más procesamiento ligado a la CPU (menos ligado a la E/S), su
prioridad efectiva se incrementa. Si omite la sección de la computación, su tarea se
ejecuta en un bucle cerrado, incrementando el servicio de planificación mientras no
se está haciendo en esencia nada. Los bucles cerrados son muy desagradables
para la planificación de las tareas. Su tarea puede con el tiempo imponerse al
tiempo de planificación mientras lleva a cabo poco trabajo.
Así mismo, si no puede pensar en nada que hacer mientras espera a que un
canal se libere, no utilice un bucle de retardo o la llamada sleep(). A menos que
necesite una respuesta en tiempo real, un bucle de retardo o la llamada sleep()
destruyen el objetivo del sondeo. Tampoco utilice un tiempo de espera o deje que
el programa se bloquee en su lugar.

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.

Sondeo de las conexiones


Un algoritmo de sondeo más raro implica la conexión de clientes desde puertos
diferentes, El servidor espera a que un cliente se conecte, y normalmente, el
programa servidor le ofrece sólo un puerto. A pesar de que esto es lo usual, puede
tener tantos puertos abiertos como quiera dentro de un programa. Sin embargo, los
diseñadores piensan que un programa debe tener un objetivo específico y un
puerto proporcionar un servicio específico.

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 ****/
}

/**** Procesar los datos entrantes ****/


/**** -O- ****/
/**** Generar más datos a enviar ****/
}

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".

Lectura bajo demanda


Los programas pueden procesar datos mientras llegan utilizando una lectura
asincrona, o lectura bajo demanda. Mientras los datos llegan, el núcleo envía una
señal al programa. El programa la recoge y la procesa. Al igual que en una fábrica
o en una línea de ensamblaje, los materiales requeridos llegan y avanzan.
La mejor utilización de esta característica es aquella que realiza muchos
esfuerzos en computación de la CPU mientras espera a una fuente de datos
individual. (Por favor, tenga en mente que aunque pueden estar abiertos varios
canales para la E/S, necesita comprobarlo manualmente para ver qué canal envió
la señal.) Considere la utilización de un documento VRML (Lenguaje de Modelado
de Realidad Virtual) para esta tarea, tal v como se muestra en el Listado 8.4.
Listado 8.4 Algoritmo de procesamiento asincrono
/********************************************************************/
/*** Ejemplo de lectura asincrona VRML: procesar los datos ***/
/*** mientras se espera la llegada de más datos. ***/
/********************************************************************/
int ready=0, bytes;
...
void sig_io(int sig)
{
/*--- obtener los mensajes en espera --•*/*/
bytes = recv(server, buffer, sizeof(buffer), 0);
if ( bytes < 0 )
perror("SIGIO");
ready = 1 ; /* avisar al bucle principal: "transacción completada" */
}
...
/*--- Habilitar la E/S asincrona o que no bloquea ---*/*/
if ( fcntl(sd, F_SETFL, 0__ASYNC j O^NONBLOCK) < 0 )

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. ****/
}

...

De nuevo, evite realizar muchas tareas en el gestor de señales. Bajo estas


circunstancias, su gestor podría simplemente advertir al bucle principal que cargue
los datos por sí mismo. Pero, puesto que el administrador de la cola puede utilizar
gestión de memoria, el llenado de la cola de procesamiento de la imagen debería
realizarse fuera del gestor.
EVENTOS DE LECTURA TRIGGER HAPPY
El buffer de recepción avisa a la tarea cuando sobrepasa la marca de nivel bajo (el
número mínimo de bytes necesarios antes de que el núcleo envíe una señal SIGIO). La
marca de nivel bajo está establecida de un modo predeterminado a 1, de modo que su
programa podría obtener señales cada vez que los buffers obtienen 1 byte. En algunos
sistemas, es posible cambiarlo a un valor mayor con setsockopt() (véase el Capitulo 9).
Sin embargo, Linux no soporta la escritura de este parámetro.

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.

Conexiones bajo demanda


Puede crear un servidor de conexiones bajo demanda {en teoría). En esencia se
parece a un lector bajo demanda, pero en lugar de leer un archivo, el programa
llama a accept(). El gestor necesita tres variables globales para comunicarse con el
bucle principal: el aceptador de socket (sd), un a ira y de descriptores, y el número
actual de conexiones. Cualquier variable local que su gestor cree se pierde al
finalizar el mismo.
El gestor podría tener el siguiente aspecto:
/************************************************************************/
/*** Ejemplo de conexión bajo demanda: establecer una conexión en ***/
/*** el gestor de señales. El bucle principal examina cada socket ***/
/* abierto a la búsqueda de mensajes. (Extracto de demand-accept.c **/
/*** en el CD.) ***/
/************************************************************************/
int Connections[MAXCONNECTIONS];
int sd, NumConnections=0;
void sig_io(int sig)
{ int client;
/*--- Aceptar conexión. Si hay demasiadas, generar error y cerrar ---*/*/
if ( (client = accept(sd, 0, 0) > 0 )
if ( NumConnections < MAXCONNECTIONS )
Connections[NumConnections++] = client;
else
{
send(client, "Too many connections!\n", 22, 0);
close(client);
}
else
perror("Accept");
}

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.

Resolución de bloqueos de la E/S no deseados


con poll() y select()
Linux ofrece dos herramientas para ayudarle a trabajar con múltiples canales
abiertos a la vez. Estas herramientas son más eficientes (y pueden ser más fáciles
de implementar) que un sondeo manual. La idea completa que hay detrás de cada
herramienta es que la llamada del sistema se bloquea hasta que cualquier canal
cambie de estado.
Un cambio de estado engloba condiciones tales como que los datos estén
disponibles para operaciones de lectura, que un canal esté libre para escritura, o
que haya ocurrido un error. En el retorno, uno o muchos de los canales pueden
haber cambiado de estado. El valor de retorno de la llamada representa el número
de canales que han cambiado.
Sin embargo, la llamada del sistema select() es un poco más compleja y en ella
se involucran a varias macros para ayudar en la administración de las listas de
descriptores:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select{int maxfd, fd_set *to_read, fü_set "to_write,
fd_set "except, struct timeval *timeout);
FD_CLR(int fd, fdset *set); /* elimina fd de set */
FD_ISSET(int fd, fd_set "set); /* comprueba si fd está en set */
FD_SET(int fd, fd_set *set); /* añade fd a set */
FD_ZERO(fd_set *set); /* inicia set para su utilización */
La rutina principal es la llamada del sistema select(). Tiene varias macros de
ayuda (FD_CLR(), FD_ISSET(), FD_SET(), FD_ZERO()) para administrar las listas
de descriptores. Los parámetros son definidos en la Tabla 8.1.

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.

set El conjunto de descriptores para manipular.


El parámetro maxfd es la ranura del descriptor con número más elevado en
cualquiera de los tres conjuntos mas 1. Cada tarea obtiene un número de huecos
para descriptores de E/S (normalmente 1024). Cada canal de E/S ocupa un hueco
(stdin es el 0, stdout es el 1, y stderr es el 2). Si los conjuntos tienen descriptores
[3,6] en to_read, [4,5,6] en to_write, y [3,41 en except, maxfd se transforma en 6 (el
número más elevado de descriptor) más 1, ó 7.
El parámetro timeout le da la flexibilidad de establecer un límite al tiempo de
espera de la llamada del sistema:

• Si es NULL, esperará indefinidamente.


• Si es un valor positivo, esperará los microsegundos especificados.
• Si es cero, regresa inmediatamente tras chequear todos los descriptores una
vez.
Por ejemplo, si tiene tres canales abiertos a clientes numerados 13,4,6], podría
esperar que enviaran al programa datos como este:
/******************************/
/*** Ejemplo de select(). ***/
/******************************/
int count;
fd_set set
struct timeval timeout;
...
FD_ZERO(&set); /* Limpia set */
FD_SET(3, &set); /* Añade el canal cliente #3 */

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:

• POLLERR. Cualquier condición de error. Devuelto si ocurre cualquier error en el


canal.

• POLLHUP. Cuelgue en el otro extremo. Devuelto si el equipo homólogo se


cuelga.

• 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.

Implementación de los tiempos de espera


Una alternativa simple al sondeo de los canales de E/S es decirle al sistema
operativo que no quiere esperar más de un tiempo especificado. Las llamadas del
sistema de Linux no ofrecen un método directo para realizarlo. Considere la
utilización de estos métodos para implcmentar los tiempos de espera:
• Con el parámetro timeout en las llamadas del sistema selectO o poll().
• Con una señal (SIGALRM) para despertar a la tarea.
SOPORTE DE TIEMPOS DE ESPERA EN LOS SOCKET LINUX
Las opciones de los socket incluyen tiempos de espera para envío (SO_SNDT!MEO) y
recepción (SO_RCVTIMEO). Desafortunadamente, estos parámetros por ahora son de
sólo lectura, en Linux, su valor predeterminado es OFF. Si intenta trasladar algún código
que utilice estos parámetros, deberá utilizar los otros dos métodos.
Las llamadas del sistema selectO y poll() (descritas anteriormente) proporcionan
un parámetro de tiempo de espera en microsegundos. La utilización de estas
llamadas del sistema quizá es el modo más fácil de implementar tiempos de
espera, pero éstos se aplicarán a todos los canales de la llamada.
Esta situación puede ocasionar un problema: suponga que tiene tres canales, y
uno de ellos no es muy sensible. Utilizando nada más que estas llamadas del
sistema, no puede detectar que el canal defectuoso debería haber agotado su
tiempo de espera y haberse cerrado. Puede mantener manualmente tiempos
individuales para cada canal, restableciendo el tiempo con cada respuesta. No
obstante, eso puede ocasionar un montón de comprobaciones y registros
innecesarios.
Puede usar en su lugar una alternativa: una señal de alarma. Una alarma es un
reloj que se ejecuta en el núcleo. Cuando la alarma se dispara, el núcleo envía una
llamada para despertar la tarea con el formato de una señal. Si la tarea está en
espera en una llamada del sistema, la señal interrumpe ia llamada, la tarea accede

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.

Resumen: elección de las estrategias de E/S


El programa de red interacciona con otros programas a través de la red. Dado
que el rendimiento y los tiempos de respuesta varían de una máquina a otra y entre
redes distintas, es importante conocer cuándo y cómo utilizar la E/S que no
bloquea para mejorar la gestión de la E/S en sus clientes v servidores.
Utilizada con cuidado y adecuadamente equilibrada, la E/S que bloquea y la que
no bloquea aumenta el grado de reacción de sus clientes y servidores a los
usuarios.
En este capítulo se han cubierto varios tópicos sobre el bloqueo (qué es, cuándo
es útil, por qué es utilizado) y el modo de no bloqueo (cuáles son los diferentes
tipos, cuándo utilizarlos, cómo utilizarlos). Se ha abordado la herramienta principal
para seleccionar la E/S que no bloquea : la llamada del sistema fcntl().
Las dos herramientas elementales del modo de no bloqueo son el sondeo y la
E/S asincrona. El sondeo intenta reiteradamente utilizar una llamada del sistema de
E/S. La E/S asincrona (o controlada por señales) sitúa la carga sobre el núcleo
para determinar cuando un canal está libre. El sondeo se concentra en la E/S,
mientras que la E/S controlada por señales se concentra en el procesamiento local.
Otra herramienta para administrar el bloqueo es el tiempo de espera. Puede
desplegar tiempos de espera en las llamadas del sistema select() o poll() o bien
puede utilizar una señal de temporizador que despierte el proceso cuando una
llamada dura demasiado.

208/512
Capitulo ix

Cómo romper las barreras del


rendimiento

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

¿Cómo sacar todo fuera de su servidor o cliente? Linux tiene varias


herramientas en la API sockets que realizan este proceso de un modo
relativamente directo. Sin embargo, tiene que contemplar el problema de la
programación desde varios ángulos al mismo tiempo, puesto que cada aspecto
recuerda y afecta a otros.
Su programa de red tiene tres figuras distinguibles: la tarea, la conexión, y los
mensajes. Trabajará con cada uno de ellos en el transcurso del programa. La tarea
implica trabajar con todos los procesos relacionados, o threads. La conexión es el
socket en sí mismo y su conducta, y los mensajes interactúan con el procedimiento
de E/S que recibe y envía mensajes. Para conseguir obtener lo máximo de su
programa de red, tiene que llegar a un equilibrio entre las tareas y el envío de
mensajes. El socket le permite especializar la conexión para que cumpla sus
necesidades.
En este capítulo se le muestran varias ideas y métodos mejores para tratar las
tareas, la conexión y el envío de mensajes. Para poder utilizar plenamente estos

209/512
algoritmos necesita conocer los conceptos presentados en capítulos anteriores.

Creación de servlets antes de la llegada del


cliente
Hasta este momento ha visto cómo crear un servidor, implementar la multitarea,
y controlar el bloqueo de la E/S. Cada una de estos pilares le ayudan a interactuar
con sus clientes y controlar el rendimiento. El Capítulo 7, "División de la carga:
multitarea," le mostró cómo delegar la tarea de recepción y prestar servicio a
conexiones entrantes, creando procesos hijos conforme los necesitara.
Sin embargo, en algunos sistemas la creación de una nueva tarea en cada
momento consume tiempo y recursos. Además, puede no querer dedicar todos los
recursos de la computadora sólo para prestar servicio a los clientes de red. (Por
otro lado, puede querer hacer precisamente eso, pero hay límites lógicos y eficaces
que debe colocar en el acceso al servidor para asegurar las prestaciones.) Puede
tener otros programas que necesiten ser ejecutados frecuentemente, o incluso
conexiones locales o remotas. De cualquier forma, necesita controlar el progreso
de su servidor. Y realizar un control del número de conexiones requiere una
programación extra.

Colocación de un tope en el número de conexiones


cliente
Si se acuerda de los capítulos anteriores, el servidor crea un nuevo proceso hijo
tan pronto como el cliente se conecta. Su programa tiene que cambiar este aspecto
para incorporar el control sobre demasiados procesos (antiguamente denominados
procesos conejo porque se multiplicaban demasiado rápidamente para
controlarlos). El algoritmo estándar que sigue realiza exactamente eso.
/******************************************************************/
/*** Algoritmo estándar de creación de procesos según avanza ***/
/*** ("test-server.c" en el sitio web) ***/
/******************************************************************/
int sd;
struct sockaddr_in addr;
if ( (sd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )
PANIC("socket() failed");
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(MYPQRT);
addr.sm_addr – INADDR_ANY;
if ( bind(sd, &addr, sizeof(addr)) != 0 )

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 */*/
}

else if ( pid > 0 )


close(client);
else
perror("fork() failed");
}
}

Este algoritmo ya lo pudo ver en el Capítulo 7, y presta muy bien servicios a


conexiones a largo plazo. Las conexiones a largo plazo incluyen a las conexiones
remotas, interacciones con bases de datos, y cualquier otra interacción donde el
cliente y el servidor mantengan contacto durante varias iteraciones.
Este algoritmo tiene un defecto importante: ¿qué ocurre si se queda sin PID?
¿qué ocurre si comienza a agotar su espacio de intercambio porque no tiene
suficíente RAM para soportar el número de procesos? Éste es el problema exacto
que presentan los generadores de procesos conejo.
DIFERENCIA ENTRE MEMORIA Y RAM
Puede pensar que el sistema de memoria virtual junto con su espacio de intercambio
puede preservar su sistema. En realidad, esto es bastante engañoso. Cuando quiere
conseguir un buen rendimiento, necesita mantener todos los procesos activos con sus
datos disponibles en la RAM. Si son movidos al espacio de intercambio, el tiempo que el
núcleo necesita para cargarlos de vuelta elimina cualquier percepción de velocidad.
Comprender su sistema, la memoria, los procesadores, y la E/S es el primer obstáculo
para exprimir el rendimiento._
Un método que podría probar es mantener un contador de procesos:

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.

Preduplicación de sus servidores


Su servidor podría tener entre 5 y 20 procesos esperando la llegada de una
conexión. Es lo que hace el servidor de carga distribuida HTTP. (Si tiene HTTPD
instalado y en ejecución, observe su tabla de procesos y podrá ver varias
instancias del mismo en ejecución.) ¿Cómo obtiene el hijo los datos si los procesos
no comparten datos? La respuesta se encuentra en el siguiente fragmento de
código.
/**************************************************/
/*** Fragmento de código del proceso hijo ***/
/**************************************************/
if ( (pid = fork()) == 0 )
{
close(sd);
Child(client); /*---Prestar servicio al nuevo cliente ---*/*/
}
Este extracto de programa muestra al hijo cerrando el descriptor de socket(sd).
El descriptor del socket cliente proporciona la conexión a su cliente. ¿Por qué
cierra sd el hijo?
Cuando crea un proceso hijo, el hijo comparte el archivo abierto del padre. Si el
hijo no necesita ese archivo, debería cerrarlo. Sin embargo, el hijo podría dejar
abierto el archivo y leer o escribir al igual que el padre. Esto le conduce a cómo
crear varios procesos prestando servicio al mismo socket.
/*********************************************************************************/
/* Creación de un conjunto de servidores-hijo para esperar conexiones */
/* ("preforking-servlets.c" en el sitio w e b ) */
/*********************************************************************************/
int ChildCount= 0 ;
void sig_child(int sig)
{
wait( 0 ) ;
ChildCount--;;

213/512
}
main()
{

/*** Adjuntar señal; crea y enlaza socket ***/


for (;;)
{
if ( ChildCount < MAXPROCESSES )
{
if ( (pid = fork()) — 0 ) /* HIJO */*/
for (;;)
{
int client = accept(sd, 0 , 0 ) ;
Child(client); /* Dar servicio a nuevo cliente
*/
}
else if ( pid > 0 ) /*---PADRE---*/*/
ChildCount++;
else /*---ERROR---*/*/
perror("fork() failed");
}
else
sched_yield(); /*--- O, sleep(1)---*/*/
}

Lo que invierte el orden de accept() y fork(): en lugar de crear el proceso


después de una conexión, este algoritmo espera a la conexión después de crear
los procesos hijo.
Por ejemplo, este fragmento de código puede crear 10 procesos. Todos los
procesos hijo acceden al bucle eterno esperando y procesando conexiones.
Aceptando que no hay conexiones esperando, los 10 procesos bloquean la E/5 (se
echan a dormir). Cuando llega una conexión, todos los procesos se despiertan,
pero sólo uno obtiene la conexión. Los nueve restantes vuelven a dormirse. Este
ciclo continua indefinidamente.
¿Qué ocurre si uno de los procesos hijo se aborta o finaliza? Ésta es la razón
por la que el padre nunca realiza la llamada del sistema accept(). El papel principal
del padre es asegurarse que siempre estén disponibles el número correcto de
hijos.
En un principio, el padre crea la cuota de procesos hijo. Cuando un hijo finaliza,
el gestor de señales capta el aviso y reduce la cuota en uno. Cuando el padre

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.

Ajuste a diferentes niveles de carga


La responsabilidad primordial del padre es asegurarse que hay suficientes
procesos hijo para prestar servicio a las conexiones entrantes. Esta
responsabilidad debe estar equilibrada con la carga de recursos de su
computadora. Si no tiene cuidado, los clientes pueden empezar a caerse debido a
los tiempos de espera porque su servidor no tiene suficientes procesos en escucha
prestando servicio a las conexiones.
La utilización de un número dedicado de procesos en escucha (servlets) fuerza
a tener un límite en el número de conexiones activas. Ésta es la restricción
primordial de tener servlets duplicados previamente. No obstante, la idea de limitar
el número de procesos que su programa crea y destruye es buena. Reduce el
barullo en su tabla de procesos y en los recursos del sistema. ¿Cuál podría ser un
buen camino para minimizar la creación y la destrucción mientras se cumplen las
necesidades de conexión de clientes?
Podría hacer el algoritmo del servlet ajustable a las demandas del sistema. Su
servidor tiene que dar respuesta a dos retos diferentes: saber cuándo crear más
servlets y saber cuándo finalizar los que sobren. El segundo reto es fácil: el servlet
se finaliza a sí mismo si está desocupado un rato.
El primer reto, saber cuándo crear más servlets, no es tan directo. Puede
recordar que todo servidor TCP tiene una cola de escucha (esto es lo que hace la
llamada del sistema listen()). El subsistema de red introduce en la cola cada
petición de conexión, y la llamada del sistema accept() confirma y crea un canal
dedicado con el cliente. Si un cliente se aposenta en una cola demasiado larga, tan
sólo puede esperar un par de minutos antes de darse por vencido.
Ser capaz de ver cuantas conexiones tiene la cola en lista de espera y cuál es la
petición en espera más larga puede ayudar a la puesta a punto del servidor. Podría
utilizar esta información para supervisar el grado de reacción a las peticiones del
sistema. Mientras la cola se llena, el servidor puede crear más servlets. Si el
retraso de la conexión crece demasiado, simplemente puede ajustar el retraso de
autofinali-zación del servlet. Desafortunadamente, esto no lo ofrecen las llamadas
de la API. Si quiere adaptabilidad en los servlet, debe utilizar otra estrategia.
Un método que crea nuevos servlets para dar respuesta a un volumen de
peticiones es como la pesca con muestra. En los sitios en los que es legal, los
pescadores lanzan muestras artificíales (cftum) de diferentes tipos de peces. El tipo
de pez que come el pescado es el que utiliza el pescador como muestra. La
respuesta de los peces indica además lo grande que es el banco de peces.
Al igual que en la pesca con peces, el servidor arroja un servlet extra cada cierto
tiempo. Mientras que el mismo número de servlets extra que se crearon finalicen
(puesto que no hay nada que hacer), el servidor continúa con este proceso.
Cuando los servlets dejan de finalizar, su servidor incrementa el número de
servlets extra que crea hasta que el número se estabiliza de nuevo.

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 */
}

void Chummer(void ("servlet)(void))


{
time(&lasttime); /* Inicia la marca horaria */
for {;;)
{
if ( !fork() ) /* primero, establece el compinche */
servlet(); /* llama a la tarea (tiene que tener exit()) */
sleep(delay); /* sueña por un rato */
/* Si no ha finalizado el hijo, doblar la frecuencia */
if ( time(0) - times[l] >= delay-1 )
if ( delay > MINDELAY ) /* no vayas por debajo del minimo */
delay/=2;/* doblar la frecuencia de envío de cebos */
}
}

Este fragmento de código muestra como seguir las huellas de la generación de


servlets y probar un incremento de carga registrando la hora de finalización del
último hijo. Con la finalización se restablece también el temporizador con el fin de
que la conexión actual pueda estabilizarse y el proceso pueda comenzar de nuevo
desde el principio.

Ampliación del control con un select inteligente


La utilización de procesos para administrar el flujo de conexiones es una forma
intuitiva de distribuir las tareas. Además, limitar el número de procesos en
ejecución y duplicar previamente los procesos nos garantiza que las conexiones
obtienen la dedicación que precisan. Sin embargo, hay un problema fundamental
con todas estas estrategias: el asalto al planificador.

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.)

Llegar a un compromiso con un select inteligente


Mientras crea su servidor potente y de alta demanda, puede utilizar varios
modos para distribuir la carga. Dos de ellos son la multitarea y la multiplexión de la
E/S. Sin embargo, si se utilizan exclusivamente, la multitarea puede perder el
control del planificador y erosionar un tiempo precioso para el cambio de contexto,
mientras que la multiplexión de la E/S pierde el ancho de banda inutilizado de la
CPU. Una alternativa a su utilización en modo exclusivo es una combinación de
ambos. La combinación de ambos en un select inteligente podría ayudarle a
disminuir las desventajas de cada uno, mientras le seguiría proporcionando todo su
potencial.
Un select inteligente cursa unos pocos procesos. Cada proceso posee un canal
de conexión y un conjunto de canales de clientes abiertos. Un multiplexor de E/S
administra todos los canales. Aquí tenemos un algoritmo para el proceso:
/*********************************************************************/
/*** Ejemplo de select inteligente: cada hijo intenta aceptar ***/
/*** una conexión. Si tiene éxito, añade la conexión ***/
/*** a la lista del select(). ***/
/*** ("smart-select.c" en el sitio web) ***/
/*********************************************************************/
int sd, maxfd=0;
fd_set set;
FD_ZERO(&set);
/*** Crea el socket y fork() los procesos ***/
/*--- En el hijo ---*/*/
maxfd = sd;
FD_SET(sd, &set);
for (;;)
{ struct timeval timeout={2,0}; /* 2 segundos */
/*--- Espera alguna acción ---*/*/
if ( select(maxfd+1, Sset, 0, 0, &timeout) > 0 )

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);
}

/* Si hay un comando/petición del cliente, procesarlo */*/


else
/* Procesar las peticiones del cliente */
/* Si el cliente se ha cerrado, eliminarlo de la lista */
}
}

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++ )
{

if { fdsflj.revents & POLLIN )


/* Procesa los mensajes entrantes */
else if ( fds[I].revents & POLLHUP )
/*** cierra la conexión ***/
}
}

else if ( result < 0 )


perror("poli() error");
}
}

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.

• SO_BROADCAST. Esta opción le permite a su socket enviar y recibir mensajes


de difusión. La dirección de difusión establece los bits activos de subred (véase
el Capítulo 2, "Elocuencia del lenguaje de red TCP/IP") todos a uno. No todas
las redes soportan la difusión (las redes ethernet y token ring sí lo hacen).
Aquellas redes que la soportan sólo permiten mensajes de datagramas.
(Booleano, deshabilitada de forma predeterminada.)

• SO_DEBUG. Esta opción habilita el registro de información sobre todos los


mensajes enviados o recibidos. TCP es el único protocolo que soporta esta
característica. Para aprender cómo leer esta cola, acuda al RFC TCPv2.
(Booleano, deshabilitada de forma predeterminada.)

• SO_DONTROUTE. En circunstancias excepcionales, puede querer que sus


paquetes no sean encaminados para alcanzar el destino. Un ejemplo de esto es
la configuración de paquetes en un router. Esta opción habilita-deshabilita el
enrutamiento. {Booleano, deshabilitada de forma predeterminada.)

• SO_ERROR. Obtiene y elimina cualquier error de socket pendiente. Si no


recoge este error antes de la siguiente operación de E/S, será errno el que
obtenga el error. (Entero, 0 de forma predeterminada, sólo recepción.)

• SO_KEEPALIVE. Si su socket TCP no recibe noticias del host externo durante


2 horas, envía una serie de mensajes intentando restablecer la conexión o
determinar el problema. Si encuentra el problema, el socket envía un error y se
cierra a sí mismo. (Booleano, habilitada de forma predeterminada.)

• SO_LINGER. El socket no se cierra inmediatamente si aún tiene datos en su


buffer. La llamada del sistema close() le envía un flag a su socket para que
finalice (sin cerrarlo) y regresa inmediatamente. Esta opción le indica al socket
que fuerce a su programa a esperar hasta que la clausura se complete. Utiliza
la estructura linger, la cual tiene dos campos: l_onoff (habilita-desha-bilita la
demora) y l_linger (tiempo de espera máximo en segundos). Si está habilitada
y l_linger es cero, el cierre de un socket aborta la conexión y da de baja todos
los datos almacenados en el buffer. Por otro lado, si l_linger no es cero, el
cierre de un socket espera a que los datos se transmitan o agoten el tiempo,
(struct linger, que se encuentra deshabilitada de forma predeterminada.)

• SO_OOBINLINE. Puede enviar mensajes muy cortos que el receptor no coloca


en la cola de datos. En su lugar, acepta el mensaje por separado (fuera de
banda), de modo que puede utilizarlo para transmitir mensajes urgentes. Este
flag fuerza a los datos fuera de banda a introducirse en la cola de datos donde
su socket pueda leerlos con normalidad.

225/512
• SO_PASSCRED. Habilita-deshabilita el pase de la identificación del usuario
(véase SO_PEERCRED). (Booleano, deshabilitada de forma predeterminada.)

• SO_PEERCRED. Establece las credenciales del equipo homogéneo


(identificador de usuario, identificador de grupo, c identificador de proceso),
(struct ucred, cero 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.)

• SCLRCVLOWAT. Igual que SO_SNDLOWAT, trabaja con las llamadas del


sistema de E/S multiplexada y basada en señales. Tan pronto como el socket
recibe el número de bytes indicado, lo notifica a su programa. En Linux esta
opción es de sólo lectura. (Entero, 1 byte de forma predeterminada.)

• SCLRCVTIMEO. Igual que SO_SNDTIMEO, coloca un límite a la cantidad de


tiempo que se ha de esperar una entrada. Si la llamada de lectura (read(),
readv(), recv(), recvfromO, o recvmsgO) excede el tiempo sin obtener ningún
dato, la lectura devuelve un error, (struct timeval, I byte de forma
predeterminada, sólo recepción.)

• SO_REUSEADDR. Utilizando esta opción, puede crear dos sockets que


comparten la conexión dirección:puerto. Puede compartir un puerto con sockets
diferentes en el mismo proceso, procesos diferentes, o programas diferentes.
Es útil la mayoría del tiempo, cuando su servidor tiene una caída y necesita
reiniciarlo rápidamente. El núcleo reserva un puerto durante varios según dos
después de la finalización del dueño. Si obtiene un error Port Already Used en
la llamada bind(), utilice esta opción para solventarlo. (Booleano, deshabilitada
de forma predeterminada.)

• SO_SNDBUF. Esta opción le permite definir el tamaño del bufferáe datos de


salida de socket. (Entero.)

• SO_SMDLOWAT. Ésta es la marca de bajo nivel para la transmisión de


información. Las llamadas del sistema de E/S multiplexada informan que un
socket está en modo escritura cuando puede escribir esa cantidad de bytes en
el socket. Si está utilizando E/S basada en señales, obtendrá una señal antes
de alcanzar la marca de bajo nivel. En Linux esta opción es de sólo lectura.
(Entero, 1 byte de forma predeterminada.)

• SO_SNDTIMEO. Esta opción le permite establecer un tiempo de espera en


todas sus llamadas de escritura (write(), writev(), send(), sendtoO, y
sendmsgO). Si una de estas llamadas ocupa demasiado tiempo en su
programa, se aborta con un error, (struct timeval, 1 byte de forma
predeterminada, sólo recepción.)

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.)

Opciones de socket específicas de IP


Las opciones de socket específicas de IP se aplican en su mayor parte a los
data-gramas y la administración de detalles de bajo nivel. Para utilizar estas
opciones, debe establecer el parámetro level en getsockopt() y setsockopt() a
SOL_IP.

• IP_ADD_MEMBERSHIP. Se une a una agrupación de multidifusión. (struct


ip_mreq, sin valor predeterminado, sólo escritura.)

• IP_DROP_MEMBERSHIP. Se da de baja de una agrupación de multidifusión.


(struct ip_req, sin valor predeterminado, sólo escritura.)

• IP_HDRINCL Esta opción le permite construir la información de cabecera del


paquete IP raw. El único campo que no rellena es la suma de comprobación
(checksum). Esta opción sólo es para paquetes raw. (Booleano, deshabilitada
de forma predeterminada.)

• IP_MTU_DISCOVER. Le permite abrir el proceso de hallazgo del MTU (unidad


máxima de transmisión). El MTU es un acuerdo entre el emisor y el receptor
sobre el tamaño de los paquetes. Tiene tres valores:
IP_PMTUDISC_DONT(0). Nunca envía tramas DF (no fragmentar).
IP_PMTUDISC_WANT(1). Utiliza indicaciones de la ruta.
IP_PMTUDISC_DO(2). Siempre utiliza tramas DF. (Entero, deshabilitado de
forma predeterminada.)

• IP_MULTICAST_IF. Establece la interfaz de salida de multidifusión. Ésta es la


dirección IPv4 asignada a la interfaz hardware. La mayoría de las máquinas
sólo tienen una interfaz y una dirección, mientras que otras pueden tener más.
Este parámetro le permite indicar la dirección de interfaz a utilizar, (struct
in_addr, INADDR_ANY de forma predeterminada.)

• IP_MULTICAST_LOOP. Habilita el bucle de prueba de multidifusión. Sus


buffers de recepción obtienen una copia de cualquier cosa que transmita.
(Booleano, deshabilitada de forma predeterminada.)

• IP_MULTICAST_TTL. Establece el número máximo de saltos (tiempo de vida)


que se le permite realizar al mensaje de multidifusión. Debe establecer este
valor para cualquier dirección efectiva de multidifusión Internet, porque sólo
permite un enrutamiento de forma predeterminada. (Entero-byte, 1 de forma
predeterminada.)

• IP_OPTIONS. Le permite establecer opciones específicas de IP. Las opciones

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.)

• IP_TOS. Esta opción le permite determinar el tipo de servicio (TOS) que


necesitan los paquetes salientes. Puede seleccionar entre cuatro tipos:
IPTOS_LOWDELAY (minimizar el retardo), IPTOS_THROUGHPUT (maximi-zar
el rendimiento), IPTOS_RELIABILITY (maximizar la fiabilidad), y
IPTOS_LOWCOST (minimizar el coste). (Entero, sin servicios extra de forma
predeterminada.)

• IP_TTL Establece el tiempo de vida (TTL) de cada paquete. Indica el número


máximo de saltos de router antes que el paquete caduque. (Entero-byte, 64 de
forma predeterminado.)

Opciones de socket específicas de IPv6


Las opciones específicas de socket IPvó se aplican a las características
extendidas de IPv6 o IPng. Para utilizar estas opciones, debe establecer el
parámetro level en getsockopt() y setsockoptí) <i SOLJPV6.

• IPV6_ADD_MEMBERSHIP. Al igual que la versión IPv4, esta opción le permite


unirse a un grupo de multidifusión IPvft. (struct ipv6_mreq, sin valor
predeterminado, sólo escritura.)

• IPV6_ADDRFORM. Esta opción !e permite convertir el socket de IPv4 a IPvó.


(Booleano, deshabilitada de forma predeterminada.)

• IPV6_CHECKSUM. Cuando trabaje con sockets raw IPvó, esta opción le


permite indicarle al socket el desplazamiento de byte de la suma de
comprobación del paquete. Si el valor es -1, el núcleo no calcula la suma de
comprobación, y el receptor también se lo salta. (Entero, -1 de forma
predeterminada.)

• IPV6_DROP_MEMBERSHIP. Al igual que la versión 1PV4, esta opción le


permite cancelar la suscripción a un grupo IPvó. (struct ipv6_mreq, sin valor
predeterminado, sólo escritura.)

• IPV6_DSTOPTS. Este valor le permite recuperar todas las opciones de un


paquete ya recibido. Esta información aparece en la llamada del sistema
recvmsgC). (Booleano, deshabilitada de forma predeterminada.)

• lPV6_HOPLIMIT. Si utiliza la llamada del sistema recvmsg() y tiene habilitada


esta opción, puede obtener el número de saltos que restan en un paquete
recibido en el campo de datos auxiliar. (Booleano, deshabilitada de forma
predeterminada.)

• IPV6_HOPOPTS. Si utiliza la llamada del sistema recvmsg() y tiene habilitada

228/512
esta opción, puede obtener las opciones salto-a-salto en el campo de datos
auxiliar. (Booleano, deshabilitada de forma predeterminada.)

• IPV6_MULTICAST_HOPS. Similar a la versión IPv4 (TTL), esta opción permite


definir el número máximo de saltos de router antes de caducar. (Entero-byte, 1
de forma predeterminada.)

• IPV6_MULTICAST_IF. Similar a la versión IPv4, esta opción le permite definir


qué interfaz (por dirección IP) utilizar para los mensajes de multidifusión. (struct
in6_addr, cero de forma predeterminada.)

• IPV6_MULTICAST_LOOP. Al igual que la versión IPv4, esta opción trae de


vuelta todos los mensajes de multidifusión que envíe. (Booleano, deshabilitada
de forma predeterminada.)

• IPV6_NEXTHOP. Esta opción le permite especificar el siguiente salto para un


datagrama cuando se usa sendmsgí). Es necesario tener acceso de root para
realizar esta operación. (Booleano, deshabilitada de forma predeterminada.)

• IPV6_PKTINFO. Normalmente no puede obtener mucha información sobre el


paquete recibido. Esta opción le permite obtener el índice del interfaz receptor y
la dirección de destino IPv6. (Booleano, deshabilitada de forma redeterminada.)

• IPV6_PKTOPTIONS. Similar a la opción de IPv4 IP_OPTIONS, puede


especificar directamente las opciones utilizando un array de bytes. En la
mayoría de los casos envía opciones IPvb a través del campo auxiliar de la
llamada del sistema sendmsg(). (Array de bytes, sin valor predeterminado.)

• IPV6_UNICAST_HOPS. Similar a la opción de IPv4 IP_TTL, esta opción le


permite especificar el número máximo de saltos de router antes de que el
paquete caduque. (Entero-byte, 64 de forma predeterminada.)

Opciones de socket específicas de TCP


Las opciones específicas de socket TCP se refieren a las habilidades de gestión
de flujo de la capa TCP. Para utilizar estas opciones, debe establecer el parámetro
level en getsockopt() y setsockoptí) a SOL_TCP.

• TCP_KEEPALIVE. La opción SO_KEEPALlVE espera 2 horas antes de


comprobar si hay una conexión. Esta opción le permite cambiar ese retardo.
Las unidades de los valores son segundos. En Linux la llamada sysctl()
sustituye a esta opción. (Entero, 7200 de forma predeterminada.)

• TCP_MAXRT. Esta opción le permite especificar el tiempo de retransmisión en


segundos. Un 0 selecciona el valor predeterminado del núcleo, y un -1 obliga al
subsistema de red a retransmitir para siempre. (Entero, 0 de forma
predeterminada.)

• TCP_MAXSEG. El flujo de TCP se divide en paquetes con porciones de datos.

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.)

• TCP_NODELAY. TCP utiliza el algoritmo de Nagle, el cual prohibe enviar


cualquier mensaje que sea más pequeño que el MSS hasta que el receptor
envíe acuse de recibo (ACK) de todos los mensajes enviados. Si habilita esta
opción, desactiva el algoritmo de Nagle, haciendo posible enviar varios
mensajes cortos antes de obtener los acuses de recibo. (Booleano,
deshabilitada de forma predeterminada.)

• TCP_STDURG. Esta opción especifica donde encontrar dentro del flujo de


datos el byte de datos OOB. De forma predeterminada es el byte que sigue al
flag OOB. Puesto que todas las implementaciones reconocen este valor
predeterminado, no debe necesitar el uso de esta opción nunca. Linux sustituye
esta opción con la llamada del sistema sysctl(). (Entero, 1 de forma
predeterminada.)

Recuperación del descriptor de socket


A medida que aprenda a realizar programación en red puede escribir muchos
servidores. Puede toparse con problemas cuando la llamada bíndO falle a causa
de un error provocado por una dirección ya utilizada. Este es el error más común
que obtienen los programad tires (incluso los más experimentados) y una de las
preguntas más comunes realizadas en Usenet. El problema se origina en el modo
en que el núcleo asigna los puertos.
La mayoría de los núcleos esperan unos pocos segundos antes de reasignar un
puerto (algunas especificaciones reclaman llegar a un minuto). Esta política es
importante para la seguridad. El retardo nos asegura que los paquetes flotantes
están muertos antes de permitir una nueva conexión.
Puede abordar este problema si utiliza la opción SO_REUSEADDR. De hecho,
alguien dijo que debería habilitar esta opción en todos sus servidores. Tal y como
afirmamos en la sección anterior, esta opción le permite conectarse de nuevo
rápidamente incluso aunque el núcleo pueda tenerlo aún asignado. Para habilitar
esta opción, puede utilizar el siguiente extracto:
/************************************************************************************/
/*** Ejemplo de reutilización de un puerto (para servidores que mueren ***/
/*** y necesitan reiniciar). ***/
/************************************************************************************/
int value = 1 ; /* TRUE */
int sd = sock e t (PF_INET, SOCK_ADDR, 0);
if ( setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)) != 0 )

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.

Envío antes de la recepción: mensajes


entrelazados
Un aspecto del grado de reacción de un servidor es su rápida recuperación. Otro
aspecto es cómo de rápido el servidor obtiene la petición del cliente. También pue 1
de utilizar TCP_NODELAY para ayudar a las prestaciones de su servidor enviando
la consulta del cliente tan rápidamente como sea posible.
Tal y como mencionamos anteriormente, TCP utiliza el algoritmo de Nagle para
limitar el número de mensajes pequeños en una WAN. Este algoritmo prohibe
enviar cualquier mensaje que sea más pequeño que el MSS hasta que el receptor
envía acuse de recibo (ACK) de todos los mensajes enviados. Por supuesto, si el
buffer áe envío tiene más bytes de datos de capacidad que la indicada por MSS,
los envía tan rápidamente como sea posible. Esto significa que si tiene varias
peticiones cortas, puede esperar mucho tiempo a sus respuestas. Es así porque no
son enviadas tan pronto como las escribe con la llamada write(), sino que se
introducen en un proceso serie de interacciones envío-acuse de recibo-envío.
La única pérdida posible cuando se utiliza esta estrategia es la pérdida de
rendimiento en la red, pero puede controlarla. Recuerde que si el número de bytes
en la cabecera comienza a tener más peso que los propios datos, el rendimiento lo
padece. Si puede limitar los mensajes de modo que no sean demasiado pequeños
(intente construirlos manualmente primero en un buffer), entonces podrá enviarlos
en una ráfaga. Como resultado se mantiene el rendimiento de la transmisión
instantánea mientras que se minimiza la pérdida de rendimiento.

Apunte sobre los problemas de E/S de


archivos
Mientras observa las opciones para construir los mensajes, le puede seducir el
uso de las llamadas del sistema estándar de E/S de archivos o las llamadas de
biblioteca de alto nivel FILE*. Hasta ahora, este texto le asegura que este
intercambio es genial.

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.

Utilización de E/S sobre la base de la


demanda para recuperar tiempo de CPU
Sus servidores, clientes, y equipos homólogos, comparten todos dos
características comunes: sus algoritmos y el subsistema de E/S. Puede controlar
sus programas con un buen diseño y perfilado. El subsistema de E/S no es tan
flexible. (Uno de los aspectos más atractivos de Linux es que puede cambiarlo.)
Las llamadas del sistema de E/S en la API sockets le ofrecen mucho control sobre
el modo de enviar y recibir sus datos. Combinado con el control de fcntl(), ofrece
algunas herramientas muy poderosas.
En la mayoría de los casos, el núcleo simplemente almacena en los buffers los
mensajes que envía. Significa que puede concentrar su atención en obtener la
información y procesarla. En el Capítulo 8, "Cómo decidir cuándo esperar E/S" se
explica cómo utilizar la E/S asincrona (o E/S basada en demanda) para permitir al
núcleo realizar algunas tareas en su lugar.
La E/S asincrona (no la de a tiempo real) trabaja con su programa colocando la
carga de la notificación en el subsistema de E/S. Puede hacer varías lecturas y
escrituras durante la vida de su programa.

Aumento de la velocidad de send()


Cuando envía un mensaje, el núcleo copia el mensaje en su buffer y comienza
el proceso de construcción de paquetes. Una vez terminado, el núcleo devuelve el
control a su programa con un estado. La única razón para el retardo es la
generación de los resultados del send(). Si tiene algún problema con su mensaje, el
núcleo puede responder un mensaje de error.
Sin embargo, la mayoría de los errores tras la conexión provienen de la
transmisión, no del comando send(). A menos que tenga algo como una referencia

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.

Envío de mensajes de prioridad alta


Durante el intercambio de mensajes, su programa puede necesitar despertar el

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 ***/
}

En este ejemplo, el servidor responde a los mensajes que el cliente envía


(también puede invertirlo). La mayoría de los elementos listados se configuran y
responden al mensaje. El cliente es un poco diferente:
/********************************************************/
/*** Error de calidad de señal entre programas: ***/
/*** el cliente envía pulsos. ***/
/*** ("heartbeat-client.c" en el sitio web) ***/
/*********************************************************/
int serverfd, got_reply=1;
void sig_handler(int signum)
{
if ( signum == SIGURG )
{ char c;

235/512
recv(serverfd, &c, sizeof(c));
got_reply = ( c -= 'Y' ); /* obtuvo respuesta */
}

else if ( signum == SIGALRM )


if ( got_reply )
{
send(serverfd, "?", 1, MSG_00B); /* ¿Vivo? */
alarm(DELAY); /* espera un rato */
gotreply = 0;
}

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 ***/
}

Estos ejemplos muestran una evaluación de la calidad de la señal que dirige el


cliente. Puede extenderlos a un error de calidad de señal bidireccional simplemente
añadiendo una comprobación en el servidor. Si el servidor no obtiene respuesta del
cliente dentro de un periodo de tiempo, sabe que tiene un error en el extremo del
cliente.
Los mensajes urgentes le dan más control sobre la conexión, siempre y cuando
ambos extremos cuenten con ese nivel de interacción. Esto es parte de la base de
protocolos que se abordan en el Capítulo 6, "Generalidades sobre el servidor".

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

Diseño de Sockets Linux


robustos
En este capítulo
Utilización do herramientas de conversión
Controle los valores de retorno
Captura de señales
Administración de recursos
Servidores críticos
Cuestiones sobre la concurrencia el ¡ente/servidor
Ataques de denegación de servicio
Resumen: servidores sólidos como una roca

De modo que quiere escribir servidores y clientes en el ámbito comercial. Es una


buena aspiración -incluso si pudiera donar el trabajo a la iniciativa Open Source. A
nadie le gusta que le critiquen su código. ¿Así que cómo conseguir crear un código
estupendo? Buena pregunta.
Podría reflexionar sobre lo que está intentando lograr. Si está escribiendo código
para un tipo específico de usuario, ¿tiene alguno en mente? ¿Puede trabajar con él
y conseguir ideas acerca del potencial? ¿Qué grado de fiabilidad deben tener los
servidores y los clientes?
La escritura de programas de red robustos es similar a la programación
ordinaria, pero primero tiene que tener una visión general del conjunto; su
programa siempre se ejecuta al mismo tiempo que cualquier programa conectado.
Esto amplía aún más el ámbito a abarcar. Tiene que considerar más cosas aparte
de hacer el código fuente legible.
En este capítulo se presenta varios consejos valiosos sobre cómo tratar con
estos asuntos. No puede abarcar todo; hay disponibles libros completos dedicados
sólo a la escritura de un código sólido. Pero puede ayudarle a evitar los obstáculos

238/512
más típicos de la programación de red.

Utilización de herramientas de conversión


El primer paso para la creación de programas de socket robustos y buenos es
utilizar las herramientas de conversión de la API socket. Tiene abundantes
herramientas que convierten las direcciones, nombres, y números binarios en un
sentido y otro. Es importante si quiere asegurar la posibilidad de trasladar su
código, así como una larga duración y capacidad de comprobación.
Tal y como se describió en el Capítulo 2, "Elocuencia del lenguaje de red
TCP/IP", la red utiliza el formato de almacenamiento binario del big endian. Podría
tener un alpha o un 68040, los cuales también son big endian. Pero probablemente
querrá que todo el mundo de la comunidad Linux pueda utilizar su trabajo creador.
Estas herramientas automáticamente crean el binario de las estructuras correcto.
Si se encuentra en una computadora big endian, no pierde nada -las bibliotecas
asignan automáticamente todas las llamadas de conversión para que no hagan
nada, de modo que su programa no malgasta sus recursos en una llamada inútil.
Miles de programadores han probado todas las bibliotecas de su máquina Linux.
Algunas de estas pruebas se pueden realizar para un fin específico (ad hoc) con
consentimiento. Sin embargo, la organización GNU es muy severa sobre la
fiabilidad de sus bibliotecas empaquetadas. La minuciosidad de estos
procedimientos de prueba han hecho robustas las bibliotecas de Linux. Esta base
le da a sus programas fortaleza adicional.
A algunos programadores les gusta crear de nuevo la biblioteca con sus propias
peculiaridades en las interfaces o en el comportamiento. Si sigue esta
aproximación sin intentar utilizar las herramientas estandarizadas, puede gastar
más tiempo inventando que desarrollando. Y además, el resultado es más grande,
más complejo, y más difícil de probar. Si, no obstante, encuentra que las
bibliotecas carecen de una función necesaria, intente seguir el estilo y filosofía de
las llamadas de biblioteca de UNIX. A continuación tiene algunos ejemplos:

• Devuelva 0 si la llamada finaliza sin errores.


• Devuelva un valor negativo si la llamada no tiene éxito y envía el error a errno.
• Utilice los números estándar de error.
• Pase por referencia de las estructuras.
• Intente definir llamadas de bajo nivel y construya llamadas de alto nivel sobre
ellas.

• Defina todos los parámetros punteros de sólo lectura como const.


• De preferencia a las definiciones de estructuras sobre tipos definidos.
• Utilice nombres de funciones y variables en minúscula en vez de en mayúscula.

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.

Controle los valores de retorno


A medida que trabaja con las llamadas de la API socket, necesita considerar su
modo de actuar y sus resultados. La programación de red es diferente a la mayoría
del resto de tipos de programación. Los errores pueden suceder en su programa en
cualquier momento, y algunas veces el error aparece sin tener en cuenta la
ejecución actual.
Lo primero que querrá comprobar son los valores de retorno. Algunas llamadas
son muy importantes, y algunas son cruciales. Generalmente, las llamadas de
conversión no devuelven un error a menos que se produzca un error crítico. A
continuación puede observar algunas llamadas críticas:

• bind(). Cuando necesita un puerto específico, debe reservarlo. Si no puede,


necesita averiguarlo tan pronto como sea posible en el programa. Los errores
que probablemente se encontrará son conflictos de puerto (puertos ya
utilizados) o que algo vaya mal con el socket

• connect(). No puede continuar si no tiene una conexión establecida. Puede ver


errores tales y como host not found o host unreachable.

• accept(). Su programa no puede comenzar una comunicación con e! cliente a


menos que el resultado de esta llamada sea mayor que cero. (Sí, es posible
tener cero para un descriptor de socket válido, pero ese es un esquema a
típico.) El error que frecuentemente puede observar es EINTR (llamada
interrumpida por una señal). Esto es correcto. Programe la llamada sigaction()
para incluir SA_RESTART en los flags o ignore el error y reinicie la llamada
manualmente.

• 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.

• gethostbyname(). Si obtiene cualquier error de esta llamada, el valor devuelto

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.

• fork()- El valor devuelto indica si se encuentra en el proceso hijo o en el padre.


Además, podría querer distanciar la ruta de ejecución del hijo de la del padre.
También puede comprobar si hay errores. Si el valor devuelto es negativo, no
ha creado el hijo, y puede tener más problemas sistemáticos.

• pthread_create(). Al igual que la llamada fork(), podría querer verificar que la


llamada tuvo éxito en la creación del nuevo hijo.

• setsockopt()/getsockopt()- Los sockets tienen muchas opciones con las que


puede trabajar para afinar su modo de actuar. Generalmente, querrá saber si la
llamada tuvo éxito para poder proseguir con esa suposición.
Todas estas llamadas comparten un destino común si fallan -su programa puede
terminar o comportarse de un modo impredecible. Ésta es una buena guía no
oficial: si la llamada daña seriamente la fiabilidad de su programa, compruebe
siempre el valor devuelto. Si hay algún problema evidente, envíe un informe a la
consola. Esto alerta al usuario antes de que se produzcan los problemas.
Las siguientes son llamadas menos importantes, algunas que se puede quitar
de en medio ignorando el valor devuelto:

• socket(). El único momento en que esta llamada produce un fallo es si solicita


un socket que no puede tener (por permisos o falta de soporte del núcleo), si
tiene un parámetro incorrecto, o si tiene una tabla de descriptores llena. En
todos estos casos, la llamada bind(), connect(), o la llamada de E/S actúa
como respaldo para atrapar el fallo con un error "no es un socket".

• listen()-Es improbable que obtenga un error de esta llamada si previamente


tuvo éxito la llamada bind()- Tiene un limite a la profundidad de la cola de
escucha. Si utiliza 15-20 entradas, su programa puede trabajar bien incluso con
un índice amplio de conexiones.

• close() o shutdown(). Si el descriptor de archivos está mal, el archivo se ha


perdido. En cualquier caso (éxito o fallo), el archivo está perdido, y su programa
puede continuar avanzando. (Puede querer comprobar la localización en su
programa de esta llamada si de repente comienza a quedarse sin descriptores
de archivos.)
Las llamadas de la API que no son tan críticas frecuentemente tienen otras
llamadas importantes que pueden adquirir el periodo de baja actividad que sigue
justo detrás de ellas. Además, si estas fallan, no provocarán fallos catastróficos.
Esto no da licencia para ignorar los valores de retorno de estas llamadas; en
cualquier circunstancia, la comprobación de estos valores acredita el éxito y de
este modo incrementa la fiabilidad de su programa.
Puede utilizar otros medios para capturar los errores que se producen mientras
no esté en una llamada del sistema o de biblioteca. Estos errores ocurren a causa

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>

int waitpid(int pid, int 'status, int options);

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 );
}

De nuevo, este ejemplo es realmente el único caso en que debería utilizar un


bucle en un gestor de señales. Perder mucho tiempo en un gestor de señales
puede provocar la pérdida de señales pendientes. La razón por la que este ejemplo
funciona con un bucle es debido al modo en que trabaja waitpid(). No necesita una
señal para capturar procesos hijos finalizados.
Suponga que introduce esta rutina porque un hijo ha finalizado. Mientras su
programa procesa la señal, otro proceso finaliza. Como está en el gestor de
señales para SIGCHLD, pierde esa señal. Sin embargo, la siguiente iteración de
waitpid() recoge el contexto del hijo. Lina llamada limpia todas las finalizaciones
pendientes (no sólo una).

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

Asignación precisa Asignación generosa


Obtiene exactamente lo que Obtiene un gran trozo que puede ser dividido
necesita. más tarde.
No malgasta la memoria asignada. Casi siempre gasta una porción de la
memoria asignada.
Requiere varias llamadas de Requiere una sola asignación y liberación.
asignación y liberación.
Más propensa a fragmentar la Menos propensa a fragmentar la memoria de
memoria de programa. programa.

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.

Memoria de datos estáticos


De entre los diferentes tipos de recursos, con el que menos problemas
encontrará es con la memoria de datos estáticos. Esta memoria incluye tanto
variables de datos iniciados y no iniciados como variables de pila. El siguiente
ejemplo muestra los diferentes tipos de memoria de datos:
int Counter; /* Datos no iniciados */
char *words[] = {"the", "that","a",0} ; /* Datos iniciados */
void fn(int argl, char *arg2) /* parámetros (pila) */
{ i n t i , i ndex; / * variables "auto" (pila) */
El mejor modo de evitar problemas con este recurso es asegurarse que el
programa inicia todo antes de utilizarlo. Puede utilizar la opción -Wall del
compilador de C para mostrar todos los avisos.

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 y eventos de comunicación


Durante una adquisición, la conexión de su cliente repentinamente se pierde.
¿Qué puede causar esto? La conexión TCP se supone que es una conexión fiable
entre el cliente y el servidor. ¿Qué podría haber ido mal? Saturación.
Los eventos de conexión y las interrupciones pueden provocar pérdida de datos,
dinero, y posiblemente la vida. Siempre que conecte dos computadoras juntas
utilizando cualquier forma de canal, se arriesga a la pérdida de esa conexión.
Cuando observe qué podría haber pasado, tiene que considerar que tipos de
medios de transmisión de información existen y cómo interactúa TCP con cada uno
de ellos.

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.

Pérdida de la conexión servidor/cliente


Por otro lado, si tiene que reiniciar una sesión, debe tener en cuenta la
posibilidad de las duplicaciones. Puede tener que reiniciar una sesión si el cliente o
el servidor pierden la conexión. Un servidor pierde la conexión cuando se bloquea.
En el momento del bloqueo, pierde todas las transacciones. Cuando el cliente
pierde la conexión, la información que envío en su momento puede perderse. La
pérdida de datos del servidor es más crítica que la del cliente.

Cuestiones sobre la recuperación de la sesión


El cliente debe reconectarse al servidor si cualquiera de los dos pierde la
conexión. Se enfrenta con varias cuestiones para restablecer la conexión. TCP
generalmente soluciona estas cuestiones en su lugar automáticamente. Sin
embargo, una sesión está condicionada a la conexión— el usuario y el servidor
nunca parecen que vayan a saltarse una evaluación de la calidad de la señal en el
caso de que se pierda la conexión.
El primer asunto con el que tiene que tratar es la pérdida de transacciones.
Durante una transacción, el servidor podría perder la conexión o bien por un fallo
del programa o bien por un fallo del sistema. Si el cliente no lleva el rastro de cada
mensaje pero da por hecho que el servidor iiene la transacción, la red podría
perder el mensaje. Incluso cuando el servidor se vuelve a levantar, el mensaje
enviado nunca llega.
El segundo asunto es lo opuesto a la perdida de transacciones—duplicación de
transacciones. Ésta es diferente de la duplicación de paquetes. Durante
comunicaciones críticas, tanto el servidor como el cliente siguen el rastro de los
mensajes que envían. En determinados momentos, ambos pueden hacer una copia
del mismo mensaje hasta que el receptor acusa la recepción del mensaje.
Por ejemplo, el cliente envía una petición para transferir $100 desde la cuenta
de ahorros a la cuenta de cheques. Hasta que el cliente obtenga la confirmación
del servidor, aguanta el mensaje en una cola de transacciones. Si el cliente pierde
la conexión, el sistema guarda la cola de transacciones. Tras su regreso, el cliente
vuelve a intentar cada transacción de la cola de transacciones. Si el servidor
ejecuta de nuevo la transacción, el administrador de cuentas transfiere un total de
$200 en lugar de $100.

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.

Técnicas de recuperación de la sesión


Parte del sistema de seguridad incluye la recuperación de sesiones. Puesto que
puede tener datos críticos, debe considerar modos de asegurar sus datos. La
sección anterior le introdujo un par de ideas para solucionar este problema.
Parte del proceso de autorecuperación es minimizar la interacción con el
usuario. También necesita considerar los temas discutidos previamente. En la
sección anterior se ofrecen algunas indicaciones de métodos para ayudarle a
recuperar una sesión.
El primer problema es sencillamente la conexión. Aunque su servidor se percate
de la perdida de la conexión, no puede iniciar la conexión si el protocolo base es
TCP. En su lugar, el cliente debe conectarse al servidor. Si quiere usar el protocolo
TCP, su cliente debe ser capaz de detectar una perdida de conexión y
restablecerla,
ESTABLECIMIENTO DE COMUNICACIÓN DE VUELTA
Conseguir que el cliente se de cuenta que la conexión se ha dada de baja puede ser más
difícil porque los errores de red tardan un tiempo en originarse. Podría establecer la
comunicación de vuelta —conectarse en ambas direcciones. Normalmente, el cliente se
conecta al servidor. En el establecimiento de la comunicación de vuelta, el servidor se
conecta de regreso al cliente. Puede utilizar el canal de regreso para enviar de vuelta
mensajes procesados (tal y como volverse a conectar). Puede utilizar el protocolo fiable
UDP en lugar de TCP para este canal de vuelta.
El proceso de conexión puede incluir forzar al cliente recuperado a que se
vuelva a autenticar o a certificarse de nuevo automáticamente o volver a
conectarse. Éste es un paso necesario para recuperar una sesión segura. Si su
usuario está aún utilizando la aplicación cuando restablece la sesión, podría volver
a llamar a la autenticación anterior y realizar el proceso de conexión en su lugar.
Sin embargo, la certificación necesita seguir el mismo procedimiento anterior.
Afortunadamente, el usuario no necesita involucrarse ni saber nada del asunto.

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.

Ataques de denegación de servicio

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.

Resumen: servidores sólidos como una roca


Puede proporcionar un cliente o servidor estable y seguro del cual puedan
depender los usuarios. Puede reconocer y evitar problemas tales y como los
ataques de red, el interbloqueo, y la inanición. De igual modo, con la captura de
señales, fortalece su programa para resistir algunos de los errores de
programación más comunes.

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

Examen objetivo de los


sockets

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

Cómo ahorrar tiempo con


objetos

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

La batería de un reloj suministra la potencia necesaria para que el reloj funcione.


Cuando se agota, simplemente compra una y reemplaza la antigua por la nueva.
Imagínese lo fácil que sería si pudiera hacer lo mismo con los programas, quitar el
antiguo e introducir el nuevo.
La tecnología orientada a objetos intenta alcanzar este nivel de
interoperabilidad. De hecho, se centra en la responsabilidad de cada componente y
proporcionar una interfaz sólida e inmodificable a la que se pueden conectar otras
componentes. Para entender hacia donde se dirige esta tecnología, resulta
necesario saber donde se origina.
Este capítulo cubre los conceptos que subyacen de la tecnología orientada a
objetos. No presenta toda la teoría de forma detallada, pero introduce los
conceptos principales. Comienza con la evolución de la ingeniería del software y
concluye con las distintas formas existentes que permiten aplicar estos conceptos a
lenguajes no orientados 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.

La evolución de la ingeniería del software


El desarrollo del software y la programación no tienen detrás una larga historia
como ocurre en la física, matemáticas u otras ciencias. No obstante, una parte de
la ingeniería del software (el ciclo de vida del desarrollo del software, informática)
confía fuertemente en el pensamiento y proceso matemático, particularmente el
álgebra de Boole, la teoría de grupos y la estadística. Incluso, los campos de la
informática y la ingeniería del software han realizado avances significativos en la
tecnología. Estos avances han definido principalmente la utilización de las
computadoras hoy en día.
Una vez que comprenda la forma de ajusfar todas estas piezas, podrá
seleccionar fácilmente las herramientas adecuadas para el trabajo apropiado. Esta
sección describe cada tipo de teoría de modelado y programación que ha
desarrollado la industria en las últimas décadas. Estos avances (o etapas) en la
tecnología representan, realmente, la expansión de un conjunto de herramientas y
metodologías. Las etapas más significativas son Programación funcional, modular,
abstracta y orientada a objetos. Esta sección presentará cada una de las etapas.

Programación funcional paso a paso


El primer método de desarrollo deriva del concepto de los diagramas de flujo. La
idea principal era que cada programa incorporaba una serie de funciones,
decisiones, datos y E/S. Los diagrama de flujo muestran, paso a paso, las etapas
de evaluación v transformación de la entrada significativa en la correspondiente
salida.
El diseño funcional integra los SSR (Strvctured System Requirements,
Requerimientos estructurados del sistema), el SSA (Strvctured System Analysis,
Análisis estructurado del sistema) y el SSD (Structured System Design, Diseño
estructurado del sistema). Cada fase define la información necesaria para resolver
un problema de programación determinado. Cada una de estas fases se integran
en círculos concéntricos donde el centro de esta disposición se corresponde con la
implementación actual.
SSR analiza las necesidades del sistema. Normalmente, se denomina Nivel 0
(cero) y define los límites existentes entre el ámbito de entrada del programa y el
correspondiente de salida. En este nivel, el diseñador, analiza aquellos sistemas o
personas que interactúan con el diseño propuesto. SSR define las funciones que
su diseño necesita ofrecer al usuario.
La siguiente etapa, SSA, ayuda a plantear cuestiones adecuadas que le
permiten definir algunos de los datos \ funciones de más alto nivel que pasan por el

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.

Cómo ocultar detalles de implementación con la


programación modular
La siguiente etapa destaca aquello que se conoce como "buena programación".
Parle del problema cotí el diseño funcional es la tendencia a violar las reglas del
ámbito. El ámbito de las variables, funciones, procedimientos y módulos definidos
se refiere a "quién tiene derecho a mirar qué". Cuando no existen las reglas de
ámbito (o no so considera el ámbito), la tentación del programador se centra en
utilizar todos los recursos disponibles para desarrollar el trabajo rápidamente. En
un entorno exento de reglas de ámbito (que, a menudo, está presente en un diseño
funcional), cada función tiene acceso al resto de Las funciones y variables. La
creación de variables globales es un ejemplo claro de una violación del ámbito.
Muchos programadores evitan el uso de variables globales debido a los efectos
colaterales que conllevan. Si tiene varias secciones del programa que dependen de
una variable determinada, y una revisión de] código modifica esta variable,

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.

Los detalles no son necesarios: programación


abstracta
Algunos detalles están lan lejos de las necesidades de la implementación, que
los programadores han comenzado a examinar los conceptos relativos a la
programación abstracta. Con la programación modular, los datos pueden ser
simplemente bloques de datos con un pequeño significado con respecto al módulo.
Un ejemplo clásico es una cola o F1FO (primero en entrar, primero en salir); el
algoritmo no necesita conocer qué está entrando, sino que simplemente debe salir.
Algunas de las primera* abstracciones incluyen colas, pilas, árboles,
diccionarios, colecciones, conjuntos, arrays, etc. Cada una de estas abstracciones
presenta un conjunto particular de métodos que puede utilizar cuando quiern. De
hecho, la versión 5 de UNIX, antes de dividirse, tenía alrededor de 10

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.

Cómo conseguir un pensamiento más natural


mediante la programación orientada a objetos
La tendencia de la programación actual es la programación orientada a objetos.
No obstante, una mejor perspectiva podría constituir un modelado responsable. La
programación orientada a objetos se centra en las elementos del sistema que el
programador está intentando programar. Extiende el concepto de programación
modular en un doble sentido: "¿qué sabe?" y "¿qué hace?" Es por ello, que el
término modelado responsable está más cercano al intento.
Los objetos suponen un paso más en el intento de aproximarse al pensamiento
natural. Cada elemento de la naturaleza tiene características (atributos) y
comportamientos (funciones o métodos). Además, poseen de capacidades
intrínsecas. Por ejemplo, un hijo puede recibir las características y
comportamientos de su padre. El comportamiento de un perro puede derivarse de
los genes de su padre.

Cómo llegar a la programación Nirvana


El principio y final de toda programación es evitar la escritura del mismo código
de forma repetida. ¿Resultaría mucho más adecuado inventar algún mecanismo
equivalente a un programa elaborado de forma cuidadosa que pudiera utilizarse
una y otra vez. modificando, ligeramente, su comportamiento para ajustarse a las
necesidades del momento?
El programador intenta alcanzar aquello que le permite conseguir dos grandes
objetivos: reusabilidad y reubicabilidad (pluggability). Las siguientes secciones
describen con detalle cada uno de estos objetivos.

Reusabilidad del trabajo


Correctamente aplicada, la programación orientada a objetos genera un diseño
que otros pueden modificar o reutilizar. El aspecto sagrado de toda la ingeniería del
software es escribir una vez y reutilizar muchas veces. Los objetos le permiten
acercarse a este estado idílico.

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.

Consolidación de la reusabilidad con la


reubicabilidad (pluggability)
Otro objetivo final es la posibilidad de reemplazar una biblioteca por otra
biblioteca mejor. Por ejemplo, las baterías disponen de interfaces específicas pero
también presentan muchos estilos diferentes de fabricantes y características. Las
baterías pueden ser sencillamente de celdas secas o estar formadas por último ion

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.

Presentación de los fundamentos de los


objetos
Como ya ha comprobado, la programación orientada a objetos representa todos
los buenos modelos de programación desarrollados hasta la fecha actual. Extiende
algunas ideas y coloca límites a otras. El resultado es un conjunto de conceptos
que se pueden utilizar la mayoría de las veces para resolver problemas de
programación. No obstante, antes de que sus expectativas crezcan demasiado,
puede resultar interesante leer las limitaciones que plantea en el Capítulo 14,
"Límites de Objetos".
Esta sección identifica los fundamentos de los objetos. Es posible que esté

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.

FIGURA 11.1 La jerarquía de herencia de Device incluye dos objetos


abstractos (BlockDevice y CharDe-vice) y tres objetos habituales (Network,
Disk y SerialPort).

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.

Características de los objetos


La incorporación de los fundamentos de la tecnología orientada a objetos
supone incluir diversas características o definiciones. Estas características pueden
no resultar novedosas pero constituyen términos muy importantes a considerar
cuando se habla de un lenguaje orientado a objetos.
Las siete características son derechos de acceso, clase, objeto, atributos,
propiedades, métodos y relaciones. Esta sección describe cada una de ellas.

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.

Extensión de los objetos


Cuando se dispone do algo muy bueno (especialmente los objetos), la tendencia
natural es extender las características o expandir su significado. Estas extensiones
están disponibles en algunos lenguajes orientados a objetos pero no en todos.
Debería ser consciente que el diseño que genere puede no implementarsc en
todos los lenguajes que desee.
Algunas extensiones son consecuencias naturales de los objetos, como las
plantillas y la generación de flujos. Otras, como las excepciones, no tienen una
relación clara pero son muy importantes para un diseño adecuado. Esta sección
identifica las extensiones más habituales.

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.

• Extensión no redefinición. No puede modificar el significado de un tipo


existente. En otras palabras, no puede modificar el significado de (int)+(int).

• No todos los operadores disponibles. Puede utilizar la mayoría de los


operadores, pero algunos no se pueden redefinir (por ejemplo, el condicional
aritmético ?:).

• Debe mantener la cuenta de parámetros. El nuevo significado del operador


debe utilizar el mismo número de parámetros que el original. Todos los
operadores sobrecargados son de un parámetro o de dos parámetros.
Puede que quiera utilizar algún tipo de precaución cuando defina los significados
nuevos de los operadores. El problema es que su código puede parecer menos
claro y generar resultados no intencionados.

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án los elementos (campos) fuertemente relacionados? Por ejemplo, si


modifica uno, debería modificar el valor o significado de otro. Si no es así,
puede que tenga una colección (un conjunto de elementos no relacionados).
Intente aislar todas las agrupaciones relacionadas. A continuación, identifique
sus correspondientes responsabilidades.

• ¿Comenzó con la clase y, a continuación, identificó el soporte de los atributos?


Un error habitual es encontrar todos los posibles atributos y, a continuación,
agruparlos en una clase. Es una práctica torpe. Un problema de programación
comienza a partir de una responsabilidad o tarea principal que puede dividirse
en subtareas más pequeñas.

• ¿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:

• ¿Ha dividido con cuidado las responsabilidades a un buen nivel? Puede


establecer una comisión y finalizar dividiendo el objeto (dos o más objetos que
deberían ser uno). Localice acoplamientos importantes entre las clases. Es
posible que tenga uno o más objetos después de la reestructuración.

• ¿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.

• ¿Utilizan los métodos datos internos (no-constantes)? Una clase adecuada


tendrá métodos que modifican el estado del objeto. Cuando no se producen
modificaciones, se tiene una función de traducción en lugar de un método.
Las colecciones de funciones son más habituales eme las colecciones de datos,
pero todavía plantean sospechas todas las colecciones de funciones accidentales.

Soporte del lenguaje


Es posible que quiera poner en práctica las posibilidades de la tecnología
orientada a objetos, una vez conocidas y entendidas. Las características que
ofrecen son muy atractivas para la mayoría de los programadores.
incluso los ingenieros de sistemas electrónicos (hardware) e incrustados
(firmware) están adoptando los fundamentos de esta tecnología.
Como podría esperar, hoy en día existen muchos lenguajes que admiten la
demanda de objetos. Puede incluso encontrar versiones de Object Cobol (para

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.

Soporte activado frente a soporte orientado


Puede que algunos términos no estén todavía muy claros. Por ejemplo,
orientado a objetos y admitir objetos. Realmente, indican los términos actuales
definidos por los profesionales de la tecnología orientada a objetos para especificar
los diferentes lenguajes. En realidad, muy pocos lenguajes son realmente
orientados a objetos u obligados a usar objetos. SmallTalk es, quizás, el único
lenguaje que está obligado a utilizar objetos. Todo aquello que desarrolle en
SmallTalk, debe realizarlo con un objeto. Lógicamente, esto no se puede pasar por
alto, cuando está creando una instancia, trabajando con el compilador o el código
fuente, o manipulando el marco de trabajo.
La mayoría de los lenguajes son lenguajes que admiten objetos: el lenguaje
permite y anima el uso de los fundamentos, características y extensiones de la
tecnología orientada a objetos. I'or ejemplo, C++ es un lenguaje que admite
objetos. Puede obviar muchas de las utilidades y compilar simplemente código C.
Incluso, puede definir casi-objetos con muchas de las características (excepto
encapsulación y privilegios) de una clase utilizando la estructura struct.
Un lenguaje que admite objetos le permite escribir código no obligado a utilizar
todas las posibilidades de la tecnología orientada a objetos. Incluso Java es un
lenguaje que admite objetos; podría escribir un main() enorme en una única
definición de clase con diferentes métodos de soporte. Los lenguajes que
realmente obligan a usar objetos no están limitados a un único punto de entrada. Al
inicio, se instancian (crear y ejecutar) todos los objetos que el programa necesita.
La forma más flexible de orientación a objetos es soporte de objetos.
Esencialmente, un lenguaje con soporte de objetos presenta características de
objetos opcionales incorporadas a la estructura del lenguaje. Puede obtener la
misma potencia del lenguaje con o sin los elementos que admiten los objetos. Un
buen ejemplo de este tipo de lenguaje podría ser Perl o Visual Basic. Por supuesto,
los lenguajes con soporte de objetos tienen sus limitaciones. Normalmente, pierden
la herencia.
Para evitar estas limitaciones, estos lenguajes, a menudo, lanzan o generan un
elemento de captura. Por ejemplo, Visual Basic ofrece un tipo variante que admite
la abstracción y herencia manual. Desafortunadamente, estos elementos de
captura rompen otros fundamentos de la tecnología orientada a objetos, como la
encapsulación, reusabilidad, mantenimiento, legibilidad y prueba. El tipo variante
de Visual Basic deja la determinación del tipo al receptor obligando a conocer
información privada sobre los datos que incluye.

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.

• Abstracción Los lenguajes que le permiten modificar el tipo de una variable


mediante una conversión de tipos pueden implementar algún tipo de
abstracción. Puede almacenar el tipo actual como parte de la estructura del
registro y utilizar esta información para realizar las tareas apropiadas.

• Polimorfismo. Puede realizar su propia asignación de nombres (por ejemplo,


Play_MIDI()).

• Herencia. Algunos lenguajes permiten simular la herencia mediante la


utilización de punteros a funciones aunque esto resulta complicado de depurar
y mantener.

• Clase y objeto. Si el lenguaje incluye registros, es un buen punto de partida.


• Atributos. Se corresponden sencillamente con los campos del registro.
• Propiedades. Cree las interfaces CRUD para cada propiedad pública.
• Métodos. Todos los lenguajes admiten funciones y procedimientos. Si no
dispone de funciones, puede simularlas mediante el paso de un valor por

277/512
parámetros.

• Derechos de acceso. Los derechos de acceso serán reglas que especifican el


uso del método. Todos los intentos se desvanecen cuando alguien rompe estas
reglas.

• Relaciones. Están disponibles las relaciones de contiene y trabaja en. La


relación de herencia depende del lenguaje.

• Plantillas. No disponibles.
• Persistencia.Puede realizar un seguimiento del estado y cargarlo, de forma
manual, cada vez que se inicie el programa.

• Generación de flujos. El empaquetado y desempaquetado de los datos requiere


conocer los detalles de los datos y posibilidades de conversión de los tipos de
datos. No es posible realizar identificación de estructuras desconocidas puesto
que los métodos no estarán acoplados con los datos.

• Sobrecarga. Puede utilizar interfaces implícitas pero sólo se utilizan aquellas


que asumen un entorno de carga dinámica Probablemente, esta opción no
estará disponible.

• Eventos y excepciones. Puede simular algunos detalles de la programación


basada en eventos y captura de excepciones, pero necesitará una función que
le permita realizar saltos entre funciones (como setjump() de C++) para poder
duplicarlos. No resulta una buena idea.
De tudas formas, no abandone la referencia de la programación orientada a
objetos. Las prácticas presentadas en esta sección le ayudarán en cualquiera de
las condiciones de trabajo que se encuentre.

Resumen: mentalidad orientada a objetos


La tecnología orientada a objetos ocupa una posición de privilegio dentro de la
investigación e innovación que se desarrolla en el campo de la programación. Los
fundamentos de los objetos (abstracción, polimorfismo, herencia y encapsulación)
ayudan al programador en el diseño y desarrollo de programas consistentes que
otros programadores puedan modificar o reutilizar. Las características definen la
forma que tiene la tecnología orientada a objetos de implementar parte de los
fundamentos y extensiones que permiten al programador desarrollar otras
posibilidades.
El resultado de un análisis y diseño orientado a objetos es muy sencillo de
reutilizar siempre y cuando se desarrolle con cuidado y disciplina. Además, es más
fácil de mantener y extender que la programación convencional. Por último, permite
al programador centrarse más en el problema que en la propia programación.

278/512
Capitulo 12

Uso de la API de red de Java

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

Hasta este momento, el libro ha tratado los sockets y la programación en red en


C. Las ventajas de la programación en C son muy claras para los diseñadores de
sistemas operativos (la potencia sin coste supone un gran beneficio). Sin embargo,
esto no siempre conduce a programas reutilizables o portables.
De forma alternativa, se puede utilizar un lenguaje de objetos y crear un
conjunto de herramientas reutilizables. Java es un buen ejemplo de reutilización
excelente e incluso de portabilidad. La base principal de Java es proporcionar dos
niveles de portabilidad: nivel fuente v nivel código. La portabilidad a nivel fuente
significa que todos los programas deben poder compilarse en cualquier plataforma.
(Se puede observar que Sun Microsystems se reserva el derecho de expirar o
Jescartar ciertas interfaces, métodos o clases.) Esto es muy potente puesto que se
puede reutilizar código en cualquier plataforma que soporte Java.
Realmente, la portabilidad a nivel de código no es nueva. La idea "compile una
vez, ejecute en cualquier parte" es fácil de implementar con las herramientas
correctas. Java compila a un código de bytes en el que se ejecuta una máquina
virtual. La Java Virtual Machine (JVM, Máquina Virtual Java) ejecuta cada comando
seeuencialmente, actuando algo así como un microprocesador. Por supuesto, la
interpretación del código de bytes nunca puede aproximarse a la velocidad del
lenguaje máquina nativo (en el que compila C), pero debido a la velocidad de los
microprocesadores modernos, la degradación del rendimiento es menos
apreciable.

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").

Exploración de diferentes sockets Java


Al igual que C, Java ofrece formas de acceder a Internet. De hecho, algunos
programadores dicen que la ventaja real de Java deriva de la independencia de la
interfaz gráfica del usuario (GUI) y la programación de red inherente. Java ofrece
algunos protocolos de Internet para su uso, entre los que destacan TCP y UDP.
El canal de red preferido por Java es el canal TCP. Como se observó en la
primera parte del libro, éste presenta la mayor fiabilidad. Además, es más fácil de
utilizar que el estilo datagrama de comunicación. Java aún ofrece una conevión
UDP, pero la biblioteca de entrada/salida no ofrece ningún soporte directo para ella.

Programación de clientes y servidores


Los canales de flujo (o TCP) presentan la mejor coincidencia con la interfaz de
Java. Java intenta abstraer los detalles y simplificar las interfaces. Puede
comprender fácilmente las decisiones posteriores que colocan el soporte para TCP
en la biblioteca de E/S. El enfoque Java para creaT sockets reduce realmente los
pasos a un par de objetos y métodos.

Clientes TCP Java


Por ejemplo, si desea un socket cliente, sólo debe crearlo:
Socket s = new Socket(String Hostname, int PortNum);
Socket s = neuv Socket (InetAddress Addr, int PortNum);
Un ejemplo de implementación podría ser el siguiente:

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();

RECOLECCIÓN DE BASURA Y AUTOMÁTICA

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);
}

El bloque try...catch completa el ejemplo. Se puede arrastrar este código


directamente a un bloque mainQ de una clase.

Servidores TCP Java


Como se vio en las secciones precedentes, Java ha simplificado la creación,
lectura, escritura y cierre de sockets. La escritura del servidor correspondiente es
incluso más sencilla. La llamada para crear un socket servidor tiene tres interfaces:
ServerSocket s = new ServerSocket(int PortNum);
ServerSocket s - new ServerSocket(int PortNum, int Backlog);
ServerSocket s = new ServerSocket(int PortHum, int Backlog,
InetAddress BindAddr);
Backlog y BindAddr ofrecen las mismas prestaciones que las llamadas al
sistema listen() (cola de conexiones en espera) y bind() (para una interfaz de red
especifica) propias del lenguaje C.
Si recuerda, un programa servidor en C tiene entre 7 y 10 líneas. En Java, se
puede crear un senidor equivalente en un par de lineas:
ServerSocket s - new ServerSocket(9999);
Socket c = s.accept();
Y, como la escucha del socket en C, se utiliza ServerSocket para esperar las
conexiones de clientes. Cuando un cliente solicita una conexión, el servidor obtiene
un objeto Socket nuevo que se puede utilizar para crear el flujo de E/S.
Nuevamente, puede utilizar estas clases para crear un SimpleEchoServer
(véase el Listado 12.2).
Listado 12.2 El servidor de eco sencillo en Java utiliza arrays de bytes en los
flujos de entrada/salida
//***************************
/ / Extracto de SimpleEchoServer

283/512
//--------------------.....................

try
{
ServerSocket s = neiv ServerSocket (9999); / / Crea el servidor
while (true)
{

Socket c = s.accept(); / / Espera la conexión


InputStream i = c.getlnputStream(); / / Obtiene el flujo de entrada
OutputStream o = c.getOutputStream(); // Obtiene el flujo de salida
do
{
byte[] line = new byte[100]; / / Crea una memoria auxiliar
i.read(line); / / Lee el mensaje del cliente
o.write(lineI; / / Vuelve a enviarlo
}

while ( lstr.trim().equals("bye") );
c.close(); I I Cierra la conexión
}
}

catch (Exception err)


{
System.err.println(err);
}

El Listado 12.2 demuestra lo sencillo que es crear un servidor en Java. Estos


ejemplos están incompletos y tienden a ser poco claros, pero las secciones
siguientes establecen las limitaciones de E/S y pueden ayudarle con un código más
sólido.

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
//----------------------------------

DatagramSocket s = new DatagramSocket(); / / Crea el socket


byte[] line = new byte[l00];
System.out.print("Introduzca el texto a enviar: " ) ;

int len = System.in.read(line);


InetAddress dest = / / Convierte el nombre del host
InetAddress.getByName("127.0.0.1");
DatagramPacket pkt = / / Crea el paquete del mensaje
new DatagramPacket(line, len, dest, 9998);
s.send(pkt); / / Envia el mensaje
s.close(); / / Cierra la conexión
Este listado envía el mensaje y cierra inmediatamente el socket. Aunque pueda

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.

Envío a múltiples destinos


El protocolo datagrama permite enviar un mensaje sencillo a un destino no
conectado. Una ventaja para los datagramas es la capacidad de enviar a varios
destinos a la vez utilizando la difusión y la multidifusión. El Capítulo 15,
"Encapsulado de red con Llamadas de procedimiento remoto (RPC)", trata con
detalle ambos métodos de envío de mensajes.
SOPORTE DE MULTIDIFUSIÓN JAVA ACTUAL
En el momento de la escritura de este libro, las opciones de socket de multidifusión no
funcionan correctamente. Este soporte se debe fijar pronto. Se probó en la versión beta
1.3 de WinNT y Linux.
Java ofrece la multidifusión en su wrapper de red, pero excluye la difusión. La

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
{

byte[] line = new byte[l00];


DatagramPacket pkt = new DatagramPacket(line, line.length);
ms.receive(pkt);
msg = new String(pkt.getData());
System.out.println("Desde "+pkt.getAddress(|+":"+msg.trim());
}

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.

Conexión a través de E/S


Los ejemplos vistos hasta el momento usan interfaces de E/S muy sencillas, que
suelen ser difíciles de aplicar. La potencia de los sockets Java está en su capacidad
para establecer una conexión a través de diferentes clases de E/S. Java dispone de
clases con diferentes formatos de lectura y escritura de datos.
Esta sección clasifica y muestra la forma de utilizar las clases de E/S que
pueden ser necesarias con sockets. El alcance no es exhaustivo puesto que el
paquete de E/S tiene muchas características. Si desea saber más, puede adquirir
otros libros Macmillan sobre Java (como Puré java 2 de Kenneth Litwak o Java
Programming on Linux de JSIathan Meyers).

Clasificación de las clases de E/S


El paquete de E/S de Java incluye varias clases, definida cada una de ellas para
realizar una tarea específica. Todas ellas envían o reciben información.
Desafortunadamente, puesto que tiene demasiadas clases, es difícil decidir cuál
utilizar y cuándo. Quizá la forma más fácil de trabajar con ellas es agruparlas de
acuerdo a sus diversas responsabilidades. De esta forma, se puede ver fácilmente
la mejor clase para el uso de sockets.
El paquete de E/S ofrece esencialmente seis tipos principales de E/S. Cada tipo
atiende a un propósito específico, y todos ellos derivan de Reader, Writer,
InputStream u OutputStream:

• Memoria. E/S basada en buffers de memoria. El paquete no accede a

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.

• Archivo. Lectura, escritura y manipulación del sistema de archivos. Estas clases


ofrecen formas de crear, leer, escribir, eliminar o manipular los archivos de un
sistema de archivos. Entre los ejemplos se encuentran FilelnputStream,
FileOutputStream, FileReader v FileWriter.

• Filtros. E/S que realiza alguna traducción o interpretación basada en caracteres.


Por ejemplo, el tratamiento del carácter de línea nueva como fin de registro, el
reconocimiento de campos mediante fabuladores o comas o la conversión de
valores binarios en números legibles. Entre los ejemplos se encuentran
FilterReader, FilterWriter, PríntWriter y PrintStream (desaprobado).

• Objetos. Envío y recepción de objetos completos. Esto es más impresionante;


Java puede enviar y recibir instancias sin que tenga que codificar nada. En la
mayoría de los casos, sólo necesita indicar la clase Serializable y puede
transmitir y recibir instancias de objetos. Las clases son ObjectlnputStream y
ObjectOutputStream.

• Conductos. IPC (Inter-Process Communications, Comunicaciones entre


procesos) como aparecen en C. Cree un conducto y enlácelo con otro conducto
y, a conlin dación, haga que dos threads se envíen mensajes cruzados entre sí.
Las clases son PipedlnputStream, PipedOutputStream, PipedReader y
PipedWriter.

• Flujo. Comunicaciones de E/S generalizadas mediante buffers. Este es el


formato que utilizan los sockets. Si desea utilizar los otros, necesita convertir la
clase de flujo en otra (véase la sección siguientel. Las clases abstractas son
InputStream y OutputStream. Las clases de conversión básicas son
InputStreamReader y OutputStrea rn Writer.
El paquete dispone de otras muchas clases que realmente no encajan en esta
clasificación. Un ejemplo es SequencelnputStream, que permite combinar dos
flujos en uno.
De todas estas clases, debería familiarizarse principalmente con las clases
ObjectlnputStream y ObjectOutputStream para el envío y recepción de clases
personalizadas o BufferedReader y PríntWriter para la E/S de cadenas. Los
programas de red suelen utilizarlas puesto que se ajustan a la mejor programación
de sockets. De forma similar, puede utilizar los objetos Byte Array InputStream o
ByteArrayOutputStream y ofrecer interfaces para mensajes datagrama.

Conversión entre clases de E/S


Algunas de las características que posee una clase puede que no estén
disponibles directamente en la clase de E/S que maneja el socket. La clase Socket

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.

Configuración del socket Java


El Capítulo 9 muestra varias formas de configurar su socket para realizar
diferentes operaciones utilizando las opciones del socket. Las versiones de Java
superiores a la 1.3 han expuesto algunas de estas capacidades en la API. Por
favor, observe que si el sistema operativo no soporta ciertas opciones del socket,
Java no puede exponerlas. Esta sección describe estas opciones expuestas.

Configuraciones compartidas por sockets Java


Todos los sockets java comparten métodos para configurar el socket. A
continuación se determina el tiempo que espera una lectura hasta la recepción de
datos. Estos métodos exponen la opción SO_TIMEOUT del socket, que determina
el tiempo de espera de la lectura. La opción SO_TIMEOUT es arcaica, y Linux la
sustituye por fcntl(), poll() o select().
GetSoTimeout()
setSoTimeout(int timeout)
El siguiente código especifica el tamaño en bytes de los buffers de envío
internos. Estos métodos exponen la opción SO_SNDBUF del socket.
getSendBufferSize()
setSendBufferSize(int size)
Las siguientes líneas de código especifican e¡ tamaño un bytes de los buffers de
recepción internos. Estos métodos exponen la opción SO_RCVBUF deí socket.
GetReceiveBufferSize()
setReceiveBufferSize(int size)
setKeepAlive(boolean on)

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)

Configuraciones Java específicas de la


multidifusión
Esta sección muestra las opciones que se pueden definir en un objeto
MulticastSocket. Estas funciones determinan la cantidad de saltos de rooferque
permite el socket antes de que deba morir el paquete.
Los siguientes métodos exponen la opción IP_MULTICAST_TTL del socket
(Aunque resulte extraño, Java no permite especificar el TTL para otros tipos de
socket.)
getTimeToLive()
setTimeToLive(int ttl)
Los próximos métodos establecen la interfaz de red principal para multidifundir
las transmisiones exponiendo la opción IP_MULTICAST_IF del socket.
getInterface()
setInterface(InetAddress inf)

Multitarea de los programas


Java dispone de varias técnicas de programación fáciles y directas. Junto con los
paquetes de red y de E/S, que reducen dramáticamente la programación y la
complejidad, tiene una estructura entrelazada integrada. Los threads Java son tan
simples como la declaración de una interfaz.
Durante la programador de la red, definitivamente necesita sacar partido de
todas las capacidades multitarea que proporciona el sistema. Esta sección resume
algunas cosas que necesita conocer si desea utilizar threads en sus programas
Java.

Uso de threads en una clase


Para crear una clase con threads, puede derivar de la clase Thread o
implementar la interfaz Runnable. En ambos casos, debe definir el método run()
donde comience la ejecución real del thread. En el primer caso, puede escribir

292/512
simplemente lo siguiente:
public class TestThread extends Thread
{
public void run()
{
/** aquí se ejecuta el thread **/
}

La extensión de Thread le permite crear el thread basado en la clase. El método


run() determina el lugar donde comienza a ejecutarse el thread. Cuando finaliza el
método, termina el thread. Las siguientes líneas crean el thread y lo ejecutan.
public static void main(String[] args)
{

start(); // <-- comienza el thread y llama a runf))


/*** hace algo mientras 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.

Cómo añadir threads a una clase


Algunas veces, sin embargo, no es posible la herencia desde Thread. El ejemplo
típico es cuando se está escribiendo una clase que deriva de Frame. Java no
permite herencia múltiple, de forma que es imposible la herencia tanto desde Frame
como desde Thread. En este caso, se puede utilizar la segunda técnica de
implementación de la interfaz Runnable. Los resultados son los mismos y el

293/512
programa sólo cambia ligeramente.
public class TestFrameThread extends Frame
implements Runnable
{

public void run()


{
/*** thread hijo * * * /
}
public void someMethod()
{
Thread t = new Thread(this);
t.start();
}

}
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()
{

/ * * * Gestiona los recursos conflictivos * * * /


}

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); }
}

/ * Envía el mensaje */ // Procesamiento de los datos


notifyAll(); / / Avisa a todos los threads en espera
}

El método wait() coloca el thread actual en la cola de espera. El planificador


despierta al siguiente thread de la cola. Si éste puede proceder, se retira del bucle
y envía el mensaje. Cuando lo ha hecho, el método notifyAII() indica a todos los

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.

• Sólo IPv4. Aparentemente, Java sólo soporta la red TCP/IPv4. En el momento


de la escritura de este libro, el sitio web java.sun.com indica un proyecto
llamado Merlin que define el soporte para IPX e IPvó. Pero eso es sólo la teoría
y hay que modificar algunos métodos de clase para que soporten otras redes.

• Sin raw sockets. Java no soporta raw sockets.


• Opciones de socket incompletas. Algunas configuraciones de opciones de
socket están perdidas. Entre éstas se encuentran TTL para TCP y UDP,
retransmisión de tiempos y otras.

• Sin creación directa de procesos. La creación de procesos (forking) está


limitada para las llamadas externas. Puede simular un proceso creando una
instancia de un objeto nuevo con thread, pero no tiene garantizada la
corrupción del recurso puesto que los threads continúan compartiendo el mismo
espacio virtual.

• Sin difusión. El paquete Java no incluye la difusión. La posible razón es la


menguante popularidad de la difusión. Probablemente puede pensar que la
difusión permite avisar sobre algo de forma rápida.
Incluso con estas limitaciones, Java es un entorno de programación bueno y
sólido. Puede hacer casi todo con las características que presenta.

Resumen: programación de redes de tipo


Java
Java incluye una amplia biblioteca que ayuda a escribir programas de red. El
paquete Network (red) incluye clases para sockets de flujo (TCP), datagrama
(UDP) y multidifusión (UDP). También incluye clases para la gestión de las
direcciones y las conversiones.
Cuando se utiliza con el paquete de E/S, el paquete de sockets incrementa sus
prestaciones. El paquete de E/S ofrece varias formas de interactuar con las
comunicaciones entre sockets, memoria y threads. La biblioteca, aunque algo

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

Diseño y uso de una


estructura de socket en C++

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

El último capítulo presentó la programación de sockets en Java. Java es un


lenguaje muy poderoso con unas ventajas muy importantes, como la
independencia de la plataforma, compilación JET (Just in Time - inmediata) y
converso res-compila dores nativos. Sin embargo, todavía aparecen personas que
no desean utilizar Java, puesto que consideran que presenta una ausencia de
estabilidad o rendimiento y se decantan por utilizar una versión de C++.
El desarrollo de un marco de trabajo (o biblioteca de clases) basado en sockets
implica poner en uso todas las tecnologías presentadas en este libro. De hecho,
este capítulo muestra algunas características adicionales de los sockets
presentadas en la Parte IV, "Sockets avanzados, incorporación del valor".
La lectura de este capítulo requiere que esté familiarizado con C+-t- y con la
programación orientada a objetos. Al igual que en capítulos anteriores, el lector
debe saber cómo escribir un programa C++ y cómo compilar y enlazar los archivos.
Puede localizar fácilmente en Internet tutoriales para estos procesos.
Este capítulo le introduce en el proceso de análisis, diseño y creación de un
marco de trabajo basado clases. Este marco de trabajo sólo proporciona un
revestimiento (una interfaz con poca funcionalidad incorporada) alrededor de la API
de Sockets. Puede usar estos principios para definir otros marcos de trabajos y no
utilizarlos sólo para los revestimientos.

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.

Cómo simplificar la conexión con los sockets


El hecho de escribir una clase que coincida con el tipo de aplicación, le permite
centrarse mucho más en la programación despreocupándose menos de la red. No
resulta difícil realizar la conexión con los sockets, pero requiere llevar a cabo un
conjunto de pasos específicos que permiten obtener los canales de comunicación
para trabajar de forma consistente.
El lenguaje C++ es más potente que C y, además, incluye un superconjunto
completo de características de C. De hecho, puede compilar programas C++ con la
misma herramienta que utiliza para C (cc). (En realidad, esto no es del todo cierto.
Cuando cc localiza una extensión .C o .cpp, entonces llama a g++ en lugar de usar
gcc.) También es importante comentar que, a pesar de esta potencia adicional, el
lenguaje C++ es un poco más complicado. No obstante, las ventajas que
proporciona están muy por encima dé los inconvenientes.
El desarrollo de un marco de trabajo basado en sockets permite definir
interfaces más sencillas para que otros puedan utilizarías. La sencillez de los
programas es un elemento fundamental en el proceso de prueba y corrección de
errores. Además, simplificar la interfaz supone facilitar su uso. De forma similar,
cuanto más fácil de usar sea una cosa, mayor probabilidad existe de utilizarla de
forma correcta y frecuente.
Un marco de trabajo para C++ diseñado y escrito correctamente ofrece unas
interfaces muy sencillas y directas que permiten al programador centrarse
realmente en el trabajo que tiene en manos.

Cómo ocultar los detalles de implementación


Las interfaces identifican la simplicidad o complejidad de un producto. Cuando
incrementa la complejidad de una interfaz, disminuye su facilidad de uso. El hecho
de ocultar los detalles de implementación es muy importante para asegurar la
simplicidad. El diseño e implementación, particularmente en un revestimiento,
intentan ocultar detalles al programador-usuario.
Por supuesto, es posible que el programador-usuario quiera conocer ciertos
detalles pero esta información puede resultar peligrosa. La encapsulación, uno de
los fundamentos de la tecnología orientada a objetos y presentada en el Capítulo
11, "Ahorro de tiempo de los objetos", protege al desarrollador del marco de trabajo
y al programador-usuario. El hecho de ocultar la implementación de ciertas etapas,
le permite proteger al programador de todas las modificaciones que se pueden
producir en estas implementaciones. Además, protege al marco de trabajo

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.

Implantación de componentes reutilizables que


faciliten el uso de las interfaces
Las interfaces y la encapsulación de los detalles de implementación crean una
separación muy fina entre el programador y el marco de trabajo. El programa debe
pasar todas las peticiones a través de esta línea de separación. Las operaciones a
realizar dentro de la clase aceptan v trabajan sobre datos y peticiones.
Es muv importante garantizar que las interfaces sean sencillas. El resultado es
claro: un conjunto de herramientas de programas que pueden usar otros muchos
programas. De hecho, pueden aparecer diferentes implementaciones para la
misma interfaz. Simplemente, debe seleccionar aquella que resulte más adecuada
a sus necesidades.
Cuando escriba estas herramientas, se hará muy clara la ventaja de la
reutilización para los programadores y usuarios. A su vez, éstos promueven su
trabajo distribuyendo diferentes programas en función de estas herramientas. (Un
ejemplo excelente es Qt.)

Demostración de los procesos del diseño del marco


de trabajo
Las herramientas del marco de trabajo aparecen en Internet. Puede localizar
formatos muy variados. Por otro lado, la localización de un marco de trabajo
basado en sockets, resulta un poco más dificultoso. El desarrollo de un marco le
permite poder configurarlo de la forma que quiera, así como tener la posibilidad de
mostrar el proceso de desarrollo de estas herramientas.
A diferencia del estilo UML del A/DOO (Análisis y diseño orientado a objetos), la
escritura de bibliotecas o marcos de trabajo está enfocada a una perspectiva de
bottom comenzando a desarrollarse completamente desde arriba. En otras
palabras, A/DOO establece que debe comenzar con los requerimientos y, de forma
eventual, desarrollar un producto; algo similar a pelar una cebolla. La idea es
minimizar el proceso de identificación de todas las características implemeniando
sólo lo que sea necesario.
La creación del marco de trabajo utiliza una perspectiva diferente: no se
conocen todavía las características que desea el programador usuario y, por tanto,
el marco de trabajo implementa todas las utilidades que el programador/usuario
puede necesitar. Por supuesto, esta idea de gritar simplemente: "Nosotros
queremos utilidades" puede derivar en el hecho de tener código inservible (código

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.

Disposición del marco de trabajo


La creación de un marco de trabajo es similar a colocar cosas en una pared:
comience por determinar las dimensiones y clave todo en un orden determinado.
Esta sección presenta Jos pasos necesarios para la creación de un marco de
trabajo, identificando las características, agrupando en componentes, organizando
la jerarquía, etc.

Definición de las características generales


EL marco de trabajo debe tener alguna dependencia con algunos requerimientos
relacionados. Un programa con muy pocos requerimientos no resulta interesante
(de hecho, no merece la pena su implementación). Sin embargo, un programa con
demasiados requerimientos dispares, normalmente, genera un resultado muy
pobre. Los requerimientos definen los parámetros sobre los que tiene que trabajar
el marco de trabajo,
A menudo, cuando dispone de una lista de apetencias, es muy probable que
esta lista no esté lo suficientemente detallada para el análisis. Prepárese para
plantear todo tipo de cuestiones. El marco de trabajo basado en sockets debe
admitir la mayoría de las posibilidades descritas en este libro.

Soporte de diferentes tipos de sockets


La característica más importante que debe tener un marco de trabajo basado en
sockets para Internet es el acceso a los protocolos. El primer protocolo que
menciona este libro es la conexión TCP. Este estilo de programación siempre tiene
un cliente y un servidor. El servidor espera una petición de conexión, creando una
conexión para esa petición del cliente y, a continuación, interactúa con dicho
cliente.
Otra posibilidad de comunicación consiste en conectar un .host con otro
homólogo. Este estilo no tiene realmente un cliente o servidor, sino que presenta
un iniciador y un replicador. Además, utiliza una conexión UDP.
Otros dos tipos de comunicaciones son la difusión (broadcasting) y la
multidifusión (multicasting), presentadas posteriormente en este capítulo. Ambas le
permiten enviar mensajes a diferentes destinos al mismo tiempo. Para más
información sobre estos protocolos, consulte el Capítulo 15, "Encapsulación de red
con llamadas a procedimientos remotos (RPC)".
Los dos últimos tipos de soc&efsson de muv bajo nivel para este marco de
trabajo: sockets rawy difíciles. Estos tipos podrían integrarse en el marco de
trabajo, pero dado el nivel de detalle y gestión que requieren, es mejor dejarlos
para el uso directo de la API. Es,muy importante que se sienta lo suficientemente
libre para revisar el marco de trabajo de forma que pueda incluir las características
que desee.

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.

Configuración de sockets y conexiones


La última característica es la configuración del socket o de la conexión creada.

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.

Agrupamiento en componentes principales


El siguiente paso en la creación de un marco de trabajo es la identificación de
todos los componentes o piezas principales. Los componentes constituyen redes
de objetos que trabajan juntas para resolver un determinado problema o
proporcionar un servicio. Al igual que con los objetos, los componentes tienen
responsabilidades e interfaces, lo que permite poder utilizarlos al igual que los
objetos.
Normalmente, estos componentes son objetos heterogéneos incluidos en una
única interfaz. Algunos analistas de objetos llaman a estos grupos paquetes,
debido al método de distribución, o patrones, puesto que los sistemas de objetos
se ajustan normalmente a formas generalizadas. Sin embargo, también puede
constituir jerarquías con objetos abstractos como interfaz principal.
Esta sección describe los cuatro componentes básicos del Marco de trabajo
basado en sockets. Cada uno de estos componentes se corresponde con una
jerarquía como se ha mencionado anteriormente.

Excepciones: Manejo de problemas


Todos los marcos de trabajo deberían identificar y utilizar excepciones. Los tipos
de excepciones interesantes a capturar incluyen, normalmente, interconexión, E/S,
límites y demás. La funcionalidad básica de la jerarquía de excepciones depende
de la clase Exception. La Figura 13.1 muestra la componente exception.

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).

Mensajes: empaquetamiento y envío de datos


Para enviar y recibir mensajes a través del subsistema de E/S, necesita bloques
de datos y buffers. La clase virtual Message ofrece la interfaz básica para
empaquetar (Wrap()) y desempaquetar (Unwrap()). Esta interfaz ayuda a
aproximarse a la señalización que ofrece la E/S de Java.
Otra clase, TextMessage, aporta un ejemplo de cómo utilizar esta interfaz. Por
ejemplo, el método Wrap() crea un bloque de memoria contigua y copia el texto en
dicho bloque. (El método que realiza la llamada tiene la responsabilidad de liberar
el bloque de memoria.) El mensaje de TextMessage es una cadena de caracteres
tipo C de longitud variable.
El método de desempaqueta miento, Unwrap(), realiza el proceso inverso. Coge
el bloque de datos del método que realiza la llamada y comienza a reconstituir la
información interna. Puede ocurrir, algunas veces, que el mensa|e esté incompleto.
En ese caso Unwrap() debe utilizar más datos para poder finalizar el proceso de
desempaquetamiento. Por ejemplo, TextMessage puede tener un tamaño superior a
los 64KB (el límite del búfer), pero TextMessage debe detectar este tipo de situación
y solicitar más datos. El método Unwrap() devuelve un valor Booleano para indicar
si se ha completad» el proceso.

Direccionamiento: identificación del origen y


destino
Cada mensaje procede y se dirige a algún host. El direccionamiento de estos
hosts es importante para enviar el mensaje al propietario adecuado. Otro
componente sencillo es HostAddress. Utiliza los nombres de direccionamiento de
struct sockaddr permitiendo manejar el nombre y la resolución a un nivel de IP,

El marco de trabajo traduce todas las direcciones a este componente. Además,


dicho componente puede admitir diferentes tipos de direccionamiento.

Sockets: establecimiento, configuración y


conexión
El último componente presenta la mayor parte de la funcionalidad. Se basa en la
clase Socket y deriva todas las conexiones básicas. Utiliza los otros componentes
para realizar el trabajo. La jerarquía permite colocar ia funcionalidad básica en la
clase Socket y todas las especializaciones en las clases heredadas
Todos los protocolos de la lista de requerimientos tienen una clase dedicada:
SocketServer, SocketClient, Datagram, Broadcast y MessageGroup (multidifusión).
Todas las configuraciones específicas de protocolos aparecen en la clase-protocolo
apropiada. Cuando necesite modificar una configuración, compruebe en primer

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.

Creación de la jerarquía del marco de trabajo


Los componentes facilitan la transición de todos los objetos dentro de la
conexión. Los componentes proporcionan la interfaz al mundo exterior y permiten
dirigir todas las características a la clase padre de más alto nivel (superclase).
Antes de comenzar a trabajar con la jerarquía del marco de trabajo, es necesario
que tenga definidas las clases no abstractas dentro de cada componente.
En este momento, debería tener diferentes clases, por ejemplo las presentadas
en la última sección. A partir de aquí, preocúpese de buscar las relaciones y las
herencias existentes. No se sorprenda si ve características que desaparecen y
vuelven a aparecer del padre al hijo. Esto es completamente normal y,
habitualmente, se puede implementar.

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.

FIGURA 13.2 Cada componente utiliza un recurso del resto.


El componente Socket está conectado con el resto de componentes, como era
de esperar. Es posible que las clases no tengan todas estas conexiones, pero no
es del todo preocupante. Posteriormente, se presenta con mayor detalle
especificando las clases que realmente debe utilizar.

Cómo evitar las herencias naturales


El segundo paso mira en las componentes e intenta ver relaciones especiales
denominadas herencias. Como se ha descrito en el Capítulo 11, la herencia le
permite crear una clase relacionada a partir de alguna otra clase (clase base) y
modificar

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.

FIGURA 13.3 Aparecen relacionadas las clases Datagram, MessageGroup y


Broadcast. Las clases SocketServer y SocketClient no están lo
suficientemente relacionadas.
Los componentes Exception v Message podrían tener muchas clases y
diferentes niveles de herencia. Sin embargo, las jerarquías son muy sencillas
desde el punto de vista de la implementación. ÍPor favor tenga en cuenta que en
UML la flecha apunta a la clase padre.)

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).

Definición de las capacidades de cada clase


El último paso en el diseño difiere del proceso reiterativo del A/DOO. La razón
principal es que A/DOO se centra principalmente en los proyectos y no en las
bibliotecas. Examinando una biblioteca, no puede conocer, de forma precisa, lo que
el usuario quiere o necesita. A menudo, el diseñador debe considerar la pregunta,
"¿Para qué se podría utilizar la biblioteca?".

FIGURA 13.4 La jerarquía completa de la clase Socket incluye dos clases virtuales:
Socket y SocketStream.

EL PROYECTO IMPLICA EL FALLO DE LA BIBLIOTECA


Muchos programadores y diseñadores consideran el proyecto como la fuente principal del
diseño de una biblioteca. Su lógica tiene mérito: escribir las herramientas como escriben
el proyecto. No obstante, esta lógica tiene defectos muy importantes: ¿por qué la mayoria
de las bibliotecas o marcos de trabajo desarrollados de esta forma tienen una carencia
importante de escalabilidad? y ¿por qué las bibliotecas comienzan a parecerse a un
mosaico formado por piezas de diferentes estilos? El problema es sencillo: los proyectas
no tienen la perspectiva completa que permite ayudar a definir la filosofía de una
biblioteca. El mejor ejemplo de una clase sobre-sobrecargada es la clase CString de la
MFC. Para tener una biblioteca buena, flexible y clara, primero tiene que definirse la
filosofia. A continuación, considere las posibilidades para cada una de las clases.
Esta sección le introduce en el proceso de 'desmenuzar' todos los detalles de
cada clase. Se invierten el primer y segundo paso cuando se compara con el
desarrollo de un proyecto normal orientado a objetos.

Atributos: incorporación de lo que se conoce


En la mayoría de los casos, el siguiente paso coloca las funciones o métodos en
cada clase. Sin embargo, cuando crea una biblioteca, especialmente un

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)

Send(Hosl Address, Message, Opions)

Receive(Message. Options)

Receive(HostAddress, Message. Options)

Closelnpul()

CloseOutpul()

int GetReceiiveSize() SetReceiveSiza(int)

int GetSendSize() SetSendSize(int)

int GetM inReceive() SetM i n Send(int]

int GetMinSend() SetMinSend(int)

timeval GetReceiveTimeOut() SetReceiveTimeout(timeval)

timeval GetSendTimeout() SetSendTimeout(timeva1)

308/512
int GetTTL() SetTTL(int)

PermitRoute{bool)

KeepAhve(bool)

Share Address (bool]

int GetType()

int GetError()

FIGURA 13.5 :ada atributo en una clase puede tener asociada una función get...
()/set...().

Métodos: incorporación de lo que realiza


Al igual que la función de unión de grupos disponible para los sockets de multi-
lifusión, algunas clases tienen implementaciones específicas de métodos que
extienden los métodos CRUD. La metodología A/DOO identifica aquello que debe
realizar cada clase para cumplir con sus responsabilidades. Estas
responsabilidades están relacionadas con las funciones mencionadas en los casos
de uso (como se ha descrito en el Capitulo 11).
No obstante, una biblioteca no presenta unos casos de uso sólidos a partir de
los cuales comenzar a trabajar. Por tanto, los métodos, a menudo, reflejan
funciones que aparecen en la API de sockets. Por supuesto, no todas las funciones
son apropiadas para cada clase. La Tabla 13.1 muestra las implementaciones
específicas por clases definidas en el componente de sockets.

309/512
Tabla 13.1 Métodos especiales implementados en el componente Socket
Clase Método Descripción

Socket Bind Ofrece la llamada bind()


Send Ofrece las llamadas send() y sendto()

Receive Ofrece las llamadas recv() y recvfrom()

Closelnput Cierra el cana) de entrada utilizando shutdown()

CloseOutput Cierra el canal de salida utilizando shutdown()

SocketServer Accept Ofrece la llamada accept()


SocketClient Connect Ofrece la llamada connect()
MessageGroup Connect Ofrece la ILimada connect()
Join Une conexiones a grupos de multidifusión utilizando
las opciones de los sockets

Drop Libera la conexión de multidifusión

Puede que una clase no tenga una implementación específica e, incluso, ni


atributos para considerarse una clase nueva. Sólo necesita un comportamiento
diferente. La clase Broadcast es un ejemplo de esta premisa: su comportamiento
difiere lo suficiente de la clase base (Datagram) como para considerar su
existencia.

Constructores: formas de creación de objetos


Normalmente, cada clase tiene un conjunto de parámetros de configuración que
definen algunos de los comportamientos o métodos iniciales. De nuevo, la clase
Broadcast no tiene ningún método o atributo adicional pero es necesario
configurarla para enviaT y recibir mensajes de difusión. Esta configuración debería
aparece en el constructor.
Los constructores difieren ligeramente de los métodos normales; pueden realizar
la mayoría de las operaciones que realiza un método habitual, tales como crear
objetos, asignar espacio, realizar cálculos, etc. A continuación se presenta un
ejemplo de declaración de un constructor para la clase Socket:
//*******************************************************************
//*** Constructor por omisión rJe Socket
//*** uso de Network (PF_INET) y Protocol (SOCK_STREAM],

310/512
/ / * * * crea un socket y coloca la referencia en el atributo
//*** SD.
Socket::Socket(ENetwork Network, EProtocol Protocol)
{

SD = socket(Network, Protocol, 0);


if ( SD <>0)
throw NetException{"Could not créate socket");
}

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.

Destructores: qué es necesario liberar


Al igual que el constructor inicia el objeto, dejándolo preparado para su uso, los
destructores invierten algunos pasos y dejan el objeto listo para liberar la zona de
memoria asignada. A diferencia de Java, que utiliza un recolector de basura para el
espacio liberado, C++ debe liberar, de forma manual, los objetos.
A menudo, los objetos son auto-contenidos y requieren algo más que liberar el
espacio de memoria asignado. Sin embargo, algunas clases (como SimpleStrlng y
TextMessage) usan parte de las zonas de memoria dinámica (heap). El compilador
no puede indicar si todos los punteros en estas clases apuntan a algo más que un
elemento. Los destructores le permiten especificar qué necesita liberar y cuándo.
Otros pasos habituales del proceso de liberación de memoria incluyen el cierre
de archivos y la desconexión de los controladores. A continuación, se presenta un
ejemplo de destructor para la clase Socket:
//**************************

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.

Prueba del marco de trabajo basado en


sockets
El marco de trabajo de sockets constituye una implementación basada en los
criterios descritos en las secciones anteriores. Las siguientes secciones le
presentan cómo utilizarlo mediante un par de ejemplos. El sitio Web que acompaña
a este texto incluye otros ejemplos que muestran cómo implementar los sockets
basados en datagramas, difusión y multidifusión.

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
{

SocketClient client(addr); // Crea el socket y la conexión


TextMessage msg(1024); // Crea un buffer para el mensaje
do // Repetir hasta "bye"
{

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
}

while ( strcmp(msg.GetBuffer(), "bye\n") != 0 );


}
catch (ExceptionS err)
{

err. Pnnt Exception {);


}

Todos los programas utilizan el mismo formato try...catch(). Es posible que


quiera utilizar este formato para capturar y recuperar una excepción localizada por
el sistema. El siguiente código muestra una llamada a catch() omitida en el Listado
13.1 que debería considerar siempre antes de realizar todas las llamadas:
//*************************************************************************
// Control general de excepciones
catch(...)
{

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
{

SocketServer server(port); // Crea y configura el socket


do // Repetir siempre
server.Accept(Echoer); // Conecta y acepta peticiones del cliente
while ( 1 );

313/512
}

Como puede comprobar, el cuerpo del servidor es muy sencillo. SocketServer


realiza todos los pasos de configuración e instalación. La variable, Echoer, es la
otra parte del proceso (véase Listado 13.3).
Listado 13.3 Echo-Server.cpp, Parte 2
//******************************************
// Rutina de servicio del Servidor (Echoer)
try
{

TextMessage msg(1024); // Generar un buffer para mensajes


client.Send(welcome); // Enviar un mensaje "welcome"
do // Repetir hasta "bye"
{

client.Receive(msg);// Obtener el mensaje


client.Send(msg); // Enviar el eche al cliente
}

while ( msg.GetSize() > 0 &&


strcmp(msg.GetBuffer(), "bye\n") != 0 ) ;
}
De nuevo, el algoritmo es muy sencillo; acepta un mensaje y lo envía de vuelta,
hasta que el cliente genere un mensaje de saludo bye. Compruebe que este
fragmento de programa tiene su propio bloque try...cath(). Esto realmente es una
muy buena idea. Si ocurre un error en esta conexión, éste no debe afectar al
socket principal puesto que sólo está asociado a dicha conexión. Si no aparece el
bloque try...catch dedicado del Listado 13.3, cuando el cliente se desconecta, de
forma repentina, puede producirse una caída del servidor dado que la excepción
sería capturada por el bloque try...catch() del main.
INCORPORACIÓN DE CONTROLADORES DE EXCEPCIONES
Tenga cuidado a la hora de colocar controladores de excepciones. Además, puede que no
quiera controlar todos los tipos de excepciones. En su lugar, resulta más interesante
controlar sólo algunos tipos específicos. Normalmente, las diferentes conexiones
requieren sus propios bloques try...catch() y las excepciones catastróficas deberían
aparecer sólo en la raiz del árbol de llamadas.
La implementación actual del servidor no proporciona la multitarea. La clase
incluye algunas de las conexiones que necesita implementar.

Multitarea de igual a igual


I'uede todavía implementar la multitarea con procesos o threads. De hecho,
puede crear tareas en sus códigos, incluso cuando el marco de trabajo actual no

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.
}

while ( !done ); delete channel;


>

La implementación de receiver() se parece bastante a la llamada Echoer de


SocketServer
Si quiere usar la multitarea con el marco de trabajo, recuerde un par de reglas;

• Reglas a seguir. Revisar las notas y consejos del Capítulo 7, "División de la


carga: multitarea". En particular, asegúrese de capturar las señales de
terminación del proceso hijo. Además, dado que los threads comparten sus
canales de E/S, el hecho de cerrar el último supone cerrar todos los threads.

• Compartición de memoria. La contención de la memoria como recurso es un


problema casi intratable. Comparta sólo aquello que sea posible.

• Limitaciones de C++. El uso de entrelazado múltiple en C++ es complicado.


Son tantos los detalles que hay detrás, que debe tener mucho cuidado con los
threads que contienen datos. La creación de objetos en la pila antes de generar

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.

Envío de mensajes desconocidos o indefinidos


El marco de trabajo actual le permite derivar sus propios tipos de mensajes a
partir de Message. Esto resulta muy útil cuando trabaje exclusivamente con el
componente Message o disponga de tipos de mensajes conocidos. No obstante, es
posible que no tenga la flexibilidad de derivar exclusivamente a partir de una única
clase, especialmente si está integrando un sistema heredado.
La herencia múltiple es, en algunas ocasiones, un mal necesario,
particularmente cuando esté intentando mezclar diferentes y, a menudo,
conflictivos marcos de trabajo. Java tiene la ventaja de las interfaces pero esto no
supone, en un futuro cercano, tener un parecido con las opciones de C++.
Del mismo modo, no necesita conocer todos los datos en detalle. Esto ocurre, a
menudo, cuando tiene que conectar un cliente muy reciente con un servidor
heredado. El servidor no conoce las extensiones que el cliente puede admitir y, por
tanto, el rendimiento puede verse alterado. El desarrollo de un protocolo de
mensajería abstracto solucionaría este problema.
El generador de mensajes abstracto no sólo contendría los datos, sino que
también incluiría la estructura de estos datos. Sería similar al envío de archivos
incorporando la estructura de directorios. Un generador de mensajes abstracto
puede llevar a cabo, de forma más adecuada y extensible, las comunicaciones
entre los hosts.

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.

Resumen: un marco de trabajo basado en


sockets de C++ simplifica la programación
La utilización de un lenguaje orientado a objetos, como C++, puede ayudar a
simplificar el desarrollo de los sockets. Ha podido comprobar que la programación
basada en sockets es muy parecida, pero es necesario que siga ciertas reglas a la
hora de desarrollar una biblioteca o marco de trabajo basado en sockets. Estas
reglas difieren muy poco de las prácticas habituales del A/DOO.
Un marco de trabajo coordina las interfaces entre componentes y los
componentes definen el trabajo o funcionalidad principal de un conjunto de clases
relacionadas. Cada clase de un componente tiene como objetivo completar este
trabajo.
El marco de trabajo basado en Sockets incluye cuatro componentes: excepción,
mensaje, dirección y socket. El componente para las excepciones se mantiene
separado de! resto de componentes, de forma que reduce la posibilidad de fallo
interno. Los componentes de dirección y mensaje aportan el soporte para el
componente de sockets.
Este capítulo le presenta el proceso de creación de un marco de trabajo para
sockets y muestra cómo puede desarrollar este marco de trabajo en sus propios
programas. La extensibilidad de C++ y el marco de trabajo le pueden ayudar a
intentar poner en práctica estas posibilidades en sus propios desarrollos.

317/512
Capitulo 14

Limitaciones de los objetos


En este capítulo
Recordatorio sobre objetos
Los objetos no lo resuelven todo
Complejidad dar y tomar
El dilema de la administración de proyectos
Resumen: tenga cuidado con las pendientes

La tecnología de objetos no lo resuelve todo. Sí, eso es cierto. Simplemente es


una herramienta nueva a utilizar en el momento apropiado. Si intenta utilizarla
siempre, desaprovechará la simplicidad de los métodos de programación
alternativos.
Cuando decide utilizar objetos, puede ser necesario que conozca algunos de los
problemas. Entre estos problemas se encuentran hacer que la gente piense en
objetos, tener en cuenta que los objetos aún no pueden realizar algunas cosas,
prepararse para la complejidad adicional y administrar un paradigma nuevo.
La potencia de la tecnología de objetos es atractiva, pero este capítulo le
permite conocer las limitaciones que presenta.

Recordatorio sobre objetos


El primer problema que se encuentran los programadores, diseñadores y
analistas cuando comienzan un proyecto de objetos es intentar localizar los propios
objetos. Si olvida hacer esto, puede caer en los mismos errores que ya han
cometido muchos. Cada paso del análisis de objetos se centra en lo que
supuestamente debe hacer el sistema, la contribución de los objetos al esfuerzo y
cuándo se sabe que está hecho.
Esta sección presenta varios errores que se pueden cometer durante el
desarrollo. Recuerde, esto es sólo un ejemplo de lo que puede ir mal.

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.

Separación entre análisis y diseño


La asignación de nombre es muy importante, pero asegurarse de que no entra
en demasiados detalles lo es aún más. Las empresas suelen definir que un
proyecto requiere una base de datos grande antes de definir el problema global.
¿Qué hace eso? ¿Es malo?
Adelantar el diseño al análisis coloca la tecnología en el centro del proceso
mientras todo lo demás gira alrededor de ella. Ningún arquitecto bueno definiría el
tamaño de una caja antes de identificar lo que va a contener. De forma similar,
concluir el análisis con "Esto debe ser escrito en Java" antes de conocer el
problema completo puede provocar daños de salud y vida (observe la negativa que
Sun coloca en Java).
El análisis es el momento de mirar el mundo a través de unas gafas de cristales
rosas. Cuando se introduce en la etapa de análisis, mire sólo lo que se necesita
hacer a nivel global. Cada vez que alguien mencione un detalle de implementación
específico, éste va directamente a la lista de espera del diseño (una lista de
elementos que se revisarán posteriormente).
RESTRICCIONES DEL SISTEMA
A veces los requerimientos tienen restricciones entre las que se encuentra el material
hardware y software específico. Se deben considerar las restricciones en el caso de que
sean moderadas y formen parte de los requerimientos de negocio. Sin embargo, suelen
ignorarse la mayoria de ellos hasta el diseño. Aún así, si e! requerimiento indica que debe
escribirse en COBOL, debe considerar la forma en que afecta a su herencia.
El análisis también supone que tiene todas las herramientas y tipos contenedor
que pueda necesitar. No se preocupe todavía sobre la forma de hacer ciertas
cosas; céntrese sólo en los aspectos principales del sistema y cómo interactúan
para implementar los casos de uso.

El nivel de detalle apropiado


Los casos de uso son muy importantes para conducirle a través del proceso de
análisis. Sin embargo, al igual que sucede con el análisis, si no alcanza el nivel de
detalle apropiado, éste será insuficiente para demostrar que el diseño satisface los
requerimientos.
Conforme avanza en los requerimientos, comienza a darle sentido al "cómo"

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.

Uso correcto de la directiva friend de C++


La herencia es una característica peligrosa (especialmente cuando hereda de
una clase externa). Otra costumbre peligrosa es el uso de friend. La directiva
friend permite que otras clases accedan y utilicen las partes privadas de su clase
(rompiendo la encapsulación). También le permite insertar su clase en un conjunto
de funciones de trabajo (como la sobrecarga de operadores).
Casi todos los expertos en objetos rechazan la directiva friend. Sólo se acepta
esta directiva en el uso de sobrecarga de operadores como un mal necesario. Hay
quien considera equivalente esta directiva a goto. No lo utilice a menos que no
pueda evitarlo de ninguna otra forma.

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.

• Sobrecargue siempre e! operador igual. Debe hacerlo para que las


conversiones de tipo se realicen de forma apropiada.

• Seleccione ef operador que mejor se ajuste a su operación. Por ejemplo,


<string>+<string> está claro, pero *<queue> no significa necesariamente Queue-
>Pop().

• Considere la sobrecarga de los operadores new() y deleteQ. Algunos autores


sugieren que es una buena idea, pero este consejo es dudoso.

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.

Los objetos no lo resuelven todo


Hay gente que afirma que los objetos son el principio y el fin de la programación.
Puede hacer casi todo utilizando objetos y la tecnología de objetos, pero algunas
tareas son más simples en otros paradigmas. Al igual que el fontanero que tiene
sólo un martillo, un programador de objetos estricto puede ver todo como un
problema de objetos.
Esta sección presenta los problemas con la tecnología de objetos.

Las novedades llegan y evolucionan


La tecnología de objetos realmente era la novedad tecnológica más reciente. La
nueva son los patrones, que acredita los sistemas de objetos comunes. La lección
está clara: Las novedades buenas evolucionan a algo mejor. En el momento en
que comienza a aprender una tecnología, otra la sustituye pasando a ocupar el
centro de atención.

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;

• El conjunto de datosdebe permanecer intacto. El motor no crea una copia ni


traduce ninguna información. En su lugar, cambia la interpretación de la
información.

• El conjunto de datos sólo puede mutar dentro de la jerarquía de herencia. Para


cambiar un objeto por otro, las dos clases deben tener un thread de identidad
común.

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.

• El motor de descubrimiento debe tener acceso a ¡os elementos privados. Esto


parece entrar en el ámbito de la encapsulación, pero para descubrir la
verdadera naturaleza del conjunto de datos, necesita conocer las partes íntimas
de la clase destino.

• El proceso debe estar capacitado para anular métodos y soportar clases


parciales. A veces, puede ser necesario desactivar métodos de clases definidas
dinámicamente de forma que el objeto mutado resultante se comporte
correctamente. Por naturaleza, algo mutado puede tener aptitudes y
limitaciones que van más allá de las definidas por el método.
El objeto mutado tiene una gran potencia en referencia a las instancias que el
código del programa no haya anticipado. Esto dota de un poco más de inteligencia
al sistema y lo adapta más adecuadamente a su naturaleza.

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.

Algoritmos masivamente paralelos


El último problema algorítmico involucra los problemas masivamente paralelos,
como las redes neuronales verdaderas. Nuevamente, como los algoritmos
heurísticos, sólo pueden simular los resultados. La conformidad real sobre un
sistema masivamente paralelo debería requerir procesadores dedicados basados
en objetivos. Incluso el programa en Fortran paralelo y altamente integrado debería
competir para satisfacer los requerimientos.

Complejidad dar y tomar


La tecnología de objetos tiene otras limitaciones que van más allá de la propia
tecnología. Los profesionales han descubierto que cuando trabajan con grandes
marcos de trabajo (jerarquías de bibliotecas) o mantienen implementaciones
existentes, no se cumplen las promesas de los objetos. Esta sección expande los

326/512
aspectos de los objetos y lo que la historia ha mostrado.

Simplificación de programas con interfaces


establecidas
Durante los años en que ha prosperado la programación de objetos, los
profesio-rales han descubierto muchos puntos de aprendizaje. Un punto de
aprendizaje es la mportancia de las interfaces establecidas. La interfaz de una clase
es crítica para su ongevidad. Sin embargo, los programadores no suelen gastar su
tiempo en el dise-io de la mejor interfaz,
Las interfaces del código abierto suelen ser demasiado especializadas o
demasíalo funcionales para sacar ventaja real a la potencia de los objetos. De esla
forma, uando otros abren el código para mantenerlo, se encuentran con que los
métodos e neluso el comportamiento global de la clase se han deteriorado con el
tiempo. AI programar en estas clases, es muy frecuente encontrar prácticas que
responden a la dea: "Si funciona aquí, ¿por qué no pegarlo en otros sitios?".
La interfaz es la parte más importante de la programación modular (pero no
basa con decir esto). Sin embargo, la interfaz debe ser simple e intuitiva. Otro
problema son los propios marcos de trabajo y sus interfaces de clases múltiples. Es
muy Lormal encontrarse con que tiene que instanciar varias clases en un orden
determinado cuando programa con estos sistemas de clases. Este orden v
complejidad son nuy evidentes en la programación GUI (Interfaz gráfica de
usuario), posiblemente lebido a las características y opciones que ofrecen las GUI.
Cuando crea sus objetos, considere lo simples y claras que son las interfaces de
as métodos. Y lo que es más importante, asegúrese de que ésta es la forma en que
el irogramador trabaja con sus clases. Si necesita interdependencias entre clases,
lágalas tan fáciles de ensamblar como sea posible.

El enigma de la herencia múltiple


Otra complejidad surge de las características de herencia. El lenguaje C++ le
permite crear clases que heredan de múltiples clases padre. La gestión de una
jerarquía e herencia es bastante difícil. El Capítulo 1 1 , "Cómo ahorrar tiempo con
objetos", lenciona el concepto de acoplamiento. La herencia desde múltiples
fuentes introduce un acoplamiento entre clases poco saludable.
Este acoplamiento establece una dependencia con las clases padre. Si estas
clases ambian, el diseñador tiene que verificar las interfaces entre ellas y la
subclase. A eces los cambios son muy sutiles pero pueden requerir resoluciones de
nombres speciales en la subclase. Lo que supone un desorden agradable.
Desde un punto de vista meramente teórico, la herencia múltiple es viable pero
poco práctica. Un diseño que incorpora herencias múltiples suele ser inservible. La
mayoría de las razones radican en la delegación impropia de las responsabilidades
o en la asignación de nombres a las clases. La causa más común es la abstracción
v delegación impropias.. A menudo, las dos clases padre tienen un conjunto común
de comportamientos que deberían extraerse y colocarse sobre ellos como una
superclase.

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.

Incremento del tamaño del código


Las herencias, abstracciones, sobrecargas y plantillas constituyen ayudas
tremendas para hacer su trabajo más rápido. Sin embargo, puede toparse con otro
problema: el incremento del tamaño de los códigos fuente y objeto. El incremento
del tamaño de un objeto puede que no sea importante si dispone de suficiente
memoria. Pero cuantas más líneas de código escriba, mayor será la probabilidad
de que existan errores en el programa.
Dependiendo de la complejidad de sus clases, puede observar un incremento de
entre un 20% y un 50% en el número de líneas de código que tiene que escribir
con respecto a un diseño modular. Se debe principalmente a la sintaxis y el soporte
del lenguaje. Con el sacrificio de incrementar la complejidad del archivo fuente,
obtiene una mayor capacidad y una verificación de tipos más fuerte.
Los primeros programadores de objetos se sorprendieron por el incremento del
tamaño del código objeto. En algunas ocasiones, para idéntica funcionalidad, la
conversión de C en C++ el programa resultante incrementó 10 veces su tamaño.
Éste es un ejemplo extremo, pero puede esperar un incremento de entre 2 y 5
veces el tamaño del programa.
Puede reducir estos incrementos haciendo lo siguiente:

• No sobrecargue los operadores.


• No intente unificar todas las clases en jerarquías de herencia individuales.
• No utilice la herencia virtual.
• No ande con rodeos con los métodos virtuales.
• No utilice nunca la herencia múltiple.

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.

El dilema de la administración de proyectos


Un programa objeto presenta un conjunto de problemas nuevo e inusual para el
id ministrador. Éste tiene que coordinar a más gente a la vez y comprobar que el
rabajo sigue adelante. Especialmente cuando están involucrados múltiples equipos
a menos que el proyecto tenga un administrador de tecnología, el administrador leí
proyecto tiene que coordinarse con mucha más gente que la que está trabajando n
el proyecto.
El administrador del proyecto de objetos tiene que ofrecer todo a todo el mundo,
tener habilidad en delegación, coordinación, organización e incluso negociación es
crítico para el éxito del proyecto. Recuerde que el éxito de un proyecto significa
mucho más que entregar un producto dentro de las restricciones temporales y
económicas impuestas. Si se disgrega el equipo una vez completado, si la
documentación no representa el producto o si el cliente no está satisfecho o se
siente vapuleado, el proyecto no tiene éxito.
Esta sección simplemente toca algunos de estos aspectos. Debe recordar que
este roblema es mucho mayor de lo que se puede esquematizar en unas páginas.
Cada dministrador debe afrontar la realidad de que el éxito del proyecto está en el
equipo, no en el producto.

Obtención del personal apropiado en el momento


oportuno
El mejor proyecto tiene el mejor personal entrando en escena y saliendo de ella,
a formación es un aspecto secundario y debe coordinarse con los tutores. La razón
or la que el producto no es el aspecto determinante del éxito es que el producto fue
solicitado y aprobado por los que deberían utilizarlo. Se aceptará si el resultado
coincide con lo que los usuarios tienen en mente.
El éxito del equipo es más importante y el administrador no tiene en un equipo
sólo programadores, sino que tiene más personal. A continuación se muestra una
lista del personal del equipo y cuándo se necesitan:

• Patrocinador del proyecto. Proporciona dinero para el proyecto. Necesita dirigir


los hitos principales del proyecto.

• Abogado del cliente. Interactúa con el cliente manteniéndose en contacto con


él. El abogado debe estar muy familiarizado con el pensamiento y la forma de
trabajo de los clientes. Este conduce los requerimientos del producto y está

329/512
involucrado a lo largo de todo el proyecto.

• Analistas de negocio. Recopilan y separan la documentación de los


requerimientos en sus partes componentes. Éstos están involucrados durante la
recolección y el análisis de requerimientos y deben mantenerse en contacto con
los abogados del cliente. Más adelante, son muy útiles durante la prueba.

• Analistas técnicos. Lleva el análisis al proceso de diseño. Éstos asocian el


formato modelado del análisis con el mundo real y deben mantenerse en
contacto regular con los analistas de negocio.

• Representantes de la calidad. Recopilan y archivan la documentación y dirigen


el proceso de prueba. Una vez que los analistas de negocio solidifican los
requerimientos, el representante de la calidad comienza a generar las pruebas
de aceptación. Cuando se completa el diseño, puede comenzar a trabajar en
las pruebas de límites y acentuación.

• Programadores. Toman el diseño y lo codifican en el lenguaje seleccionado.


• Sistemas de legado (si son necesarios). Lo utilizan los analistas técnicos como
recurso para asegurar que concuerdan las interfaces.
Este es un equipo mucho más grande de lo que muchos administradores creen.
Si el administrador del proyecto ejecuta el proceso adecuadamente, cada persona
entra y sale de escena y coloca una marca de aceptación en el producto.

Fenómeno WISCY ("Whisky")


Con todo el personal que necesita estar involucrado, el administrador suele
afrontar el problema "¿Por qué no está codificando todavía el gestor de
administración del sistema?" (WISCY, "Why Isn't Sam Coding Yet?"). Tanto el
patrocinador del proyecto como el abogado del cliente suelen realzar este aspecto.
Eso es natural: uno desea ver los resultados de su dinero y el otro desea ver el
producto.
De acuerdo con las mejores costumbres de la industria, sólo se dedica un 20%
del tiempo total del proyecto a la codificación. El otro 80% del tiempo se divide a
partes iguales entre análisis/diseño y prueba (40% para análisis y diseño, 20% para
codificación y 40% para prueba). Normalmente, la industria americana del software
se ajusta más a la proporción 40% para análisis y diseño, 34% para codificación y
33% para prueba. De esta forma, como máximo la codificación puede consumir el
33% del tiempo. ¿Por qué no empezamos a contar el tiempo desde que se
resuelven :odos los requerimientos y definiciones y el diseño?
Para tranquilizar al patrocinador del proyecto y al abogado del cliente, se les
permite conocer el aspecto de la planificación y se les mantiene informados del
progreso. Involúcrelos en las presentaciones y muéstreles la documentación.
Permita-es conocer la importancia de sus proyectos para usted, el administrador.
Si eso no funciona, puede utilizar el diseño Just-ln-Time (JIT). El diseño JIT le
permite una codificación rápida pero puede introducir huecos. Cuando ocurra eso,

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.

Definición de "integración del sistema"


Cuando el proyecto construye el producto, compone y ensambla las piezas una
a una. La ultima definición que enturbia un poco los objetos aparece cuando las
piezas se ajustan como un sistema. Cuando se trabaja con marcos de trabajo
internos o ajenos, podría considerar cualquier ensamble como un sistema. Cor otro
lado, algunos proyectos se dividen en programas que cooperan de forma local o
distribuidos en la red. De esta forma, ¿cuándo integra realmente el sistema?
Realmente, la integración del sistema se realizó hace mucho tiempo. Esto sólo
ha llegado a ser aparente dentro de los últimos años de Internet (ciclos de vida del
producto de 3 meses). Para las empresas que siguen utilizando ese término, el
significado es completamente diferente del original. Para los objetos, la integración
se produce siempre que entregue un trabajo fácil o demasiado frío.

Resumen: tenga cuidado con las pendientes


La tecnología de objetos proporciona una buena potencia, pero ésta tiene un
coste incrementa!. El incremento del coste proviene de la teoría, tecnología y
persona!.
Como cuando escala una montaña con mucha pendiente, tiene que ir
avanzando :on cuidado y esperar un pequeño resbalón en cualquier momento.
Algún resbalón puede tener su origen en el intento de hacer demasiadas cosas
a a vez. Intente controlar lo más urgente. Eduque y comparta el proceso de
aprendizaje. La tarea más importante que puede realizar es involucrar a sus
clientes. Si los :lientes se sienten integrados e importantes, serán felices con los
resultados que obtengan.
Lo siguiente en importancia es trabajar con el equipo extendido. Intente localizar
as dificultades de la tecnología de objetos y permita que cada persona sea
conscien-e de su importancia dentro del proyecto. De la misma forma en que un
director ntroduce y extrae actores de una escena, puede asignar responsabilidades
a ios participantes cuando sean necesarios.
El éxito del equipo supera al éxito del producto puesto que el producto es un
leseo conocido. El resto ofrece una satisfacción más duradera. Un cliente

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

Sockets avanzados: más


prestaciones
En esta parte
15 Encapsulado de red con Llamadas de procedimiento remoto (RPC)
16 Cómo añadir seguridad a los programas de red y SSL
17 Cómo compartir mensajes con multidifusión, difusión y Mbone
18 La potencia de sockets raw
19 IPv6: la próxima generación de IP

334/512
Capitulo 15

Encapsulado de red con


Llamadas de procedimiento
remoto (RCP)

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

Desde una perspectiva de usuario desarrollador, el conocimiento de todas las


excentricidades de la programación de red puede resultar mucho más laborioso de
lo que se desea. A veces, usted o sus peers (los usuarios desabolladores) sólo
desean centrarse en el desarrollo de los programas a mano y dejar los detalles de
la red a las bibliotecas o herramientas establecidas. Las llamadas a procedimiento
remoto (RPC) se ajustan perfectamente a este hecho.
Las RPC manejan toda la conexión y la traducción de datos para sus usuarios
desarrolladores. Desde la perspectiva de los usuarios desarrollado res, la
componente red es una pequeña parte del conjunto de características del
programa. Para ellos dedicar más de una porción pequeña de su tiempo a escribir
y probar las interfaces de red sería muy costoso e ineficiente.
Una tras otra, podría escribir herramientas (llamadas de servicio) que
proporcionen a sus programas formas robustas de comunicación. Estas
herramientas deberían interactuar con el cliente, el servidor y los peers.
Dependiendo del protocolo, sus llamadas de servicio pueden ser complejas,
requiriendo que sus herramientas ofrezcan grandes cantidades de comprobaciones
y retransmisiones (UDP), o simples (TCP).

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.

Repaso del modelo OSI


En el Capítulo 5, "Explicación del modelo de capas de red", leyó que el modelo
de red jerarquiza todas sus características y operaciones, ocultando información al
usuario y al programa. La operación incluye información sobre la forma en que la
red pasa realmente los datos de un programa a otro. El modelo jerárquico de red
también incrementa la probabilidad de que el destino reciba el mensaje. De
acuerdo a la teoría de redes, ningún usuario interactúa con un nivel distinto del
nivel de aplicación (nivel 7).
La pila del protocolo IP, sin embargo, se detiene efectivamente en el nivel 4 (el
nivel de red). TCP es el último protocolo y tiene mayor fiabilidad que la pila del
protocolo TP. Puede crear un socket que actúe casi como un flujo de archivo. Lo
que le ofrece una gran potencia y flexibilidad. El protocolo cede la responsabilidad
de la implementación a los niveles superiores de la aplicaciones.
Esto presenta un pequeño problema. Sin la definición de los niveles superiores,
muchas de las aplicaciones que han surgido suelen duplicar el esfuerzo. Considere
los protocolos FTP y Telnet. Cada uno proporciona una fase de autenticación, el
nombre de conexión del usuario. Aunque las interfaces de conexión pueden ser
pequeñas y mínimas, duplican del procedimiento. Por lo tanto, esto complica los
métodos a utilizar por el programador profesional. Si desea incorporar la
autenticación en programa cliente, básicamente tiene que escribir la suya propia.
Puede proporcionar parte de esta funcionalidad usando sus propias interfaces.
Asi, otros programas pueden usar estas funciones. Al igual que los diversos
modelos de red, puede desear que las interfaces sean transparentes a la red. Los
programas transparentes a la red nunca fuerzan al usuario (o programador) a
interactuar con la red. La idea global es quitarle a la conexión de red todo el
esfuerzo posible.

Comparación de programación de red y


procedimental
Siempre que comienza a considerar la interfaz de red, abre un ámbito de
programación que los ingenieros de software raramente alcanzan. El reto que esto
supone es muy interesante, pero no es suficiente para comenzar simplemente a
programarlo. Debe adelantar la forma en que el cliente y el servidor (el sistema)
van a funcionar juntos.
La programación de red no se parece a la programación procedimental. La
programación procedimental es muy indulgente. Si escribe un programa que
utilizan dos personas (un cliente v un servidor), éstas esperan una calidad superior
y una mejor recuperación de los errores.

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.

Limitaciones del lenguaje


El primer problema que se presenta es la propia interfaz del lenguaje. El
lenguaje de programación C permite realizar cosas que no son significativas en una
interfaz de red.
En primer lugar, la información que se envía debe estar en formato valor. Los
punteros no son significativos en un entorno de red por razones obvias; la dirección
de una computadora no coincide con la de otra. Su programa debe pasar todos los
datos por valor. Esto va contra la idea de algunos programadores. A nadie le gusta
pasar una estructura o un array por valor; la copia de datos consume memoria y
tiempo de CPU.
COPIA DE DATOS EN C
El lenguaje C supone que no desea copiar tipos de datos grandes debido al potencial
requerido en cuanto a rendimiento para desplazar los datos. De esta forma, intenta pasar
la información mediante un puntero o referencia siempre que sea posible. Todos los
lenguajes complejos admiten escalares (tipos simples como int y char) y vectores (arrays
y estructuras). Por defecto, C pasa los escalares por valor. Sin embargo, algunos
compiladores se quejan cuando intenta pasar una estructura por valor. Para pasar un
array por valor, tiene que declarar un tipo nuevo o utilizar una estructura. En todos los
casos, el programa utiliza la pila hardware para almacenar sus datos y debe asegurarse
de que tiene suficiente espacio en la pila para acomodar los datos.
Esto no supone el problema de obtener información a menos que permita que el
valor devuelto sea el resultado. Puede decantarse por pasar todo por valor, pero en
algún momento el programa no tiene que enviar los datos. Con este enfoque,
continúa pasando sus parámetros por referencia, pero el programa copia los datos
al menos en una etapa muy postrera.
El siguiente ejemplo muestra un prototipo de una interfaz de la llamada de
servicio getphoto(). Los tipos image_t y host_t son tipos personalizados que crea.
Si es aventurero, puede devolver el resultado también por valor. Aún así, podría
dejar abierto el valor de retorno para indicar un error; si devuelve una estructura o
un tipo por valor, no puede detectar errores efectivamente.
/*************************************************************/
/*** Ejemplo de paso por valor llama a una servidor ***/
/*** de cámara para obtener una foto reciente. ***/
/************************************************************/
image__t *getphoto(host_t host);

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) */

int getuserinfo(char* user, char* host, userinfo_t *data);


Este ejemplo es más típico. Organice los parámetros en el siguiente orden: sólo
de entrada, entrada/salida y sólo de salida. Cada parámetro se pasa por referencia
y la llamada supone que el usuario lee las notas de publicación.

Mantenimiento de sesiones conectadas


La comunicación entre cliente y servidor a nivel de llamada de servicio supone
canales independientes. No es deseable que un usuario se conecte más de una
vez o incluso hay que ser consciente de que el programa cliente realice múltiples
conexiones. Esto forma parte del concepto de mantenimiento de sesiones de red.
El formato más sencillo de una llamada de servicio es sin estado, no requiriendo
más del servidor que la realización de algunas tareas. Pero si desea crear una
sesión, tiene que realizar un trabajo adicional tanto en el cliente como en el
servidor.

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.

Suministro de métodos middleware


Puede escribir un conjunto de llamada* de servicio que interactúen con un
servidor. Las llamadas de servicio aportan una interfaz pasiva (o activa) entre la
red, ocultando la implementación de la conexión de red. En la mayoría de los
casos, la interfaz, o middleware, es pasiva, sin realizar ningún cálculo computacional
(sólo la copia y traducción de información de un lugar a otro).
Middleware es un proceso importante para los productos modernos. La mayoría
de las aplicaciones interaetúan con él desde varios niveles v requerimientos. Hacer
sus componentes middleware muy robustas es crítico.
El primer paso en la creación de llamadas de servicio middleware buenas y
sólidas es definir una interfaz duradera. No puede esperar escribir una interfaz para
que otros utlicen lo que intenta revisar dentro de seis meses. La interfaz a corto
plazo para una tecnología fundamental como las redes es probable que no sea

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.

Stubbing en las llamadas de red


Si está escribiendo una interfaz pasiva, la interfaz entre su programa cliente y
las funciones de soporte de red sólo necesitan ser un conjunto de sencillas
llamadas a procedimiento. Estas llamadas a procedimiento implementan diferentes
partes de la fundamentación. Los procedimientos también pueden ser rutinas que
hacen algo más que volver a empaquetar los parámetros.
Los ejemplos getphoto() y getU5erinfo(), discutidos anteriormente, son ejemplos
de interfaces que no hacen nada más que recuperar datos. Estas llamadas fácil
mente podrían formar parte de la fundamentación referente a la seguridad; cuando
un oficinista de seguridad necesita identificar a alguien por la cara y las
credenciales, utiliza una herramienta que recupera y visualiza esa información.
Los sftibs de red identifican lo que el llamador espera y lo que el servidor
necesita para satisfacer la petición. La interfaz también debe ser simple; cuanta
más información tenga que proporcionar su desarrollador, más difícil será su uso.

Adición de la implementación de llamadas de


servicio
Una vez que la llamada de servicio del cliente acepta la petición, la llamada
tiene que empaquetar los datos y embarcarlos hacia el servidor. El servidor, por su
parte, implementa las llamadas de servicio del servidor que aceptan y
desempaquetan los datos y la petición. Éste es el corazón del middleware.

Definición de los servicios del cliente


Eos servicios del cliente suelen estar basados en el procesamiento por
demanda; la interacción con la red sólo está activa durante la petición. Las
necesidades básicas del generador de peticiones de servicio son el destino, la
petición y los datos. La mayoría de las peticiones son transaccionales, de forma
que el middleware no tiene que soportar nada más que el empaquetamiento y el
envío de la información. El módulo de llamada de servicio está inactivo entre
peticiones. UDP se ajusta bien a este caso puesto que pasa un único mensaje
entre cliente y servidor
Puede encontrar ocasiones que requieran una conexión abierta (una
conversación en lugar de una simple petición). TCP se adapta mejor a esta
circunstancia. De hecho, el papel de los servicios del cliente cambia ligeramente
puesto que el canal tiene que mantenerse abicrlo y puede permitir el
mantenimiento de más de una conversación con más de un servidor. Junto a las
necesidades básicas de una conexión por demanda, su módulo debe hacer un
seguimiento de la sesión y la conexión. Los módulos de red que soportan este nivel

341/512
de conectividad suelen añadir dos interfaces más, una llamada de servicio de
sesión abierta y otra cerrada.

Respuesta con servicios del servidor


Los servicios del servidor actúan de la misma forma que los del cliente. Justo
cuando puede crear módulos de red para el programa del cliente, puede escribir
módulos de red para el servidor. Los servidores suelen hacer mucho más que serv
ir datos. Sin embargo, las similitudes entre los módulos de servicio de red de
cliente y servidor terminan repentinamente allí.
El primer gran aspecto que se le presenta cuando comienza a escribir el
proveedor de servicios del servidor es la consideración de que lleguen varias
peticiones al mismo tiempo. Podría aceptarlas y procesarlas sequencialmente
(recuerde que el núcleo encola todos los mensajes hasta que los lee la llamada al
sistema recv(j). Sin embargo, no puede estar seguro del tiempo que la petición
tendrá que esperar en la cola, que puede llegar a ser muy largo.
La multitarea del servidor reduce los retardos de un sistema transaccional. Linux
se ejecuta de forma más eficiente cuando se utiliza la multitarea en la ejecución de
los programas (utilizando threads y procesos). Cada petición nueva crea una tarea
para atenderla. La organización difiere un poco de la jerarquía de programa que
muestra el Capítulo b, "Generalidades sobre el servidor".
1. El programa cliente llama al módulo de red del cliente.

2. El módulo de red del cliente (generador de peticiones de servicio) empaqueta


la petición y los datos y los envía a través de la red. Espera una respuesta.
3. El módulo de red del servidor (proveedor de servicios) acepta la petición y la
pasa al programa servidor.
4. El programa servidor (procesador de peticiones) procesa la petición y
devuelve los resultado.
5. . El módulo de red del servidor acepta el resultado, lo empaqueta y lo lanza a
la red.
6. El módulo de red del cliente acepta el mensaje con el resultado, lo
desempaqueta y lo devuelve al programa cliente.
7. El programa cliente acepta el resultado.

Puede observar que la ubicación en la jerarquía del módulo de servicios de red


difiere entre el cliente y el servidor. A menudo, el módulo de red del cliente es una
hoja (el nivel más bajo) de la jerarquía del programa. El programa servidor, por el
contrario, coloca el módulo de red en la cumbre de la jerarquía, una vez realizadas
todas las iniciaciones. La escritura de módulos reutilizables para esta interfaz
invertida puede resultar problemática.
Hay dos métodos que puede usar para resolver el problema de la interfaz
invertida. El primer método consiste en definir un procedimiento externo al que

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.

Implementación del nivel de presentación


Durante el proceso de aceptación y paso de los datos, suele tener que copiar los
datos en mensajes y pasarlos. Sin embargo, no puede estar seguro de que todos
los clientes se comuniquen como su servidor. Un ejemplo obvio es endianness. Los
programas para Intel que se comunican con los programas para Alpha tienen
órdenes diferentes de los bytes para datos binarios. Otro ejemplo es ASCII frente a
EBCDC.
El nivel de presentación del modelo OSI crea una forma de convertir la
información de un formato a otro. Cuando crea su fundamentación, necesita
considerar los tipos de clientes y servidores que puede ejecutar su programa. Al
igual que para el primer paso, la independencia del orden de los bytes es muy
importante.

Creación de RPC con rpegen


Las distribuciones de Linux suelen incluir una herramienta, rpegen, que le ayuda
a escribir sus propias RPC. Esta herramienta simplifica ampliamente la
programación de RPC y ofrece formas de ayudar a construir su programa. Esta
sección muestra la forma de utilizar esta herramienta y describe algunas de las
sintaxis involucradas.

Lenguaje de interfaces de rpcgen


La herramienta rpegen es otro traductor de lenguaje que prolifera en el mundo
de UNIX. Este lenguaje permite definir la interfaz y los datos que los programas
utilizan y transmiten a través de la red. El archivo, por convenio, utiliza la extensión
.x en su nombre. Su formato es muy similar a C, con algunas excepciones.
COM Y COM+ DE MICROSOFT
El uso y el aprendizaje de la forma de manipular las definiciones RPC es útil en otras
áreas. La "nueva" definición de COM y COM+ de Microsoft esencialmente utiliza los
protocolos de RPC. De hecho, puede leer el formato de archivo X en uno de los archivos
de proyecto COM. Éste es ligeramente diferente y tiene algunas extenciones, pero sigue
el mismo flujo básico.

Generación de la interfaz básica


Por ejemplo, si desea obtener la hora del servidor en formato UTC (número de

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;

La interfaz XDR (traducción de datos) de rpegen se encarga de toda la


conversión.
El siguiente paso es ejecutar rpegen:
rpegen -a rpctime.x
La opción -a crea todos los archivos que pueda necesitar para crear y ejecutar el
cliente y el servidor. Sin la opción -a, sólo obtiene los archivos de interfaz del
cliente (rpctime_clnt.c) y el servidor (rpctime_svc.c). Si definió los tipos, obtiene el
archivo XDR (rpctime_xdr.c). No revise ninguno de estos archivos, puesto que se
generan dinámicamente desde el archivo *.x. Con la opción -a, rpegen genera
archivos de ejemplo para el cliente (rpctime_client.c) y el servidor (rpctime_ser-
ver.c) y un Makefile.rpctime compila todo. Esto facilita mucho el desarrollo.

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");
}

/*... Añada el siguiente código ...*/*/


else
printf("%d ]%s", *result_l, ctime(result_l));
El código añadido simplemente imprime el resultado de la llamada. El código del
servidor es similar:
/************************************************************************/
/*** Fragmento de código del servidor para la RPC de la hora ***/
/************************************************************************/
static long result;
time(&result); /*---Código personalizado---*/*/
return &result;
La herramienta rpcgen añade una sección comentada que indica que inserte el
código del servidor entre la declaración de la variable y la sentencia return. Cuando
lo haya hecho, compile y pruebe lo siguiente:
make -f Makefile.rpctime
./rpctime_server & ./rpctime_client 127.0.0.1
El resultado es el tiempo en segundos y en formato ASCII estándar. La herramienta es

345/512
muy sencilla de utilizar y desarrollar.

Uso de archivos X más complicados


Una vez que haya tenido éxito con rpegen, puede utilizar alguno de sus tipos. La
herramienta rpegen añade dos tipos nuevos: string y bool_t. El tipo bool_t permite que C
disponga de valores booleanos. Sólo se permiten los valores 1 y cero, aunque el tamaño real
pueda ser mayor.
El tipo string requiere una explicación un poco más detallada. El lenguaje C le
permite utilizar char* para representar un puntero a carácter (char), un array de
caracteres (char) o una cadena terminada con un carácter nulo (NULL). Es
demasiado ambigüo. De esta forma, se creó un tipo, string, para reducir la
ambigüedad. El formato tiene el siguiente aspecto:
string filename<100>; /'---Hasta 100 caracteres de longitud---*/*/
string myname<>; /*---Cualquier longitud---*/*/
Todas las cadenas tienen Longitud variable. Si conoce la longitud máxima que
va a tener la cadena, puede ser conveniente definirla con esa longitud. Esto
garantiza que el XDR sólo transmita el número de bytes especificado.
De forma adicional, puede incluir uniones en el archivo X. Estas uniones tienen
un aspecto similar a un cruce entre Pascal y la sentencia switch de C:
/*****************************************************/
/*** Fragmento de código para la unión RPC ***/
/*****************************************************/
unión proc_res switch (int Err)
{
case 0:
string Data<>; /*---Si Err es 0, el valor es Data---*/*/ default:
void; /*---Si Err es otro valor, no hay datos---*/*/
};

En este ejemplo, la unión puede devolver una cadena o nada, dependiendo de


la variable clave Err. La herramienta rpegen convierte esto en una estructura con
una unión:
/*******************************************************/
/*** Fragmento de código para el genrpe RPC ***/
/*******************************************************/
struct proc_res
{

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.

Incorporación de registros y punteros a la lucha


El último tipo debería resultar muy familiar. La estructura (struct) es idéntica a la
declaración de registros en C y se reliena con el mismo propósito. Pero, la
estructura (struct) se utiliza para un propósito adicional. El uso del conjunto de
herramientas rpegen tiene una limitación significativa (aunque no intratable): no
puede pasar más de un parámetro a través de una llamada de servicio. Todas las
llamadas con parámetros múltiples deben utilizar registros para pasar los datos
como entrada y como salida.
Una definición de estructura sería la siguiente:
/****************************************************/
/*** Fragmento del archivo X RPCList RPC ***/
/***************************************************/
typedef struct NodeStruct 'TNode;
struct NodeStruct
{
string Name<>;
TNode Next;
};

Puede observar que la estructura contiene un tipo puntero (TNode) en su


cuerpo. Si recuerda, en la parte anterior del capítulo se indicó que no puede enviar
punteros a través de la red (los espacios de memoria no son los mismos, de forma
que no tiene sentido una referencia de un puntero). El XDR rpegen es muy
inteligente; éste copia y traduce todas las referencias de punteros en una
estructura compleja de forma que el cliente o el servidor puedan comprender los
resultados. Puede enviar árboles, listas enlazadas y cualquier almacenamiento en
memoria dinámico y no cíclico.
La única peculiaridad es la sintaxis del archivo X. Un tipo puntero debe
declararse como typedef:
/************************************/
/*** Ejemplo de sintaxis RPC ***/
/************************************/
typedef struct NodeStruct TNode;

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.

Creación de llamadas con estado con


conexiones abiertas
El paso continuo de información desde el cliente hacia el servidor involucra
mucho más que los datos. Cada programa suele tener que realizar un seguimiento
del estado de la máquina con el fin de responder correctamente a una petición. Las
secciones previas se centraron en programas que no suponen nada acerca de
otras llamadas: también se refieren a otro tipo de llamada. Cada llamada suele ser
independíenle de las demás (una conexión sin estado). Esto no es muy útil si tiene
que mantener algún tipo de información de llamada a llamada.
Por falta de un mejor término, una conexión con estado debe retener algún tipo
de información de forma que puedan producirse las comunicaciones apropiadas.
Un par de ejemplos de esto son las conexiones (la persona debe estar conectada
antes de poder recibir ningún dato) y las transacciones de bases de datos (el
servidor no puede transmitir la tabla entera de una vez).
El primer procedimiento de acción es responder a estas preguntas: "¿Por qué
son comunes las conexiones sin estado?" y "¿Por qué son una carga las
conexiones con estado?"

Diagnosis del problema del estado


La conexión sin estado es muy sencilla. El cliente envía información y el servidor
responde. SÍ se pierde la pregunta, el cliente simplemente la vuelve a formular.
Este enfoque resulta muy pobre para las necesidades propias de una conexión con
estado.
La conexión con estado tiene que mantener una relación. Como se observó en
los capítulos y secciones anteriores, un servidor y un cliente deben asegurarse de
que un acuerdo es determinista y recuperable. Entre los problemas con la conexión
con estado se encuentran los siguientes:

• Estado actual La sesión debe mantener un seguimiento de las acciones ya


realizadas. A nadie le gusta responder preguntas o pasar por el aro una y otra
vez.

• 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.

• Recuperación del estado. Si se pierde un estado, puede que el sistema tenga


que volver (deshaciendo los pasos intermedios) a un estado previo conocido.
Aún cuando disminuyan las conexiones, el cliente y el servidor deben
mantenerse donde terminaron. Como último recurso, ambos sistemas deben
estar capacitados para deshacer completamente las transacciones hasta el
punto de desconexión.
El problema del estado debe dirigir cada uno de estos aspectos. En algunos
casos, la recuperación es discutible. Pero, por norma general, el cliente debe
identificar unívocamente la sesión actual del servidor y viceversa.

Recuerde dónde está


La conexión con estado más simple es aquella que realiza un seguimiento del
estado actual. Puede hacerlo con un identificador de sesión. Este identificador
debe estar codificado de forma que sólo el cliente y el servidor conozcan su
trascendencia. Como ejemplo sencillo, el cliente solicita la primera fila del resultado
de una consulta a una base de datos. Para obtener la siguiente fila, el servidor
debe estar capacitado para asociar la siguiente llamada con la consulta inicial.
Una forma simple de acceder a este problema consiste en dividir las llamadas
en al menos tres etapas: iniciación, transacción y finalización. El único propósito del
estado de iniciación sería el establecimiento de la sesión y el comienzo del
procesamiento. Las llamadas basadas en transacciones manipulan el estado
comenzado por la iniciación. Y el estado de finalización limpia las transacciones
pendientes, cierra la conexión y asegura los datos.
Una vez cerrada la sesión, el identificador de sesión pierde su validez.

Seguimiento de una ruta específica


Los tres estados principales (inicia liza ció n, transacción y finalización)
constituyen el punto de partida del suministro de un mapa de estados. El mapa de
estados muestra todos los estados y cómo se pasa de uno a otro. Dada la
información actual acerca de la sesión, el servidor y el cliente saben exactamente
lo que pueden hacer y cómo alcanzar el estado siguiente.
El determinismo en un mapa de estados significa que debe tener estados únicos
y transiciones únicas. Específicamente:

• Ninguna transición puede conducirá dos estados distintos. Ejemplo: si el


servidor cambia de directorio, el cliente debe reflejar el cambio.

• Dos estados no pueden tener ¡a misma transición. Ejemplo: no puede haber


transacción si el cliente y el servidor esperen un mensaje.
Sin la previsibilidad que aporta el determinismo, no puede recuperarse desde
ningún estado erróneo.

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:

• Volver a establecer la conexión. Fuerza al cliente a volver a iniciar la sesión. El


servidor deshace todas las transacciones realizadas en la sesión anterior.

• Deshacer e! estado. Tanto el cliente como el servidor vuelven a un estado


conocido. Las transacciones intermedias se descartan. Si el estado conocido
interfiere con el cliente o con el servidor, seleccionan un estado diferente. Si
todos los demás fallan, vuelven a establecer la conexión.

• Forzar el estado. El servidor indica al cliente cuál es el estado correcto. El


cliente responde si lo acepta. Si no, ambos ejecutan la operación de deshacer
un estado.
Dependiendo de lo críticas que sean las transacciones, puede seleccionar una
idea por encima de otra. Sin embargo, cada idea depende de la anterior. No es
posible seleccionar fácilmente una idea sin seleccionar además alguna otra (u
otras! más simple.

Resumen: creación de una caja de herramientas


para RPC
Puede utilizar su experiencia en la programación de sockets para ayudar a otros
usuarios-desarrollad ores que deseen beneficiarse de la programación de red sin
tener que preocuparse de los detalles de implementación. Las RPC presentan dos
formas: la creación de interfaces ampliables o el uso de herramientas estándar
como rpcgen.
La creación de herramientas ampliables le permite definir el funcionamiento de
la interfaz y personalizar las políticas de transporte. Las interfaces no están
restringidas, permitiéndole enviar tantos parámetros como desee y en el formato y
orden que mejor le parezca. Sin embargo, no puede enviar punteros, y debe copiar

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

Cómo añadir seguridad a los


programas de red y SSL

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

Un sistema seguro no aporta más funcionalidad que un sistema inseguro, sin


embargo, los datos que ofrece son más fiables. Un sistema inseguro ofrece
servicios y la posibilidad de compartir información, pero es más propenso a los
ataques convirtiéndolo en un sistema no fiable. Esta no-fiabilidad genera cierta
inseguridad sobre la validez de los servicios e, incluso, puede perjudicar la
reputación o sello de cualquier compañía.
En la mayoría de los casos, se asume eme la seguridad se tiene en cuenta
cuando se desarrolla el producto. En muy pocas ocasiones, los requerimientos de
la empresa especifican expresamente: "El servidor debe ser ímprenetable frente a
los ataques por medio de los servicios denegados." La seguridad es un
requerimiento fundamental que el cliente debe asumir como parte del producto.
Tenga claro que debe garantizar la seguridad en todos sus programas. Sin
embargo, ta mayor dificultad con que se encontrará es decidir cuanta seguridad va
a ser necesaria.
Este capítulo presenta la mayoría de los cuestiones relativas a la seguridad en
la programación: conceptos, términos, impacto de Internet, métodos e
implementación, así como la seguridad a nivel de sockets (SSL - Secure Sockets
Layer).

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.

Todo está visible


Cuando escriba programas de red debe recordar siempre una regla importante:
Internet no puede garantizarle un canal privado. AL igual que cualquier subasta en
el Stock Exchange de Nueva York, cualquier persona que se encuentre allí, podrá
escucharle. Sin embargo, la mayoría de la veces, estas personas no se interesan
por los mensajes. No obstante, es posible que, en otras ocasiones, existan
fisgones interceptando o, incluso, revisando estos mensajes de forma intencionada.
De forma similar, el mensaje puede moverse hacia o desde zonas de confianza.
Una zona de confianza es una red que una compañía considera segura. Por
ejemplo, dos zonas de confianza habituales son los backbones de AT&T y US
Sprint. Otras corporaciones importantes contratan estas redes mediante líneas
dedicadas. Son de total confianza, ya que los hosts de la zona ofrecen garantía de
privacidad.
Las corporaciones crean o contratan zonas de confianza formando parte de sus
propias redes o intranets. La intranet se corresponde con el sistema de red propio
de la corporación. Dos o más corporaciones forman una extranet cuando colaboran
y comparten información en una red especial (que, a menudo, utiliza el mismo
medio físico). De todas formas, los límites de definición de Internet, intranet y
extranet comienzan a estar difusos puesto que cada compañía necesita obtener, de
forma más rápida y efectiva, la información que pasa de un host a otro.
De forma alternativa, los mensajes individuales pueden desplazarse por Internet,
equivalente a tierra de nadie, donde están sujetos a escucha. Incluso las
herramientas restringidas anteriormente a root, ahora están accesibles a todo el
mundo. Utilizando estas herramientas, un superusuario menos benevolente podría
acceder, e incluso, revisar los mensajes. El poder de un sistema operativo flexible,
como Linux, puede suponer también su principal problema.
La mejor solución es ser consciente de la existencia de riesgos y proteger los
datos que necesite enviar. Ponga en práctica soluciones adecuadas, como
enmascarar siempre la información del nombre de usuario y contraseña y no enviar
nunca un número de tarjeta de crédito sin cifrarlo.

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.

• Intervenciónde la línea. Un espía escucha los mensajes esperando recibir


algunos datos importantes a utilizar. Los sniffers de red entran dentro de esta

356/512
categoría.

• Corte de la línea. Un programa malicioso limita el acceso al servidor o cliente.


Esto provoca retraso en la réplicas o respuestas por parte de la red o host, e
incluso, puede eliminarlas. Dos ejemplos son el ataque mediante ping v Denial-
of-Service (DoS).

• Secuestro de la línea. En lugar de impedir las comunicaciones, un programa


podría obtener el control de la comunicación entre host y cliente. Las pilas de
algunos protocolos, como el caso de Linux, dificultan esta posibilidad debido a
la forma que tienen de generar las numeraciones de secuencias TCP.
Indudablemente, pueden aparecer más formas de intrusión de la seguridad,
pero todas se ajustan, más o menos, a una de estas categorías. (Los virus son un
problema completamente diferente. Pueden provocar problemas de seguridad,
pero no constituyen ataques de seguridad directos.) Las cuestiones de seguridad
para la programación en red se aplican a todo dispositivo conectado a la red. La
seguridad es bastante independiente de la implementación del sistema operativo
(para una excepción, véase la siguiente sección).
Linux tiene seguridad predefinida en el sistema y, de hecho, constituyó un
diseño fundamental. El efecto es obvio: un sistema muy seguro y fiable. Otros
sistemas operativos que adoptan esta filosofía tienen un nivel importante de
seguridad. Por supuesto, aquellos sistemas que no siguen esta filosofía nunca
podrán ser tan seguros. Además, la seguridad en los programas de red es baslante
independiente de la filosofía de los fundamentos del sistema. Por tanto, el grado de
seguridad de red confía en los programas que escriba.

Pirateo del TCP/IP


Hoy en día, la mayoría de las computadoras utilizan protocolos de Internet.
Incluso Novell y Microsoft han comenzado a adaptar sus propios protocolos para
facilitar el acceso a Internet. Ahora, ¿cómo de pirateable es el IP?
Actualmente, el Protocolo de Internet es más fácil de piratear. Los fisgones de la
red, como se presentan en este libro, constituyen un buen ejemplo de lo fácil que
resulta examinar los mensajes de red. Un pirata de protocolos podría simular todos
los protocolos utilizando sockets raw (recuerde que se trata de un protocolo
privilegiado) y desbordando el recipiente con mensajes ficticios. Los protocolos
más sencillos de piratear son ICMP, UDP y RDP. (RDP (Reliable Datagram
Protocol, Protocolo de datagramas fiable) no está todavía implementado en la
mayoría de los sistemas operativos.)
Cada protocolo en la pila TCP/IP presenta sus vulnerabilidades. Algunos
expertos en seguridad afirman que el TCP/IP es el más difícil de piratear. Esto es
verdad hasta cierto punto. Ellos afirman que el número de secuencia del paquete
constituye un nivel de control, de forma que si no conoces el siguiente número de
secuencia no podrán piratear las comunicaciones. El pirata debe duplicar
exactamente el siguiente número para poder inundar el recipiente asumiendo que
el otro mensaje es un duplicado.

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.

Cómo garantizar la seguridad en un nodo de red


¿Cómo debe comenzar a bloquear un sistema cuando se produce un ataque o
intrusión? La seguridad en la red está formada por un conjunto grande y
complicado de herramientas y reglas. Esta sección identifica algunos consejos
adecuados que permiten el control del sistema. Algunas ideas no tienen una
relevancia directa con la programación en red, pero son muy importantes. Tenga en
cuenta que esto requiere una exhaustividad importante; para incrementar la
seguridad, debe considerar ideas que proceden de múltiples fuentes.

Acceso restringido
Puede incrementar la seguridad de los programas de su servidor y cliente
utilizando las siguientes sugerencias:

• Permisos de archivos. El primer paso en la seguridad de una red es asegurar


que sus archivos tengan los permisos y propietarios adecuados. Algunos
programas de red pueden necesitar acceso root (por ejemplo, IP raw); tenga
mucho cuidado con estos programas. De hecho, algunos programas que
necesitan acceso root ceden el privilegio root cuando abren con éxito el socket
raw.

• 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.

• Reducción de los agujeros en los puertos. Limitar el número de puertos


disponibles. Cada puerto abierto en el sistema incrementa el riesgo de
seguridad. (No exagere esta sugerencia. Simplemente, recuerde que cuantos
más puertos tenga, más puertas abiertas tiene un programa para poder entrar.)

• 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.

• Separación de ¡os servicios no necesarios. Si no va a utilizar un servicio

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

Algunos administradores de red consideran que un cliente no puede iniciar las


comunicaciones con un servidor que tenga una dirección IP ficticia. Esto no es
completamente cierto y puede generar un concepto falso de seguridad. Por ejemplo, el
protocolo FTP utiliza una sesión de vuelta; el cliente realiza una petición al servidor para
llevar a cabo una descarga, pero el servidor crea el canal que permite al cliente obtener el
archivo. El cliente puede estar realmente detrás de un firewall. Esto puede realizarse
mediante un evento del firewall cor, la máscara activada.

Zonas desmilitarizadas (DMZ)


Los firewalls le pueden ayudar a incrementar la seguridad analizando la
información que pasa del cliente al servidor. Algunas compañías dan un paso más
en la utilización del concepto de firewall: creación de Zonas desmilitarizadas o
DMZ. Este concepto no resulta muy novedoso, por ejemplo, dos o tres muros
rodeando las ciudades y castillos en Europa y Medio Este demuestran su

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:

• Minimizar puertos (firewalls expuestos). La mayoría de los firewalls expuestos al


mundo exterior limitan el número de puertos disponibles. Estos puertos ofrecen
precisamente lo que el cliente necesita para obtener la información de la
compañía. Los puertos expuestos proporcionan más puertas a utilizar por los
piratas para obtener el acceso.
EL PROBLEMA DEL RPC
Muchos programadores de red reconocen el problema importante de intrusión a medida
que aumenta el número de puertos. Sin embargo, es posible que los programadores
menos experimentados no tengan conocimiento de la existencia de herramientas para
crear puertos abiertos. Por ejemplo, el programa portmap de RPC (Remote Procedure
Calis, Llamadas a procedimiento remoto) permite crear puertos. Además, la Invocación
de método remoto de Java (RMI - Remote Method Invocation) y C0IWCOM+ de Microsoft
se sitúan por encima de las RPC. Es posible que los webmasters que desarrollan scripts
para los servidores no sepan que están generando nuevos agujeros de seguridad con
cada objeto nuevo.

• Minimizar servicios (servidores expuestos). Los servidores más cercanos a


Internet ofrecen sólo los servicios mínimos absolutos. Normalmente, estos
servicios están fuertemente vinculados a los puertos que filtra el firewall de
forma pasiva. Algunos incluyen HTTP, HTTPS y FTP (de sólo lectura).

• Limitarbases de datas (DMZ más internas). Las bases de datos, a menudo,


almacenan la información más valiosa de la compañía. Ninguna base de datos
privada debería estar en cualquiera de las zonas desmilitarizadas. A menudo,
algunos sistemas colocan el LDAP en una de las DMZ internas (en lugar de la

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.

• Utilizarservicios compilados (servidores expuestos). Los scripts como Perl, a


pesar de ser más sencillos que un programa C/C++, son bastantes legibles y
constituyen un riesgo de seguridad. Tan a menudo como sea posible, intente
generar herramientas que eviten al responsable modificar el comportamiento de
su servidor si se produce una intrusión.

• Separar enmascaramiento. Cada DMZ puede utilizar un falso espacio IP


separado de enmascaramiento. La modificación del direccionamiento de una
DMZ a otra dificulta la intrusión en la intranet pero complica el mantenimiento
de la red.
La comparación con las ciudades antiguas cercadas por muros y castillos es
muy apropiada al estado actual de Internet. Internet no tiene reglas y, a menudo, se
rige por un estado de pura anarquía. El establecimiento de medidas de seguridad
necesarias permite proteger a sus clientes.

Cómo garantizar la seguridad del canal


La red tiene dos elementos distintivos que le permiten ajustar sus necesidades:
la red tísica y el mensaje. Resulta muy importante proteger ambos elementos de
los ataques que se puedan originar.

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.

Algoritmos de cifrado públicos


Internet ofrece muchos conjuntos de herramientas diferentes de cifrado
simétrico y público. Las herramientas más habituales se basan en la clave pública
RSA (Rivest, Shamir y Adlemn, nombres de los autores), DES (Data Encryption
Standard) y RC2/RC4 (Rivest Ciphers). Algunos cifrados están patentados y
requieren, para su uso, pagar los correspondientes derechos de autor (en octubre
del 2000, se desbordaron las patentes de RSA).
La mayoría de las distribuciones de Linux incluyen con la instalación las suites
de cifrado o vínculos a sitios donde poder descargarlas.

Problemas con el cifrado


Los cifrados presentan un conjunto interesante de problemas. Primero, y hasta
hace muy poco, Estados Unidos limitaba la exportación de algoritmos potentes de
cifrado. De hecho, ésta es una razón utilizada por algunos distribuidores para no
colocar las suites de cifrado en sus CD-ROM. En su lugar, el proceso de instalación
intenta obtener la suite a partir de un servidor en un país extranjero. En el 2000, el
gobierno levantó esta restricción y, ahora, resulta más fácil utilizar el cifrado.

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.

Seguridad a nivel de sockets (SSL)


Como se habrá dado cuenta, para comunicarse dos hosts con cierta seguridad,
deben estar de acuerdo en los aspectos relativos al cifrado. La SSL (Secure
Sockets Layer, Seguridad a nivel de sockets) define este protocolo. Se trata de un
proceso diseñado con sumo cuidado que permite limitar, de forma muy estricta, el
problema del pirateo. SSL utiliza cifrado simétrico y público para establecer las
conexiones, negociar los protocolos e intercambiar los datos.
El ámbito de esta sección no puede incluir los detalles de SSL para controlar las
claves y el canal de comunicación. En su lugar, esta sección le muestra la forma de
escribir un cliente y un servidor SSL utilizando la API OpenSSL. De nuevo, este
capítulo no puede cubrir la API completa (esta información podría fácilmente
completar otro libro).

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-

4. Ejecute make test para verificar los algoritmos.

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/

7. Cree la referencia a los archivos incluidos:

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.

Un pequeño problema de este proceso es que todas las compilaciones utilizan


bibliotecas estáticas, por tanto, no se sorprenda sí sus archivos de ejecución tienen
un tamaño de 600KB. Si quiere utilizar bibliotecas compartidas, debe convertir los
archivos libssl.a y libcrypto.a a archivos *.so.
Una vez, finalizado el proceso, está preparado para escribir sockets seguros. La
etapa de enlazado debe incluir las bibliotecas en un orden especifico. El proceso
de enlazado del programa falla con referencias externas sin resolver si dos
bibliotecas se intercambian:
cc test.C -Issl -lcrypto
Tenga en cuenta que la API incluye algunas demos con archivos fuentes C+-K
Esto es excesivo. Todas las llamadas a las bibliotecas pueden ser código C plano.
(De hecho, si examina el código fuente, podrá comprobar que, realmente, es
código fuente C en un archivo C++.)

Creación de un cliente SSL


Ahora que ha instalado la API, intente escribir un par de programas
cliente/servidor sencillos. La creación de un cliente/servidor SSL es tan sencilla
como la creación de un socket normal. De hecho, OpenSSL trabaja por encima de
este marco de trabajo.
El primer paso en el ámbito del cliente consiste en configurar el estado de la
biblioteca SSL:
/********************************************/
/*** Iniciar el estado del Cliente SSL ***/
/*******************************************/
SSL_METHOD *method;
SSL_CTX *ctx;
OpenSSL_add_all_algorithms(); /* Cargar el cifrado, etc. */
SSL_load_error_strings(); /* Cargar/Registrar el mensaje de error*/
method = SSLv2_client_method (); /* Crear el nuevo método/cliente */

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.

Creación de un servidor SSL


El cliente y el servidor tienen muy pocas diferencias. Ambos deben iniciar el
contexto, establecer la conexión e iniciar el protocolo SSL. El servidor necesita un
par de pasos más. Primero, la iniciación del contexto varía ligeramente:
/*********************************************/
/*** Iniciar el estado del Servidor SSL ***/
/*********************************************/
SSL_METHOO *method;
SSL_CTX *ctx;
OpenSSL_add_all_algorithms(); /* Cargar cifrado */
SSL_load_error_strings(); /* Cargar/registrar los mensajes de error */
method = SSLv2_server_mettiod(); /* Crear nuevo método-servidor */
ctx = SSL_CTX_new(method); /* Crear nuevo contexto */
Es posible que se haya dado cuenta que el programa utiliza SSLv2 y no SSLv3.
Puede utilizar cualquiera de los dos. No obstante, si utiliza Netscape para
conectarse con su servidor SSL, entonces debe usar SSLv2. Por alguna razón,
incluso después, Netscape piensa que está conectado a un socket SSLv3.
A diferencia del cliente, el servidor debe cargar su archivo de certificados. Esto
constituye dos partes: el certificado y la clave privada. Debe cargar ambos como
parte de la iniciación. Ambas partes pueden aparecer en el mismo archivo.

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.

Resumen: el servidor seguro


El socket de red abre muchas posibilidades relativas al incremento de la
productividad y la distribución del procesamiento. Sin embargo, la información que
comparten los hosts no está oculta para otros hosts conectados en la misma red.
La colocación de información vital o secreta en Internet constituye una invitación a
la pérdida, espionaje, daño y demás. La naturaleza de Internet puede ser su
responsabilidad.
Puede evitar las pérdidas planificando v estableciendo, por adelantado, la
seguridad de sus productos. La mayoría de las compañías no pueden ignorar el
hecho de tener obligatoriamente una presencia en Internet para sobrevivir. Los
firewalls, normativas y cifrado son formas habituales de cerrar un sitio y bloquearlo
frente a los ataques.
Puede utilizar ciertos protocolos y algoritmos que permiten reducir la posibilidad
de pirateo, cuando dos o más hosts se comunican a través de una configuración
pública, como puede ser Internet. El cifrado se utiliza de distintas formas y
proporciona diferentes niveles de seguridad. Dos cifrados estándares son la clave
simétrica y la clave publica. Ambos se utilizan para las comunicaciones tales como
SSL.
El desarrollo de programas en una API SSL depende fuertemente de la

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

Cómo compartir mensajes con


multidifusión, difusión y Mbone

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.

Difusión de mensajes a un dominio


La primera forma de distribución de mensajes, la difusión [RFC919, RFC922],
utiliza la naturaleza de las subredes y las máscaras de red para determinar el
destino. Una difusión es una forma de distribución de mensajes obligatoria; tanto la
subred como los hosts tienen que recibir el mensaje. (En realidad, si un host no
tiene habilitada la difusión, el hardware no acepta el mensaje. Aún así, el mensaje
ocupa el ancho de banda y todos los hosts pueden leerlo.)

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.

Programación para activar la difusión


Puede activar la difusión mediante la opción de socket SO_BROADCAST. El
resto del programa aparenta ser un programa normal de envío de mensajes con

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]);

Una vez activada la difusión, puede enviar mensajes a la dirección de difusión.


Tal vez desee crear un proceso o thread para el receptor y el emisor. A diferencia
de otras transmisiones en que puede esperar una respuesta simple para una
transmisión simple, la difusión puede generar muchas respuestas para cada
mensaje enviado. Si hace esto, asegúrese de desconectar la cola de entrada a uno
de los canales.
/***************************************************************/
/*** Divide las responsabilidades en emisor y oyente. ***/
/*** Cierra el canal de escucha del emisor. ***/
/***************************************************************/
if ( fork() )
Receiver(sd);
else
{
shutdown(sd, SHUTRD); /* cierra el canal de entrada */
Sender(sd);
}

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.

Multidifusión de mensajes a un grupo


La multidifusión [RFC1112] resuelve algunos de los problemas de la difusión. La
idea principal de la difusión es el envío de un mensaje a todos los que se
encuentran en un rango de direcciones. La multidifusión utiliza direcciones a las
que los hosts pueden unirse bajo consentimiento para escuchar los mensajes.
La multidifusión utiliza una única dirección IP para enviar mensajes a varios
receptores. Esta presenta las siguientes ventajas sobre la difusión:

• Todos los protocolos. Soporta protocolos de datagramas y de generación de


flujo (UDP y TCP). Ahora mismo los datagramas soportan la multidifusión, pero
los flujos no tienen implementaciones inmediatas. Puede encontrar
implementaciones gratuitas (no Open Sourcé) de entrega fiable con
multidifusión.

• Accesible a WAN. Puede unirse a grupos globales de multidifusión. Sin


embargo, en el momento de la escritura de este libro, existen secciones de la
WAN que no soportan o pasan mensajes de multidifusión.

• Oyentes limitados. Un mensaje de multidifusión no obliga necesariamente a que


todas las JSJ1C lo tomen (véase la sección "Cómo la red proporciona la
multidifusión" más adelante en este capítulo).

• 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.

Unión a grupos de multidifusión


El uso de la multidifusión es tan simple como la unión a un servicio de listas. Al
igual que un servicio de listas, obtiene tantos mensajes como se envíen, de forma
que es fácil invadir una conexión de red restringida. Si se une a un grupo,
asegúrese de tener el ancho de banda para soportar el flujo de datos. Lo más
probable es que pueda descartar los paquetes y hacer la conexión menos útil.
Las direcciones de difusión siguen un patrón específico para que los routers
puedan reconocerlas. Estas direcciones proceden directamente de la asignación IP
(véase el Capítulo 2). El rango es 224.0.0.0-239.255.255.255. Estas direcciones
están reservadas para la difusión. Este rango de direcciones se subdivide en
rangos más pequeños para indicar el ámbito de direcciones o la distancia que
puede recorrer un mensaje antes de que un rauferlo bloquee. La Tabla 17.1
muestra estas divisiones.
Tabla 17.1 Asignación y ámbito de las direcciones de multidifusión
Ámbito TTL típica Rango de direcciones

Grupo 0 224.0.0.0-224.0.0.255

Sitio < 32 239.255.0.0-239.255.255.255


Organización < 128 239.192.0.0-239.195.255.255
Global <= 255 224.0.1.0-238.255.255.255
Para unirse a un grupo, utilice la llamada de servicio del núcleo setsockopt()
con un parámetro nuevo. El parámetro nuevo es la estructura ip_mreq:
/*******************************************************************************/
/* Estructura ip_mreq para seleccionar una dirección de multidifusión */
/*******************************************************************************/
struct ip_mreq
{

struct in_addr imr_multiaddr; /* grupo de multidifusión conocido */


struct in_addr imr_interface; /* interfaz de red */
};
El campo imr_multiaddr especifica el grupo de multidifusión al que desea unirse.

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");

Sí espera conectar otro programa a esa misma dirección/puerto, debe utilizar la


opción SO_REUSEADDR del socket. En ese momento, Linux no soporta la
compartición de la dirección/puerto (SO_REUSEPORT). Para activar la
compartición de direcciones, utilice el siguiente fragmento de código:
/***************************************************/
/*** Habilita la compartición de direcciones ***/
/***************************************************/
if(setsockopt(sd, SOL_S0CKET, S0_REUSEADDR, Son, sizeof(on)) != 0 )
panic("No puede reutilizar las direcciones/puertos");

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.

Envío de mensajes de multidifusión


El envío de un mensaje a un grupo de multidifusión es tan sencillo como el envío
de un mensaje datagrama. Recuerde que UDP le permite conectarse a esa
dirección, simplificando el envío y la recepción en multidifusión.
APROPIACIÓN DE LAS DIRECCIONES DE
MULTIDIFUSIÓN
Aunque puede configurar una dirección para un servicio especifico, no puede garantizar
la propiedad de esa dirección. Otros también pueden enviar mensajes a esa dirección. Si
su servicio requiere un cierta cantidad de continuidad, puede que necesite ofrecer una
forma de distinguir sus mensajes del resto. El cliente continúa tomando todos los
mensajes enviados a la red. Es responsabilidad del núcleo y de su programa la
clasificación de los mensajes reales.
El uso de la multidifusión presenta un efecto lateral interesante que tiene un
gran potencial: no tiene que ser un miembro de un grupo de multidifusión para
enviar un mensaje. Un programa simplemente puede enviar el mensaje a una
dirección-puerto de difusión. Un servidor de multidifusión a través de flujos, de
hecho, nunca se une realmente a un grupo de multidifusión. Para recibir mensajes,
debe unirse al grupo.
Por razones legítimas, la capacidad de dar servicio a los mensajes sin estar
unido supone un aspecto realmente positivo. De esta forma, su servidor puede no
verse afectado por el tráfico asociado con la multidifusión.

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:

• Tabla de etiquetas. La NIC contiene literalmente la dirección completa. Ésta


comprueba cada mensaje de la red frente a su propia MAC y todos los MAC de
la tabla. Es fiable 100%.

• 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.

• Modo promiscuo. Algunas tarjetas no pueden soportar la multidifusión. La


alternativa es colocar la tarjeta en modo promiscuo y recibir todos los mensajes.
El siguiente nivel debe descartar todos los mensajes no deseados. Esto es
menos atractivo.
Las tarjetas de red no tienen el concepto de puertos, de forma que los niveles
superiores de la pila IP deben colocar los mensajes en sus colas apropiadas o
descartarlos. Recuerde que, aunque no esté escuchando un puerto, su host
obtiene todos los mensajes multidifundidos en la dirección a la que se una. Si
selecciona UDP o TCP, el núcleo se encarga de filtrar los puertos. Si selecciona un
raw socket, debe realizar el filtrado en el programa.

Salida del mensaje de multidifusión


El primer paso en la obtención de mensajes de multidifusión es escribir el
programa y permitir la multidifusión en el hardware y el núcleo del host. El siguiente
paso es indicar a los routers que desea el mensaje. Esto no es todo. Sus mensajes
de multidifusión se enfrentan a varios obstáculos antes de su salida a la banda
base de la WAN (red de área amplia).

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.

Obtención del soporte hardware


La ultima sección describió la forma en que el sistema operativo y el hardware
soportan la multidifusión. Desafortunadamente, algunas NIC (como la Ftbeniet
ü\press) no soportan completamente la multidifusión y el modo promiscuo. Para
obtener un mensaje de multidifusión, el hardware debe soportar el modo promiscuo
(al mínimo detalle). Al probar algunos de los algoritmos de este capítulo, este
problema entorpeció el desarrollo durante un corto espacio de tiempo.
Puede ver la lista de NIC de soporte en /.../linux/Documentation/networking/
multicast.txt. Si sus intentos de programación no funcionan, asegúrese de que su
tarjeta tiene soporte absoluto.

Cuellos de botella de rendimiento


Con la obtención de mensajes de difusión o multidifusión existe otro problema
(especialmente para aquellas NIC que utilicen el modo promiscuo para la
multidifusión). El programa tiene que filtrar muchos mensajes no deseados. La
difusión también presenta ese problema y la multidifusión redujo bastante su
efecto. Pero el problema continúa ahí.
De forma similar, la conexión de un cliente a la red puede no estar capacitada
para manejar todos los mensajes que envía el grupo de multidifusión (por ejemplo,
un video suministrado por un módem a 56 Kb por segundo). ¿Qué ocurre? El
router encola los mensajes hasta que expiran. El conocimiento de los cuellos de
botella entre su servidor y el cliente pueden ayudarle a mantener una alta fiabilidad
v disponibilidad de uso.
Una solución potencial al problema de los cuellos de botella podría requerir
algún control más fuerte de_.de los roiifers.

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.

Exclusividad de los mensajes


Otra forma de responsabilidad aparece cuando trabaja con los protocolos de
multidifusión. Puede que esté familiarizado con el refleje, de mensajes potenciales
que presentan los datagramas. En la multidifusión, el problema es más
pronunciado debido al requerimiento de copiar el mensaje para caminos diferentes.
Un ejemplo de esta duplicación se produce cuando un host se conecta a Internet
y tiene que pasar a través de varios routers. En un momento determinado, el
mensaje pasa a través de un par de routers del mismo subdominio. No debería
ocurrir puesto que una red adecuada sólo debería tener un n mter por dominio, pero
el caso es que ocurre.
Cuando un host se une a un grupo, los dos routers ven la petición y pasan el
mensaje al router padre. Posteriormente, un mensaje de multidifusión llega al
padre. El padre reconoce el mensaje como uno de los que los routers anunciaron
utilizando el protocolo IGMP. Este pasa el mensaje para que siga avanzando y los
dos routers lo pasan a sus subdominios. Debido a la copia implícita (para cada
router), el host obtiene dos mensajes idénticos. Para evitar este problema, en
primer lugar asegúrese de etiquetar todos sus mensajes y en segundo lugar intente
mantener clara la topología de la red.

Resumen: compartición eficiente de los


mensajes
Si necesita compartir mensajes entre múltiples hosts sin enviarlos directamente
a cada uno. puede utilizar la difusión o la multidifusión. La difusión, la forma más
antigua, utiliza el direccionamiento IP con la máscara de red para crear v enviar un
mensaje. Ésta utiliza los recursos de la red de forma más eficiente pero obliga a
cada host de la subred a escuchar el mensaje. Además, la difusión no está
permitida en una WAN {red de área amplia).
La multidifusión resuelve muchas de las limitaciones de la difusión y tiene una
gran extensibilidad. Aunque los routers pueden necesitar enviar diferentes copias
del mismo mensaje, es un buen método para compartir mensajes sobre la LAN (red
de área local) y la WAN (red de área amplia). La escucha de los mensajes de
multidifusión es voluntaria, a diferencia de la difusión.
La multidifusión es una tecnología creciente con cada ve/ más routers de
multidifusión en su lugar para comunicar los routers de unidifusión. Sin embargo,
puede cjue no vea desaparecer por completo la difusión puesto que está presente
en algunos protocolos de bajo nivel.

383/512
Capitulo 18

La potencia de los sockets raw

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

Algunas veces los materiales disponibles no se ajustan al objetivo y


necesidades del proyecto. Cuando esto ocurre, se tiene que comenzar con los
materiales en bruto. Con TCP y UDP no se tiene acceso al interior de los paquetes
IP. Por ello, se tiene que explorar con profundidad y hacer un trabaje» con más
fundamento.
El socket ra w ofrece muclia flexibilidad. También, los programas que utilizan los
sockets raw tienden a ser muy simples y rápidos. En este capítulo se detalla el uso
y el objetivo de los sockets raw y se dan consejos de cómo construir los paquetes.

¿Cuándo se deben usar sockets raw?


El material en bruto de la programación IP es una única capa sobre la trama de
red real. Cuando se utiliza un socket raw, no se tiene la sobrecarga fija de los
protocolos de alto nivel que pueden reducir el rendimiento de la red. Pero, a la vez,
pierde las características de los protocolos de alto nivel. Se debe elegir
cuidadosamente cuándo usar los sockets 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.

Cómo controlar la cabecera IP


Adicionalmente, si uno mismo desea administrar la cabecera IP, debe utilizar
sockets raw con la opción de socket apropiada (IP_HDR!NCL). Se puede recordar
del Capítulo 3, "Distintos tipos de paquetes de Internet", que la cabecera IP tiene
varios campos que pueden normalmente configurarse con setsockopt(). También,
la cabecera IP tiene campos que no se pueden configurar.
Por ejemplo, si desea (por cualquier razón) comprobar los campos de
fragmentación, tendría que utilizar socket raw con la opción IP^HDRINCL
habilitada. Otro uso de esta característica es la programación de algunas versiones
alternativas de IP tratadas en la Tabla 3.1 del Capítulo 3.

Aceleración a través de la red física


Come se advirtió anteriormente, los protocolos de bajo nivel son muy rápidos.
Los sockets raw sólo son un nivel abstracto sobre la trama de red. Esencialmente,
los paquetes raw vuelan a través de la red a la velocidad de la red tísica.
Muchas tramas de red tienen de 1 a 2 KB de tamaño para redes de 1(1 Mbps.
Las redes rápidas ofrecen tamaños de trama más grandes. Si necesita velocidad,
pruebe a igualar el tamaño de trama al de la red. Esto reduce la necesidad de
dividir el mensaje. Es realmente difícil realizar ese sondeo; no se puede obtener
directamente el tamaño de trama. Podría preguntar al usuario o consultar la
configuración del hardware, pero eso requeriría mucho más trabajo (con una
recompensa menor).
Si necesita trabajar en el nivel más bajo de la pila IP, los sockets raivson la
solución. Alternativamente, si una comunicación de datos es importante y se
necesita precisión, muchos programas utilizan los protocolos de alto nivel para
ofrecer la mayor parte de las comunicaciones.

¿Cuáles son las limitaciones?


Los sockets raw ofrecen mucha flexibilidad, pero no son la panacea. La elección
de cuándo utilizar sockets raw requiere el conocimiento previo de cuales son las
limitaciones.

• Pérdida de fiabilidad. Se pierden muchas características de los protocolos de


alto nivel. Por ejemplo, TCP ofrece conexiones fiables—los datos enviados son
datos recibidos. (Por supuesto, los sockets raw y UDP comparten el mismo
nivel de desconfianza.)

• 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.

• Comunicación sin estándar. El emisor y el receptor deben entender qué se está


haciendo. No se puede escribir un simple emisor socket raw sin escribir un
receptor compañero. ICMP tiene su propio receptor en al pila IP.

• ICMP manual. Como se advirtió anteriormente, ICMP no tiene una interfaz


como TCP (SOCK_STREAM) o UDP (SOCK_DGRAM). En vez de eso, se tiene
que crear un socket raw, establecer aparte una cabecera como aparece en el
Listado 3.2 (véase el Capítulo 3), rellenarla, calcular la suma de comprobación,
adjuntar cualquier dato y enviarlo.

• 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.

• Privilegiosde root. De forma distinta a otros protocolos, el programa se debe


ejecutar con privilegios de root.
Si puede convivir con estas limitaciones, los sockets raw pueden ser una
bonificación para acelerar los programas.

Cómo poner los sockets raw a funcionar


Después de que se haya elegido usar sockets raw, se necesita conocer cómo
funcionar con ellos. Desdichadamente, no se dispone de una herramienta
apropiada. En este apartado se tratan algunos algoritmos ausentes y se introduce
una nueva llamada del sistema.

Selección del protocolo correcto


El primer paso para crear un socket raw es seleccionar el protocolo correcto. El
archivo /etc/protocol (véase la Tabla A.7 del Apéndice A) incluye los números de
protocolo, nombres y sinónimos. Puede utilizar este archivo para obtener el número
de protocolo correcto del nombre alfabético estándar sin tener cjue definir ninguna
constante. La llamada del sistema a utilizar es getprotobyname():
#include <netdb.h>
struct protoent* getprotobyname(const char* name);
Esta llamada, si se ejecuta con éxito, devuelve una estructura (struct protoent*).
Esta estructura mantiene un campo (p_proto) que usaría para realizar la llamada
del sistema socket();

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.

Creación de un paquete ICMP


Después de crear un socket raw, necesita iniciar un paquete de mensaje. Cada
paquete tendrá algún tipo de información de cabecera (incluso su propio paquete
es probable que tenga una cabecera). En lo que a la capa IP de bajo nivel
concierne, todo son datos. Si elige incluir una cabecera, esto no afecta al comando
sendto(), así que necesita administrar la cabecera en el programa.
El paquete ICMP tiene una cabecera y un cuerpo de datos. Para facilitar la
programación (y la independencia de la arquitectura), Linux incluye la definición de
la cabecera en las bibliotecas:
#include <netinet/ip_icmp>
struct icmphdc *icmp_header;
Puesto que no es probable incluir más información en el paquete, a excepción
de la cabecera, podría definir la propia estructura como se define a continuación:
#define PACKETSIZE 64 /* bytes */
struct packet_struct
{

struct icmphdr header;


char message[PACKETSIZE-sizeof(struct icmphdr)];
} packet;
Puede usar esta estructura para el envío y recepción de mensajes.

Cómo calcular una suma de comprobación


El próximo paso, al menos para el paquete ICMP, es calcular la suma de
comprobación. La suma de comprobación es una suma en complemento a uno del
paquete entero. Desgraciadamente, este algoritmo no pertenece a la biblioteca
estándar en las distribuciones de Linux (aún cuando se utiliza repetidamente en el
núcleo). El Listado 1S.1 presenta la subrutina ausente.
Listado 18.1 La suma de comprobación de ICMP
unsigned short checksum(void *b, int len)
{ unsigned short *buf = b, result;

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. */
}

El receptor valida el paquete a través del algoritmo inverso—suma el contenido


entero del paquete. El resultado sería cero (debido a la suma de comprobación en
complemento a uno). Esta es una forma muy hábil de comprobar paquetes, pero
tiene sus limitaciones.
La limitación principal es el hecho de que el algoritmo solamente puede detectar
errores de bit únicos en palabras de 16 bits. No posee capacidad para detectar
defectos similares de palabra en palabra. Por ejemplo, si el paquete tiene dos
palabras cada una con un bit movido en la misma posición, la suma de
comprobación resultaría la misma.

Cómo controlar la cabecera IP


La cabecera IP utiliza el mismo algoritmo anterior para su propio campo de
suma de comprobación. Esto llega a ser un problema cuando elige crear su propia
cabecera IP. De forma diferente al protocolo ICMP, la cabecera IP hace un par de
funciones por el programador. Por ejemplo, calcula siempre la suma de
comprobación, y la suma de comprobación sólo abarca la cabecera IP. (Recordar
que ICMP calcula la suma para el paquete entero.) Si quiere calcular la suma de
comprobación, del paquete entero—será calculada de nuevo.
La capa IP deja todos los campos IP raw (como se muestra en la Tabla 3.4)
vacíos excepto para el número de versión de red IP y la suma de comprobación. Si
establece el número de versión a cero, sendtoO establece este valor a cualquier
valor de red actual que exista (por ejemplo, 4 para PFJNET o 6 para PFJNET6). Si
establece otro valor, la capa IP lo deja como está.
A menos que esté haciendo algo muy inusual, setsockopt() modifica muchos de
los campos funcionales en la cabecera IP.

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".

¿Cómo opera ping?


Cuando se crea el primer programa socket raw, se comienza normalmente con
ping, un cliente estándar "¿está ahí?". Cuando se envía una pregunta como esa, la
capa ÍP casi siempre responde—no hay ninguna aplicación o servidor esperando
ese tipo de mensaje.
Puesto que el usuario puede desear comprobar el tiempo de ida y vuelta del
mensaje, el programa tiene a menudo dos procesos en ejecución—uno para enviar
y el otro para recibir. El proceso emisor normalmente espera un segundo entre
cada transmisión, y el receptor sigue los tiempos del mensaje.

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");
}

La llamada display() transforma buf a packet_struct y comprueba el ID. Si el ID


coincide, el paquete pertenece realmente al proceso.
El ID se necesita debido a cómo el núcleo entrega los mensajes a los sockets
raw. Como se advirtió anteriormente, el núcleo no tiene forma de determinar el
destino correcto de un paquete raw sin los puertos, así que entrega todos los
paquetes raw de un protocolo en particular.
El protocolo es una clave para el núcleo. Si el programa registra un socket raw
con un protocolo ICMP, el programa obtiene todos los mensajes de ICMP (incluso
aquellos que son de otros procesos). ¿Cómo indica la diferencia? ICMP incluye un
campo ID dentro del cual muchos programas colocan su PID (ID del proceso). El
host que responde repite al origen todo lo que recibe, incluyendo el ID. Si el ID no
coincide con el PID, el receptor lo desecha.

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++;

if ( setsockopt (sd., SDL_IP, IP_TTL, &TTL, /* Establecer TTL. */


sizeof(TTL)) !=0)
perror("Set TTL option");
/* Iniciar el mensaje (véase el emisor de MyPing).*/
if ( sendto(sd, &pckt, sizeof(pckt), 0, addr, /*¡ENVIAR!*/
sizeof(*addr)) <=0)
perror("sendto");
if ( recvfrom(sd, buf, sizeof(buf),0, /* Obtener respuesta. */
&r_addr, &len) > 0 )
{ struct hostent *hname;
ip = (void*)buf;
printf("Host #%d; %s \n", cnt-1, /* Escribir la IP del router. "/
inet_ntoa(ip >saddr));
hname = gethostbyaddr( /* Intentar conseguir el nombre. */
(void*)&r_adür.s_addr, sizeof(r.addr.s_addr),
raddr.sinfamily);
if ( hname != NULL )
prtntf (“(%s)\n", hname->n_name);
else
perror("Ñame");
}
else
perror("recvfrom");
}

/* Repetir hasta que el destino sea igual que el de la respuesta. */*/


while ( r_addr.sinaddr.saddr != addr->sin_addr.s_addr );
Cuando se ejecuta unas cuantas veces este programa, se puede observar que
el camino parece cambiar mucho. Esto es normal y no es un error. Para obtener
una explicación de la dinámica de la red se puede consultar el Capítulo 2,
"Elocuencia del lenguaje de red TCP/IP".

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

Problemas actuales de direccionamiento


Cómo poner a prueba f Pv6
Pros y contras de IPv6
Resumen: traslado del código hacia el futuro

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.

Problemas actuales de direccionamiento


IPv4 fija principalmente su atención en el direccionamiento de un conjunto de
computadoras dentro de un subconjunto. Como se describió en el Capítulo 2,
"Elocuencia del lenguaje de red TCP/IP", el número IP está compuesto de 4 bytes
(o un número de 32 bits). Además, las direcciones están agrupadas dentro de
clases de red. Cuando las compañías compran bloques de direcciones, las
obtienen en estas clases. Estas clases se componen desde unos pocos cientos a
unos pocos millones de direcciones.

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.

Resolución de la reducción del espacio de


direcciones IPv4
IAB ha tomado algunas medidas para resolver el problema de la reducción de
direcciones. El coste de posesión de direcciones IP auténticas se ha elevado, así
que las compañías ahora utilizan enmascaramiento y DHCP para limitar el número
de direcciones asignadas.
Adicional mente, Internet podría adoptar un esquema de direccionamiento más
ancho que el meramente de 32 bits. !Pv6 [RFC2460] responde a esa pregunta de
forma exacta. IPv6 tiene 12S bits de largo (o un ancho de 16 bytes). Esto abre las
posibilidades dentro del próximo siglo—por lo menos. El número efectivo de
direcciones en IPv4 está sobre dos mil millones de nodos de red (sacando las
direcciones especiales). El espacio de direcciones efectivo de !Pv6 es de ¡l\I(Pq!

¿A qué se parece IPv6?


Una dirección IPv6 parece distinta de una dirección IPv4 debido al gran número
de dígitos que un programador ahora tiene que administrar. Cada dirección consta
ele ocho números hexadecimales separados con dos puntos. Por ejemplo, una
dirección válida es 2FFF:80:0:0:0:0:94:1. Para abreviar, se puede reemplazar la
repetición de ceros con dos caracteres de dos puntos (2FFF:80::94:1).
Los puertos se pueden adjuntar al final de la dirección utilizando un punto y un
número de puerto en decimal. Por ejemplo, el puerto A Q para la dirección anterior
seria 2FFF:80::94:1.80. No se puede utilizar un separador de dos puntos porque
vuelve ambigua la abreviación.
FJ espacio de direcciones IPv6 (como en IPv4) se ha dividido dentro de regiones
o grupos [RFC189?,l<FC24711. Los grupos intentan combinar direcciones distintas
(como por ejemplo, IPX). En el momento de la escritura de este libro, la definición
está todavía en proceso y quedan muchas agrupaciones por definir.
De las diferentes agrupaciones, una de ellas es el valor de la dirección numérica
que comienza con 001, en la cual estos primeros tres bits reemplazan
específicamente el direccionamiento IPv4. La Figura 19.1 muestra lo que esta
dirección contiene.

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 )

ID NLA Agregación de siguiente nivel. Grupo subdividido ton el TLA. 32


bits.)

ID SLA Agregación de nivel de sitio. Podría ser una corporación o


conglomerado grande. (16 bits.)

ID de interfaz ID de máquina específica. (64 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.

¿Cómo funcionan juntos IPv4 e IPv6?


Los mecanismos de enrutamiento prometidos añaden alguna atracción al
cambio pensado. Pero Internet, por supuesto, tiene todavía que transformarse
completamente a IPv6. De hecho, se puede mantener siempre IPv4 más o menos.
Así que, ¿cómo se mezclan los servidores y los clientes?
Aproximadamente todos los sistemas que ahora soportan la pila de protocolos
IPv6 también soportan la pila de protocolos IPv4. Estos sistemas de pilas duales
pueden existir por mucho tiempo, hasta que la mayor parte de las aplicaciones se
emigren completamente a IPv6. El problema principal es cómo conseguir que
funcionen juntos.
En realidad, el incremento de tamaño de la dirección tiene poco efecto en la pila
de protocolos. Los protocolos principales que Internet utiliza son UDP y TCP, y
éstos están incluidos de cualquier forma en el paquete IP. Cuando envía un
mensaje desde un cliente IPv4 a un servidor de pila dual, la pila IPv4 respondería

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.)

Cómo poner a prueba IPv6


Para poner a prueba a IPvó, no sólo hay que cambiar ligeramente los
programas, sino que el núcleo y las herramientas de red deben soportarlo también.
Algunas distribuciones excluyen d soporte a IPvó para reducir la complejidad de la
instalación o los riesgos de seguridad. En este apartado se describe el proceso de
habilitación de éste soporte y se muestra cómo transformar los programas a IPvú.

Configuración del núcleo


Puede averiguar rápidamente si la versión del núcleo soporta IPvó. Si tiene el
directorio /proc, mire en /proc/net. Si ve algún archivo denominado igmp6 o if_inet6,
tiene IPvó. Si no ve estos archivos, puede que necesite cargar el módulo ipvS.o.
ERROR EN IFCONFIG
La herramienta ifconfig hallada en net_tools tiene un defecto y no carga automáticamente
el módulo ¡pv6.o. Si tiene este problema, puede que deba recompilar Ípv6 del núcleo
como si no fuera un módulo.
Si necesita reconfigurar el núcleo, diríjase al directorio fuente. (Algunas
distribuciones no instalan automáticamente el fuente del núcleo por alguna razón,
así que puede que necesite descargarlo e instalarlo. El sitio web www.kernel.org es
un buen lugar para ello.) Asegúrese que todas las configuraciones son correctas en
todas las opciones, v seleccione Experimental Drivers. Lo siguiente es seleccionar
IPv6 en el menú Network Settings. Puede encontrarlo en el apartado It ls Safe to
Leave These Untouched. Algunas configuraciones del núcleo permiten seleccionar
de forma exclusiva a IPv6. No lo haga. De nuevo, puede desear incluir el IPv6 en el
núcleo y no compilarlo como un módulo. Puede guardar la configuración para
alguna vez depurarla.
Debe asegurarse de guardar una copia de seguridad del núcleo anterior. Si tiene

397/512
un problema, puede utilizarlo para regresar al estado anterior que no tiene
problemas.

Configuración de las herramientas


Lo siguiente que necesita comprobar son las herramientas. Si tiene confirmado
el soporte del núcleo, ejecute ifconfig sin ningún argumento. Esto muestra la
configuración actual de cada interfaz de red. Si soporta IPvó, el listado incluve una
dirección IPvó en la segunda o tercera línea de cada interfaz.

Si no ve las direcciones, ejecútelo otra vez con la opción —helpp. Este


comando lista ahora todos los protocolos soportados. Probablemente no vea IPv6
con esta opción. Si no está, necesita conseguir el RPM netjtools, reconfi gura rio y
compilarlo. Después de la instalación, configúrelo utilizando configure.sh y habilite
IPvó. Puede incluir IPvó sobre IPv4, si lo desea, pero eso puede ser innecesario.
Compile el paquete y copie todos los ejecutables a su lugar normal (a menudo
es/sbin).
De nuevo, la realización de una copia de seguridad de su herramienta antigua
es una idea muy buena. (Debe observar que la configuración por omisión de
configure.sh es la misma que la instalación antigua. Así que, si la copia de
seguridad no funciona, podría simplemente recompilar.)
Después de compilar e instalar las herramientas y el núcleo, reinicie el host
Cuando el sistema esté preparado de nuevo, la ejecución de ifconfig muestra las
direcciones asociadas para las direcciones IPv4 asignadas. Puede incluso añadir
un alias de la siguiente forma: ifconfig eth0 add <IPv6 Address>
Por ejemplo:
ifconfig eth0 add 2FFF::80:453A:2348
Una vez tenga el sistema listo y funcionando, puede empezar a escribir unos
cuantos programas IPv6.

Transformación de las llamadas IPv4 a IPv6


Con una computadora configurada correctamente para el soporte de IPvó, ya
puede escribir algunos programas. Sólo necesita cambiar unos pocos parámetros
para realizar la transformación. Esencialmente, todo funcionará como se espera, si
ha seguido las reglas definidas en los capítulos anteriores.

El primer cambio está en la estructura socket. En vez de la utilización de


sockaddr_in, use sockaddr_in6:
struct sockaddr_in6 addr;
bzero(&addr, sizeof(addr));
Use esta estructura para las llamadas del sistema accept(), connect() y bind().
Similarmente, la opción socket para obtener la dirección del socket origen o del
socket remoto necesita esta estructura nueva. El segundo cambio es el tipo de
socket. No puede recibir protocolos distintos desde el mismo socket. Aunque IPvó
es también un superconjunto de IPv4, debe elegir un tipo de socket distinto:

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.

Transformación de sockets raw a IPv6


Si está utilizando un socket TCP o UDP, eso es todo lo que hay que realizar. Sin
embargo, si está programando un socket raw o ICMP, tiene que utilizar formatos nuevos de
registro especiales. Raw-6 e ICMP6 incluyen campos nuevos o revisados para soportar las
capacidades adicionales.
La cabecera IPvó es mucho más simple que la cabecera IPv4 y sólo dobla la sobrecarga
fija (40 bytes contra 20 bytes). La cabecera es también de tamaño fijo y no se permiten
opciones adicionales (se debe observar que no es exactamente cierto, como se describe en
el próximo apartado). IP (para sockets raw) se puede definir en el orden de bytes de red
como se sigue:

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.

Transformación de sockets ICMPv6 a IPv6


El esquema físico de una cabecera ICMPv6 [RFC2463] es el mismo que para
una ICMPv4, pero los campos type y code tienen diferencias considerables. Por
ejemplo, una petición y una respuesta de eco tienen asignados números nuevos
(128 y 129, respectivamente). Varios códigos no están soportados completamente.
Para obtener una lista completa de códigos, puede mirar el Apéndice A, "Tablas de
datos".

El nuevo protocolo multidifusión


Otro cambio en la arquitectura se encuentra en la forma en que IPvó administra
la generación de multidifusión. Éste tiene que trabajar con tres aspectos: hardware,
direccionamiento y encaminamiento.
Como se describe en el Capítulo 17, "Cómo compartir mensajes con
multidifusión, difusión y Mbone", la generación de multidifusión tiene que funcionar
con el hardware para indicarle que coja los paquetes que no son de su propia
dirección ethernet o MAC. Estas direcciones se basan en las direcciones
multidifusión solicitadas—IPv6 toma los cuatro últimos bytes de la dirección y crea
una dirección MAC con el prefijo 33:33. Por ejemplo, si la dirección multidifusión
solicitada es FF02::725:6832:D012, la MAC resultante sería 33:33:68:32:D0:12 (se
ignora el 725).
El direccionamiento IPv6 para la generación de multidifusión difiere ligeramente
de IPv4. El primer byte, FE, indica que es una dirección multidifusión, pero el
siguiente byte contiene información adicional sobre el tipo de grupo multidifusión y
detalles de encaminamiento, cada cuatro bits de ancho.
El primer subcampo tiene cuatro bits de señalización, cada uno representando
una característica. El bit menos significativo (bit número 0) es cero (0) para indicar
una dirección multidifusión conocida. Es uno (1) para indicar una dirección

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.

5 Lugar Definido por los administradores de red, lo* mensajes


permanecen dentro del área del lugar.

8 Organización Definido por los administradores de red, los mensajes


permanecen dentro del área de la organización.
14 Global Todos los routers permiten que los mensajes pasen a
través de la red global (hasta que caduquen).

Por último, las reglas para el encaminamiento (como se describió en el Capítulo


17) permanecen igual. IPvó utiliza el IGMP opcional del IPv4 para pasar la consulta
de unión o de baja a un grupo multidifusión. Puesto que ambos usan la dirección
MAC para obtener mensajes de multidifusión, IPvó no tuvo que definir nada
adiciona] aparte de la implementación de 1PV4.
El procedimiento para la unión a un grupo de multidifusión IPvó es casi el mismo
que el de fPv4, pero necesita utilizar una estructura distinta para la selección de la
dirección:
/***********************************************************/
/*** Definición de la estructura multidifusión IPv6. ***/
/***********************************************************/
struct ipv6_mreq

{
struct in6_addr ipv6mr_nultiaddr;/* Dirección multidifusión IPV6. */
unsigned int ipv6mr_interface; /* Número de interfaz. */
};

El primer campo, ipv6mr_multiaddr, es la dirección del mensaje de difusión.


Coloque la dirección IPv6 en este campo (por ejemplo, FF02::10). El siguiente
campo es el número de interfaz: 0 significa todas las interfaces, 1 para la primera

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.

Pros y contras de IPv6


Como un protocolo, IPv6 pone en orden algunas de las características
anticuadas en IPv4 y ubica el protocolo dentro de las redes de mayor rendimiento.
La primera ventaja, y la más obvia: la dirección de 128 bits ofrece mayor rango de
direccionamiento. Las redes de hoy son mucho más sofisticadas que las que
existían cuando IPv4 fue introducido. Los routert. pueden resolver más fácilmente y
rápidamente estas direcciones.
Otra gran característica es el mayor tamaño de paquete que incrementa su
utilidad en entornos gigabit. IPv4 estaba limitada a paquetes de 64 KB. IPv6 tiene
soporte para una carga útil gigante que lo hace más económico (no está tan
penalizada en la red). Esta carga útil soporta paquetes de 4 CB.
La generación de multidifusión es otro aspecto positivo del protocolo. Sin
embargo, muchas de las características nuevas en la generación de multidifusión
han sido mejoradas dentro de IPv4, así que no puede observar nada más que una
diferencia.
IPv6 tiene tres limitaciones. Primero, dobla la sobrecarga fija. Una cabecera
normal de mensaje IPv4 tiene sobre 20 bytes de tamaño. Si los mensajes tienen
1.500 bytes de tamaño, entonces la sobrecarga fija IP es aproximadamente el 1%.
IPv6 dobla esa cabecera a 40 bytes, incrementando la sobrecarga fija de un
mensaje normal sobre un 2%. Sin embargo, de forma distinta a IPv4, el tamaño del
paquete no está incluido en el tamaño de carga útil de IPv6. Esto recupera unos

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.

Incorporación esperada de Linux


La comunidad de desarrollo de Linux ha estado encima de IPvó. Todas las
versiones desde la 2.2.0 incorporan soporte completo para la versión actual de
IPvó. Desafortunadamente, IPvó tiene un inconveniente principal: no está aceptado
completamente como un estándar, y mucho no está definido todavía.
Linux soporta todo lo que puede con las definiciones limitadas. Puede utilizarlo
ya en el desarrollo, pero recuerde que lPvfS es un objetivo en movimiento, así que
el sistema operativo no está todavía adaptado al lUO'.'í. Puede encontrarlo en el
apartado de opciones experimentales de configuración del núcleo.
SOPORTE DE INTERNET Y 6BONE
Tal y como el soporte inicial para la generación de multidifusión presentó problemas y se
dirigió a Mbone, muchos routers no soportan IPv6. Como Mbone, entusiastas IPv6
crearon 6bone, un subsistema de generación de mensajes IPv6 sobre IPv4 (IP en IP).
Sbone coloca el paquete Ipv6 dentro de un paquete IPv4 que el servidor de
encaminamiento se lo pasa a otro. Aunque no es tan predominante como los servidores
Mbone, este soporte está creciendo (particularmente en Europa y en el lejano oriente).
La última nota: debe ser consciente que debido a su naturaleza experimental,
IPvó puede causar algunos agujeros de seguridad en el sistema. Puede que no
desee trabajar con él en una red intranet que conecta a Internet.

Resumen: traslado del código hacia el futuro


IPv6 responde a las limitaciones encontradas actualmente en la estructura de
direccionamiento de IPv4. Por la evtensión del rango de direccionamiento en varios
órdenes de magnitud, IPv6 parece ser el claro futuro para Internet. Todavía está en
la infancia, la asignación del direccionamiento ha asumido las redes estándar,
como IPX, Ya tiene soporte en redes europeas y del lejano oriente.
Con las direcciones extendidas, la apariencia física ha cambiado e se han
incluido algunas notaciones abreviadas para la representación y programación
fácil. El traslado de programas de 1PV4 a IPv6 es directo siguiendo las líneas
directivas y ejemplos de este capítulo. La llamada del sistema socket() ayuda
realmente a la programación de red a adaptarse a redes distintas.

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

En este apéndice se identifican y describen todas las tablas y formatos de datos


más relevantes para la programación de sockets.

Dominios: primer parámetro de socket()


En la Tabla A.l se recogen los valores del primer parámetro de la llamada del
sistema Socket()- También puede utilizar estos mismos tipos en una llamada del
sistema bind()- Aunque la mayoría de los programas usan el estilo AF tanto para
socket() como para bind(), lo correcto es utilizar el estilo PF para socket() y el
estilo AF para bind()- Si no le resulta cómodo usar el estilo PF, puede utilizar el
estilo AF con garantía, debido a que los archivos de la cabecera C definen el estilo
AF como el estilo PF. Las definiciones de estructura se encuentran en

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

PF_UNSPEC Sin especificar struct sockaddr {


unsigned short int sa_family;
unsigned char sa_data[14];
};

PF_LOCAL Método BSD para el acceso a pipes #define UNIX_PATH_MAX 108


PF_UNIX locales con nombre

PF_FILE «include <linux/un.h>


struct sockaddr_un addr; struct sockaddr_un {
addr.sun_family = AFUNIX; sa_family_t sun_family;
strcpy(addr.sun_path, char sun_path[UNIX_PATH_MAX] ;
"/tmp/mysocket"); } ;

PF_INET Familia de protocolos Internet IPv4 struct sockaddr_in {


#include <linux/in.h> sa_family_t sin family;
struct sockaddr_in addr; unsigned short int sin_port;
bzero(&addr, sizeof(addr)); struct in_addr sin_addr;
addr.sin family = AF_INET; unsigned char pad[];
addr.sin_port = htons(9999); };
if ( inet_aton("127.0.0.1",
&addr.sinaddr) == 0)
perror("Addr conversión");

AF_AX25 Radio amateur AX.25 typedef struct {


«include <linux/AX25.h> char ax25_call[7];
} ax25_address;
struct sockaddr_ax25 {
sa_family_t sax25_family;
ax25_address sax25_call;
int sax25_ndigis;
};

PF_IPX Novell Internet Protocol struct sockaddr_ipx {


#include <linux/ipx.h> sa_family_t sipx_family;
_u16 sipx_port;
_u32 sipx_network;
unsigned char

408/512
sipx_node[IPXNODELEN];
_u8 sipx_type;
/* padding */
unsigned char sipx zero;
};

PF_APPLETALK Applelalk DDP struct sockaddr_at {


«include <linux/atalk.h> sa family_t sat family;
u8 sat_port;
struct at_addr {
u16 s_net;
u8 s_node; } sat_addr;
char sat zero[];
};

PF_NETROM NetROM Radio amateur


PF_BRIDGE Bridge multi pro tocólo
PF_ATMPVC ATM PVCs
PF_X25 (Reservado para el proyecto X.25) typedef struct {
#include <linux/x25.h> char x25_addr[16]; } x25_address;
struct sockaddr_x25 {
sa family_t sx25 family; /* X.121
Address */
x25 address sx25 addr;
};
PF_INET6 Protocolo IPv6 struct in6_addr_family {
Winclude <linux/in6.h> unión {
u8 u6_addr8[16];
_u16 u6_addr16[8];
_u32 u6_addr32[4];
#if (-0UL) > Bxffffffff
#ifndef
_RELAX_IN6_ADDR_ALIGNMENT
/* Alas, protocols do not respect 64bit
alignment. rsvp/pim/... are broken.
However, it i s good idea to force correct
alignment, when it is possible. */
_u64 u6_addr64[2];
#endif
#endif

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

PF_NETBEUI (Reservado para el proyecto struct sockaddr_netbeui


802.2LLC)
{
«include <linux/netbeui.h>
sa_family snb_family;
char snb_name [ NB_NAME_LEN];
char snb_devhint[IFNAMSIZ];
PF_SECURITY Llamada de seguridad pseudo AF

410/512
PF_KEY API de administración de claves
PF_KEY

PF_NETLINK Alias para emular 4.4 BSD struct sockaddr_nl


PF_ROUTE {
sa family_t nl.family;

«include <linux/netlink.h> unsigned short nl_pad;


_u32 nl_pid;
_u32 nl_groups;
};
PF_PACKET Familia de paquetes struct sockaddr_pkt {
«include <linux/if_packet.h> unsigned short spkt_family;
unsigned char spkt_device[14];
unsigned short spkt_protocol;
};
struct sockaddr_11
{
unsigned short sll_family;
unsigned short sll_protocol;
int sll_ifindex;
unsigned short sll_hatype;
unsigned char sll_pkttype;
unsigned char sll halen;
unsigned char sll_addr[8];
};
struct ec_addr
Ash {
PF_ASH Acom Econet /* Station number.*/
PF_ECONET «include <linux/if_ec.h> unsigned char station;

/* Network number. "/


unsigned char net;
};
struct sockaddr_ec {
unsigned short sec family;
unsigned char port;
/" Control/flag byte. */
unsigned char cb;
/* Type of message. */

411/512
unsigned char type;
struct ec_addr addr;
unsigned long cookie;
};

PF_ATMSVC ATM SVCs struct sockaddr_irda {


PF_SNA Provecto Linux SNA sa family_ t sir_family;
PF_IRDA sockets IRDA /* LSAP/TSAP selector */
#include <linux/irda.h> unsigned char sir_lsap_sel;
/" Device address */
unsigned int sir_addr;
/* Usually <service>;IrDA:TinyTP */
char sir_name[25];
};

Tipos: segundo parámetro de socket()


El segundo parámetro, tipos (type), permite seleccionar la capa del protocolo.
Algunas de las constantes definidas en la Tabla A.2 son meros marcadores de
posición para cuando el kernel soporta el protocolo.
Tabla A.2 Valores de protocolo para el parámetro type en la llamada
socket()
Tipo de protocolo Descripción
SOCK_STREAM (TCP). Comunicación fiable de dos direcciones en un flujo de datos.
Puede utilizar esta clase de socket en llamadas a funciones E/S de
alto nivel donde intervenga el tipo FILE*. Este protocolo ofrece una
conexión virtual a la red usando puertos y un canal cliente dedicado.
Una vez establecida la conexión, la llam-da acceptO devuelve un
nuevo descriptor socket específico para el cliente nuevo.
SOCK_DGRAM (UDP). Comunicación sin conexión, no fiable. Los mensajes son
independientes y puede perderse alguna durante la transmisión. Este
protocolo virtualiza la red mediante puertos y permite enviar y recibir
mensajes desde muchos puntos sin volver a establecer la conexión.
SOCK_RAW (IP). Accede a los campos c interfaces internos de la red. Si desea
crear mensajes ICMP, deberá crear un socket raw. Sólo accesos al
punto root.
SOCK_RDM (KDM—Reliably Delivered Messages). Garantiza la llegada de
cada paquete a su destino, pero no el correcto orden de los paquetes.
SOCK_SEQPACKET
(No ha sido implementado aún en Linux ni en otros sistemas
operativos UNIX.)
Datagramas fiables y secuenciados, de longitud fija, basados en la
conexión. (Este protocolo aún no ha sido implementado en Linux.)

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

ip 0 IP # internet protocol, pseudo number


icmp 1 ICMP # internet control message protocol
igmp 2 IGMP # Internet Group Management
ggp 3 GGP # gateway-gateway protocol
ipencap 4 IP-ENCAP # IP encapsulated in IP
st 5 ST # ST datagram mode
tcp 6 TCP # transmission control protocol
egp 8 EGP # exterior gateway protocol
pup 12 PUP # PARC universal packet protocol
udp 17 UDP # user datagram protocol
hmp 20 HMP # host monitoring protocol
xns-idp 22 XNS-IDP # Xerox NS IDP
rdp 27 RDP # "reliable datagram" protocol
iso-tp4 29 ISO-TP4 # ISO Transport Protocol class 4
xtp 36 XTP # Xpress Tranfer Protocol
ddp 37 DDP # Datagram Delivery Protocol
idpr-cmtp 39 IDPR-CMTP # IDPR Control Message Transport
rspf 73 RSPF # Radio Shortest Patn First
vntp 81 VMTP # Versatile Message Transport
ospf 89 OSPFIGP # Open Shortest Path First IGP
ipip 94 IPIP # Yet Another IP encapsulation
encap 9a ENCAP # Yet Another IP encapsulation

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

Códigos de estado HTTP 1.1


Para escribir su propio servidor web, debe conocer y utilizar los códigos de
estado estándar HTTP 1.1 [RFC2blf>. RFC2817]. La Tabla A.3 incluye dichos
códigos.

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

203 Non-Authoritative Information

204 No Contení

205 Reset Contení

206 Partial Content

3 xx Redírection 300 Múltiple Choices

301 Moved Permanently

302 Moved Temporarily

303 See Other


304 Not Modified

305 Use Proxy

4xx Client Error 400 Bad Request

401 Unauthorized

402 Payment Required

403 Forbidden

404 Not Found

405 Method Not Allowed

406 Not Acceptable

407 Proxy Authentication Required

408 Request Timeout

416/512
409 Cortflict

410Gone

411 Length Required

412 Precondition Failed

413 Request Entity Too Large

414 Request-URI Too Long


415 Unsupported Media Type

5xx Server Error 500 Intemal Server Error

501 Not Implemenled

502 Bad Gateway

503 Service Unavailable

504 Gateway Timeout

505 HTTP Versión Not Supported

Opciones de socket (get/setsockopt())


En las Tablas A.4 basta A.7 se describen diversas opciones de sockets ¡unto a
los larámetros requeridos. No todas las opciones son compatibles en tamaño entre
dis-intos tipos de UNIX. Por ejemplo, IP TTL en Linux permite un tipo int, pero llena
ólo el primer byte. En AIX de IBM existe la misma restricción para dicha opción,
pero con un tipo char.
Tabla A.4 Opciones generales de socket
Nivel Opción Descripción * R W Valor Tipo

SOL_SOCKET SO_ATTACH_ Adjuntar filtro ? ? ? lnteger int_


FILTER
SOL SOCKET SO Unir a dispositivo ? ? ? string char*
-BINDTODEVlCE
SOL_SOCKET SO_BROADCAST Habilitar difusión Y Y Y Boolean int
SOL_SOCKET SO_BSDCOMPAT Requerir compatibilidad BSD Y Y Y Boolean int
bug-por- bug
SOL_SOCKET SO_DEBUG Habilitar depuración socket Y Y Y Boolean int
SOL_SOCKET SO_DETACH_ Separar filtro ? ? ? lnteger int
FILTER
SOL_SOCKET SO_DONTROUTE Prohibir enrutamiento Y Y Y Boolean int
SOL_SOCKET SO_ERROR Ultimo error Y Y Y lnteger int

SOL_SOCKET SO_KEEPALIVE Habilitar mantener conexión Y Y Y Boolean int


abierta.

417/512
SOL_SOCKET SO_LINGER Retrasar hasta Y Y YLinger struct
enviar datos linger

S0L.50CKET SO NO_CHECK No venfiiar Y Y 1Boolean int

SOL.SOCKET SO.OOBINLINE Coloi A T en línea Y Y YBoolean int


fuera de banda

SOL.SOCKET SO.PASSCRED Habilitar pasar Y Y YBoolean int


credenciales de
usuario

SQL .SOCKET SO_PEERCRED Credenciales de Y 1 Y[. redenhal struct


punto ucred

SOL.SOCKET S0_ PRIORITY Fstablei er prioridad Y Y YInleger int


di' cola

SOL SOCKET SO RCVBUF Recibir tamaño de Y Y YInleger int


buffer

SOL SOCKET SO RCVLOWAT Recibir marca de Y Y M1 n1eger int


di»na bajj

SOL SOCKET SO RCVTIMEO Reiibir limite Y Y Ylime struct


timeval

SOL. SOCKET SO.REUSEADD Reutilizar diretcion Y Y YBoolean int


R

SOL^SOCKET SOJIEUSEPORT Reutili/ar dirección N Boolean int


(muí tic aslingl

SOL.SOCKET SO_SECURITY_ A u ten ti i ación de N - - lnteger int


AUTHENTI sefpjndad
CATION
SOL.SOCKET SO^SECURITY. Red de ciicf! ptüeión N integer int
ENCRYPTION_ de segundad
NETWORK

SOL_SOCKÉT SO_SECURITY_ Transporte de N Inleger int


ENCRYPTION_ eiicriptación de
TRANSPORT seguridad

SOL SO SNDBUF Enviar (amaño Y Y Y Integer int


^SOCKET de buffer

SOL_50CKE SO_5NDLO Enviar marca de Y Y Y lnteger int


T WAT agua baja

SOL_SOOC SO_SNDTIM b'nviai límite de Y Y Y Time stru


ET EO tiempo ct
timeval

418/512
SOL_SOCK SO_TYPE Tipo de socket Y Y Inleger int
ET

S IP.ADD Añadir perte- Y Y Y D struct ¡p_mreq


OL JVtEMBER ire
JP SHIP cci
ón
nencia multi- I
Pv
4
mu
lti-
difusión d
ifu
sió
n
S IPJ1RO Dejar pcrlenen- Y Y Y D struct ¡p_mreq
OL PJVIEMBE ire
JP RSHIP cci
ón
cia multidi- U
'v4
mu
lti-
fusión d
ifu
sió
n
S IP.MDRI Habilitar creación Y Y Y B int
OL NCL ool
JP ea
n
manual de cab. IP
S IPJvITL Descubrir MTU Y Y Y l int
OL LDISCOVE nte
JP R ge
r
S IP_MUL Interfaz multi- Y Y Y D struct in_addr
OL TICASTJF ire
JP cci
ón
difusión de salida I
Pv
4
S IPJV1U Habilitar impback Y Y Y B int
OL LTICA$T_L ool
JP OOP ea
n
multidifusión
S IPJvIUL Multidifusión TTL Y Y Y l int
OL TICASTTT nte
JP L ge
r

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

S IPJ>KT Opc. de paquete N - - O mt[]


OL OPTIONS pci
JP on
es
S IP_REC Habilitar recibir Y Y Y B int
OL VERR ool
JP ea
n
paquetes de error
S IPJÍEC Habilitar opciones Y Y Y B mi
OL VOPTS ool
JP ea
n
de recepción
S IP_REC Obtener TOS Y Y Y l int
OL VTOS nte
JP ge
r
recibida

S IPJÍ Obtener TTL Y Y Y lnte int


OL ECVTT ger
JP L
recibido
S IP_ RETOPTS Y Y Y Bo ¡nt
OL RETO olean
JP PTS
S IPJÍ Habilitar alertas N - - Bo int
OL OUTE olean
JP R_ALE
RT
de roíifcr
S IP_ Tvpe of Service Y Y Y lnte ¡nt
OL TOS ger
JP
(1 OS)
S IP_ Time to Live (TTL) Y Y Y lnte int
OL TTL ger
JP

420/512
Tabla A.6 Opciones socket de nivel IPv6

N Opci Descripción R W Valor Tipo


ivel ón *

S IPV Unir pertenencia


OL 6
JP ADD_
V6
ME muí (¡difusión ? 7 Dir struct jp_mreq6
MBEK ección
SHrP
IPv
ó multi-
difu
sión
S IPV Cambiar dirección ~i 7 7 lnte int
OL. 6_ADD ger
IPV RF0R
6 M
para socket

S IPV AUTHHDR 7 7 7 lnte int


OL 6.AUT ger
JP HHDR
V6
S IPV Desplazamiento de ■> 7 ? lnte ¡nt
OL 6_CHE ger
JP CKSU
V6 M
suma de control
para raw sockets
S IPV Dejar pertenencia 7 7 ? Dir struct ip_mreq6
OL 6J)RO ección
JP P_
V6
ME multidifusión 7P\
MBÉR -6
SHIP multidif
usión

Nivel Opción Descripción 7 R w Valor Tipo


S0LJ IPV6 Habilitar 7 T Boolea int
PV6 DSTOPTS obtener opciones n
de destino
SOLJ IPV6_HOPLI Habilitar 7 7 7 Boolea ¡nt
PV6 MIT obtener limite de n
saltos
S0LJ IPV6_H0P0P Habilitar 7 7 7 Boolea int
PV6 TS obtener opciones n
salto a salto

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!)

SOL TCP TCP_MAXRT ~lempo máximo N lnteger mt

Je retransmisión

SOL TCP TCP_MAXSEG Fi].ir tamaño máximo Y Y Y int


Intr¡;er

422/512
de segmentn (butler

Je transmitió ni

SOL.TCP TCP NODELAY Habí litar algi 'ritmo Y Y Y Boolean int

MaKlc

SOLTCP TCP_STDURG Especificar ubicación N - Hoolean int


-
Je byte urgente

(reemplazado por

*VSCtl CilIFí

SOL_TCP TCP_CORK No enviar nunca Y Y Y Bi «ilean inl

■.cemento- parcial-

mente completos

SOL _ TCP TCP_KEEPIDLE Iniciar actividad Y 1 Y int

tras este periodo

SOL TCP TCP_KEEPINTVL Intervalo entre Y Y Y int

actividades

SOL_TCP TtP.KEEPCNT Número de Y Y Y mt

actividades

antes di1 cesar

SQL_TCP TCP_SYNCNT Número de retrans- Y Y Y int

misiones de SYN

SOL_TCP TCP_LINGER2 Vida del estado Y Y Y mt

HN-WAIT-2

huérfano

SOL_TCP TCP_DEFER_ □espertar al oyente Y Y Y mt

ACCEPT sólo cuando

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.

SIGINT Interrupción de) teclado.


SJGQUIT Salir desde el teclado
SIGILL Instrucción ilegal.
SIGABRT Señal de abortar de abort(3).
SIGFPE Excepción de punto flotante.
SIGKILL Seña] de acabar
SIGSEGV Referencia de memoria no válida.
SIGPIPE Conducto roto: escribir en el conducto sin lectores
SIGALRM Señal de temporizador de alarm(2).
SIGTERM Señal de lermirurvión
SIGUSR) 30,10,16 Seña] 1 definida por el usuario
SIGUSR2 31,12,17 Señal 2 definid.! por el usuario.
SIGCHLD 20,17,18 Secundario parado o terminado.
SIGCONT W, 18,25 Continuar si estj parado.

SIG STOP 7,11.23 Detener pr- iceso


SIGTSTP 18,20,24 Stop tecleado en tty.
SIGTT1N 21,21,26 Entrada tty p¡ira proceso en segundo plano.
SIGTTOU 22,22,27 Salida tty para proceso en segundo plano
SIGIOT Trap ¡ÜT, sinónimo de SIGABRT.
SIGEMT

SIGBUS 10,7,1 n Error de bus.


SIGSYS 12,-,12 Argumento no válido para rutina (SV1D).
SIGSTKFLT Fallo en ta pila del coproeesador.

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]

SIGCLD Sinónimo de SIGC11LD.


SIGXCPU 24,24,10 Excedido limite de tiempo de CPU 14.2 BSD).
SIGXFS2 límite de tamaño de archivo (4 2 BSDi
SlfjVTALRM 2f.,2f>,28 Reloj de alarma virtual (4 2 BSD).
SIGPROF 27,27,29 Reloj de alarma de perfil.
SIGPWR 29,30,19 Fallo en la alimentación (System VI.
Tabla A.B Códigos de señales estándar de Linux (continuación)

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 Destino no alcanza ble.

0 Red no alcanzable. Hosi no alcanzable.


l
Protocolo no alcanzable.

3 Puerto no ale.ilzable.

4 Fragmentación necesaria, pero bit DF dettnido.

Fallo en ruta de origen.

f> Red de destino desconicida.

7 Host de destino desconocido.

b Host de origen aislado (obsoleto).

V Red de destino administrativamente prohibida.

10 1 Jost de destino administra ti v Límente prohibido.

11 Red no alcanzable para TOS.

425/512
12 Flost no alcanzable para TOS.

13 Comunicación administrativamente prohibida.

Señal Valor Acción Comentario


SIGINFO 29,-,- G Sinónimo de SICFWR.

SIGLOST AG Bloqueo de arcbivi> perdido


SIGWINCH 2H,28,20 BG Señal de red unen.; ion de ventana (4.3
BSD, Sun).
SIGUNUSED -,31,- AC Señal no utilizada.
i.
A~l_a ficción pri-determinada es tenninar
eiproceht

B— 1,7 acción predeterminada es ,pnerar Li


señal
C~l a acción predeterminada es iokarel nucieu

D—l.a acción predeterminada es deterner el proceso.


E~La señal no puede ser captada

f — í.,i señal no puede ser ignorada.

( ', -Señal no correspondiente a l'OSIX ¡.

426/512
Tabla A.9 Descripciones de códigos CMP (continuación)
Tipo Código Descripción
14 Violación de prioridad de host.

15 Corte de prioridad activo.

4 0 Aplacar origen—la puerta de enlace solicita al host que reduzca la tasa


de transferencia.

5 Redirigir.

0 Redirigir para red.

1 Redirigir para host.

2 Redirigir para tipo de servicio y red.

3 Redirigir para tipo de servicio y host.

8 0 l'eiicíón de eco.
9 0 Anuncio de router
IÜ U Solicitud de router.
11 Tiempo excedido.

0 TTL igual a 0 durante el tránsito.

1 TTL igual a 0 durante el ensamblado de fragmento.

12 Problema de parámetro.

0 Cabecera 11* no válida. Falla una opción.


1
13 (] I'clición de hora.
14 0 Respuesta de hora.
15 0 Petición de información.
16 ü Respuesta de información
17 (1 ['ctteión de dirección de mascara.
IS 0 Respuesta de dirección de máscara.

Asignación de multidifusión IPv4


En La Tabla ATO se definen las asignaciones de multidifusión actuales
[RFC2365] ordenadas por espectro.
Tabla A.10 Asignación de multidifusión propuesta
Rango de direcciones Ámbito TTL típico
224.0.0.0-224.0.0.255 Grupo ü

224.0.1.0-238.255.255.255 Global <=255

Tabla A.10 Asignación de multidifusión propuesta


(continuación)
Rango de direcciones Ámbito TTL típico
239.0.0.0-239.191.255.255 (no definido)

239.192.0.0-239.195.255.255 Organización < 128


239.196.0.0-239.254.255.255 (no definido)

239.255.0.0-239.255.255.255 Sitio <32

Asignación de direcciones IPv6 propuesta


En la Tabla AT I puede verse la asignación de direcciones IFvó propuesta en bits.
Tabla A.11 Asignación de direcciones IPv6 propuesta

Asignación Prefijo de dirección

(reservado) 0000 0000


(no asignado) 0000 0001
NSAP 0000 001
IPX 0000 010
(no asignado) oooo on
(no asignado) 00001
(no asignado) 0001
Agregar direcciones globales de 001
unidifusión
(no asignado! 010
(no asignado) 011
(no asignado) 100
(no asignado) 101
(no asignado) 110
(no asignado) 1110
(no asignado) 1111 0
(no asignado) 1111 10
(no asignado) 1111 110
(no asignado) 1111 1110 0

Enlace-dirección de unidifusión local 111I 1110 10


fiitio-dirección de unidifusión local 11111110 11
Direcciones de multidifusión 11111111

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.

0 No existe ruta para el destino.

1 Administrativamente prohibido (filtro firewall).

2 No es vecino (ruta de origen estricta incorrecta).

3 Dirección no alcanzable (general).

4 Puerto no alcanzable.

2 0 Paquete demasiado grande.


3 Tiempo excedido.

0 Límite de salto excedido durante el tránsito.

1 Tiempo de reensamblado de fragmento excedido.

4 Problema de parámetro.

01 Campo de cabecea erróneo. Cabecera siguiente n


reconocida.

2 Opción no reconocida.

128 0 Petición de eco (ping).


129 0 Respuesta de eco (ping).
130 0 Consulta de pertenencia a grupo.
131 0 Informe de pertenencia a grupo.
132 0 Reducción de pertenencia a grupo.
133 0 Solicitud de router.
134 0 Anuncio de router.
135 0 Solicitud vecina.
136 0 Anuncio vecino.
137 0 Redirigir.

Campo de ámbito de multidifusión IPv6


En la Tabla A.13 se definen los distintos valores del campo de ámbito en las
direcciones IPvó multidifusión.
Tabla A. 13 Descripciones del campo de ámbito de multidifusión IPV6
Ámbito Rango Descripción

0 (no definido)

1 Nodo Local en el mismo Aosf (como 127.0.0.1).


2 Enlace Los mensajes permanecen en el grupo del router. Los
routers nunca dejan pasar estos mensajes.

3-4 (no definido)

5 Sitio Según han definido los administradores de la red, los


mensajes permanecen en la vecindad del sitio.
6-7 (no definido)

8 Organización Según han definido los administradores de la red los


mensajes permanecen en la vecindad de la organización.
9-13 (reservado)

14 Global Todos los router permiten el paso de los mensajes a


través de la red global hasta que expiren.
15 (reservado)

Campo f l a g de multidifusión IPv6


La Tabla A.14 muestra los campos flag definidos actualmente para direcciones
de multidifusión IPvó.
Tabla A. 14 Descripciones de campos f/aqde multidifusión IPv6

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

En este apéndice se describen todas las bibliotecas y las llamadas de sistema


de red.

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á

soportado en el dominio. ENFILE Memoria insuficiente en el núcleo para albergar


una
estructura de socket nueva.
EMFILE Desbordamiento en la tabla de archivos de proceso.

EACCES y ENOBUFS Denegado el permiso para crear un socket del type o


protocol especificado.
ENOMEM Memoria disponible insuficiente. No se puede crear el
socket hasta liberar los recursos suficientes.
EINVAL protocol desconocido o familia de protocolo no dispo-
nible.

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.

addr Si no es cero, la llamada coloca la definición de la direc-

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 >

int connect(int sockfd, struct sockaddr "addr, int addrlen);

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

un intento de conexión anterior.


EAFNOSUPPORT La dirección pasada no tenía la familia de dirección

correcta en su campo sa_family.


E ACCES El usuario intentó conectarse a una dirección de difusión

sin tener habilitada la señal de difusión del socket.

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]

Debe ser PF_LOCAL o PFJJNIX.


SOCK_STREAM; esto crea un socket como pipe(). En algunos sistemas UNIX, el
canal es bidireccional. Posix 1, sin embargo, no requiere la bidireccionalidad.
Debe ser cero (0).
Array de enteros donde la llamada almacena los descriptores del socket nuevos,
si ha habido éxito.

Posibles errores
EMFILE Hay demasiados descriptores en uso por este proceso.

EAFNOSUPPORT El equipo no soporta la familia de direcciones indicada.

EPROTONOSUPPORT El equipo no soporta el protocol especificado.


EOPNOSUPPORT El protocol especificado no soporta pares de sockets. EFAULT
La dirección sockfds no corresponde a una parte válida
del espacio de direcciones del proceso.

Ejemplo
'*** Crear un par Pe sockets ***/ .nt sockfd[2]; ;truct sockaddrux addr; _f
( socketpair(PF_LOCAL, S0CK_STREAM, perror("socketpair");
1, sockfd) != a |

Comunicación por un canal


Una vez establecidos el socket y la conexión, se puede utilizar el API para enviar
' recibir mensajes. En este apartado se definen las API de llamadas para todos los
•■ockelsde E/S.
iend0
Envía un mensaje al peer, cliente o servidor conectado. Es parecida a la llamada
leí sistema wnte(), pero send() permite definir parámetros adicionales para el con-rol
del canal.

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).

• MSGJDONTROUTE. Enviar el mensaje pasando por alto todos los routers. Si no


tiene éxito, la red devuelve un error.
• MSGJ30NTWAIT. No permitir bloqueo. Parecida a la opción de la llamada
fcntlO, pero sólo se aplica a esta llamada. Si la llamada se bloquea, devuelve
EWOULDBIOCK en errno.

• M5G JvIOSIGNAL. Si el peer corta la conexión, no emite una señal SIGPIPE


local.

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 )

if ( (bytes_wrote +- bytes) >= msg_len ) break; if ( bytes < 0 )


perrorf"send");
/*** Enviar un mensaje URGENTE (TCP) a un destino conectado ***/ int sockfd;
int bytes, bytes_wrote=0;
/*--- Crear socket, conectar a servidor ■■-*/ if ( send(sockfd, buffer, 1, MSGJJOB) != 1 )
perrorf"Urgent message");
Envía un mensaje a un destino específico sin conexión. Esta llamada de sistema
se usa normalmente para sockets raw y UDP. El uso de esta llamada para sockets
TCP se llama Transaction TCP (T/TCP). (Sin embargo, Linux no soporta aún
T/TCP.)

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;

if ( (Sd = socket(PF_INET, SOCKDGRAM, 0 ) ) < 0 )


perror("socket"); bzero(&addr, sizeof(addr)|; addr.sin family = AFJNET; addr.sin_port =
htons(DESTPORT); inet_aton(DEST_ADDfl, &addr. sin_addr);

if ( send(sockfd, buffer, nsglen, 0 , &addr, sizeof(addr)) < 0 ) perror("sendto");

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.

Referencia a una estructura msghdr que contiene el destino, flags y mensajes. La


definición de la estructura es como sigue: struct íovec
{

void *iov_Pase; /* Buffer start */ _kernel_size_t iov_len; /* Buffer length */


> ;

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]);
}

msg.msg_iov = 10; msg.msg_iovlen - MSGS;


if ( (bytes = sendmsg(sd, Smsg, 0)) < 0 ) perrorf"sendmsg");

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.

• MSG_NOSIGNAL Este fiag desactiva el salto de SIGPIPE en síx'kcts en serie


cuando desaparece el otro extremo.
• MSG_ERRQUEUE. Este flag especifica que los errores encolados se deben
recibir de la cola de errores. El error se pasa a un mensaje auxiliar con un tipo que
depende del protocolo (para IP, IP_RECVERR). El error se notifica en una estructura
sock extended_error.

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));
}

msg.msg_iov = io; msg.msg_íovlen = MSGS;


if ( (bytes = recvmsg(sa, &msg, B>) < 8 ) perror("recvmsg");

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:

5HUT_RD (0) —Convierte el canal en sólo salida.


5HUT_WR (1) —Convierte el canal en sólo entrada.
5HUT_RDWR (2) —Cierra ambos extremos, con la misma funcionalidad que
dose().
Esto funciona sólo en sockets conectados.

Posibles errores
EBADF
ENOTSOCK
ENOTCONN
sockfd no es un descriptor válido.

sockfd es un archivo, no un socket.

El socket especificado no está conectado.

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");

Conversiones de datos de red


Cuando se trabaja con datos en una red, es necesario tener en cuenta el orden e tes
de los mismos, conversión de direcciones, etc. El API de Socket incluye una ta de
heramientas dimensionable que facilitan la obtención de la información cesaría. En
este apartado se describen las herramientas utilizadas en el libro (y ;mas otras).

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>

unsigned long int inetaddr(const char *ipaddress);

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).

addr Destino. Generalmente, se puede rellenar el campo sin_addr a


partir de la estructura sockaddr_in.

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

prsnt Cadena ASCII de la dirección IP (por ejemplo, 187.34.2.1 ó


FFFF::8090:A03:3245).

addr Destino. Normalmente, se puede rellenar el campo sin_addr a


partir de la estructura sockaddrj, o el campo sin6_addr a partir de la
estructura sockaddr_in6.

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).

addr Dirección binaria (normalmente, el campo de dirección de


struct sockaddr _in>.

str Buffer de cadena.

len Número de bytes disponibles en str.

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.

addr Buffer que almacena la estructura de la dirección.

addr_len Número de bytes disponibles en addr. Este valor se pasa solo


como referencia (la llamada cambia este eampo).

Posibles errores
EBADF El argumento sockfd no es un descriptor válido.

ENOTSOCK El argumento sockfd es un archivo, no un socket.

ENOTCONN El socket no está conectado.

ENOBUFS No había recursos suficientes en el sistema para realizar


esta operación.

EFAULT El parámetro addr señala a una parte de la memoria que no


forma parte del espacio de direcciones del proceso.

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.

len Número de bytes disponibles en name.

Posibles errores
EINVAL len es negativo o, para gethostname() en Linux/i386, len es
menor que el tamaño real.

EFAULT name es una dirección no válida.

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>

struct hostent *gethostbyname(const char *name);

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.

NO_ADDRESS, NO_DATA El nombre solicitado es válido, pero no posee


dirección IP.

NO_RECOVERY Ha tenido lugar un error de servidor de nombres


no recuperable.

EAGAIN Ha ocurrido un error temporal en un servidor de


nombres autorizado. Inténtelo más tarde de
nuevo.

Ejemplo
int i;
struct hostent *host;
host = gethostbyname("sunsite.une.edu");
if ( host != NULL )
{

printf("Official ñame: %s\n", host->h_name);


for ( i = 0; host->h_aliases[i] != 0; i++ )
printf(" alias[%d]: % s \ n " , i+1, host->h_aliases[i]);
printf("Address type=\d\n", host->h_addrtype);
for ( i = 0; i < host->h_length; i++ )
printf("Addr[%d]: %s\n", i+1, inet_ntoa(host->h_addr_list[i]));
}
else
perror("sunsite.unc.edu");

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 */
};

El campo p_proto es el número de puerto.

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.

level Nivel de característica (SOL_SOCKET, SOL_IP, SOL_TCP,


SOL_IPV6).

optname Opción a revisar.

optval Puntero para el valor nuevo.

optlen Longitud del valor en bytes.

Posibles errores
EBADF El argumento sd no es un descriptor válido.

ENOTSOCK El argumento sd es un archivo, no un socket.

ENOPROTOOPT Opción desconocida en el nivel indicado.

EFAULT La dirección señalada por optval no forma parte del


espacio de direcciones del proceso.

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.

level Nivel de característica (SOL_SOCKET, SOLJP, SOLTCP,


SOLJPV6).

optname Opción a revisar,

optval Sitio para el valor.

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.

ENOTSOCK El argumento sd es un archivo, no un socket.

ENOPROTOOPT Opción desconocida en el nivel indicado.

EFAULT La dirección señalada por optval no forma parte del


espacio de direcciones del proceso.

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

En este apéndice se describen todas la páginas del manual de la biblioteca del


núcleo y llamadas del sistema oue, aun no estando relacionadas directamente con
sockets, se suelen emplear en combinación con éstos.

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.

ENOMEM fork() no ha podido asignar las estructuras del núcleo


necesarias por falta de memoria libre.

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.

flags Dos tipos de información combinados lógicamente por medio


de OR; espacios de VM para compartir y señal de terminación.
Este flag soporta todos los tipos de señal y, cuando termina la
tarea, el sistema operativo levanta la señal definida.
A continuación se describen los espacios de VM disponibles:
• CLONE_VM. Comparte el espacio de datos entre tareas.
Utilice este flag para compartir todos los datos estáticos, datos
preiniciados y bloque de asignación. En caso contrario, copia el
espacio de datos.
• CLONE_FS. Comparte la información del sistema de
archivos: directorio de trabajo actual, rout del sistema, sistema de
archivos del roory permisos de creación de archivos
predeterminados. En caso contrario, copia la configuración.
• CLONE_FILES. Comparte archivos que se encuentran
abiertos. Cuando una tarea cambia el puntero del archivo, las
demás tareas detectan el cambio. Análogamente, si una tarea
individual cierra el archivo, las demás tareas no podrán acceder
al mismo. En otro caso, crea referencias nuevas para abrir
¡nodos.
• CLONE_SIGHAND. Comparte tablas de señales. Tareas
individuales pueden optar por ignorar señales abiertas (utilizando
sigprocmaskO) sin afectar a otros peers. En caso contrario, copia
las tablas.
• CLONE_PID. Comparte ID de proceso (PID). Utilice este flag
con precaución; no todas las herramientas existentes soportan
esta característica. La biblioteca PThreads no utiliza esta opción.
En otro caso, asigna una PID nueva.

arg Con este parámetro se puede pasar una referencia de puntero


a cualquier valor de datos. Cuando el sistema operativo termina
de crear la tarea hija, llama a la rutina fn con el parámetro arg. Si
utiliza esta característica, asegúrese de que el vlaor arg apunte a
la zona de datos compartidos (CLONE_VM).

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.

ENOMEM _clone() no ha podido asignar las estructuras del núcleo


necesarias debido a falta de memoria.

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.

path Ruta y nombre de archivo absolutos del programa a eiecutar.

argv Array de cadena de parámetros de línea de comandos. El valor


del primer elemento del array debe ser arg() (o el nombre del
programa). El último elemento del array es siempre cero (0).

arg Parámetro de línea de comandos. Este va seguido de puntos


suspensivos (...) para indicar que hay varios argumentos. El
primer arg es siempre el nombre del programa, y el último es
siempre cero (0).

envp Array de cadena de parámetros de entorno. Todos los parámetros


tienen el formato <param>=<value> (por ejemplo, TERM=vt100). El
último elemento del array es siempre cero (0).

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.

EPERM El sistema de archivos tiene montado nosuid, el usuario


no es un súperusuario y el archivo tiene un bit SUID o
SGID habilitado.

EPERM El proceso está siendo traceado. el usuario no es un


súperusuario y el archivo tiene un bit SUID o SGID
habilitado.

E2BIG El argumento es demasiado grande.

ENOEXEC Hay un ejecutable con formato no reconocido, está


diseñado para una arquitectura incorrecta, o posee algún
otro error de formato que impide su ejecución.

EFAULT El nombre de archivo señala fuera del espacio de


direcciones accesibles.

ENAMETOOLONG El nombre de archivo es demasiado largo.

ENOENT No existe el archivo, un script, o el intérprete ELF.

ENOMEM Memoria del núcleo disponible insuficiente.

ENOTDIR Un componente del prefijo de la ruta de acceso del


nombre de archivo, script, o intérprete ELF no es un
directorio.

EACCES Se ha denegado el permiso de búsqueda en un


componente del prefijo de la ruta del nombre de archivo,
o del nombre de un intérprete de script.

ELOOP Se han hallado demasiados enlaces simbólicos al


resolver el nombre de archivo, elnombre de un script o un
intérprete ELF.

ETXTBUSY El ejecutable ha sido abierto para escritura por más de


un proceso.

EIO Ha ocurido un error de E/S.

ENFILE Se ha llegado al límite del número total de archivos


abiertos en el sistema.

EMFILE El proceso tiene el número máximo de archivos abiertos.

EINVAL Un ejecutable ELF posee más de un segmento


PT_INTERP.

EISDIR Un intérprete ELF es un directorio.

ELIBBAD Un intérprete ELF no tiene un formato reconocido.

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().

PID Indica el proceso a esperar:


< -1. Espera cualquier proceso hijo (secundario) cuyo 11* de grupo
de procesos coincida con el valor absoluto de PID.
== -1. Espera cualquier proceso hijo; tiene el mismo
comportamiento que wait().
== 0. Espera cualquier proceso hijo cuyo ID de grupo de procesos
sea igual que el del proceso que origina la llamada.
> 0. Espera el proceso hijo cuyo IP de proceso sea igual al valor de
PID.

Options WNOHANG. Regresa de inmediato si no se ha salido de ningún


proceso hijo.
WUNTRACED. Regresa de procesos hijos (secundarios) que se
encuentran detenidos y cuyo estado no ha sido informado.

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.)

EINVAL Si el argumento de opciones no es válido.

EINTR Si WNOHANG no ha sido configurado y se ha captado una señal


SIGCHLD o no bloqueada. Basta probar de nuevo.
Ejemplo
void sigchild(int signum) /* Sólo obtiene un proceso en espera */
{ int status;
wait(&status);
if ( WIFEXITED(status) )
printf("Child exited with the value of %d\n”, WEXITSTATUS(status));
if ( WIFSIGNALED(StatUS) )
printf("Child aborted due to signal #%d\n", WTERMSIG(status));
if ( WlFSTOPPED(status) |
printf("Child stopped on signal #%d\n", WSTOPSIG(signal));
}

void sig_child(int signum) /* Elimina todos los procesos en espera */


{
while ( waitpid(-1, 0, WNOHANG) > 0 );
)

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.

attr Atributos inicíales del thread. Véase pthread_attr_init para más


información.

start_fn Rutina en la que se inicia el thread. Esta función debe devolver un


valor void*.

arg Parámetro pasado a start_fn. Este parámetro debe configurarse


como una referencia de memoria sin pila no compartida (a menos
que se se tenga intención de bloquearlo).

Posibles errores
EAGAIN Recursos del sistema insuficientes para crear un proceso para
el nuevo thread.

EAGAIN Ya existen más de PTHREAD_THREADS_MAX activos.

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.

retval Puntero para el valor devuelto (pasado sólo como referencia).

Posibles errores
ESRCH No se ha podido encontrar un thread correspondiente al
especificado por tchild.

EINVAL El thread tchild ha sido separado.

EINVAL Hay otro thread que espera la terminación de tchild.

EDEADLK El argumento tchild hace referencia al thread que realiza la


llamada.

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.

EINVAL El thread tchild ha sido separado.

EINVAL Existe otro thread a la espera de que termine tchild.

EDEADLK El argumento tchild hace referencia al thread que realiza la


llamada.

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.

mutexattr Cualquier atributo a configurar. Si es NULL, la llamada utiliza la


confiugración predeterminada
(PTHREAD_MUTEX_INITIALIZER).

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.

EDEADLK (pthread_mutex_try_lock) El thread que realiza la llamada ya


ha bloqueado el mutex (sólo en mutex de verificación de
errores).

EBUSY (pthread_mutex_lock) El thread que realiza la llamada se


encuentra actualmente bloqueado.

Ejemplo
pthread_mutex_t mutex = fastmutex;
if ( pthread_mutex_lock(&mutex) == 0 ) {
/*** trabajar con datos críticos ***/
pthread(mutex_unlock(&mutex);
}

pthread_mutex_t mutex = fastmutex;


/*---Realizar otro proceso en espera cteL semáforo---*/
while ( pthread_mutex_trylock(&mutex) != 0 && errno == EBUSY )
{

/**** Trabajar en otra cosa durante la espera ****/


}

/*--- ¡Semáforo verde! Trabajar en la sección crítica ahora ---*/


if ( errno != ENOERROR )
{
/**** 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.

sig_fn Rutina del programa llamada por el planificador.

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.

sigact Comportamiento deseado y controlador de señales usando la


siguiente estructura:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

sa_handler Puntero de función del controlador de señales.

sa_mask Conjunto de señales a bloquear al recibir una señal en el


controlador de señales.

sa_restorer Obsoleto; no se utiliza.

sa_flags Cómo manejar las señales. Se pueden usar los siguientes


flags:
• SA_NOCLDSTOP. Si la señal es SIGCHLD, ignorar los
casos en que el proceso hijo se detiene o hace una pausa.
• SA_ONESHOT o SA_RESETHAND. Restablecer la
configuración predeterminada del controlador una vez
obtenida la primera señal.
• SA_RESTART. Intentar reiniciar una llamada del sistema
interrumpida. Normalmente, las llamadas del sistema
interrumpidas devuelven un error EINTR. Esta opción trata de
reiniciar la llamada y evitar errores EINTR.
• SA_NOMASK o SA_NODEFER. Permitir que señales
parecidas interrumpan el controlador. Normalmente, si el
controlador está respondiendo a una señal en particular como
5IGCHLD, el núcleo suspendo otras señales SIGCHLD. Esto
puede conducir a una pérdida de señales. Con esta opción, el
controlador se puede interrumpir. Utilice esta opción con
precuación.

oldsigact Almacén de comportamientos anteriores. Aquí se puede


copiar la configuración antigua.

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.

EFAULT El parámetro sigact o oldsigact señala a una parte de la memoria


que no forma parte del espacio de direcciones del proceso.

EINTR Se ha interrumpido una llamada del sistema.


Ejemplo
void sig_handler(int signum)
{
switch ( signum )
{
case SIGCHLD:
...
}
...

struct sigaction sigact;


bzero(Ssigact, sizeof(sigact));
sigact. sa_handler = sighandler; /* configurar el controlador */
sigact.sa_flags = SANOCLDSTOP | SA_RESTART; /* establecer opciones */
if ( sigaction(SIGCHLD, fisigact, 2) == 0 )
perror("sigaction() failed");

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.

sigset Conjunto de señales de destino.

Oldsigset Si no es NULL, la llamada coloca aquí una copia de los valores


anteriores.

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.

EINTR Se ha interrumpido una llamada del sistema.

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().

memset() configura con val el bloque especificado.

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,

val Valor con el que rellenar el segmento,

bytes Número de bytes a escribir (tamaño del segmento de memoria).

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.

F_GETFD Valor de flag.

F_GETFL Valor de flags.

F_GETOWN Valor del propietario del descriptor.

F_GETSIG Valor de la señal enviada cuando ha sido posible leer o


escribir, o cero, para el comportamiento tradicional de SIGIO.

Todos los demás comandos devuelven cero.

Parámetros
fd Descriptor a manipular.

cmd Operación a realizar. Algunas operaciones son duplicados de funciones


existentes. Algunas operaciones requieren un operando (arg o flock). Cada
operación queda agrupada en funciones específicas:
• Duplicar descriptor (F_DUPFD). Al igual que dup2(arg, fd), esta
operación reemplaza fd con una copia del descriptor que hay en arg.
• Manipular cerrar-cd-ejecuiar (F GETFD, F SETFD). El núcleo no pasa
todos los descriptores de archivo al proceso exec hijo. Con este
parámetro se puede probar o configurar cerrar-al-ejecutar.
• Manipular flags de descriptor (F_GETFL, F_SETFL). Usando estos
comandos se pueden obtener los fíags (establecidos por la llamada del
sistema open()) del descriptor. Sólo se pueden establecer O.APPEND,
0_NONBLOCK, y 0_ASYNC.
• Manipular bloqueos de archivo (F_GETLK, F_SETLK, F.SETLKW).
GETLK recupera la estructura de bloqueo que contiene actualmente el
archivo. Si el archivo no está bloqueado.
• Determinar quién pasee las señales de E/S (F_GETOWN, F_SETOWN).
Devuelve o establece el PID del propietario actual de la señal SIGIO.
• Determinar la clase de seña! a enviar (F_GET-SIG, F_SETSIG). Obtiene
o establece el tipo de señal cuando pueden realizarse más operaciones
de E/S. El valor por omisión es SIGIO.

arg Valor a establecer.

flock Clave de bloqueo.

Posibles errores
EACCES Operación prohibida por bloqueos mantenidos por otros
procesos.

EAGAIN Operación prohibida porque el archivos ha sido asignado a


memoria por otro proceso.

EBADF fd no es un descriptor de archivo abierto.

EDEADLK Se ha detectado que el comando F_SETLKW especificado


podría causar un bloqueo total.

EFAULT lock queda fuera del espacio de direcciones accesible.

EINTR Para F_SETLKW, el comando ha sido interrumpido por una seña!.


Para F_GETLK y F_SETLK, el comando ha sido interrumpido por
una señal antes de verificar o adquirir el bloqueo—
probablemente, al bloquear un archivo remoto (bloqueo sobre
NFS), aunque también puede ocurrir localmente.

EINVAL Para F_DUPFD, arg es negativo o mayor que el valor máximo


permitido. Para F_SETSIG, arg no es un número de señal
permitido.

EMFILE Para F_DUPFD, el proceso tiene ya abiertos el número máximo


de descriptores de archivo.

ENLOCK Demasiados bloqueos de segmentos abiertos, tabla de


bloqueos llena, o fallo en un protocolo de bloqueo remoto (por
ejemplo, bloqueo sobre NFS).

EPERM Se ha intentado borrar el f!¿ig 0_APPEND en un archivo que tiene


activado el atributo de sólo-anexar.

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.

ENFILE La tabla de archivos del sistema está llena.

EFAULT El proceso no es actualmente el propietario de la memoria a la


que señala fd (referencia de memoria no válida).

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:

POLLIN. Hay datos que leer.

POLLPRI. Hay datos urgentes que leer.

POLLOUT. Escribir ahora no bloqueará.

POLLERR. Condición de error.

POLLHUP. Cuelgue.

POLLNVAL. Petición no válida; fd no abierto.

POLLRDNORM. Lectura normal (sólo Linux).


POLLRDBAND. Lectura fuera de banda (sólo Linux).

POLLWRNORM. Escritura normal (sólo Linux).

POLLWR6AND. Escritura fuera de banda (sólo Linux).

nfds Número de registros a verificar durante ta llamada.

timeout Límite de tiempo en milisegundos. Si timeout es negativo, la


llamada esperará indefinidamente.

Posibles errores
E N O M E M No hay espacio para ubicar tablas del descriptor de archivos.

E FA U LT El array facilitado como argumento no está contenido en el


espacio de direcciones del programa que realiza la llamada.

EINTR A ocurrido una señal antes de cualquier evento requerido.

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 )
{

fds[fd_count].events = POLLIN | POLLHUP;


fds[fd_count++].fd = accept(fds( 0 ] .fd, 0 , 0 ) ;
}
for ( i = 1 ; i < fc_count; i++ )
{
if ( (fds[i].revents & POLLHUP) != 0 )
{
close(fds[i].fd);
/*** Subir FDs para rellenar franja vacia ***/
fd_count--;
}
else if ( (fds[i}.revents & POLLIN) != 0 )
/*** Leer y procesar datos ***/
}
}

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).

buffer Buffer de memoria para aceptar los datos leídos.

buf_len Número de bytes a leer y número de bytes legales en el buffer.

Posibles errores
EINTR La llamada ha sido interrumpida por una señal antes de leeT
ningún dato.

EAGAINElO Se ha seleccionado E/S de no bloqueo con O_NONBLOCK y


no había datos disponibles para leer.

EIO Error de E/S. Esto sucede cuando el proceso se encuentra en


un grupo de procesos en segundo plano, intenta leer de su tty
controlador, ignora o bloquea SIGTTIN, o su grupo de procesos
ha quedado huérfano. También puede ocurrir cuando se
produce un error de E/S de bajo nivel al leer de un disco o una
cinta.

EISDIR fd hace referencia a un directorio.


EBADF fd no es un descriptor de archivo válido o no está abierto para
lectura.

EINVAL fd está asociado a un objeto que no permite la operación de


lectura.

EFAULT buf se encuentra fuera del espacio de direcciones accesible.

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.

readfds Conjunto de descriptores a verificar para leer.

writefds Conjunto de descriptores a verificar para escribir.

exceptfds Conjunto de descriptores a verificar para datos fuera de banda.

timeout Tiempo máximo de espera para la llegada de datos, en milisegundos.


Este es un puntero para un número. Si el número es cero (no el
puntero), la llamada vuelve inmed ata mente una vez verificados todos
los descriptores. Si el puntero es NULL (cero), se deshabilita la
característica timeout de selección.

fd Descriptor de archivo a añadir, quitar o verificar.

set Conjunto de descriptores de archivo.

Posibles errores
EBADF Se ha dado un descriptor de archivo no válido en alguno de los
conjuntos.

EINTR Se ha capturado una señal no bloqueada

EINVAL n es negativo.

ENOMEM select no ha podido asignar memoria para tablas internas.

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++ )
{

sockfd = socket(PF_INET, SOCK_STREAM, 0);


addr.sin_port = htons(ports[i]);
if ( bind(sockfd, &addr, sizeof(addr)) != 0)
perror("bind() failed");
else
{
FD_SET(sockfd, &set);
if ( max < sockfd )
max = sockfd;
}
}

if ( select(max+1, &set, 0, &set, &timeout) > 0 )


{
for ( i = 0; i <= max; i++ )
if ( FD_ISSET(i, &set) )
{ int client = accept(i, 0, 0);
/**** procesar las peticiones del cliente ***/
}
}

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).

buffer Mensaje a escribir.

msg_len Longitud del mensaje.

Posibles errores
EBADF fd no es un descriptor de archivo válido o no está abierto para escritura.

EINVAL fd está asociado a un objeto que no es apto para escritura.

EFAULT buf está fuera del espacio de direcciones accesible.

EPIPE fd está conectado a un canal o socket cuyo extremo de lectura está


cerrado. Cuando sucede esto, el proceso de escritura recibe una señal
SIGPIPE; si captura, bloquea o ignora el error, se devuelve EPIPE.

EAGAIN Se ha selecconado E/S sin bloqueo mediante OJ^JON-BLOCK y no


había espacio en el canal o socket conectado a fd para escribir los
datos inmediatamente.

EINTR La llamada ha sido interrumpida por una señal antes de escribir ningún
dato.

ENOSPC El dispositivo que contiene el archivo al que hace referencia fd no tenía


espacio para los datos.

EIO Ha ocurrido un error de E/S de bajo nivel al modificar el inodo.

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:

Range Exception Cualquier excepción de rango. Usada por la clase


MessageGroup.

FileException Cualquier excepción de archivo. Usada por la clase


Socket.

NetException (Clase)
Constructor:
NetException(SimpleString s);
Descripción general: excepción genérica de red.
Clase superior: Exception
Excepciones secundarias:

NetConversionException Excepción de conversión de dirección de host


(inet_ntop/inet_pton). Usada por la clase
HostAddress.

NetDNS Exception No se pudo resolver excepción de nombre de host.


Usasda por HostAddress.

NetlOException Excepción send()/recv() exception. Usada por


Socket.
NetConnectException Excepción al tratar de usar bínd(), connect(),
listen(), o acceptf). Usada por ServerSocket,
ClientSocket, y MessageGroup.

NetConfigException Excepción al establecer u obtener opción de


socket. Usada por todas las clases Socket.

Clases de soporte C++


En este apartado se describen varias clases relacionadas con la estructura, pero
nás simples que las de otras bibliotecas de clases. Estas pueden ser reemplazadas
in problemas por bibliotecas de clases C++ estándar.

SimpleString (Clase)
Constructor:
SimpleString(const char* s);
SimpleString(const SimpleStringS s);
Descripción general: tipo de cadena ligero y muy sencillo.
Métodos:

+(char *)+(Simplestring&) Anexa la cadena a la instancia actual,

const char* GetString(); Recupera el mensaje de la cadena.

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.

int GetPort(void) const; Obtiene el número de puerto.

ENetwork GetNetwork(void) const; Obtiene el tipo de red.

struct sockaddr* GetAddress(void) const; Obtiene la dirección real del socket.

int GetSize(void) const; Obtiene el tamaño de dirección del


socket.

int ==(HostAddress& Address) const; Compara si es igual.


int !=(HostAddress& Address) const; Compara si es distinto.

const char* GetHost(bool byName=1); Recupera el nombre de host

Excepciones:
Exception
NetConversionException
NetDNSException

Clases de mensajería C++


Esta jerarquía de clases permite definir clases que empaquetan y
desempaquetan automáticamente los datos internos. Es sencilla y directa, aunque
no en la medida de la jeraquía de Java.

Message (Clase abstracta)


Constructor: (ninguno)
Descripción general: patrón para crear mensajes específicos para enviar o
recibir.
Métodos:
virtual char* Wrap(int& Bytes) const; Interfaz para empaquetar objeto.

bool Unwrap(char* package, int Bytes, Interfaz para desempaquetar objeto.


int MsgNum);

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:

=(const char* str); Asigna una nueva cadena a objeto.


=(const TextMessage& s);

+=(const char* str); Anexa cadena a objeto.


+={const TextMessage& s);

const char* GetBuffer(void) const; Obtiene el texto.


char* Wrap(int& Bytes) const; Empaqueta objeto a enviar.

bool Unwrap(char* package, int Desempaqueta objeto recibido.


Bytes, int MsgNum);

GetSize(void) const; Obtiene longitud de la cadena.

void SetSize(int Bytes); Establece longitud de la cadena.

int GetAvaÍlable(void) const; Obtiene by tes disponibles en el buffer.

Excepciones: (ninguna)

Clases de sockets C++


Esta jerarquía define las clases que conforman las interfaces de socket Contiene
cinco clases diferentes que se pueden ejemplificar: SocketServer, SocketClient,
Datagram, Broadcast, y MessageGroup. Esta jerarquía se puede expandir
fácilmente con clases OpenSSL (SSLServer y SSLClient).
Socket ← SocketStream ← SocketServer
← SocketClient
← Datagram ← Broadcast
← MessageGroup
FIGURA D.2
Jerarquía de clases de sockets C++.

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:

int GetMaxSegmentSize(void); Obtiene-establece tamaño del


segmento (MBS).
void SetMaxSegmentSize(short
Bytes);

void DontDelay(bool Setting); Habilita-deshabilita algoritmo Nagle.

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:

void Accept(void (*Servlet) (const Acepta una conexión y llamada Servlet


Sockets Client)); con controlador Socket.

void Accept(HostAddress& Addr, Acepta conexión y captura ID de host.


void (*Server)(const Socket&
Client));

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:

void MinimízeDelay(bool setting); Pide retardo mínimo de paquete.

void MaximizeThroughput(bool Pide rendimiento máximo de red.


Setting);

void MaximizeReliability(bool Pide máxima fiabilidad.


Setting);

void MinimizeCost{bool Setting); Pide mínimo coste.Establece


negociación de fragmentación.

void PermitFragNegotiation(EFrag Establece negociación de fragmentación


Setting);

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:

Connect(HostAddress& Conecta a direcciones de grupo de


Address); multidifusión.

void Join{HostAddress& Se une a grupo de multidifusión.


Address, int IFIndex=0);

void Drop(HostAddress& Deja grupo de multidifusión.


Address);

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.UnknownHostException Nombre de host no hallado en DNS.

java.net.UnknownServiceException Intento de servicio no soportado.

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.ConnectExcept ion Host no disponible, no hallado, que no responde, o no


se ha escuchado el proceso en el puerto de destino.

java.net.NoRouteToHostExce No se ha podido establecer una ruta al destino.


ption

Clases de soporte Java


Al igual que la estructura de C++, Java utiliza varias clases de soporte para
actuar como interfaz con su API de Socket. En este apartado se describen sólo las
que están directamente relacionadas con sockets.

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.

void setData(byte[] buf, int offset,


int len);

int getLength(); Obtiene o establece la longitud de los


void setLength(int length); datos del mensaje.

int getOffset(); Obtiene el desplazamiento de los datos a


enviar o recibir.

int getPort(); Obtiene o establece el puerto de origen o


void setPort(int port); destino del paquete.

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.

InetAddress getAIIByName(String Devuelve todas las direcciones de


host); Internet para el host.

InetAddress getLocalHost(); Obtiene la dirección IP del host local.

Métodos:
String getHostAddress(); Obtiene la dirección numérica.

byte[] getAddress(); Obtiene la dirección binaria.

boolean Comprueba si la dirección está en el rango de


isMulticastAddress(); multidifusión.

String getHostName(); Obtiene el nombre real del host.

Excepción:
Unknown Host Exception

Clases Java de E/S


Java posee un formidable conjunto de clases que operan con distintas E/S.
Desafortunadamente, no son muy intuitivas, y conectarlas es como trabajar con un
puz-le. Consulte el Capítulo 12, "Uso de las API de red de Java", para más
información acerca de cómo conectar estas piezas para conseguir flujos de utilidad.
Object ← InputStream ← ByteArrayInputStream
← Ob|ectlnputStream
← OutputStream ← ByteArrayOutputStream
← ObjectOutputStream
← Reader ← BufferedReader
← Writer ← PrintWriter
FIGURA D.4
Jerarquía de clases Java de E/S.

Java.io.InputStream (Clase abstracta)


Constructor:
InputStream();
Descripción general: clase genérica para entrada básica de flujo.
Clase superior: Object
Métodos:
int available(); Devuelve el número de bytes que se pueden
leer sin bloqueo.

void close(); Cierra el canal.

void mark(int readlimit); Establece el número máximo de bytes de buffer


para mark() y reset().

boolean markSupported(); Comprueba si el flujo soporta mark{)/reset().

int read(); Lee un byte del flujo.

int read(byte[] arr); Lee un array de bytes en arr.

int read(byte[] arr, int offset, int Lee un array de length bytes en arr,
length); comenzando en offset.

void reset(); Devuelve el último sitio marcado.

long skip(long n); Omite los n bytes siguientes del flujo.

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.

Clase superior: InputStream


Métodos: (ninguno; muchos métodos de InputStream reemplazados)
Excepciones: (ninguna)

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.

void close(); Cierra este canal.

void defaultReadObject(); Lee los campos no estáticos y no


transitorios de la clase en este flujo.

int read(); Lee un byte o un array de len bytes


int read(byte[] arr, int offset, int len); comenzando en offset.

readFully(byte[] arr); readFulIyO lee todos ios bytes para llenar


readFully(byte[l arr, int offset, int len); el array, bloqueando cuando es necesario.

boolean readboolean(); Lee el tipo designado.


byte readByte();
char readChar();
double readDouble();
float readFloat();
int readlnt();
long readLong();
short readShort();
int readUnsignedByte();
int readUnsignedShort();

String readUTF(); Lee la instancia Object. Se puede descubrir


Object readObjectO; el tipo y convertirlo después con los
operadores de reparto.

Excepciones:
lOException
ClassNotFoundException
NotActiveException
OptionalData Exception
InvalidObjectException
SecurityException
StreamCorruptedException

Java.io.OutputStream (Clase abstracta)


Constructor: OutputStream();
Descripción general: clase genérica para entrada de flujo básica.
Clase superior: Object
Métodos:
void close(); Cierra el canal.

void flush(); Limpia los datos escritos de los buffers.

void write{byte b); Escribe un byte en el flujo.

int write(byte[] arr);i Escribe un array de bytes (arr) en el flujo.

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.

byte[] toByte Array (); Devuelve el array de datos convertidos.

int size(); Devuelve el tamaño actual del buffer.

String toString{String Crea una cadena, tradnciendo caracteres


encoder); con encoder.

void write(int b); Escribe un byte en el flujo.

void write(OutputStream o); Envía un array de datos a través de


OutputStream.

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.

void defaultWriteObject(); Escribe los campos no estáticos y no


transitorios de la clase actual en el flujo. Sólo
se puede llamar desde el método
writeObjectO durante la señalización.

int flush(); Limpia los datos escritos de los buffers.

int reset(); Coloca la información escriba en el flujo.

void useProtocolVersion(int Fuerza una versión de señalización


versión); anterior.

void write(byte b); Escribe un byte o un array de bytes


comenzando en offset, en len bytes.
int write(byte[] arr);
int write(byte[] arr, int offset, int
len);

void writeboolean(boolean b); Escribe el tipo designado.


void writeByte(byte b);
void writeBytes(String s);
void writeChar(int c);
void writeChars(String s);
void writeDouble(double d);
void writeFloat(float f);
void writelnt(int i);
void writeLong(long I);
void writeShort(int us);
void wrlteUTF(String s);

int wríteFields(); Escribe los campos del bufferen el flujo.

void writeObject(Object o); Escriba la instancia Object.

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.

void mark(int readlimit); Establece el número máximo de bytes de


bufferpara mark() y reset().

boolean markSupported(); Comprueba sí el flujo soporta


mark()/reset().

int read(); Lee un byte del flujo.

ínt read(byte[] arr, int offset, int Lee un array de bytes en arr comenzando
length); en offset, en length bytes.

String readLine(); Lee nueva línea y devuelve String.


boolean ready(); Devuelve un valor verdadero si está listo
para leer.

void reset(); Devuelve el último lugar marcado.

long skip(long n); Omite los n bvtes siguientes del flujo.

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.

void close(); Cierra este canal.

void defaultWriteObject(); Escribe los campos no estáticos v no transitorios de la


case actual en este flujo. Sólo se puede hacer la
llamada desde el método writeObject(), durante la fase
de señalización.

int flush(); Limpia los datos escritos en los buffers.

int reset{); Coloca los datos escritos en el flujo.

void write(byte b); Escribe un byte o un array de bytes comenzando en


offsei, en len bytes.
int write(byte[] arr);
int write(byte[] arr, int offset,
int len);

void print(boolean b); Imprime el tipo designado, El tipo Objeto utiliza el


método String .va lueOf() para convertir los datos.
void print(char c);
void print(char[] s);
void print(double d);
void print(float f);
void print(int i);
void print(long I);
void print(Object obj);
void print(String s);

void println(); Imprime el tipo designado y termina la línea con un


código de línea nueva. Si está activado autoFlush,
void println(boolean b); limpia el flujo.
void println(char c);
void println(char[]s);
void println(double d);
void println(float f);
void println(int i);
void println(long I);
void println(Object obj);
void println(String s);

void write(int b); Escribe un byte en el flujo.

int write(char[] arr); Escribe un array de caracteres (arr) en el flujo.

int write(char[] arr, int offset, Escribe un array de caracteres (arr) comenzando en
int len); offset, en len bytes.

int write(String s); Escribe cadena en el flujo.

int write(String s, int offset, Escribe cadena en el flujo, comenzando en offset, en


int len); len bytes.

Excepciones:
lOException
SecurityException

Clases de sockets Java


El API de Socket Java soporta cuatro clases IP V 4 básicas: Socket,
ServerSocket, DatagramSocket, y MulticastSocket. En este aparado se describe la
interfaz para ada una de dichas clases.
Object ← Socket
← ServerSocket
← DatagramSocket ← MulticastSocket
FIGURA D.5
Jerarquia de clases de sockets Java.

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);

Descripción general: ésta es la interfaz básica de comunicación (TCP) para


todo el tráfico de red.
Clase superior: Object
Métodos:
void close(); Cierra el socket.

InetAddress getlnetAddress(); Obtiene la dirección del host del peer.

InputStream getlnputStream(); Obtiene el InputStream para los mensajes


recibidos.

boolean getKeepAlive(); Mantiene abierta la. conexión.

void setKeepAlive(boolean on); Obtiene la dirección local a la que está


InetAddress getLocalAddress(); conectado el socket

int getLocalPort(); Obtiene el puerto local.

OutputStream Obtiene el OutputStream para los mensajes


getOutputStream(); enviados.

int getPort(); Obtiene el número de puerto del peer.

int getReceiveBufferSize(); Obtiene-establece el tamaño del buffer.


void setReceiveBufferSize(int
size);

int getSendBufferSize(); Obtiene-establece el tamaño del buffer.


void setSendBufferSize(int size);

int getSoLinger(); Obtiene-establece el tiempo de retrado del


void setSoLinger(boolean on, int socket (en segundos).
linger);

int getSoTimeout(); Obtiene-establece el límiíe de tiempo para


E/S . Si está activado, se aborta la lectura
void setSoTimeout(int timeout); del canal transcurrido el tiempo
especificado.

boolean getTcpNoDelay(); Hábilita-deshabilita el algoritmo Nagle, que


void setTcpNoDelay(boolean on); determina el proceso para enviar
información. Si está desactivado, la
computadora envía los datos sin esperar
confirmación.

void shutdownlnput(); Cierra el canal de entrada.

void shutdownOutput(); Cierra el canal de salida.

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.

void close(); Cierra el socket

InetAddress Obtiene la dirección local a la que está conectado el


getlnetAddress(); socket.

int getLocalPort(); Obtiene el puerto local.

int getSoTimeout(); Obtiene-establece el límite de tiempo para E/S. Si


está activado, se aborta la lectura del canal
void transcurrido el tiempo especificado.
setSoTimeout(inttimeout);

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.

void connect(lnetAddress addr, Conecta peers para envío implícito.


int port);

void disconnect(); Desconecta peers conectados.

InetAddress getlnetAddress(); Obtiene la dirección de host del peer.

InetAddress getLocalAddress(); Obtiene la dirección local a la que eslá


conectado el socket.

int getLocalPort(); Obtiene el puerto local.

int getPort(); Obtiene el número de puerto del peer.

int getReceiveBufferSize(); Obtiene-establece el tamaño del buffer de


recepción.
void setReceiveBufferSize(int
size);

int getSendBufferSize(); Obtiene-establece el tamaño del b u f f e r de


envío.
void setSendBufferSize(int size);

int getSoTimeout(); Obtiene-establece el limite de tiempo para E/S.


void setSoTimeout(int timeout); Si está activado, se aborta la lectura del canal
transcurrido el tiempo especificado.

void receive(DatagramPacket p); Recibe mensaje.


void send(DatagramPacket p); Envía mensaje.

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:

InetAddress getInterface(); Obtiene-establece la dirección local a la que


esta conectado el socket.
void setlnterface(lnetAddress addr);

int getTimeToLive(); Obtiene-establece el tiempo de existencia de


cada mensaje.
void setTimeToLive(int TTL);

void joinGroup(lnetAddress addr); Se une al grupo de multidifusión.


void leaveGroup(lnetAddress addr); Abandona el grupo de multidifusión.
void send(DatagramPacket p, int Envía mensaje con TTL específico.
TTL);

Excepciones;
lOException
SocketException

También podría gustarte