web-dev-qa-db-fra.com

Comment faire en sorte que deux clients se connectent directement après avoir connecté un serveur de point de réunion?

J'écris un serveur de point de rendez-vous/relais jouet qui écoute sur le port 5555 pour deux clients "A" et "B". 

Cela fonctionne comme ceci: chaque octet reçu par le serveur du premier client connecté A sera envoyé au client second connecté B, même si A et B ne connaissent pas leur IP respective:

A -----------> server <----------- B     # they both connect the server first
A --"hello"--> server                    # A sends a message to server
               server --"hello"--> B     # the server sends the message to B

Ce code fonctionne actuellement:

# server.py
import socket, time
from threading import Thread
socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket.bind(('', 5555))
socket.listen(5)
buf = ''
i = 0

def handler(client, i):
    global buf
    print 'Hello!', client, i 
    if i == 0:  # client A, who sends data to server
        while True:
            req = client.recv(1000)
            buf = str(req).strip()  # removes end of line 
            print 'Received from Client A: %s' % buf
    Elif i == 1:  # client B, who receives data sent to server by client A
        while True:
            if buf != '':
                client.send(buf)
                buf = ''
            time.sleep(0.1)

while True:  # very simple concurrency: accept new clients and create a Thread for each one
    client, address = socket.accept()
    print "{} connected".format(address)
    Thread(target=handler, args=(client, i)).start()
    i += 1

et vous pouvez le tester en le lançant sur un serveur et en effectuant deux connexions netcat: nc <SERVER_IP> 5555

Comment puis-je alors transmettre aux clients A et B l'information qu'ils peuvent se parler directement sans faire transiter les octets via le serveur?

Il y a 2 cas:

  • Cas général, c’est-à-dire même si A et B ne sont pas dans le même réseau local

  • Cas particulier où ces deux clients sont sur le même réseau local (exemple: utilisant le même routeur domestique), cela sera affiché sur le serveur lorsque les 2 clients se connecteront au serveur sur le port 5555:

    ('203.0.113.0', 50340) connected  # client A, router translated port to 50340
    ('203.0.113.0', 52750) connected  # same public IP, client B, router translated port to 52750
    

Remarque: une tentative infructueuse précédente ici: UDP ou TCP perforation de trous pour connecter deux homologues (chacun derrière un routeur) et Perforation UDP avec un tiers

6
Basj

Étant donné que le serveur connaît les adresses des deux clients, il peut leur envoyer cette information et ainsi connaître l'adresse de chacun. Le serveur peut envoyer ces données de plusieurs manières: octets bruts, décapés, codés JSON. Je pense que la meilleure option est de convertir l'adresse en octets, car le client saura exactement combien d'octets doivent être lus: 4 pour l'IP (entier) et 2 pour le port (unsigned short). Nous pouvons convertir une adresse en octets et revenir avec les fonctions ci-dessous. 

import socket
import struct

def addr_to_bytes(addr):
    return socket.inet_aton(addr[0]) + struct.pack('H', addr[1])

def bytes_to_addr(addr):
    return (socket.inet_ntoa(addr[:4]), struct.unpack('H', addr[4:])[0])

Lorsque les clients reçoivent et décodent l'adresse, ils n'ont plus besoin du serveur et peuvent établir une nouvelle connexion entre eux. 

Maintenant, nous avons deux options principales, pour autant que je sache. 

  • Un client agit en tant que serveur. Ce client fermerait la connexion au serveur et commencerait à écouter sur le même port. Le problème avec cette méthode est que cela ne fonctionnera que si les deux clients sont sur le même réseau local ou si ce port est ouvert pour les connexions entrantes. 

  • Perforation. Les deux clients commencent à envoyer et à accepter simultanément des données. Les clients doivent accepter les données à la même adresse que celle utilisée pour se connecter au serveur de rendez-vous, connu l'un de l'autre. Cela créerait un trou dans le fichier nat du client et les clients seraient en mesure de communiquer directement, même s'ils sont sur des réseaux différents. Ce processus est décrit en détail dans cet article Communication entre homologues entre traducteurs d'adresses réseau , section 3.4 Homologues derrière différents NAT.

