sábado, 27 de diciembre de 2008

Programación de Sockets con Python

Un socket nos permite comunicarnos con otras computadoras, de esa manera la informacón puede viajar libremente por todos lados. Mucha gente se aterroriza al enterarse que tiene que desarrollar alguna aplicación haciendo uso de sockets, pues el dia de hoy descubriremos que no es nada del otro mundo, este manual se enfoca solamente a sockets en Python, si alguna vez han trabajado con sockets en C se darán cuenta que en Python es mucho más fácil (que no lo es??), bueno, los puntos que trataremos en este tutorial son:
  • Estructura de un socket.
  • Algunos elementos del socket.
  • Programacion orientada a sockets.
  • Hola mundo del socket.
  • Un hola mundo real.
  • Un pequeño chat.
  • Sockets con clase.
  • Base de cliente telnet.
Existen tres dominios de comunicación para un socket:
  • a) El dominio de UNIX. (AF_UNIX)
  • b) El dominio de internet. (AF_INET)
  • c) El dominio NS.

El dominio de UNIX. (AF_UNIX)

Se utiliza para la comunicacón entre procesos del sistema.

El dominio de internet. (AF_INET)

Se utiliza en procesos que se estan comunicando atravéz de TCP(UDP)/IP, es el que veremos.

El dominio NS.

Se utilizaba sobre los procesos que se comunicaban sobre el protocolo de comunicación de Xerox.

Python solo soporta los dos primeros tipos de comunicacón, solamente trataremos con la familia AF_INET.

Estos son algunos elementos que pertenecen a los sockets:

SocketDescripcion
socketCrea un socket del tipo y familia especificada.
socket.acceptAcepta nuevas conexiones.
socket.connectConecta el socket a la dirección dada en el puerto dado.
socket.sendEnvia datos al socket especificado.
socket.recvRecive información.
Programación orientada a sockets.


No es muy complejo adaptarse a la manera en que trabajan los sockets, primero que nada se necesita una aplicacion que haga el trabajo de servidor, las principales cosas que hace un servidor son las siguientes:
  • Ser creado.
  • Ser asignado a una dirección y darle un puerto.
  • Esperar por nuevas conexiones.
  • Aceptar nuevas conexiones.
Eso es lo basico que hace un servidor, claro que tambien es importante que mande y reciva información.
Después se crean los clientes, lo que realizan los o el cliente es:
  • Ser creado.
  • Conectarse a una dirección y puerto dado.
Simple no, claro que tambien es importante que mande o reciba información.

Un hola mundo del socket??


Bueno, basta de tanta teoría, comenzemos con lo bueno.
Pues asi es, lo que haremos a continuación es como un hola mundo en los sockets, crearemos un servidor al cual nos vamos a conectar por medio de telnet.
Bueno, pues comencemos con código:

# hls.py
# Creamos un servidor al cual nos podremos conectar.

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("", 8000))
server.listen(1)
server.accept()


Y ¿que es lo que estamos haciendo con esto?, pues es sencillo vamos a verlo por pasos:

import socket

Importamos el modulo de los sockets.

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Creamos el socket de la familia AF_INET y del tipo SOCK_STREAM.

server.bind(("", 8000))

Le asignamos una dirección y un puerto al servidor, por medio de una tupla (add, host).

server.listen(1)

Permite al servidor aceptar conexiones.

server.accept()

Acepta una nueva conexion.

Una vez que este Script es ejecutado nos damos cuenta que lo unico que hace es que abre una terminal, uno se pregunta, y esto de que me sirve, bueno, a continuación telneteamos al servidor que acabamos de crear:

Telnet localhost 8000

El cliente (Telnet) se conectará al servidor y este se cerrará, pues solamente le dijimos que aceptara una conexón.

Ahora un hola mundo

Bueno, si quieren un hola mundo mejor hecho hagamos lo siguiente:

# hlsd.py
# Creamos un servidor al cual nos podremos conectar y nos da la bienvenida.

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("", 8000))
server.listen(5)
while 1:
add, port = server.accept()
add.send("hola mundo!!!")


Lo unico nuevo aqui es esto:

server.listen(5)
while 1:
add, port = server.accept()
add.send("hola mundo!!!")

Lo del socket.listen(5) nadamás se refiere a que ahora es capaz de retener 5 conexiones (no se refiere al total de conexiones, si no como que al total que hay en espera) antes de rechazar alguna, despues esta lo del ciclo infinito, (while 1:) bueno, asi es, nuestro nuevo servidor estara aceptando conexiones por siempre, y cuando alguien se conecta guardara la tupla que nos regresa el server.accept() en las variables de add y port respectivamente, despues le enviará un mensaje al cliente, el hola mundo.

