Ejercicio obligatorio 4
Fecha de entrega: Domingo 2 de junio
Introducción
IRC
El protocolo de Internet Relay Chat (IRC) data del año 1993 y establece un mecanismo sencillo para crear salas de chat.
El protocolo es un protocolo de texto, en el mismo hay un servidor al que los clientes se conectan y tanto los clientes como el servidor escriben y leen líneas de a líneas de texto.
La estructura de IRC es la que replican hoy en día sistemas de chat como Discord o Slack: Uno se conecta a un servidor utilizando un determinado nick. De ahí puede o enviar mensajes personales a otros usuarios o entrar a canales. En los canales todo lo que se diga llegará a todos los usuarios conectados a ese canal.
Sockets
Un socket es un canal que se puede utilizar para comunicar dos aplicaciones. Un socket de internet es abierto por una aplicación determinada y se identifica por una dirección IP y por un puerto. La aplicación que se conecte a ese socket podrá dialogar con la aplicación que lo abrió.
En el mundo de Unix todo lo que es comunicación de procesos se maneja mediante una interfaz de archivos. Es decir, una vez pasado el proceso de apertura del canal de comunicación, lo que se obtiene es un descriptor de archivo donde se pueden realizar operaciones de lectura y escritura. Desde el lado del usuario se trata de manipular archivos, mientras que es el sistema operativo el que maneja los detalles de implementación.
Unix no utiliza la interfaz de archivos de ISO-C99 basada en FILE *
sino una
muy similar (y más sencilla) basada en descriptores de archivo (file
descriptors, fd). Un descriptor de archivo es apenas un int
que representa a
un archivo abierto.
Para leer y escribir de ese archivo se pueden usar dos funciones equivalentes a
fread()
y fwrite()
que son las funciones:
ssize_t read(int fd, void *buff, size_t nbytes);
ssize_t write(int fd, const void *buff, size_t nbytes);
ambas funciones leen del archivo fd
(o escriben respectivamente) nbytes
en
(o desde) el puntero buff
.
Al conectar un socket con un servidor lo que se obtiene es un fd
que comunica
con éste. Si se escribe en el fd
eso será leído por el servidor, si el
servidor se comunica con nosotros podremos leer del fd
lo que nos envió.
Poll
Ya sabemos que la llamada a funciones de lectura de archivos son bloqueantes.
Es decir, si nosotros llamamos, por ejemplo, a getchar()
esa llamada va a
quedarse congelada hasta que efectivamente ingrese algún carácter en stdin
.
Si queremos tener una aplicación que hable con el usuario a través de stdin
y
que además hable con un servidor a través de otro archivo, para atender a los
dos tenemos que poder saber de antemano si hay datos en los buffers de tal
manera que cuando llamemos a una función de lectura estemos seguros de que esa
llamada no bloqueará y nos devolverá el dato que ya está esperando.
Unix resuelve esto de manera sencilla con un mecanismo que se llama polling. Uno puede decirle al sistema operativo que está interesado en determinados descriptores de archivos y el sistema operativo puede quedarse escuchando en todos a la vez y avisarnos cuál de todos tiene un dato, de tal manera que cuando vayamos a leer podamos hacerlo de forma no bloqueante.
Protocolo de IRC
Como ya dijimos, el protocolo de IRC es un protocolo orientado a líneas. A su vez las líneas tienen diferentes campos... y ya conocemos el formato de estos campos: Es el formato dato en el EJ2. Los campos se separan por espacios, puede haber un : al comienzo. Si un campo empieza con : todo lo que sigue hasta el final de la línea es un único campo. Ya está.
En IRC cada usuario tiene un nick que es el que lo identifica. A su vez cada canal tiene un nombre, que es el que lo identifica. Los nombres de canal empiezan con numeral (#).
Cada comando que queramos mandarle al servidor será una secuencia de comando con sus respectivos parámetros, por ejemplo:
PRIVMSG pepito :Hola amigo, como estas?\n
Este comando le manda un mensaje personal al usuario pepito
, el mensaje es
"Hola amigo, como estas?"
.
Todos los comandos que se le envían al servidor tienen una estructura similar a esa.
A su vez el servidor nos va a responder cosas (de forma asincrónica) que pueden ser o respuestas a nuestros comandos, para avisarnos del resultado de lo que pedimos, o puede mandarnos mensajes que no solicitamos... como por ejemplo, si pepito nos respondiera.
Los mensajes del servidor tienen una estructura similar pero son un poco más sucios, por ejemplo, si yo me llamara juancito y enviara el mensaje anterior el usuario pepito recibiría del servidor algo tipo:
:juancito!~juanx@143.232.32.1 PRIVMSG pepito :Hola amigo, como estas?\n
donde la estructura es "<remitente> PRIVMSG <destinatario> <mensaje>"
la
cadena "juancito!~juanx@143.232.32.1"
codifica el nick juancito
pero además
el ID del usuario y la IP de mismo.
Si tanto juancito como pepito estuvieran en el canal #algoritmos
y pepito
mandara el mensaje:
PRIVMSG #algoritmos :Hola companeres!\n
a Juancito le llegaría un mensaje:
:pepito!~pepe@80.32.1.2 PRIVMSG #algoritmos :Hola companeres!\n
misma estructura que el anterior, pero el destinatario es el canal. Este mensaje le va a llegar a todos los miembros del canal con excepción de a pepito. A pepito no va a llegarle nada. Pobre pepito.
Los mensajes que se generan por interacciones de los usuarios tienen todos ese formato. Se van a generar menesajes no sólo cuando un usuario mande un mensaje sino también cuando un usuario entre a un canal en el que estamos, cuando se vaya de ese canal, cuando cierre la sesión, cuando se cambie de nick, etc. En todos los casos el servidor nos notificará solamente sobre usuarios que compartan canal con nosotros.
Los mensajes del servidor tienen un formato diferente, por ejemplo, si cuando juancito le escribió el mensaje a pepito, pepito no hubiera estado conectado el servidor le hubiera respondido a juancito algo tipo:
:ta130.com.ar 401 pepito juancito :No existe ese nick\n
la estructura no es fija, pero el aspecto suele ser "<servidor> <código
mensaje> <usuario> <información adicional...>"
. Los códigos de mensaje están
tipificados (y son un montón),
la información adicional muchas veces explica en texto un error, pero también
consiste en información sobre ese comando puntual. Por ejemplo, si pidiera la
información de todos los canales abiertos recibiría un listado de códigos 322
donde la información sería el nombre de los canales.
Como el servidor puede hablarnos incluso cuando todavía no terminamos de conectarnos no necesariamente el tercer parámetro sea nuestro nick.
El único (creo) comando del servidor que no tiene esa estructura es el
comando PING
, el servidor cada un determinado tiempo nos va a mandar un
ping, que nosotros tenemos que responder con un pong, si no respondemos va a
asumir que no estamos ahí y nos va a desconectar. El ping del servidor tiene
esta estructura:
PING :abcdefghi\n
donde el segundo campo es un código secreto que nosotros tenemos que utilizar en la respuesta:
PONG :abcdefghi\n
si el código no coincide el servidor va a echarnos.
Bienvenida
En todo protocolo existe un apretón de manos (handshake) que establece la conexión inicial. Como ya dijimos, todos los usuarios de IRC tienen un nick, en ese handshake le vamos a decir al servidor quiénes somos, y el servidor nos aceptará (o nos echará si, por ejemplo, ya hubiera otro usuario con ese nick).
Al establecer la conexión el usuario tiene que enviar estos dos comandos:
USER usuario host server :Nombre Real\n
NICK usuario\n
(¿Se acuerdan del juanx que aparecía en juancito!~juanx
?, ese usuario se toma
del comando USER, no necesariamente es el NICK que se proporcione después.).
Si todo está bien el servidor tiene que responder con:
:servidor 001 usuario :Bienvenido al IRC usuario\n
Recién cuando llega el mensaje 001
es que estamos conectados, no antes.
Comandos de IRC
He aquí una lista de comandos de IRC:
- PRIVMSG destinatario mensaje:
Manda el mensaje al destinatario (usuario o canal).
- NICK nick:
Se cambia el nick a nick.
- JOIN canal:
Entra al canal.
- PART canal:
Sale del canal.
- NAMES canal:
Pide la lista de los usuarios en un canal.
- TOPIC canal:
Pide el tópico (temática) del canal.
- TOPIC canal topico:
Establece el tópico (temática) del canal.
- LIST:
Pide la lista de todos los canales del servidor.
- USERS:
Pide la lista de todos los usuarios del servidor.
- QUIT:
Sale del servidor.
- PING palabrasecreta:
Pinguea al servidor.
- PONG palabrasecreta:
Contesta un ping del servidor.
A su vez estos son los comandos que el servidor puede enviarnos a nosotros:
- remitente PRIVMSG destinatario mensaje:
Recibimos un mensaje del remitente, el destinatario somos nosotros o un canal común.
- remitente NICK nick:
El usuario remitente se cambió el nick a nick. (A partir de ahora los mensajes nos llegarán con el nuevo nick como remitente.)
- remitente JOIN canal:
El remitente entró al canal.
- remitente PART canal:
El remitente salió del canal.
- remitente QUIT canal motivo:
El remitente se desconectó.
- remitente TOPIC canal topico:
El remitente le cambió el tópico (temática) al canal.
- PING palabrasecreta:
Nos está pidiendo que respondamos.
- PONG palabrasecreta:
Está respondiendo a un PING nuestro.
Por fuera de eso, el servidor nos puede enviar los mensajes que ya dijimos
donde tienen el formato ya presentado servidor-código-nick-mensaje con la
salvedad de que el código es o un número o la palabra "NOTICE"
si el servidor
nos está avisando de cosas no tabuladas.
Trabajo
El objetivo de este trabajo es implementar un cliente de IRC muy sencillo que se conecte a un servidor y permita chatear.
El comportamiento del cliente debería ser así:
La aplicación se va a iniciar con la dirección del servidor y el nick a usar.
El cliente debe conectarse al servidor.
Una vez que esté conectado al servidor y todo el tiempo que dure la aplicación el cliente tiene que estar leyendo la entrada del usuario y además debe estar mostrando los mensajes que vienen del servidor.
La entrada del usuario se lee por stdin
en la terminal ya implementada en el
EJ3. Los mensajes del servidor "interrumpen" al usuario.
Se asume que el usuario domina los comandos de IRC. Si la entrada del usuario
empezara con '/'
se asume que está ingresando un comando al servidor. Lo que
el usuario haya escrito debe ser enviado al servidor, con dos modificaciones:
Primero hay que omitir la barra, después, si hubiera más de 2 comandos se debe
insertar un : después del segundo. Por ejemplo, si el usuario ingresa:
/topic #algoritmos Este cuatri la rompemos
el cliente debe enviarle al servidor "topic #algoritmos :Este cuatri la rompemos"
.
El prompt empezará estando vacío. Si el usuario hace join a un canal el prompt será el nombre del canal, si el usuario le manda un mensaje privado a alguien (usuario o canal) el prompt se convertirá en ese destinatario.
Si el usuario no empieza su entrada con la '/'
entonces se asumirá que lo que
está escribiendo se lo está mandando al destinatario de su prompt. O sea, una
secuencia como:
/join #algoritmos
Hola a todos
/privmsg juancito Excepto a vos
No, mentira ;)
Debería traducirse en:
join #algoritmos
privmsg #algoritmos :Hola a todos
privmsg juancito :Excepto a vos
privmsg juancito :No, mentira ;)
(Dicho sea de paso, en todos los clientes de IRC el comando para mandar
mensajes es "/msg"
y no "/privmsg"
, como algo optativo se puede hacer esa
traducción. Se agradece.)
Identificamos varios tipos de mensajes del servidor: Mensajes personales, mensajes que vienen de un canal, mensajes que me avisan de que alguien hizo algo, mensajes que vienen directo del servidor; y además están los mensajes que nosotros estamos mandando. Colorear de forma que puedan distinguirse unos de otros. Particularmente los mensajes formatearlos para que sea claro quién los envió y si son mensajes personales o de canal.
Por último el cliente tiene que ser capaz de responder al PING de forma automática sin interacción del usuario.
¿Tengo que aprender sockets, poll y compañía?
No, amigo, por supuesto que no.
Te proveemos un TDA comunicador_t
que se encarga de conectarse y que te avisa
si hay datos en stdin
o en el descriptor de archivos del servidor.
Sólo tenés que usar el TDA.
El pseudocódigo de uso del TDA es:
c = cominicador_crear();
if(! comunicador_conectar(c, servidor, puerto)) {
error = comunicador_get_error(c);
}
int fd;
while((fd = comunicador_esperar_datos(c)) != -1)) {
if(fd == 0)
// Sé que hay datos en stdin
else
// Sé que hay datos en fd para leer con read()
}
comunicador_destruir(c);
Dado que el servidor se comunica de a líneas, es válido asumir que si hay al
menos un dato en el fd se pueden leer bytes de a uno por vez hasta
eventualmente leer un '\n'
. Reimplementar la función de lectura de línea del
EJ2 para leer con read en vez de getchar. Siendo que el canal de comunicación
es binario, si se lee un '\r'
descartar el dato.
En la implementación de la cátedra se observó que a veces
comunicador_esperar_datos()
bloquea la evacuación del buffer de stdout
y
genera un delay entre que se imprime algo y se muestra. Se puede agregar una
llamada a fflush(stdout);
después de imprimir la terminal para garantizar que
los datos del buffer se imprimieron realmente.
¿Cómo pruebo al cliente?
Te proveemos un servidor ya compilado que muestra por stdout
todo lo que está
pasando. El servidor se invoca como:
$ ./server <puerto>
donde el puerto por default es 6667, pero tal vez necesites cambiarlo si quedó ocupado porque algo te crasheó antes.
El servidor que te damos es bastante benevolente, tiene mucha más paciencia que un servidor de IRC real, y es bastante explícito en contarte qué está pasando.
Del servidor se sale con control + C (no, no lo estás matando, sabe cómo salir de forma limpia, y si no me creés correle Valgrind ;)).
Aplicación
Tenés que implementar una aplicación que se ejecute como:
$ ./cliente servidor puerto nick
y que permita conectarse al servidor y chatear.
Se provee el siguiente paquete con la implementación del comunicador y con el
servidor: archivos_20241_ej4.tar.gz
.
Nota
Para este trabajo no es necesario modularizar el problema, pero se recomienda hacerlo para practicar modularización y Makefile.
En el caso de modularizar en archivos entregar tanto los archivos como el Makefile que compila el proyecto.
Entrega
Deberá entregarse el código fuente del programa desarrollado o los fuentes y el Makefile en caso de haber modularizado.
El programa debe:
Compilar correctamente con los flags:
-Wall -Werror -std=c99 -pedantic
permitir interactuar con el servidor provisto con los comandos que se describieron en este enunciado.
La entrega se realiza a través del sistema de entregas.
El ejercicio es de entrega individual.