无需端口转发的P2P多人联机工作原理是什么?如何用Python模拟实现?
P2P网络无需端口转发的实现原理
前置背景:NAT拦截的本质
家庭/企业内网的路由器普遍采用NAPT(网络地址端口转换)技术,所有内网设备共享同一个公网IP。路由器会维护一张会话映射表,只有当内网设备先主动向某公网地址发起过请求,路由器才会生成对应的「内网IP:端口 <-> 公网IP:端口 <-> 目标公网地址:端口」的映射条目,后续该目标地址发来的数据包才会被放行到对应的内网设备;没有对应映射条目的外来数据包会被直接拦截,这也是C/S架构下服务端需要手动做端口转发的原因。
P2P打洞的核心逻辑
P2P能绕过NAT拦截靠的是**NAT穿透(俗称打洞)**技术,整个过程需要一台有公网IP的中继服务器做协调,步骤如下:
- 所有准备加入P2P网络的对等节点,先主动和公网中继服务器建立连接,中继服务器会记录每个节点的公网IP和端口,同时每个节点的路由器已经生成了对应中继服务器地址的映射条目。
- 当节点A需要和节点B建立通信时,先向中继服务器发送连接B的请求,中继服务器会把B的公网IP+端口返回给A,同时把A的公网IP+端口推送给B。
- 节点A和B同时向对方的公网IP+端口发送探测数据包:此时A发往B的数据包虽然第一次会被B的路由器拦截,但B已经主动向A的地址发过探测包,B的路由器已经生成了允许A地址的数据包进入的映射条目,后续A再发的数据包就会被正常放行到B的内网设备,反之亦然。
- 打洞成功后,两个节点就可以直接进行点对点通信,不需要再经过中继服务器转发数据。
你提到的《深岩银河》这类P2P游戏就是用的这个逻辑,游戏厂商会提前部署公网的中继服务器做节点协调,玩家不需要手动配置端口转发。
Python Socket实现简单P2P连接演示
以下用UDP Socket实现极简的P2P打洞演示,需要你提前准备一台有公网IP的服务器运行中继服务。
1. 中继服务器代码 relay_server.py
import socket # 中继服务配置,端口需要在服务器的安全组/防火墙放开 RELAY_HOST = "0.0.0.0" RELAY_PORT = 9999 peer_map = {} # 存储节点ID对应的公网地址 if __name__ == "__main__": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((RELAY_HOST, RELAY_PORT)) print(f"中继服务已启动,监听端口{RELAY_PORT}") while True: data, addr = sock.recvfrom(1024) msg = data.decode().strip() # 节点注册:格式为 register|节点ID if msg.startswith("register|"): peer_id = msg.split("|")[1] peer_map[peer_id] = addr print(f"节点{peer_id}已注册,地址{addr}") # 连接请求:格式为 connect|己方ID|目标ID elif msg.startswith("connect|"): _, self_id, target_id = msg.split("|") if target_id not in peer_map: sock.sendto(b"error|目标节点不存在", addr) continue # 把双方地址发给对方 target_addr = peer_map[target_id] self_addr = peer_map[self_id] sock.sendto(f"peer|{target_id}|{target_addr[0]}|{target_addr[1]}".encode(), self_addr) sock.sendto(f"peer|{self_id}|{self_addr[0]}|{self_addr[1]}".encode(), target_addr)
2. 对等节点代码 p2p_peer.py
import socket import threading # 替换成你自己的中继服务器公网IP和端口 RELAY_HOST = "你的中继服务器公网IP" RELAY_PORT = 9999 def recv_handler(sock): while True: data, addr = sock.recvfrom(1024) msg = data.decode().strip() if msg.startswith("peer|"): _, peer_id, peer_ip, peer_port = msg.split("|") peer_port = int(peer_port) print(f"收到节点{peer_id}的连接信息,地址为{peer_ip}:{peer_port},正在打洞...") # 先发一个探测包打洞 sock.sendto(b"hole_punch", (peer_ip, peer_port)) elif msg == "hole_punch": print(f"打洞成功,对方地址{addr},现在可以直接发消息了") else: print(f"收到消息:{msg},来自{addr}") if __name__ == "__main__": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 启动接收线程 threading.Thread(target=recv_handler, args=(sock,), daemon=True).start() # 先注册到中继服务器 self_id = input("输入你的节点ID:") sock.sendto(f"register|{self_id}".encode(), (RELAY_HOST, RELAY_PORT)) print("注册成功,输入 connect 目标ID 发起连接:") while True: input_msg = input("> ") # 连接命令格式:connect 目标ID if input_msg.startswith("connect "): target_id = input_msg.split(" ")[1] sock.sendto(f"connect|{self_id}|{target_id}".encode(), (RELAY_HOST, RELAY_PORT)) # 打洞成功后直接输入内容即可发送给对端 else: # 替换成打洞成功后提示的对端公网IP和端口 target_addr = ("对端公网IP", 对端端口) sock.sendto(input_msg.encode(), target_addr)
使用说明
- 先在公网服务器运行
relay_server.py,确保9999端口已在防火墙/安全组放开 - 在内网的两台设备上分别运行
p2p_peer.py,分别输入不同的节点ID完成注册 - 在任意一台设备上输入
connect 对方节点ID,等待打洞成功提示 - 打洞成功后把代码里的target_addr替换成提示的对端地址,即可直接点对点发送消息
内容的提问来源于stack exchange,提问作者Matt
相关产品推荐
相关产品推荐