Y que es lo grandioso de los sockets??

Para todos aquellos que estan comenzando a desesperarse porque no hemos hecho aplicaciones utiles, pues ahora vamos a realizar un tipo de chat, no es muy estetico, pero es un comienzo.
# servchat.py
# Creamos un servidor de chat.

import socket
import select

def accept_new_connection():

try:
global server
global desc
newsock, (remhost, remport) = server.accept()
server.settimeout(.1)
print "Se ha conectado %s:%s" % (str(remhost), str(remport))
desc.append(newsock)
except:
pass

def broadcast(msg, sock):

global desc
global server
host, port = sock.getpeername()
msg = "[%s:%s]: %s" % (str(host), str(port), str(msg))
for destsock in desc:
if destsock != sock and destsock != server:
destsock.send(msg)

def get_msg(sock):

try:
msg = sock.recv(1024)
sock.settimeout(.1)
return msg
except:
global desc
host, port = sock.getpeername()
print "[%s:%s] ha salido." % (str(host), str(port))
desc.remove(sock)
return None


global server
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("", 8000))
server.listen(5)
global desc
desc = [server]
while 1:
accept_new_connection()
(sread, swrite, sexc) = select.select(desc, [], [])
for sock in sread:
if sock != server:
flag = get_msg(sock)
if flag:
broadcast(flag, sock)
Pues a mi parecer esto se puso interesante, como se acaban de dar cuenta hemos dado un gran paso, de unos simples Scripts que solo aceptaban conexiones y mandaban un mensaje, dimos un giro al realizar una aplicacion que maneje "usuarios" y los comunique, la verdad es que esa no es la unica manera de hacer un servidor de chat, bueno la base de uno, hay infinidad de maneras, tal vez esta incluso sea una pesima manera de realizarlo, pero funciona para nuestros propositos de este tutorial.

Muy bien, a la explicación:
# servchat.py
# Creamos un servidor de chat.

import socket
import select
Lo unico diferente aqui es que ahora incluimos el modulo de select, no se preocupen, será explicado en su debido momento.
global server
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("", 8000))
server.listen(5)
global desc
desc = [server]
while 1:
accept_new_connection()
(sread, swrite, sexc) = select.select(desc, [], [])
for sock in sread:
if sock != server:
flag = get_msg(sock)
if flag:
broadcast(flag, sock)
Despues pasamos a las funciones, primero hay que entender lo que se espera realizar, muy bien, estamos creando variables globales (global), para que estas puedan ser modificadas en las funciones, despues creamos el servidor, asignandole una direccion y un puerto, despues hacemos el ciclo infinito el cual ya fue explicado, y ya adentro de ese ciclo aceptamos nuevas conexiones (accept_new_connection()), ahora sigue lo del select, como pueden ver le estamos mandando una lista donde se encuentran los sockets que se estan usando (incluyendo el server) lo unico que nos importa es saber cuando un socket se intenta comunicar con el servidor, es por eso que los otros dos parametros los dejamos vacios (select.select(desc, [], [])), el select nos devolvera los que se intentan comunicar, despues verificamos cada elemento que nos fue regresado (excluyendo al server (if sock != server:)) para saber que es lo que intenta hacer, despues recivimos el mensaje de cada socket que se comunicó (get_msg()) y lo mandamos a todos (broadcast()), dentro de la función de get_msg() también verifica que siga conectado ese cliente.
def accept_new_connection():

try:
global server
global desc
newsock, (remhost, remport) = server.accept()
server.settimeout(.1)
print "Se ha conectado %s:%s" % (str(remhost), str(remport))
desc.append(newsock)
except:
pass
Lo que esta haciendo esta función es utilizar las variables globales, para poderlas modificar, entonces lo que intenta es recivir una nueva conexion, pero solo lo hace por un periodo corto de tiempo (server.settimeout(.1)), si ese tiempo se acaba se pasa a la esepción la cual no hace nada, pero si en ese tiempo llega una nueva conexion entonces la acepta y guarda el nuevo socket en la lista (desc.append(newsock)).
def get_msg(sock):

try:
msg = sock.recv(1024)
sock.settimeout(.1)
return msg
except:
global desc
host, port = sock.getpeername()
print "[%s:%s] ha salido." % (str(host), str(port))
desc.remove(sock)
return None
En esta función intentamos recivir un mensaje (msg = sock.recv(1024)) del socket que pasamos como parametro, pero una véz más le dejamos un tiempo limite, si ese tiempo es excedido significa que el cliente se ha desconectado y lo quitamos de la lista, y devolvemos None, para que no entre a la condicional que tenemos en el programa principal.
Pero si nos manda un mensaje lo regresamos para que entre a la codicional en el programa principal, y ahi sera llamada la siguiente función:
def broadcast(msg, sock):

