基于UDP打洞(内网穿透)实现P2P聊天程序代码及原理分析

基于UDP打洞(内网穿透)实现P2P聊天程序代码及原理分析

代码链接https://github.com/laike9m/PyPunchP2P
ps:我分析的是别人的代码

1:运行

假设服务端的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)

2.客户端代码client.py分析

我们知道我们要向服务端发送信息以便让服务端知道客户端的网关地址。

  1. 读取参数
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 <host> <port> <pool>" % sys.argv[0])
            sys.exit(65)
  1. 对于发送信息我们可以使用Python socket编程来实现收发信息。
    Python 中,我们用 socket()函数来创建套接字
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类型,服务端接收消息后,会把各自的网关地址发给对方。

3.服务端代码讲解

  1. 首先创建套接字,然后监听5678端口,来接收客户端的信息
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
  1. 假设客户端A首先给服务端发送信息是,服务端会记下客户端的A的网关地址,然后等待客户端B的连接,并记录B的网关地址,然后把客户端A的网关地址发送给客户端B,然后把客户端B的网关地址发送给客户端A。
 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
在这里插入图1
服务器记录了客户端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

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是使用Python进行UDP打洞实现P2P通信的参考代码: ```python import socket import threading # 定义本机IP和端口号 MY_IP = '192.168.1.100' MY_PORT = 10000 # 定义远程主机IP和端口号 REMOTE_IP = '192.168.1.200' REMOTE_PORT = 20000 # 创建本机UDP套接字 my_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定本机IP和端口号 my_sock.bind((MY_IP, MY_PORT)) # 创建远程主机地址 remote_addr = (REMOTE_IP, REMOTE_PORT) # 发送数据的函数 def send_data(): while True: # 从标准输入读取数据 data = input('请输入要发送的数据:') # 发送数据到远程主机 my_sock.sendto(data.encode(), remote_addr) # 接收数据的函数 def recv_data(): while True: # 接收数据 data, addr = my_sock.recvfrom(1024) # 输出接收到的数据和发送方的地址 print('接收到来自{}的数据:{}'.format(addr, data.decode())) # 向远程主机发送一条空数据包,打洞 my_sock.sendto(b'', remote_addr) # 启动发送数据和接收数据的线程 send_thread = threading.Thread(target=send_data) recv_thread = threading.Thread(target=recv_data) send_thread.start() recv_thread.start() ``` 运行该代码可以实现P2P通信。首先,代码创建了本机UDP套接字,绑定了本机IP和端口号,并定义了远程主机的IP和端口号。然后,代码创建了两个线程,一个用于发送数据,一个用于接收数据。发送数据的线程从标准输入读取数据,并将数据发送到远程主机。接收数据的线程从UDP套接字接收数据,并输出接收到的数据和发送方的地址。最后,代码向远程主机发送一条空数据包,打洞,从而实现P2P通信。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值