代码链接https://github.com/laike9m/PyPunchP2P
ps:我分析的是别人的代码
假设服务端的IP是192.168.88.100,监听5678端口
python server.py 5678
客户端A 和B运行
python client.py 192.168.88.100 5678 100 0
数字100用于匹配客户端,可以任意数字,但是服务器只会链接具有相同数字的客户端。如果链接了两个客户,则两个人可以通过在终端上键入内容进行聊天。第四个参数0用来指定NAT类型。0代表完全形NAT
FullCone = "Full Cone" # 0
RestrictNAT = "Restrict NAT" # 1
RestrictPortNAT = "Restrict Port NAT" # 2
SymmetricNAT = "Symmetric NAT" # 3
UnknownNAT = "Unknown NAT" # 4
NATTYPE = (FullCone, RestrictNAT, RestrictPortNAT, SymmetricNAT, UnknownNAT)
我们知道我们要向服务端发送信息以便让服务端知道客户端的网关地址。
class Client():
def __init__(self):
try:
master_ip = '127.0.0.1' if sys.argv[
1] == 'localhost' else sys.argv[1]
# 第一个参数服务端的IP地址数赋值给master_ip
# sys 的 sys.argv 来获取命令行参数
self.master = (master_ip, int(sys.argv[2]))
self.pool = sys.argv[3].strip() # 去除首尾空格
self.sockfd = self.target = None
self.periodic_running = False
self.peer_nat_type = None
except (IndexError, ValueError):
print(sys.stderr, "usage: %s " % sys.argv[0])
sys.exit(65)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# AF_INET表示IPv4网络协议的套接字类型SOCK_DGRAM表示非连接的
s.sendto()
#发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.recvfrom()
# 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
def request_for_connection(self, nat_type_id=0):
self.sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sockfd.sendto(self.pool + ' {0}'.format(nat_type_id), self.master)
# ' {0}'.format(nat_type_id)格式化输出,self.master为(ipaddr,port)的元组
# sendto发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组
# 指定远程地址,返回值是发送的字节数。
data, addr = self.sockfd.recvfrom(len(self.pool) + 3)
# 接收UDP数据,返回值是(data,address)
#其中data是包含接收数据的字符串,address是发送数据的套接字地址。
if data != "ok " + self.pool:
print(sys.stderr, "unable to request!") # sys.stderr 目的就是返回错误信息
sys.exit(1) # 中途退出程序,0为正常退出1为异常退出
self.sockfd.sendto("ok", self.master)
sys.stderr = sys.stdout
print(sys.stderr,
"request sent, waiting for partner in pool '%s'..." % self.pool)
data, addr = self.sockfd.recvfrom(8)
# 这里接收服务端发送过来的客户端B的网关地址
self.target, peer_nat_type_id = bytes2addr(data)
print(self.target, peer_nat_type_id)
# 输出客户端B的网关地址和类型
self.peer_nat_type = NATTYPE[peer_nat_type_id]
print(sys.stderr, "connected to {1}:{2}, its NAT type is {0}".format(
self.peer_nat_type, *self.target))
客户端A和B都和服务端发送了参数100和各自的NAT类型,服务端接收消息后,会把各自的网关地址发给对方。
def main():
port = sys.argv[1]
try:
port = int(sys.argv[1])
except (IndexError, ValueError):
pass
sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sockfd.bind(("", port))
print "listening on *:%d (udp)" % port
poolqueue = {}
# A,B with addr_A,addr_B,pool=100
# temp state {100:(nat_type_id, addr_A, addr_B)}
# final state {addr_A:addr_B, addr_B:addr_A}
symmetric_chat_clients = {}
ClientInfo = namedtuple("ClientInfo", "addr, nat_type_id")
while True:
data, addr = sockfd.recvfrom(1024)
# help build connection between clients, act as STUN server
print "connection from %s:%d" % addr
pool, nat_type_id = data.strip().split()
# data.strip().split(',')的用法是先对data执行strip函数,去掉在字符串中任何都不希望出现的空格,
# 得到ata的基础上在去执行split(’,')函数。
sockfd.sendto("ok {0}".format(pool), addr)
print("pool={0}, nat_type={1}, ok sent to client".format(pool, NATTYPE[int(nat_type_id)]))
data, addr = sockfd.recvfrom(2)
if data != "ok":
continue
print "request received for pool:", pool
try:
a, b = poolqueue[pool].addr, addr
nat_type_id_a, nat_type_id_b = poolqueue[pool].nat_type_id, nat_type_id
sockfd.sendto(addr2bytes(a, nat_type_id_a), b) #把客户端A的网关地址
sockfd.sendto(addr2bytes(b, nat_type_id_b), a) # 把客户端B网关地址发给客户端A
print "linked", pool
del poolqueue[pool]
except KeyError: # KeyError异常 试图访问字典里不存在的键
poolqueue[pool] = ClientInfo(addr, nat_type_id)
# 当接收一个客户端的连接时,poolqueue[pool].addr是不存在的,然后执行异常
# 把客户端的网关地址和NAT类型保存下来
# 当另一个客户端发来消息时,此时有值,不执行异常
当记录下客户端A的网关地址后,会等待客户端B的连接。
3) 客户端A和客户端B的聊天实现
def recv_msg(self, sock):
while True:
data, addr = sock.recvfrom(1024) #接收数据
if addr == self.target or addr == self.master:
sys.stdout.write(data) #输出数据
def send_msg(self, sock):
while True:
data = sys.stdin.readline() # sys.stdin.readline()可以实现标准输入,把输入的信息给data
sock.sendto(data, self.target)
@staticmethod
def start_working_threads(send, recv, *args):
ts = Thread(target=send, args=args)
ts.setDaemon(True)
ts.start()
tr = Thread(target=recv, args=args)
tr.setDaemon(True)
tr.start()
def chat_fullcone(self): #掉用线程,传的参数是接收和发送信息的函数
self.start_working_threads(self.send_msg, self.recv_msg, self.sockfd)
4.运行演示
1)首先在服务端进入项目文件cd /usr/PyPunchP2P,然后运行server.py
2)客户端A运行client.py
运行成功显示等待另一个小伙伴
此时服务器已经收到了客户端A的网关地址192.168.88.10:34518
3)客户端B运行client.py
上图客户端B运行成功,并收到了服务器发送的客户端A的网关地址192.168.88.100:34518
服务器记录了客户端B的网关地址192.168.88.20:55068
同时如下图,客户端A也收到了服务器发送的客户端B的网关地址
4)客户端A和客户端B之间可以互相通信
5.小结
对于双方都是完全锥形NAT,不需要考虑限制问题,我们只需要知道客户端A的网关地址和客户端B的网关地址,然后分别告诉它们,我们就可以让客户端A和客户端B直接通信实现P2P聊天。
参考文献:[1]https://github.com/laike9m/PyPunchP2P/blob/master/client.py
[2]https://blog.51cto.com/wangbojing/1968118