【问题标题】:How to make 2 clients connect each other directly, after having both connected a meeting-point server?如何让 2 个客户端在都连接了会面点服务器后直接相互连接?
【发布时间】:2019-04-28 00:18:35
【问题描述】:

我正在为两个客户端“A”和“B”编写一个在端口 5555 上侦听的玩具会合点/中继服务器。

它的工作原理是这样的:服务器从第一个连接的客户端 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 &lt;SERVER_IP&gt; 5555

我怎样才能将信息传递给客户端 A 和 B,使他们可以直接相互通信,而不需要通过服务器传输字节?

有两种情况:

  • 一般情况,即即使 A 和 B 不在同一个本地网络中

  • 这两个客户端在同一个本地网络中的特殊情况(例如:使用同一个家庭路由器),当两个客户端将连接到服务器上的端口 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 or TCP hole punching to connect two peers (each one behind a router) UDP hole punching with a third party

【问题讨论】:

  • 同一网络上的主机直接通过二层局域网地址进行通信。除非数据包发往不同的网络,否则这些帧不会通过路由器。查看this question的答案。
  • @RonMaupin 举个简单的例子:在同一个家用路由器上通过 WiFi 连接的两台笔记本电脑需要路由器相互传递数据,对吧? 如果我的脚本(请参阅问题)检测到相同的公共 IP 上有哪些信息,应该将哪些信息传递给它们每个人?
  • 没有。家用路由器实际上是一个科学怪人盒子。你所说的是一个路由器/防火墙/交换机/WAP,都在一个盒子里。 Wi-Fi 上的帧是桥接的,它们从不通过盒子里的路由器,只通过 WAP。路由器在不同网络之间路由第 3 层数据包。网桥(WAP 和交换机也是网桥)将桥接同一网络上的第 2 层帧。
  • 如果服务器注意到两个客户端使用相同的公共IP,服务器可以传递什么样的信息,以允许客户端A和B之间的直接连接?那么它们应该如何相互连接呢?客户端 A 不必知道客户端 B 的本地 IP,反之亦然。软件SyncThing 的工作原理是这样的:两个客户端可以见面,如果他们相距很远,可以交换数据。如果它们在同一个本地网络中,则在它们之间直接建立连接(仅家庭路由器)!它非常好用,您无需提供 IP,它会自动检测到它。
  • 如果您将服务器配置为网络基础设施设备,那么您需要配置一个网桥来桥接同一网络上的帧,否则,您需要配置一个路由器来在不同网络之间路由数据包。

标签: python sockets networking nat hole-punching


【解决方案1】:

由于服务器知道两个客户端的地址,它可以将该信息发送给它们,因此它们将知道彼此的地址。服务器可以通过多种方式发送此数据 - 腌制、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])

客户端收到地址并解码后,就不再需要服务器了,可以在它们之间建立新的连接。

据我所知,现在我们有两个主要选项。

  • 一个客户端充当服务器。该客户端将关闭与服务器的连接并开始侦听同一端口。这种方法的问题在于,只有当两个客户端都在同一个本地网络上,或者该端口对传入连接开放时,它才会起作用。

  • 打孔。两个客户端同时开始相互发送和接受数据。客户端必须在它们用于连接到集合服务器的相同地址上接受数据,该地址彼此是已知的。这将在客户端的 nat 上打一个洞,即使客户端在不同的网络上,它们也可以直接通信。这个过程在这篇文章Peer-to-Peer Communication Across Network Address Translators,第3.4节不同NAT后的对等体中有详细说明。

UDP 打孔的 Python 示例:

服务器:

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 还是公共 IP,都运行良好。

我已经尝试过 TCP 打孔,但我的成功有限(有时它似乎有效,有时却无效)。如果有人想尝试,我可以包含代码。概念大致相同,两个客户端同时开始发送和接收,在Peer-to-Peer Communication Across Network Address Translators,第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,如果匹配,则它们必须在同一个本地网络上。

【讨论】:

  • 非常感谢您的详细回答,我会仔细研究。关于您的代码“UDP 打孔的 Python 示例”的问题。您能否详细说明我们应该如何在客户端 A、客户端 B、服务器上运行相同的代码? (我应该使用参数来指定它是服务器还是客户端?)。它也不应该包括一个 socket.close() 和一个重新打开:一旦两个客户端通过集合服务器“相遇”,他们不应该关闭与服务器的连接(不再需要),然后直接重新打开客户端 A 和客户端 B 之间的套接字?这是如何在您的解决方案中完成的?谢谢
  • 不,您不应该关闭套接字并创建一个新的。新端口将分配给新套接字,客户端将无法通信。 UDP 客户端可以连接到多个服务器。我试着清理一下代码;服务器代码应该在服务器上运行,客户端代码在客户端 A 和客户端 B 上运行。服务器需要两个客户端。当然这是一个非常基本的例子,基本上是一个 POC。
  • 非常感谢您的回答!这说得通!我尝试从 Wifi 热点打孔,但没有成功,但我会在家中使用 2 个不同的连接(4G 与 Wifi)重试,它可能会工作。我不知道一个套接字最初由客户端 A 打开以到达服务器的 IP,因此在 NAT 路由器上打开一个端口(例如,43210)也会接受返回的字节来自另一个 IP,例如客户端 B。是这样工作的,还是我误解了什么?
  • 是的,就是这样。客户端连接到服务器并获取对等方的地址。然后它开始同时从该地址发送和接受数据,而另一个客户端也这样做。或者,您可以关闭与服务器的连接,创建一个新套接字,并将其绑定到与用于连接服务器的原始套接字相同的端口(我们可以使用.getsockname() 获得),因此端口保持不变并且对等方可以连接。这就是我尝试用 TCP 套接字做的事情,但正如我所说,我几乎没有成功。也许客户端应该尝试多个连接。
  • 好的,谢谢。假设当我(客户端 A)到达服务器 123.123.123.123 端口 4000 时,我的 NAT 转换为端口 22000。然后服务器将看到我的 IP + 22000,并将此信息传递给您(客户端 B)。然后你会尝试通过我的 IP + 22000 与我联系。但我的路由器只有如果我也向你发送了字节才会接受你的数据包。我也做过。但是当我从发送字节到服务器(使用端口 22000)切换到发送字节给你时,它还会使用 22000 吗?否则,您将无法在 22000 上联系到我。
【解决方案2】:

接受的答案给出了解决方案。以下是“客户端 A 和客户端 B 在同一个本地网络中” 的情况下的一些附加信息。 如果服务器注意到两个客户端具有相同的公共 IP,则服务器确实可以检测到这种情况。

那么服务器就可以选择Client A作为“本地服务器”,Client B作为“本地客户端”。

然后服务器将向客户端 A 询问其“本地网络 IP”。 Client A can find it with:

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)

【讨论】:

    猜你喜欢
    • 2014-12-26
    • 1970-01-01
    • 2013-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-17
    • 1970-01-01
    相关资源
    最近更新 更多