p2p原理初探
背景
机缘巧合5月朋友在做向日葵远程桌面的API调试遇到窗口创建后,无法显示对方桌面图像问题,直到现在这个问题依然没能处理,但远程连接却建立成功。看了一下日志发现远程桌面的原理是P2P打洞,记一个流水帐。分为以下几个部分
- 向日葵远程桌面API
- P2P打洞
- 延申
向日葵远程桌面
向日葵远程桌面和teamViewer一样都是远程的软件, 相比速度没那么快,胜在免费和广告少,之前在做ETC停车场项目中有用到。
向日葵是有上海贝锐公司开发,它家还有广为人知的内外穿透花生棒和蒲公英路由器管理软件。可以在http://developer.oray.com申请成为开发者,并下载sdk。一个远程桌面的接口如下

)
如上,被控制端根据app id和secret调用SLCreateRemote向Oray服务器请求,创建一个地址和session;主动控制端根据地址和session连接被控制端,建立连接之后两者就能进行正常的通讯。你的服务器可有可无,更多也是做些自动等级被控制端的地址和session。
主动控端调用流程如下:
SLInitialize
SLCreateRemote
SLSetReniteCallback
SLOpebRemoteLog
SLCreateRemoteEmptySession
SKSetRemoteSessionOpt
SLConnectRemoteSession
SLDestroyRemoteSession
SLDestroyRemote
SLUninitialize
初步完成了被控制端的自动创建,并返回地址和session;主控端的程序则无法显示界面,原因未知。
通过抓包看到连接是要先同故宫Oray服务器打洞,然后使用udp互相通信,而不经过服务端;此外向日葵提供sdk中nodejs调用C++也很有借鉴意义,暂时不深入。
p2p打洞
主要参考UDP打洞、P2P组网方式研究,从概念和一个简单例子来了解p2p技术。
NAT概念
网络地址转换(NAT,Network Address Translation)属接入广域网(WAN)技术,是一种将私有(保留)地址转化为合法IP地址的转换技术。NAT可以分为四种类型rfc3489
先做一个约定:
内网A中有:A1(192.168.0.8)、A2(192.168.0.9)两用户
网关X1(一个NAT设备)有公网IP 1.2.3.4
内网B中有:B1(192.168.1.8)、B2(192.168.1.9)两用户,
网关Y1(一个NAT设备)有公网IP 1.2.3.5
公网服务器:C (6.7.8.9) D (6.7.8.10)
- Full Cone: 这种NAT内部的机器A连接过外网机器C后,NAT会打开一个端口,然后外网的任何发到这个打开的端口的UDP数据报都可以到达A。不管是不是C发过来的(NAT源端口映射),即从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000 ,192.168.0.8可以收到任意外部主机发到1.2.3.4:62000的数据报。
- Restricted Cone: 这种NAT内部的机器A连接过外网的机器C后,NAT打开一个端口,然后C可以用任何端口和A通信,其他的外网机器不行(目的IP映射) ,从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先给服务器C 6.7.8.9发送一个数据报后,192.168.0.8才能收到6.7.8.9发送到1.2.3.4:62000的数据报。
- Port Restricted Cone: 这种NAT内部的机器A连接过外网的机器C后,NAT打开一个端口,然后C可以用原来的端口和A通信,其他的外网机器不行(NAT目的端口映射),从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先向外部主机地址端口6.7.8.9:8000发送一个数据报后,192.168.0.8才能收到6.7.8.9:8000发送到1.2.3.4:62000的数据报
- Symmetic: 对于这种NAT.连接不同的外部目标。原来NAT打开的端口会变化,而Cone NAT不会,虽然可以用端口猜测,但是成功的概率很小
对于NAT类型探测如下如

)
github上的P2P-Over-MiddleBoxes-Demo有python版本的实现,代码理解如下
STUN_SERVERS = [
('stun.pppan.net', 3478),
('stun.ekiga.net', 3478),
('stun.ideasip.com', 3478),
('stun.voipbuster.com', 3478),
]
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ntype = test_nat(sock, STUN_SERVERS[0], local_ip)
def test_nat(sock, stun_server, local_ip='0.0.0.0'):
resp = test_I(sock, stun_server) //给STUN_SERVERS第0个发消息
if resp is None:
return NAT.UDP_BLOCKED //没收到公网的ip,网络不通
m1 = get_mapped_address(resp) //取到公网ip得到的发出去的ip
changed_address = get_changed_address(resp) //得到网关ip和端口
if m1 == local_address: //可能是个公网ip
# we can't tell whether it's public if we don't specify the local address
resp = test_II(sock, stun_server) //改变ip和端口
if resp is None:
return NAT.SYMMETRIC_UDP_FIREWALL
return NAT.PUBLIC //一样能发送内容,则为公网
resp = test_II(sock, stun_server) //再次改变ip和端口
if not resp is None:
return NAT.FULL_CONE //无法送达
resp = test_I(sock, changed_address) //发给网关和端口
m2 = get_mapped_address(resp)
if m2 != m1:
return NAT.SYMMETRIC
resp = test_III(sock, stun_server) //不改变ip只改变端口
if resp is None:
return NAT.PORT_RISTRICT
else:
return NAT.ADDR_RISTRICT
要真正实现p2p打洞,就是要A1和B1互相给对象公网ip发一条信息,建立连接,原理如下
A1在客户机时 192.168.0.8:4000——6.7.8.9:8000
X1在网关时 1.2.3.4:62000——6.7.8.9:8000
服务器C 6.7.8.9:8000
B1在客户机时 192.168.1.8:4000——6.7.8.9:8000
Y1在网关时 1.2.3.5:31000——6.7.8.9:8000
两内网用户要实现通过各自网关的直接呼叫,需要以下过程:
1、 客户机A1、B1顺利通过格子网关访问服务器C ,均没有问题(类似于登录)
2、 服务器C保存了 A1、B1各自在其网关的信息(1.2.3.4:62000、1.2.3.5:31000)没有问题。并可将该信息告知A1、B1。
3、 此时A1发送给B1网关的1.2.3.5:31000是否会被B1收到?答案是基本上不行(除非Y1设置为完全圆锥型,但这种设置非常少),因为Y1上检测到其存活的会话中没有一个的目的IP或端口于1.2.3.4:62000有关而将数据包全部丢弃!
4、 此时要实现A1、B1通过X1、Y1来互访,需要服务器C告诉它们各自在自己的网关上建立“UDP隧道”,即命令A1发送一个 192.168.0.8:4000——1.2.3.5:31000的数据报,B1发送一个192.168.1.8:4000——1.2.3.4:62000的数据报,UDP形式,这样X1、Y1上均存在了IP端口相同的两个不同会话(很显然,这要求网关为Cone NAT型,否则,对称型Symmetric NAT设置网关将导致对不同会话开启了不同端口,而该端口无法为服务器和对方所知,也就没有意义)。
测试程序
有一个服务端的server,得到连接的名字和共有私有地址,然后提供接口让所有的连接获取现有的所有连接的名字和共有地址;客户端则启动的时候,登录携带自己的名称,可取获取所有连接数,并给其他连接的公网地址发送udp请求,实现打洞。
//================================
//p2p_server.py
#!/usr/bin/python
#coding=utf-8
import socket, sys, SocketServer, threading, thread, time
SERVER_PORT = 1234
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', SERVER_PORT))
user_list = []
def server_handle():
while True:
cli_date, cli_pub_add = sock.recvfrom(8192)
now_user = []
headder = []
cli_str = {}
headder = cli_date.split('\t')
for one_line in headder:
str = {}
str = one_line
args = str.split(':')
cli_str[args[0]] = args[1]
if cli_str['type'] == 'login':
del cli_str['type']
now_user = cli_str
now_user['cli_pub_ip'] = cli_pub_add[0]
now_user['cli_pub_port'] = cli_pub_add[1]
if len(headder) >= 4:
now_user['user_name'] = headder[1].split(':')[-1]
now_user['private_ip'] = headder[2].split(':')[-1]
now_user['private_port'] = headder[3].split(':')[-1]
user_list.append(now_user)
toclient = 'info#%s login in successful, the info from server' %now_user['user_name']
sock.sendto(toclient, cli_pub_add)
print('-'*100)
print("%s 已登录,公网IP:%s 端口:%d\n" %(now_user['user_name'], now_user['cli_pub_ip'], now_user['cli_pub_port']))
print("以下是一登陆的用户列表")
for one_user in user_list:
print('用户名:%s 公网ip:%s 公网端口:%d 私网端口:%s' %(one_user['user_name'], one_user['cli_pub_ip'], one_user['cli_pub_port'], one_user['private_port']))
break
elif cli_str['type'] == 'alive':
pass
elif cli_str['type'] == 'logout':
pass
elif cli_str['type'] == 'getalluser':
print('-'*100)
for one_user in user_list:
toclient = 'getalluser#username:%s pub_ip:%s pub_port:%d pri_ip:%s pri_port:%s' %(one_user['user_name'], one_user['cli_pub_ip'], one_user['cli_pub_port'], one_user['private_ip'], one_user['private_port'])
sock.sendto(toclient, cli_pub_add)
if __name__ == '__main__':
thread.start_new_thread(server_handle, ())
print('服务器进程已启动,等待客户连接')
while True:
for one_user in user_list:
toclient = 'keepconnect#111'
sock.sendto(toclient, (one_user['cli_pub_ip'], one_user['cli_pub_port']))
time.sleep(1)
//================================
//p2p_client.py
#coding=utf-8
import socket, SocketServer, threading, thread, time
CLIENT_PORT = 4321
SERVER_IP = "49.235.232.69"
SERVER_PORT = 1234
user_list = {}
local_ip = socket.gethostbyname(socket.gethostname())
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', CLIENT_PORT))
to_user_ip = ''
to_user_port = ''
#sock.bind((local_ip,CLIENT_PORT))
def server_handle():
print('客户端线程已近启动,等待其他客户端连接')
while True:
data, addr = sock.recvfrom(8192)
#print('server_handle recv '+ data)
data_str = data.split('#')
data_type = data_str[0]
data_info = data_str[1]
if data_type == 'info':
del data_str[0]
print(data_info)
if data_type == 'getalluser':
data_sp = data_info.split(' ')
user_name = data_sp[0].split(':')[1]
del data_sp[0]
user_list[user_name] = {}
for one_line in data_sp:
arg = one_line.split(':')
user_list[user_name][arg[0]] = arg[1]
if data_type == 'echo':
print(data_info)
if data_type == 'keepconnect':
message = 'type:alive'
sock.sendto(message, addr)
if __name__ == '__main__':
thread.start_new_thread(server_handle, ())
time.sleep(0.1)
cmd = raw_input('输入指令>>')
while True:
args = cmd.split(' ')
if args[0] == 'login':
user_name = args[1]
local_uname = args[1]
address = 'private_ip:%s private_port:%d' %(local_ip, CLIENT_PORT)
headder = 'type:login\tuser_name:%s\tprivate_ip:%s\tprivate_port:%d' %(user_name, local_ip, CLIENT_PORT)
sock.sendto(headder, (SERVER_IP, SERVER_PORT))
elif args[0] == 'getalluser':
headder = 'type:getalluser\tuser_name:a1'
sock.sendto(headder, (SERVER_IP, SERVER_PORT))
#print('获取用户列表中...')
time.sleep(1)
for one_user in user_list:
print('username:%s pub_ip:%s pub_port:%s pri_ip:%s pri_port:%s' %(one_user, user_list[one_user]['pub_ip'], user_list[one_user]['pub_port'], user_list[one_user]['pri_ip'],user_list[one_user]['pri_port']))
elif args[0] == 'connect':
user_name = args[1]
to_user_ip = user_list[user_name]['pub_ip']
to_user_port = int(user_list[user_name]['pub_port'])
elif args[0] == 'echo':
m = ' '.join(args[1:])
message = 'echo#from %s:%s'%(local_uname, m)
sock.sendto(message, (to_user_ip, to_user_port))
time.sleep(0.1)
cmd = raw_input('输入指令>>')
测试:发现同一个局域网内,两台主机之间通过公网ip的打洞是可能异常的(据说直接方法是,通过发私有ip和端口试试),具体原因未知;如果两个位于不同公有ip能成功
延申
p2p是现在磁力下载等的基础,还有zeroNet分布式p2p网络的存在,让p2p应用广泛;此外udp是一个越来越被重视的,比如流媒体推h264流就用udp,http3的QUIC实现也是用udp加速连接。
浙公网安备 33010602011771号