在连接了会议点服务器之后,如何使2个客户端直接相互连接?

Bas*_*asj 6 python sockets networking nat hole-punching

我正在写一个玩具会议点/中继服务器,在端口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
Run Code Online (Sandbox Code Playgroud)

此代码目前正在运行:

# 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
Run Code Online (Sandbox Code Playgroud)

你可以通过在服务器上启动它来测试它,并做两个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
    
    Run Code Online (Sandbox Code Playgroud)

备注:此前不成功的尝试:UDP或TCP打孔连接两个对等体(每个对等在路由器后面) UDP打孔与第三方

t.m*_*dam 5

由于服务器知道两个客户端的地址,因此它可以将这些信息发送给他们,因此他们会知道彼此的地址.服务器可以通过多种方式发送此数据 - pickled,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])
Run Code Online (Sandbox Code Playgroud)

当客户端接收并解码地址时,他们不再需要服务器,并且可以在它们之间建立新的连接.

据我所知,现在我们有两个主要的otions.

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

  • 打孔.两个客户端同时开始发送和接受彼此的数据.客户端必须接受与用于连接到集合服务器的地址相同的数据,这些地址彼此相似.这将在客户端的nat中打一个洞,即使客户在不同的网络上,客户也能够直接进行通信.本文详细阐述了本文" 跨网络地址转换器的对等通信",第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)
Run Code Online (Sandbox Code Playgroud)

客户:

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)
Run Code Online (Sandbox Code Playgroud)

此代码要求集合服务器打开一个端口(在本例中为4000),并且两个客户端都可以访问.客户端可以位于相同或不同的本地网络上.代码在Windows上进行了测试,无论是本地IP还是公共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 ...
Run Code Online (Sandbox Code Playgroud)

这里服务器向每个客户端发送两个地址,对等方的公共地址和客户端自己的公共地址.客户端比较两个IP,如果它们匹配则它们必须位于同一本地网络上.