现在大部分的个人主机都是通过路由器连接外网,整个内网使用一个公共IP,由路由器进行内网IP、端口和外网IP、端口的映射(NAT)。
而这种映射方式只能由内网主动向外发送请求时才会建立,并通过映射出的该端口监听返回消息。
根据映射方式的不同,建立映射的规则也不同,允许接收消息的范围也不同。如(参考:https://www.cnblogs.com/dyufei/p/7466924.html)
1、对称 NAT(Symmetric NAT) 尽量会保持内网端口和映射端口相同。但是同一端口发出的指向不同外网服务器\端口的请求会映射不同的端口。
2、锥形NAT(Cone NAT)同一内网主机/端口的消息都会映射到相同的端口
2.1 完全锥形映射(Full Cone NAT ):映射的端口允许接收任意公网IP的消息
2.2 限制锥形( Restricted Cone NAT):映射的端口只允许接收建立映射时对应的公网IP发送的消息
2.3 端口限制锥形 NAT ( Port Restricted Cone NAT ):在2.2的基础上增加了接收端口限制
在锥形NAT的情况下进行udp打洞是相对容易实现的,具体思路如下:
1、需要一台作为中继的公网服务器S
2、假设有内网主机A、B,首先A主动向S发出请求,此时NAT建立映射(ip1,port1)->(ipa,porta)。然后B向S发出请求,建立(ip2,port2)->(ipb,portb)。
3、此时服务器S拥有A、B的公网映射信息,并将A的信息发给B,B的信息发给A。
4、如果是完全锥形映射,此时AB可以直接发送消息通信。但是另外两种并不能行。
5、此时A主动向B发送消息,A的NAT会建立A->B的映射,映射端口同样是porta。该消息会被B的NAT阻拦,并不会被接收,但此时A已经向B打开了通道。
6、此时再由B向A发送消息,打开B->A 的通道,由此A、B已经可以正常进行通讯
简单demo如下
Sever.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import socket import _thread import json import time udpSocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udpSocket.bind(( "***.***.***.***" , 40002 )) #注意如果使用云服务器,此处应绑定云服务器内网ip add = [] #保存所用连接用户 severPort = 30001 def acc(sc): cont,destinf = sc.recvfrom( 1024 ) print (cont.decode( "utf-8" )) print (destinf) jsn = json.loads(cont.decode( "utf-8" )) if (jsn[ "cod" ] = = 1 ): #用户连接 if ( len (add)> 0 ): for x in add: print (x) jtt = { "cod" : 2 , "msg" :destinf} sc.sendto(json.dumps(jtt).encode( "utf-8" ),x) #向所有用户广播新加入用户的地址 jtn = { "cod" : 3 , "msg" :add} sc.sendto(json.dumps(jtn).encode( 'utf-8' ),destinf) add.append(destinf) acc(sc) _thread.start_new_thread(acc,(udpSocket,)) s = input ("") while ( 1 = = 1 ): time.sleep( 10 ) |
Client.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import socket import _thread import json s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(( 'www.baidu.com' , 0 )) ip = s.getsockname()[ 0 ] #获取本机局域网IP udpSocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udpSocket.bind((ip, 30001 )) #绑定端口 otherAddr = [] #保存其他用户的地址信息 severAddr = "***.***.***.***" severPort = 40002 def acc(sc): global otherAddr cont,destinf = sc.recvfrom( 1024 ) jsn = json.loads(cont.decode( "utf-8" )) if (jsn[ "cod" ] = = 2 ): #0打洞1初始连接2服务器向其他用户广播新加入地址3向新加入用户通知已在线人员4用户消息 print ( str (jsn[ 'msg' ]) + "进入聊天" ) otherAddr.append(jsn[ 'msg' ]) #把新加入用户添加到otherAddr sc.sendto( '{"cod":0}' .encode( 'utf-8' ), tuple (jsn[ 'msg' ])) #打洞 if (jsn[ "cod" ] = = 3 ): otherAddr = jsn[ 'msg' ] print ( "进入聊天,其他人有" + str (jsn[ 'msg' ])) for x in jsn[ 'msg' ]: sc.sendto( '{"cod":0}' .encode( 'utf-8' ), tuple (x)) #打洞 if (jsn[ "cod" ] = = 4 ): print ( str (destinf) + ":" + jsn[ 'msg' ]) acc(sc) _thread.start_new_thread(acc,(udpSocket,)) jsn = { "cod" : 1 , "msg" : "a" } udpSocket.sendto(json.dumps(jsn).encode( "utf-8" ),(severAddr,severPort)) #向服务器第一次请求 while ( 1 = = 1 ): s = input ("") if (s! = ""): jsn = { "cod" : 4 , "msg" :s} for x in otherAddr: #向所有其他用户广播 udpSocket.sendto(json.dumps(jsn).encode( "utf-8" ),(x[ 0 ],x[ 1 ])) |