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打孔与第三方
由于服务器知道两个客户端的地址,因此它可以将这些信息发送给他们,因此他们会知道彼此的地址.服务器可以通过多种方式发送此数据 - 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,如果它们匹配则它们必须位于同一本地网络上.