Communication
entre postes distants :
Du point de vue réseau
Comment deux postes distants communiquent-ils entre eux ?
Prenons l’exemple de deux entreprises : une personne de l’une communique avec une personne de l’autre si elle connaît l’adresse de l’entreprise et le bureau de la personne avec laquelle elle souhaite communiquer.
Autrement dit, il est nécessaire d’avoir deux couples (adresse, bureau) pour communiquer dans les 2 sens.
Pour un ordinateur, c’est à peu près la même situation : il est relié au monde extérieur par une carte réseau, par exemple le câble (câble Ethernet).
Plusieurs applications peuvent utiliser cette carte réseau en même temps.
Donc deux applications sur deux machines qui souhaitent communiquer entre elles auront besoin de l’adresse de la machine ou plutôt l’adresse de la carte réseau et d’un numéro, non pas de bureau, mais de port qui permettra de reconnaître l’application concernée.
L’adresse est une adresse dite logique ou adresse IP (IP du protocole IP qui signifie « internet Protocol »)
Les deux couples (adresse IP, numéro de port) permettront aux deux applications de communiquer entre elles.
Remarque :
Dans le cas où ce sont deux applications d’un même poste, l’IP utilisée sera de 127.0.0.0 à 127.0.0.8 (localhost).
Dans le cas où les deux postes sont dans le même réseau local, c’est à dire reliés au même routeur (même salle de classe, même appartement avec un routeur), l’adresse IP dite locale ou privée, car invisible de l’extérieur, peut être par exemple 192.168.0.3
Dans le cas où les deux postes sont sur des réseaux locaux différents, l’IP utilisée sera celle du routeur (IP publique car visible de partout sur internet) : il faudra alors configurer le routeur pour rediriger les communications sur la bonne machine du réseau local, en associant une adresse IP locale de la machine au numéro de port utilisé.
Pour tester le cas d’un simple dialogue, il faudra que je donne l’IP publique de
mon routeur qui sera donc celui à écrire dans le module client.
De mon côté, c'est-à-dire
du côté serveur, quand j’exécuterai le module serveur, ce sera l’IP locale de
ma machine qui sera utilisée.
Mon routeur
redirigera les requêtes que vous enverrez au port 40000 sur ma machine.
La redirection de
port se fait sur l’interface du routeur.
Il sera donc
possible de m’envoyer un mail bien identifié pour ceux qui souhaitent essayer le
module client, afin que je lance le module serveur à une heure bien précise et
que j’envoie par mail mon IP publique.
Les numéros de port de 0 à 1023 sont réservés et il y a en tout 65536 ports possibles car un port est codé sur 2 octets soit 16 bits.
Les IP version 4 sont codés sur 4 octets, chaque octet étant
écrit en décimal.
Le site « comprendre l’ordinateur » est très bien fait et explique simplement cette partie réseau.
Du point de vue programmation
Les langages de programmation comme C++, java, Python et même JavaScript avec la plate forme Node.js permettent la communication entre postes distants.
On parle d’hôtes (hosts en anglais), l’un doit être le serveur et l’autre le client, même s’ils font la même chose, par exemple dans le cadre d’un simple dialogue.
La classe Socket sera alors utilisée.
On aura alors à créer un objet « socket » instance de cette classe.
Cette classe comporte de nombreuses méthodes qui permettront l’échange de données entre les deux machines.
Seul le couple (adresse IP, numéro de port) du serveur sera utile dans le code à écrire.
Dans le cas du serveur, 3 méthodes seront utilisées :
bind() listen() et accept() (initialise la connexion, écoute et accepte les demandes de connexion de l’extérieur ou requêtes)
Dans le cas du client la méthode connect() sera utilisée.
Dans les deux cas les méthodes send() et recv() permettront de recevoir et d’envoyer des messages et comme des « bytes » seulement sont transmis il faudra les convertir en chaînes caractères par les méthodes encode() et decode().
La méthode close() permet d’interrompre la communication.
Les méthodes bind et connect ont comme paramètres l’adresse IP et le numéro de port du serveur.
La méthode listen a comme paramètres le nombre maximum de connexions entrantes simultanées.
les méthodes recv et send ont comme paramètre le nombre maximal de bytes à envoyer.
Voilà deux exemples (client, serveur) en python :
Simple dialogue
Serveur
import socket, sys
HOST = '192.168.0.15'
PORT = 40000
# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2) liaison du socket à une adresse précise :
try:
mySocket.bind((HOST, PORT))
except socket.error:
print ("La liaison du socket à l'adresse choisie a échoué.")
sys.exit()
while True:
# 3) Attente de la requête de connexion d'un client :
print ("Serveur prêt, en attente de requètes ...")
mySocket.listen(5)
# 4) Etablissement de la connexion :
connexion, adresse = mySocket.accept()
print ("Client connecté, adresse IP %s, port %s" % (adresse[0], adresse[1]))
# 5) Dialogue avec le client :
while True:
msgClient = connexion.recv(1024).decode()
if msgClient.upper() == "FIN" or msgClient =="" :
connexion.send("FIN".encode())
break
else:
print ('C>', msgClient)
msgServeur=input("répondez: ")
connexion.send(msgServeur.encode())
print ("Connexion interrompue.")
connexion.close()
ch
= input("<R>ecommencer <T>erminer ? ".encode())
if ch.upper() =='T':
break
Client
#!/bin/python
import
socket
import
sys
import tkinter
#-*-
coding: utf-8 -*-
host =
'192.168.0.15'
port =
40000
import
socket, sys, threading
from tkinter import*
class ThreadReception(threading.Thread):
def
__init__(self, conn):
threading.Thread.__init__(self)
self.connexion = conn # du socket de connexion
def run(self):
compteur=0
while True:
compteur=compteur+1
if compteur==5:
l.delete("1.0",END+"-4l")
compteur=0
message_recu =self.connexion.recv(1024)
l.insert(END,"Le serveur a écrit: "+message_recu.decode()+"\n")
v1.set("")
if message_recu.upper() =="FIN":
break
self.connexion.close()
class fe(threading.Thread):
def
__init__(self, f):
threading.Thread.__init__(self)
self.fenetre
= f
def
run(self):
b=Button(self.fenetre,text = "FIN", command = self.send_FIN)
b.pack(side="bottom")
L1 = Label(self.fenetre, text="Votre
message")
L1.pack( )
e=Entry(self.fenetre,textvariable=v1)
e.pack()
ba=Button(self.fenetre,text = "envoyer", command = self.send_many)
ba.pack()
L3 = Label(self.fenetre, text="")
L3.pack( )
self.fenetre.mainloop()
def
send_many(self):
connexion.send(v1.get().encode())
l.insert(END,"Mon message:
"+v1.get()+"\n")
def
send_FIN(self):
connexion.send("FIN".encode())
self.fenetre.quit()
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
f=Tk()
l=Text(f)
l.pack(side="top")
try:
connexion.connect((host, port))
except socket.error:
print ("pas de connexion avec le serveur.")
sys.exit()
print ("Connexion avec le serveur.")
th_R = ThreadReception(connexion)
th_R.start()
v1=StringVar()
fen = fe(f)
fen.start()
Le client envoie deux nombres et le serveur calcule
leur produit qu’il renvoie au client :
Serveur
import socket, sys
HOST = '192.168.0.15'
PORT = 40000
tab=list(range(2))
i=0
# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2) liaison du socket à une adresse précise :
try:
mySocket.bind((HOST, PORT))
except socket.error:
print ("La liaison du socket à l'adresse choisie a échoué.")
sys.exit()
while 1:
# 3) Attente de la requête de connexion d'un client :
print ("Serveur prêt, en attente de requètes ...")
mySocket.listen(5)
# 4) Etablissement de la connexion :
connexion, adresse = mySocket.accept()
print ("Client connecté, adresse IP %s, port %s" % (adresse[0], adresse[1]))
# 5) Dialogue avec le client :
while 1:
msgClient = connexion.recv(1024).decode()
if msgClient.upper() ==
"FIN" or msgClient =="" :
connexion.send("FIN".encode())
break
tab[i]=int(msgClient)
i=i+1
print
('C>', msgClient)
if i==2:
i=0
msgServeur=str(tab[0]*tab[1])
connexion.send(msgServeur.encode())
print ("Connexion interrompue.")
connexion.close()
ch
= input("<R>ecommencer <T>erminer ? ".encode())
if ch.upper() =='T':
break
Client
#!/bin/python
import
socket
import
sys
import tkinter
#-*-
coding: utf-8 -*-
host =
'192.168.0.15'
port =
40000
import
socket, sys, threading
from tkinter import*
class ThreadReception(threading.Thread):
def
__init__(self, conn):
threading.Thread.__init__(self)
self.connexion = conn # du socket de connexion
def run(self):
compteur=0
while True:
compteur=compteur+1
if compteur==5:
l.delete("1.0",END+"-4l")
compteur=0
message_recu =self.connexion.recv(1024)
l.insert(END,"Le serveur a calculé le produit des 2 nombres "+v1.get()+ " et "+v2.get()+": "+message_recu.decode()+"\n")
v1.set("")
v2.set("")
if message_recu.upper() =="FIN":
break
self.connexion.close()
class fe(threading.Thread):
def
__init__(self, f):
threading.Thread.__init__(self)
self.fenetre
= f
def
run(self):
b=Button(self.fenetre,text = "FIN", command = self.send_FIN)
b.pack(side="bottom")
L1 = Label(self.fenetre, text="Premier nombre")
L1.pack( )
e=Entry(self.fenetre,textvariable=v1)
e.pack()
L2 = Label(self.fenetre, text="Deuxième nombre")
L2.pack( )
e2=Entry(self.fenetre,textvariable=v2)
e2.pack()
ba=Button(self.fenetre,text = "envoyer", command = self.send_many)
ba.pack()
L3 = Label(self.fenetre, text="")
L3.pack( )
self.fenetre.mainloop()
def
send_many(self):
connexion.send(v1.get().encode())
l.insert(END,"Premier nombre: "+v1.get()+"\n")
connexion.send(v2.get().encode())
l.insert(END,"Deuxième nombre: "+v2.get()+"\n")
def send_FIN(self):
connexion.send('FIN'.encode())
self.fenetre.quit()
connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
f=Tk()
l=Text(f)
l.pack(side="top")
try:
connexion.connect((host, port))
except socket.error:
print ("pas de connexion avec le serveur.")
sys.exit()
print ("Connexion avec le serveur.")
th_R = ThreadReception(connexion)
th_R.start()
v1=StringVar()
v2=StringVar()
fen = fe(f)
fen.start()
Remarques :
Comme vous pouvez le constater, le client utilise le module tkinter de python.
L’utilisation des deux modules tkinter et socket pose un problème.
Pour éviter ces problèmes il faut utiliser des « threads ».
C’est une classe qui permet dans un même programme (processus) d’exécuter plusieurs tâches en parallèle.
Cela évite d’éventuels blocages dans l’exécution d’instructions dans un même processus.
Dans les des deux modules clients, des classes sont crées qui héritent de la classe thread.
Ces classes peuvent donc utiliser les méthodes de la classe thread, en particulier la méthode start() qui exécute la méthode run() de la classe correspondante.
Les méthodes init permettent « d’initialiser », c’est à dire de créer des objets qui sont des instances de la classe ; c’est le cas des variables th_R et fen.
Chaque classe a un attribut.
Plusieurs variables sont globales et peuvent être utilisées à l’intérieur des classes :
connexion, f, l, v1, v2 .
Il faudra changer l’adresse IP, c’est à dire la valeur de la variable host et prendre comme valeur l’IP du serveur.