global desc
global server
host, port = sock.getpeername()
msg = "[%s:%s]: %s" % (str(host), str(port), str(msg))
for destsock in desc:
if destsock != sock and destsock != server:
destsock.send(msg)
lo que realiza esta función es utilizar las variables globales para asi mandar un "broadcast", el mensaje que recibe como parametro es lo que mandara a todos.


Los sockets tambien tienen clase.

Ahora veamos el ejemplo anterior pero orientado a objetos, esta vez no lo voy a explicar, pues la explicación ya esta en la página anterior, aqui esta el codigo:
# objchat.py
# Creamos un servidor de chat con clase.

import socket
import select

class objchat(object):

def __init__(self, host = "", port = 8000):

self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.server.listen(5)
self.desc = [self.server]
while 1:
self.accept_new_connection()
(self.sread, self.swrite, self.sexc) = select.select(self.desc, [], [])
for self.sock in self.sread:
if self.sock != self.server:
self.flag = self.get_msg(self.sock)
if self.flag:
self.broadcast(self.flag, self.sock)

def accept_new_connection(self):

try:
self.newsock, (self.remhost, self.remport) = self.server.accept()
self.server.settimeout(.1)
print "Se ha conectado %s:%s" % (str(self.remhost), str(self.remport))
self.desc.append(self.newsock)
except:
pass

def broadcast(self, msg, sock):

self.host, self.port = sock.getpeername()
msg = "[%s:%s]: %s" % (str(self.host), str(self.port), str(msg))
for self.destsock in self.desc:
if self.destsock != sock and self.destsock != self.server:
self.destsock.send(msg)

def get_msg(self, sock):

try:
self.msg = self.sock.recv(1024)
self.sock.settimeout(.1)
return self.msg
except:
self.host, self.port = self.sock.getpeername()
print "[%s:%s] ha salido." % (str(self.host), str(self.port))
self.desc.remove(self.sock)
return None

if __name__ == "__main__":
objchat()
Bueno, estas dos ultimas lineas sirven para que cuando este modulo sea importado no se ejecute el objchat(), pero si se esta ejecutando como programa pues que si se corra.


Que cliente telnet apareció??

Ha, pues es una especie de cliente telnet que hice, claro, no funciona como un cliente telnet real, solamente manda y recibe datos a un servidor, pero no es capaz de interactuar con una cuenta de shell, pero es excelente para los propositos de este manual, vamos a ver el codigo:
# objtel.py
# This is a Telnet class.

import sys
import socket
import select
import threading

class obj_tel(object):

print """
Telnet client made in Python.
By Juan Francisco Benavides Nanni.
------------------------------------
"""


def __init__(self):

self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "Give me the Host or IP address: ",
self.tmp = str(sys.stdin.readline())
self.host = ''
for self.i in self.tmp:
if self.i != '\n':
self.host += self.i
self.flag = 0
self.port = ''
while self.flag == 0:
print "\nGive me the port number: ",
self.tmp = str(sys.stdin.readline())
try:
self.port = int(self.tmp)
self.flag = 1
except ValueError:
print "\nPort number can't contain strings.\n"
try:
self.client.connect((self.host, self.port))
print "\nConnection successfully made.\n"
except:
print "\nCan't connect.\n"
sys.stdin.readline()
self.exit_client()
self.telnet()

def telnet(self):

self.opt = [self.client]
self.msg = ''
while 1:
(self.rlist, self.wlist, self.xlist) = select.select(self.opt,
[], [], .9)
if self.rlist != []:
print "%s" % (str(self.client.recv(1024)), )
else:
self.slp = threading.Thread(target = self.get_data)
self.slp.start()

def get_data(self):
self.tmp = str(sys.stdin.readline())
self.msg = ''
for self.i in self.tmp:
if self.i != '\n':
self.msg += self.i
self.client.send(self.msg)

def exit_client(self):
raise SystemExit

if __name__ == "__main__":
telnet = obj_tel()
Pues bueno, en este Script podemos ver que hay varias cositas nuevas, asi es que comenzemos con la explicación:

# objtel.py
# This is a Telnet class.