Un exemple en Python pour le poinçonnage de trous UDP: 

Serveur:

import socket

def udp_server(addr):
    soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    soc.bind(addr)

    _, client_a = soc.recvfrom(0)
    _, client_b = soc.recvfrom(0)
    soc.sendto(addr_to_bytes(client_b), client_a)
    soc.sendto(addr_to_bytes(client_a), client_b)

addr = ('0.0.0.0', 4000)
udp_server(addr)

Client:

import socket
from threading import Thread

def udp_client(server):
    soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    soc.sendto(b'', server)
    data, _ = soc.recvfrom(6)
    peer = bytes_to_addr(data)
    print('peer:', *peer)

    Thread(target=soc.sendto, args=(b'hello', peer)).start()
    data, addr = soc.recvfrom(1024)
    print('{}:{} says {}'.format(*addr, data))

server_addr = ('server_ip', 4000) # the server's  public address
udp_client(server_addr)

Ce code nécessite que le serveur de rendez-vous ait un port ouvert (4000 dans ce cas) et soit accessible aux deux clients. Les clients peuvent être sur le même réseau ou sur des réseaux locaux différents. Le code a été testé sur Windows et il fonctionne bien, qu’il s’agisse d’une adresse IP locale ou publique.

J'ai expérimenté la perforation TCP mais mes succès ont été limités (parfois, il semble que cela fonctionne, parfois non). Je peux inclure le code si quelqu'un veut expérimenter. Le concept est plus ou moins identique, les deux clients commencent à envoyer et à recevoir simultanément et il est décrit en détail dans Communication homologue à homologue entre traducteurs d'adresses réseau , section 4, TCP Perforation.


Si les deux clients sont sur le même réseau, il sera beaucoup plus facile de communiquer entre eux. Ils devront choisir quel serveur sera le serveur, puis ils pourront créer une connexion serveur-client normale. Le seul problème ici est que les clients doivent détecter s’ils se trouvent sur le même réseau. Là encore, le serveur peut résoudre ce problème car il connaît l'adresse publique des deux clients. Par exemple:

def tcp_server(addr):
    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    soc.bind(addr)
    soc.listen()

    client_a, addr_a = soc.accept()
    client_b, addr_b = soc.accept()
    client_a.send(addr_to_bytes(addr_b) + addr_to_bytes(addr_a))
    client_b.send(addr_to_bytes(addr_a) + addr_to_bytes(addr_b))

def tcp_client(server):
    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    soc.connect(server)

    data = soc.recv(12)
    peer_addr = bytes_to_addr(data[:6])
    my_addr = bytes_to_addr(data[6:])

    if my_addr[0] == peer_addr[0]:
        local_addr = (soc.getsockname()[0], peer_addr[1])
        ... connect to local address ...

Ici, le serveur envoie deux adresses à chaque client, l'adresse publique du correspondant et la propre adresse publique du client. Les clients comparent les deux adresses IP. S'ils correspondent, ils doivent se trouver sur le même réseau local.

5
t.m.adam

La réponse acceptée donne la solution. Voici quelques informations supplémentaires dans l’affaire "Les clients A et B se trouvent sur le même réseau local" . Cette situation peut effectivement être détectée par le serveur s’il constate que les deux clients ont la même adresse IP publique. 

Ensuite, le serveur peut choisir le client A comme "serveur local" et le client B comme "client local".

Le serveur demandera alors au client A son "adresse IP du réseau local". Le client A peut le trouver avec :

import socket
localip = socket.gethostbyname(socket.gethostname())  # example: 192.168.1.21

puis renvoyez-le au serveur. Le serveur communiquera cette "adresse IP du réseau local" au client B.

Ensuite, le client A exécutera un "serveur local":

import socket
soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
soc.bind(('0.0.0.0', 4000))
data, client = soc.recvfrom(1024)
print("Connected client:", client)
print("Received message:", data)
soc.sendto(b"I am the server", client)

et le client B s'exécutera en tant que "client local":

import socket
soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server = ('192.168.1.21', 4000)   # this "local network IP" has been sent Client A => server => Client B
soc.sendto("I am the client", server)
data, client = soc.recvfrom(1024)
print("Received message:", data)
1
Basj