NAT穿透进行P2P文件传输
实现一个简单的p2p文件传输,主要解决NAT穿透问题,使用tcp协议传输。
NAT背景介绍
简介
NAT(Network Address Translation ,网络地址转换) 是一种广泛应用的解决IP 短缺的有效方法, NAT 将内网地址转和端口号换成合法的公网地址和端口号,建立一个会话,与公网主机进行通信。
NAT 不仅实现地址转换,同时还起到防火墙的作用,隐藏内部网络的拓扑结构,保护内部主机。 NAT 不仅完美地解决了 lP 地址不足的问题,而且还能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机。 这样对于外部主机来说,内部主机是不可见的。
但是,对于P2P 应用来说,却要求能够建立端到端的连接,所以如何穿透NAT 也是P2P 技术中的一个关键。
分类
NAT 从表面上看有三种类型:
静态NAT :静态地址转换将内部私网地址与合法公网地址进行一对一的转换,且每个内部地址的转换都是确定的。
动态NAT :动态地址转换也是将内部本地地址与内部合法地址一对一的转换,但是动态地址转换是从合法地址池中动态选择一个未使用的地址来对内部私有地址进行转换。
地址端口转换NAPT :它也是一种动态转换,而且多个内部地址被转换成同一个合法公网地址,使用不同的端口号来区分不同的主机,不同的进程。
从实现的技术角度,又可以将NAT 分成如下几类:
全锥NAT :全锥NAT 把所有来自相同内部IP 地址和端口的请求映射到相同的外部IP 地址和端口。任何一个外部主机均可通过该映射发送数据包到该内部主机。
限制性锥NAT :限制性锥NAT 把所有来自相同内部IP 地址和端口的请求映射到相同的外部IP 地址和端口。但是, 和全锥NAT 不同的是:只有当内部主机先给外部主机发送数据包, 该外部主机才能向该内部主机发送数据包。
端口限制性锥NAT :端口限制性锥NAT 与限制性锥NAT 类似, 只是多了端口号的限制, 即只有内部主机先向外部地址:端口号对发送数据包, 该外部主机才能使用特定的端口号向内部主机发送数据包。
对称NAT :对称NAT 与上述3 种类型都不同, 不管是全锥NAT ,限制性锥NAT 还是端口限制性锥NAT ,它们都属于锥NAT (Cone NAT )。当同一内部主机使用相同的端口与不同地址的外部主机进行通信时, 对称NAT 会重新建立一个Session ,为这个Session 分配不同的端口号,或许还会改变IP 地址。
解决问题
了解了NAT之后,开始思考如何解决两台在不同的NAT后面的主机直接相连的问题。静态NAT只要知道所给的公网地址即可,不在我们讨论的范围内。
思考问题并找到重点
假设有主机A和主机B分别在两个NAT转换设备NATA和NATB后面。
A与B之间要通信,我们可假设NATA中转发表有下面这个表项:
内网IP:Port | 公网IP:Port |
---|---|
192.168.0.2:7000 | 202.103.142.29:5000 |
NATB转发表中如下:
内网IP:Port | 公网IP:Port |
---|---|
192.168.1.12:8000 | 221.10.145.84:6000 |
这样A中绑定了 192.168.0.2:7000 的socket只需要连接221.10.145.84:6000即可与B中绑定了192.168.1.12:8000的socket进行通信。B同理。
所以如何在转发表中留下这样一个表项并让对方知道并可以连接就是我们要解决的重点。
解决重点
首先转发表中没有转发表项的话,两方无论如何也是无法连上的。这时候我们就需要借助有公网ip的Server帮我们搭个桥。
还是使用这张图
A与Server 129.208.12.38 相连,在NAT-A中插入
内网IP:Port | 公网IP:Port |
---|---|
192.168.0.2:7000 | 202.103.142.29:5000 |
B也与Server 129.208.12.38 相连,在NAT-B中插入
内网IP:Port | 公网IP:Port |
---|---|
192.168.1.12:8000 | 221.10.145.84:6000 |
然后服务器将 A 的源地址和端口 202.103.142.29:5000 发给 B, 将 B 的源地址和端口 221.10.145.84:6000 发给 A 。这样双方就有了对方的外部IP地址和端口的信息。
这时候对于全锥NAT来说就可以直接相连了,但是对于 端口限制性锥NAT 和 限制性锥NAT 还不可以直接相连。因为只有当内部主机先给外部主机发送数据包, 该外部主机才能向该内部主机发送数据包。
如果有一方是(端口)限制性锥形NAT,就得由这一方作为客户端主动相连,另一方作为服务端进行连接。如果双方都是(端口)限制性锥形NAT,就得先由一方先行与对方连接,结果必然失败,但是在这一方的NAT中保留了接受对方IP和端口的信息,称之为打孔。这时候另一方再与先发送请求的一方连接即可成功。
对于对称NAT, 由于当同一内部主机使用相同的端口与不同地址的外部主机进行通信时, 对称NAT都会重新建立一个Session ,为这个Session 分配不同的端口号,或许还会改变IP地址。穿透起来非常麻烦。若有兴趣可参考文后链接。
对于udp来说,直接发送数据即可。但是对于tcp来说,由于需要在短时间内绑定同一端口连接不同地址,所以需要设置socket选项SOL_SOCKET level的SO_REUSEADDR为True。一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。
实现代码(Python)
服务器于阿里云,长春与重庆连接试验成功。可以本地指定不同端口看看表面效果。
获取本机ip地址在本地地址较多时可能获取得不对。还未找到办法。
发送双方ip:port信息时 我根据先来后到标记了 1 和 0 ,通过判断这个来决定是否为主动连接那一方。
仅为实验代码,多有纰漏请指出。
主机端:
import os
from time import sleep
import struct
import socket def p2p_connect(local_address, local_port, send_file_path, recv_folder_path,server_address,server_port):
if not os.path.exists(send_file_path):
raise FileNotFoundError(send_file_path)
if not os.path.exists(recv_folder_path):
os.mkdirs(recv_folder_path) # 若为windows 只有mkdir
sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((local_address, local_port))
sock.connect((server_address, server_port))
rcv_msgs = sock.recv(1024).decode()
while rcv_msgs.startswith("#"):
print(rcv_msgs)
rcv_msgs = sock.recv(1024).decode()
rcv_msgs = rcv_msgs.split("|")
remote_addr = rcv_msgs[0]
remote_port = int(rcv_msgs[1])
is_server = rcv_msgs[2] == ""
print(rcv_msgs)
sock.close() if is_server:
try_conn = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 打孔
try_conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try_conn.bind((local_address, local_port))
try_conn.connect_ex((remote_addr, remote_port))
try_conn.close()
recv_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
recv_sock.bind((local_address, local_port))
recv_sock.listen(1)
conn, addr = recv_sock.accept()
conn.sendall(os.path.split(send_file_path)[1].encode()) # 发送文件名
with open(send_file_path, "rb") as f:
size = os.path.getsize(send_file_path)
print("共发送", size, "字节")
conn.sendall(struct.pack(">I", size)) # 发送文件大小
data = f.read(1024)
while data:
conn.sendall(data)
data = f.read(1024)
conn.sendall("")
conn.close()
recv_sock.close()
else:
conn = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
while conn.connect_ex((remote_addr, remote_port)) != 0: # 注意网络情况,可能为死循环
sleep(1)
file_name = conn.recv(1024).decode() # 接收文件名
size = struct.unpack(">I", conn.recv(1024))[0] # 接收文件大小
print("接收 : ", file_name, " (", size, "bytes)")
with open(os.path.join(recv_folder_path,file_name), "wb") as f:
count = 0
data = conn.recv(1024)
print("\r已完成 : {:.0f}%".format(count / size*100), end="", flush=True)
while data:
f.write(data)
length = len(data)
count += length
print("\r已完成 : {:.0f}%".format(count / size*100), end="", flush=True)
data = conn.recv(1024)
print(" 传输完成")
conn.close() if __name__ == '__main__':
name = socket.gethostname()
local_port = 22000 # 本地端口
local_address = socket.gethostbyname(name) #本地地址
file_path="text.xml" # 待传输文件
folder_path="" # 接收文件文件夹
remote_address="123.45.67.89" # 服务器地址
remote_port=30000 # 服务器端口
p2p_connect(local_address,local_port,file_path,folder_path,remote_address,30000)
服务器端:
import socket sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("123.45.67.89", 30000))
sock.listen(5) conn1, addr1 = sock.accept()
conn1_info = addr1[0] + "|" + str(addr1[1]) + "|0"
conn1.sendall("#你已连接上,请等待另一名用户\n".encode())
conn2, addr2 = sock.accept()
conn2_info = addr2[0] + "|" + str(addr2[1]) + "|1"
conn2.sendall("#你已连接上,另一名用户已就绪\n".encode()) conn1.sendall(conn2_info.encode())
conn2.sendall(conn1_info.encode()) conn1.close()
conn2.close()
sock.close()
背景参考: P2P,UDP和TCP穿透NAT
NAT穿透进行P2P文件传输的更多相关文章
- 《c# 实现p2p文件分享与传输系统》 二、 设计 - 续(NAT穿透)
c#实现P2P文件分享与传输系统 二.设计 - 续(NAT穿透) 首先要抱歉,因为这些日子较忙,没有写文章,这个系列拖了很久,现在开始继续. 上一篇文章介绍了p2p系统Tracker Server和 ...
- P2P技术(2)——NAT穿透
P2P可以是一种通信模式.一种逻辑网络模型.一种技术.甚至一种理念.在P2P网络中,所有通信节点的地位都是对等的,每个节点都扮演着客户机和服务器双重角色,节点之间通过直接通信实现文件信息.处理器运算能 ...
- java基于P2P的聊天和文件传输实例
用java的NIO技术编写的 1. 支持聊天功能 2. 拖拽文件能够实现文件传输功能.也能够是目录 3. 启动时能够选择server端或client端启动 4. 本人原创.学习NIO和java的网络通 ...
- 《c# 实现p2p文件分享与传输系统》 一、 模型
c#实现P2P文件分享与传输系统 一.模型 P2P的概念大家都不陌生,也就是所谓的“点对点传输”,即不直接通过服务器,在两台或多台客户端之间传输数据,实现信息交流和资源共享.P2P技术已经发展了很多年 ...
- NAT穿透解决方案介绍
最近公司要实现在各种网络环境下面的多屏互动(机顶盒.android phone.iphone及PC端)的需求:由于IP地址资源有限的原因,目前我们使用的各种终端设备都位于局域网后面也就是多台设备共享同 ...
- NAT穿透解决
1.各种网络环境下的P2P通信解决方法: (1)如果通信双方在同一个局域网内,这种情况下可以不借助任何外力直接通过内网地址通信即可: (2)如果通信双方都在有独立的公网地址,这种情况下当然可以不借 ...
- 网络协议之NAT穿透
NAT IPv4地址只有32位,最多只能提供大致42.9亿个唯一IP地址,当设备越来越多时,IP地址变得越来越稀缺,不能为每个设备都分配一个IP地址.于是,作为NAT规范就出现了.NAT(Networ ...
- 《c# 实现p2p文件分享与传输系统》 二、 设计
c#实现P2P文件分享与传输系统 二.设计 在上一篇文章中,介绍了P2P网络的常用模型,并确定了EasyP2P系统的框架,本文将就此设计完成它的主要结构和运作流程. 1. 首先是Tracker Ser ...
- NAT穿透解决方案介绍(转)--Java ICE实现
转:http://www.cnblogs.com/javaminer/p/3575282.html 最近公司要实现在各种网络环境下面的多屏互动(机顶盒.android phone.iphone及PC端 ...
随机推荐
- VS2013 F12无法转到函数的定义处,总是从“元数据”获取的问题 ——解决方法
右键项目名称-->点击菜单中的"添加"-->点击"引用"-->在弹出窗中点击"解决方案"下的"项目", ...
- iOS 之 socket 与 http
http连接:短连接,发送一次请求,服务器响应后连接就断开. socket连接:长连接,连接后长期保持连接状态.
- Memo 的当前行、当前列与当前字符
procedure TForm1.Memo1Click(Sender: TObject); begin Text := Format('当前列:%d, 当前行:%d', [Memo1.CaretP ...
- validform表单验证插件最终版
做个笔记,以后直接用吧. 报名界面: <%@ page language="java" pageEncoding="UTF-8" contentType= ...
- jQuery 在Table中选择input之类的东西注意事项
jQuery 在Table中选择input之类的东西注意事项: 如果不在td标签中,是不能进行正确选择的: <table id="tblFormId"> <tr& ...
- android的引用库类
在eclipse中的项目里,有时需要外来的jar文件.添加后就可以消去程序中的红条条啦~~~~~~~~~可以照下面的说明添加. 方法/步骤 打开eclipse,导入项目 右击 项目 , “Bu ...
- Spring @Aspect实现切面编程
参考:http://blog.csdn.net/cdl2008sky/article/details/6268628 参考:http://www.360doc.com/content/12/0602/ ...
- Treeview显示磁盘下的文件,并且可操作
#region TreeView树形显示磁盘下文件夹 /// <summary> /// IconIndexs类 对应ImageList中5张图片的序列 /// </summary& ...
- ubuntu16.04无法设置选择wifi的解决办法
在公司上班一直连接的有线,直到昨天拿回家才发现ubuntu无法选择使用wifi上网,这让人非常无奈,截图类似如下: 而正常情况下我们应该在启用联网的下面有wifi链接的选项,如图: 我隐约猜测是和驱动 ...
- 蓝桥网试题 java 基础练习 十进制转十六进制
---------------------------------------------------------------------------------------------------- ...