import sys
import socket
import select
import threading
Pues aqui estamos importando dos nuevos modulos que no habiamos visto antes, el de sys y el de threading, no es el objetivo de este tutorial el de explicar que es un thread, como tampoco lo era el de explicar en más detalle el uso del select, lo que nos importa aqui es que un thread nos sirve para crear un "hilo", por ejemplo, el programa principal se continuará ejecutando, pero tambien le dará recursos al "hilo" para que sea ejecutado por aparte, a grandes rasgos y para nuestros propositos lo dejaremos asi, muy bien, el modulo de sys lo incluimos para poder recivir input del teclado, pero se verá a su debido tiempo, continuemos:
class obj_tel(object):

print """
Telnet client made in Python.
------------------------------------
"""


def __init__(self):

self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "Give me the Host or IP address: ",
self.tmp = str(sys.stdin.readline())
self.host = ''
for self.i in self.tmp:
if self.i != '\n':
self.host += self.i
self.flag = 0
self.port = ''
while self.flag == 0:
print "\nGive me the port number: ",
self.tmp = str(sys.stdin.readline())
try:
self.port = int(self.tmp)
self.flag = 1
except ValueError:
print "\nPort number can't contain strings.\n"
try:
self.client.connect((self.host, self.port))
print "\nConnection successfully made.\n"
except:
print "\nCan't connect.\n"
sys.stdin.readline()
self.exit_client()
self.telnet()
Pues cuando el objeto es creado se imprimira un mensaje de bienvenida, y despues entrara al constructor, ya dentro del constructor lo primero es crear el socket, y despues le pedimos la dirección y el puerto al que desea conectarse el usuario, despues se intenta conectar y si no lo logra termina la aplicación, si si lo logra continua y llama al siguiente metodo:

    def telnet(self):

self.opt = [self.client]
self.msg = ''
while 1:
(self.rlist, self.wlist, self.xlist) = select.select(self.opt,
[], [], .9)
if self.rlist != []:
print "%s" % (str(self.client.recv(1024)), )
else:
self.slp = threading.Thread(target = self.get_data)
self.slp.start()
En este metodo lo que se hace es crear una lista con un solo elemento (el cliente) y verificar en el select.select si es que nos esta llegando un mensaje, pero esta vez hicimos uso de un cuarto parametro, y ese parametro nos sirve para darle un tiempo limite, despues verifica si nos llega mensaje, si nos llega pues lo imprimimos, pero si no nos llego mensaje intentamos mandar uno, es por eso que hacemos uso del thread, porque tal vez no se queria mandar un mensaje, y si lo pusieramos para que lo mandara asi normal, sin thread, se quedaria esperando a mandarlo, es por eso que se utiliza aqui, despues ya se ejecuta el thread (self.slp.start()).

    def get_data(self):
self.tmp = str(sys.stdin.readline())
self.msg = ''
for self.i in self.tmp:
if self.i != '\n':
self.msg += self.i
self.client.send(self.msg)
Como lo mencionamos aqui estamos intentando mandar datos al servidor, pero si no hay nada que mandar no hay problema, porque no se queda estancado en este metodo gracias al thread (como ya lo habiamos mencionado).

    def exit_client(self):
raise SystemExit
Lo unico que hace es que el programa se termine si necesidad de errores ni nada de eso.

if __name__ == "__main__":
telnet = obj_tel()
Eso ya lo vimos.


Ya para finalizar...

Pues asi es como trabajan los sockets, la verdad hay un sin fin de maneras de como trabajar con ellos, las cuales no fueron vistas aqui, pero espero pronto poder hacerle una actualización a este documento.



Fuente:
Autor. Juan Francisco Benavides Nanni
Contacto. elnanni@gmail.com
Pagina. Make Me a BlogJob.
Fecha. 17/02/2006


REFERENCIA ADICIONAL:

HOWTO de programación de Sockets. http://wiki.python.org/moin/HowTo/Sockets

2 comentarios:

Anónimo dijo...

Muy util la verdad. Lo he provado en Fedora con python 2 y va de maravilla, ahora solo me falta adaptarlo a python 3 para que corra en Blender. Se agradece mucho el tutorial, aunque sigo sin entender al 100% el codigo ahora entiendo un poco más xD. Saludos!

vicuña dijo...

Hola, muy util la entrada.
Eso si tengo una duda, como puedo enviar un mensaje a un cliente en especifico?
La idea es que busque en la base de datos que mensaje enviar y si ese identificador concuerda con el del socket, lo envia sólo a ese.

Porque en lo que muestras, envia a todos los clientes y necesito que se envíe solo a uno, que no es la respuesta automatica, si no que sea como un mensaje privado.

Gracias