Enunciado de la práctica "Sala de chat multiusuario con memoria compartida"

Objetivos

Los objetivos fundamentales de esta práctica son:

  • Iniciarnos en la programación de aplicaciones reales que usan los servicios ofrecidos por el sistema operativo.
  • Aprender como operan algunos de los aspectos del sistema que se han visto en las clases de teoría: hilos, memoria compartida, sincronización, creación de procesos, tuberías, etc.
Para ello desarrollaremos nuestro propio chat multiusuario, que funcionará de forma similar a como se puede ver en este vídeo:

  1. El programa permite indicar por línea de comandos el nombre de una sala de chat y, opcionalmente, un nombre de usuario de la sala.
    1. Si no se indica el nombre de usuario, la aplicación usa el nombre de la cuenta del usuario en el sistema.
    2. Cuando se le pasa la opción -h o --help, muestra ayuda sobre su uso.
  2. El primer cliente que se conecte crea la sala y es el responsable de destruirla al irse.
  3. Cualquier cliente puede mandar mensajes. Cuando un cliente escribe un mensaje, los reciben y lo muestran.
    1. Si un usuario escribe :quit, no se envía. El programa en cuestión simplemente termina.
    2. Si un usuario escribe !ls, !cat /etc/passwd o cualquier otro ! comando; el comando se ejecuta y su salida se muestra tanto en su terminal como en la de los otros usuarios del chat.
  4. Y hace cualquier otra cosa que se te pueda ocurrir a ti ;)
Sin embargo no pretendemos alcanzar estos objetivos de una sola vez. En su lugar desarrollaremos nuestra aplicación en 3 pasos.

Ayuda

Pero antes de hablar de esos pasos vamos a comentarte algunas cosas que te pueden ayudar.

Lo primero es que la documentación sobre lo que necesitas es extensa. Si por ejemplo quieres saber como se usa la llamada al sistema shm_open, puedes ejecutar el comando:

man shm_open

o visita la web linux.die.net y buscar shm_open. Ambas cosas te darán acceso a las páginas del manual.

Si lo que te interesan son cosas relacionadas con la librería estándar de C++, como std::mutex, std::condition_variable o std::unique_lock, puedes visitar es.cppreference.com.

Manejo de errores

Si lees detenidamente las páginas del manual de las llamadas al sistema que vamos a usar, todas pueden fallar. La norma suele ser que indiquen el fallo devolviendo un entero menor de 0. En caso de que eso ocurra, la variable global errno contiene siempre el motivo del error.

No ignores los errores, detéctalos y muestra, por la salida de error (std:cerr), información sobre dónde ocurrió y el motivo. Cada código de error posible en errno se puede convertir en una cadena de texto explicativa mediante la función strerror(). Te ayudará a corregir los fallos que tengas mucho más rápidamente.

Parte 1

Vamos a partir de un proyecto inicial que tiene la estructura básica sobre la que vamos a trabajar. El proyecto está hecho para un IDE denominado Qt Creator, así que debes tenerlo instalado. Obviamente es posible usar otro editor, pero te recomendamos que te acostumbres a desarrollar usando un IDE. También, por diversos motivos, necesitarás tener instalada la librería libboost-dev y libboost_system-dev.

Del proyecto inicial te interesará:

  • El archivo main.cpp, que contiene la función principal main().
  • chatroom.cpp y chatroom.h que es donde se implementa la clase ChatRoom.
Esos archivos están llenos de comentarios con la cadena "TODO:" que te indicarán lo que debes programar en cada sitio hasta completar esta parte.

El objetivo para la primera parte es crear un programa capaz de:

  1. Crear un objeto de memoria compartida, usando como nombre el de la cuenta del usuario en el sistema, y manda mensajes a través de él.
  2. Si el objeto de memoria compartida existe, acceder a él y simplemente recibir los mensajes para mostrarlos por la salida estándar.
  3. Salir si el usuario escribe ":quit". En ese caso, si el programa fue quien creo el objeto de memoria compartida, deberá destruirlo.
  4. Mostrar los errores al usuario en caso de que algo falle.

A la hora de implementar te recomendamos que sigas el siguiente orden:

  1. ChatRoot::connectTo() y ChatRoot::~ChatRoot().
    1. connectTo()  se encarga de crear el objeto de memoria compartida con shm_open(), si no existe, y de mapearlo en memoria con mmap().
    2. Además, si el objeto tuvo que ser creado, hay que darle un tamaño adecuado con ftruncate() e inicializar un objeto SharedMessage en la memoria compartida con el operador new de emplazamiento.
    3. El destructor ~ChatRoot() se debe encargar de desmapearlo con munmap() y, si el objeto tuvo que ser creado, de destruirlo con shm_unlink().
  2. ChatRoot::runSender() y ChatRoot::runReceiver(). Ambos debe iterar indefinidamente. El primero leyendo una línea de la entrada estándar (std::cin) y mandando dicho mensaje con ChatRoot:: end(). El segundo leyendo un mensaje con ChatRoot::receive() y mostrándolo por la salida estándar (std::cout). Para salir, sería interesante que hubiera alguna forma de romper el bucle de ChatRoot::runSender(), por ejemplo saliendo de él si el mensaje es ":quit" o cuando se pulsa Ctrl+D (cuando eso ocurre cout.eof() devuelve true).
  3. ChatRoot::SharedMessage, send() y receive().
    1. Piensa que campos debes guardar en la memoria compartida y añadelos a ChatRoot::SharedMessage.
    2. Seguro que necesitas un mecanismo de sincronización. ¿Tal vez un std::mutex? ¿y una variable de condición std::condition_variable? Seguramente los programas que reciben mensajes sólo deban mostrar un mensaje cuando llega alguno nuevo y ese es un buen uso para una variable de condición. Añade lo que necesites a ChatRoot::SharedMessage.
    3. Implementa send(). Recuerda que debes bloquear la memoria antes de modificarla. Y que debes notificar a los receptores que hay un mensaje nuevo a través de la variable de condición.
    4. Implementa receive(). Recuerda que debes bloquear antes de leer, para que el mensaje no pueda cambiar mientras lees. Es importante que no muestres mensajes constantemente, sino que sólo lo hagas cuando haya llegado uno nuevo. Si no hay ningún mensajes nuevo, puedes dormir en una variable de condición y esperar a que quien lo envía te haga despertar.

Parte 2

Por el momento, aunque tenga varios programas conectados a la sala, sólo uno, el propietario del objeto de memoria compartida, es capaz de mandar mensajes. Mientras que todos los demás sólo reciben. En esta parte vamos a resolver eso:

  1. Añadiendo opciones de línea de comandos para que cada usuario pueda elegir su nombre de usuario y el nombre del objeto de memoria compartida al que quiere conectarse (identificador de la sala de chat). Además este es el momento de añadir la opción -h o --help con ayuda sobre el uso de tu nuevo programa.
  2. Lanzando dos hilos. Uno que constante lee de la entrada estándar y manda mensajes y otro que recibe los mensajes y los muestra por la salida estándar.

Parte 3

Finalmente sería interesante que si un usuario escribe una expresión de la forma !comando, el comando indicado se ejecutara y que su salida estándar se enviara a todos los clientes conectados a la sala. Para eso:

  1. Implementar ChatRoom::execAndSend para que ejecute el comando especificado, lea su salida estándar y la mande al chat.
  2. Detectar, al leer la entrada estándar, expresiones de la forma !comando e invocar a ChatRoom::execAndSend() en lugar de a ChatRoom::send()