(https://cdn2.iconfinder.com/data/icons/social-hand-drawn-icons/64/social_38-512.png)
UnderCh4t
Este es un pequeño servidor que acabo de crear en Python en respuesta a este post (https://underc0de.org/foro/dudas-generales-121/chat-con-python/) de
@Theraritho5 (https://underc0de.org/foro/index.php?action=profile;u=36008).
Voy a ir explicando, paso a paso, cómo armar el servidor y, finalmente, pongo todo el código completo.
Debido a que mi idea es ir explicando todo el proceso sin dejar cabos sueltos, ésta va a ser el primero de una serie de artículos en los que intentaré guiarlos en este maravilloso mundo de los sockets.
Comencemos!
CREANDO EL SERVIDOR
Lo primero que haremos es una clase llamada
Servidor, la cual, en primera instancia, podrá realizar las siguientes tareas:
- Sockets (vengan por acá (https://es.wikipedia.org/wiki/Socket_de_Internet) los que no saben de qué estamos hablando ;) ):
· crear un socket (servidor)
· establecer interfaz de conexión: esto es, adjudicar ip y puerto local a la que se deberán conectar los clientes
· poner el servidor en escucha
· atender conexiones entrantes
. cerrar socket
- Informar estado del proceso
- Manejar errores
Veamos cómo quedaría el código:
import socket
import sys
class Servidor(object):
def __init__(self, ip, puerto, max_con=5):
self.__ip = ip
self.__puerto = puerto
self.__max_con = max_con
self.__clientes = {} # { socket, [ip, puerto]}
##########
# SOCKET #
##########
def ejecutar(self):
try:
self._crear_socket()
self._bind()
self._escuchar()
self._cerrar_socket()
except KeyboardInterrupt:
self._cerrar_socket()
except Exception as e:
self._mostrar_error(e, critico=True)
def _crear_socket(self):
pass
def _bind(self):
pass
def _escuchar(self):
pass
def _atender_cliente(self):
pass
def _cerrar_socket(self):
pass
###########
# PROCESO #
###########
def _mostrar_info(self, msj):
print " [+] {}".format(msj)
def _mostrar_error(self, error, critico):
print " [!] Error: {}".format(error)
if critico:
self._salir()
def _salir(self):
sys.exit(" [+] Finalizando aplicación")Bien. Hasta aquí no hay mucho que decir. Hasta ahora hemos creado una clase 'Servidor' con dos grupos de métodos: los que se encargan de la conexión y los que se encargan de informar proceso y errores.
Vayamos con los métodos de conexión. El primero que vemos es el método 'ejecutar'. Este método constituye la guía de nuestra aplicación. Él se va a encargar de ir llamando, uno a uno, a cada método necesario para establecer nuestras conexiones.
Lo primero que hace es llamar a '_crear_socket'. Al menos que cambiemos un poco su contenido, no creo que pase mucho jeje. Vamos con eso:
def _crear_socket(self):
self._mostrar_info("Creando socket")
try:
self.__servidor = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__servidor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except Exception as e:
self._mostrar_error(e, critico=True)Voy a intentar explicar, de forma sencilla, qué significa todo esto que acabamos de escribir. Más allá del manejo de errores, los logs y el debugging, lo que realmente queremos es que este método nos cree un socket. La pregunta es: ¿cómo hacemos eso? El módulo socket (https://docs.python.org/2/library/socket.html) de python es quien nos permite realizar esta tarea. Para crear un socket, debemos escribir lo siguiente:
mi_socket = socket.socket()Todo muy lindo hasta acá pero los que saben y entienden de redes se estarán preguntando: ¿qué características tendrá este socket? ¿sobre qué protocolos se llevará a cabo la comunicación? Bueno, por defecto, el objeto utilizará los protocolos IP y TCP. ¿Y si quisiera utilizar UDP, en vez de TCP? ¿Puedo hacerlo? Absolutamente. Tal como se indica en la documentación oficial (https://docs.python.org/2/library/socket.html#socket.socket), podemos indicar la familia, el tipo y el protocolo que querramos utilizar:
mi_socket = socket.socket(familia, tipo, protocolo)Para no complicarnos, sólo nos centraremos en la familia y el tipo. Dentro de las familias de direcciones, encontramos las siguientes:
- AF_INET: le indica al kernel que vamos a utilizar el protocolo IPv4.
- AF_INET6: le indica al kernel que vamos a utilizar el protocolo IPv6.
- AF_UNIX: a diferencia de las familias INET que están ligadas a una tupla ip-puerto, las familias UNIX se vinculan a un archivo específico de tu sistema de archivos. Nos sirve, por ejemplo, para comunicación entre procesos de un mismo host.
Dentro de los tipos, los más utilizados son:
- SOCK_STREAM: le indica al kernel que para transmitir los datos vamos a utilizar un protocolo confiable (TCP, por ejemplo).
- SOCK_DGRAM: le indica al kernel que para transmitir los datos vamos a utilizar un protocolo no confiable (como es el caso de UDP)
Entonces, si quisiéramos hacer una conexión basada en los protocolos IP y UDP, bastaría con escribir lo siguiente:
familia = socket.AF_INET # IPv4
tipo = socket.SOCK_DGRAM # UDP
mi_socket = socket.socket(familia, tipo)¿Vamos bien hasta ahí? Si es así, entonces continuemos que queda bastante por delante. Tenemos que saber que, una vez creado nuestro socket, podemos acceder a las opciones del mismo para modificarlas. Para hacerlo, el objeto socket nos brinda un método que nos facilita la tarea: setsockopt().
Tal y como nos indica la documentación oficial (https://docs.python.org/2/library/socket.html#socket.socket.setsockopt), para modificar una opción debemos indicar el nivel del socket (ya lo veremos unos renglones más abajo), la opción que queremos modificar y el valor:
mi_socket = socket.socket()
mi.socket.setsockopt(nivel, opcion, valor)Cuando hablamos de nivel, nos referimos, por decirlo de forma sencilla, al protocolo en el que se encuentra la opción que queremos modificar. Cada nivel tiene un valor que lo identifica, por ejemplo:
- SOL_SOCKET: es el nivel del socket y el valor que lo identifica es 1.
- SOL_TCP: es el nivel del protocolo TCP, cuyo identificador es el 6.
- SOL_UDP: nivel del protocolo UDP, identificado por el número 17.
Como a nosotros sólo nos interesa acceder a las opciones del socket, vamos a utilizar SOL_SOCKET para indicar el nivel.
Dentro de las opciones (acá pueden ver un listado completo (http://linux.die.net/man/3/setsockopt)), a mi me interesa una en particular: SO_REUSEADDR. Esta opción nos permite reutilizar un socket local que se encuentre en estado TIME_WAIT, sin tener que esperar su tiempo natural de expiración (para los que no engancharon nada -> leer (http://inedit00.blogspot.com.ar/2012/02/hoy-vamos-explicar-un-poco-que-es.html) ;) ). La opción sólo acepta valores booleanos, es decir, reutiliza el socket o no lo utiliza (reutilizar: SI/NO).
Bien, ahora que entendemos un poco mejor cómo es esto de las opciones, vamos a modificar esta opción:
mi_socket = socket.socket()
nivel = SOL_SOCKET # Nivel del socket
opcion = SO_REUSADDR # Opción a modificar
valor = True # Sí, queremos reutilizar el socket
mi_socket.setsockopt(nivel, opcion, valor)Bien. Con esto queda concluido nuestro método para crear sockets. Con todo lo visto, ya son capaces de adaptar el método a sus necesidades. De hecho, podríamos modificar un poco el método para que sea más flexible:
def _crear_socket(self, familia, tipo, opciones):
self._mostrar_info("Creando socket")
try:
self.__servidor = socket.socket(familia, tipo)
for opcion in opciones:
nivel, opcion, valor = opcion[:]
self.__servidor.setsockopt(nivel, opcion, valor)
except Exception as e:
self._mostrar_error(e, critico=True)
Con esto doy por concluida esta primera parte. En los próximos días iré desarrollando los demás métodos que tenemos pendientes y más ;)
Saludos!
WhiZ
P.D.: muchas gracias @po6xsecpo por la correción.
Gracias por el aporte.
En la línea 7 del primer script tenemos un error, la asignación del parámetro al atributo puerto está como:
self.__port = port
Y el parámetro es "puerto". Por ende dará error.
Duda. En la línea 4 del método _crear_socket(self)
self.__servidor = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Al usar self.__servidor, estás declarando e inicializando el objeto self.__servidor ó este debía existir previamente entre los atributos de la clase Socket(object) ?
Gracias por el aporte. Estaré al tanto de la serie