Как заставить 2 клиентов соединяться друг с другом напрямую, после того как оба подключили сервер точки встречи? - PullRequest
0 голосов
/ 26 ноября 2018

Я пишу игрушечный сервер точки встречи / ретрансляции, прослушивающий порт 5555 для двух клиентов «A» и «B».

Это работает так: каждый байт, полученный сервером от первого подключенного клиента A, будет отправлен второму подключенному клиенту B, , даже если A и B не знают свой соответствующий IP:

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

Этот код в настоящее время работает:

# 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

, и вы можете протестировать его, запустив на сервере, и выполнить с ним два соединения netcat: nc <SERVER_IP> 5555.

Как я могу затем передать клиентам A и B информацию о том, что они могут напрямую общаться друг с другом, не передавая байты через сервер?

Есть2 случая:

  • Общий случай, т. Е. Даже если A и B не находятся в одной локальной сети

  • Особый случай, когда эти два клиентав той же локальной сети (пример: использование того же домашнего маршрутизатора), это будет отображаться на сервере, когда 2 клиента подключатся к серверу через порт 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
    

Примечание: предыдущая неудачная попытка здесь: Пробивка дырок UDP или TCP для соединения двух пиров (каждый за маршрутизатором) и Пробивка дырок UDP с третьей стороной

Ответы [ 2 ]

0 голосов
/ 05 декабря 2018

Принятый ответ дает решение.Вот некоторая дополнительная информация в случае «Клиент A и Клиент B находятся в одной локальной сети» .Эта ситуация действительно может быть обнаружена сервером, если он заметит, что оба клиента имеют одинаковые общедоступные IP-адреса.

Затем сервер может выбрать клиента A в качестве «локального сервера», а клиента B - в качестве «локального клиента».

Затем сервер запросит у клиента A свой «IP-адрес локальной сети». Клиент A может найти его с помощью :

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

и затем отправить его обратно на сервер.Сервер сообщит этот «IP-адрес локальной сети» клиенту B.

Затем клиент A запустит «локальный сервер»:

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)

, а клиент B будет работать как «локальный».клиент ":

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)
0 голосов
/ 30 ноября 2018

Поскольку сервер знает адреса обоих клиентов, он может отправлять эту информацию им, чтобы они знали адреса друг друга.Есть много способов, которыми сервер может отправлять эти данные - зарезервированные, json-кодированные, необработанные байты.Я думаю, что лучшим вариантом является преобразование адреса в байты, потому что клиент будет точно знать, сколько байтов нужно прочитать: 4 для IP (целое число) и 2 для порта (беззнаковое короткое).Мы можем преобразовать адрес в байты и обратно с помощью функций ниже.

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])

Когда клиенты получают и декодируют адрес, им больше не нужен сервер, и они могут установить новое соединение между ними.

Теперь у нас есть два основных варианта, насколько я знаю.

  • Один клиент выступает в качестве сервера.Этот клиент закрыл бы соединение с сервером и начал бы слушать на том же порту.Проблема этого метода заключается в том, что он будет работать только в том случае, если оба клиента находятся в одной локальной сети или этот порт открыт для входящих подключений.

  • Перфорация.Оба клиента начинают отправлять и принимать данные друг от друга одновременно.Клиенты должны принимать данные по тому же адресу, который они использовали для подключения к серверу рандеву, который связан друг с другом.Это пробило бы дыру в натах клиента, и клиенты могли бы общаться напрямую, даже если они находятся в разных сетях.Этот процесс подробно описан в этой статье Одноранговая связь через трансляторы сетевых адресов , раздел 3.4 Одноранговые узлы за различными NAT.

Пример Python для UDPПерфорация отверстий:

Сервер:

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)

Клиент:

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)

Этот код требует, чтобы на сервере рандеву был открыт порт (4000 в данном случае),и быть доступным обоим клиентам.Клиенты могут находиться в одной или разных локальных сетях.Код был протестирован в Windows, и он хорошо работает с локальным или общедоступным IP-адресом.

Я экспериментировал с пробоем TCP-дырок, но у меня был ограниченный успех (иногда кажется, что он работает, иногда - нет).т).Я могу включить код, если кто-то хочет экспериментировать.Концепция более или менее одинакова, оба клиента начинают отправлять и получать одновременно, и она подробно описана в Одноранговая связь через трансляторы сетевых адресов , раздел 4, Перфорация отверстий TCP.


Если оба клиента находятся в одной сети, будет намного проще общаться друг с другом.Им нужно будет каким-то образом выбрать, какой из них будет сервером, затем они смогут создать нормальное соединение сервер-клиент.Единственная проблема здесь заключается в том, что клиенты должны определить, находятся ли они в одной сети.Опять же, сервер может помочь с этой проблемой, так как он знает общедоступный адрес обоих клиентов.Например:

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 ...

Здесь сервер отправляет два адреса каждому клиенту: публичный адрес партнера и собственный публичный адрес клиента.Клиенты сравнивают два IP-адреса, если они совпадают, они должны быть в одной локальной сети.

...