Skip to content

基于TCP和Nat1的p2p实现

此处讨论全锥形nat 一个使用socket实现的p2p客户端和服务端 通过tcp打洞实现外网client访问内网client的 demo

端口复用 ​

当两个应用进程都需要占用8080端口时使用的技术,只需要调用一行api即可。

c++
tcp_sock.set_option(boost::asio::ip::tcp::socket::reuse_address(true));

首先随便启动一个进程 ​

该进程监听在8080端口 或则说占用了8080端口 image

再启动一个没有设置端口复用的进程 ​

可以看到,当它尝试绑定本机8080端口,就会出现10048代码 image

启动一个设置了端口复用的进程 ​

可以顺利bind本机8080端口,同时8080端口仍然被另一个进程listen中 image

端口复用注意

当你需要使用某个端口而该端口又被其他应用程序使用时,你可以使用端口复用。

但是要注意,由于一个tcp连接是由四元组(源ip,目ip,源prot,目port)确定的。

所以你使用端口复用时不能使两个及以上tcp连接的四元组相同。

p2p代码 ​

python
import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client_socket.bind(('0.0.0.0', 8081))
client_socket.connect(('192.168.1.8', 11844))
while True:
    message = str(input("send:"))
    print(message)
    client_socket.send(message.encode())

client_socket.close()
python
import socket
import select

# 外网云服务器 

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8080))
server_socket.listen(1)


sock_list = [server_socket]
while True:
    readable, writable, exceptional = select.select(sock_list,[],[],0)

    for client_sock in readable:
        if client_sock is server_socket:
            client_socket, client_address = server_socket.accept()
            print("client connect: ", client_address)
            sock_list.append(client_socket)
        else:
            data = client_socket.recv(1024)
            
            print(str(client_socket.getsockname())+": ", data.decode())
            
            client_socket.send("server reply".encode())
                

for soc in sock_list:
    soc.close()
python
import socket
import select



client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client_socket.bind(('0.0.0.0', 8082))
client_socket.connect(('192.168.1.8', 8080))
client_socket.send("hello".encode())



server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 8082))

server_socket.listen(1)
print(str(server_socket.getsockname())+"listen...")

sock_list = [server_socket]
while True:
    readable, writable, exceptional = select.select(sock_list,[],[],0)

    for client_sock in readable:
        if client_sock is server_socket:
            client_socket, client_address = server_socket.accept()
            print("client connect: ", client_address)
            sock_list.append(client_socket)
        else:
            data = client_socket.recv(1024)
            
            print(str(client_socket.getpeername())+": ", data.decode())
            
            client_socket.send("server reply".encode())

nat的类型 ​

基于UDP的P2P应用需要考虑NAT的类型,因为不同的NAT组合的穿透的方式并不一致,有的能通, 有的不能通。

一般来讲, NAT可以分为四种类型,分别是:

1, 全锥型(Full Cone)

2, 受限锥型(Restricted Cone), 或者说是IP受限锥型

3, 端口受限锥型(Port Restricted Cone), 或者说是IP + PORT受限锥型

4, 对称型(Symmetric)

其中1,2,3属于同一种类型,都是锥型,区别只是路由器的不同的安全策略。

还有些NAT不属于这四种中的任何一种,就不在本文的讨论范围了。

为什么有四种类型的NAT

NAT缓解了IPV4地址不够用的问题,同时也也带了限制,那就是NAT外部的主机无法主动跟位于NAT内部的主机通信,NAT内部主机想要通信,必须主动和公网的一个IP通信,路由器负责建立一个映射关系,从而实现数据的转发, 这就是NAT的工作原理。

假定

公网server1 ip是1.1.1.1, 监听端口是1111

公网server2 ip是2.2.2.2, 监听端口是2222

NAT router ip是8.8.8.8

NAT内部client是192.168.0.3

client发送数据的时候,不管是tcp还是udp必须本地绑定一个端口,一般来讲,这个过程都是自动的。

假定client(192.168.0.3, 100)给 server(1.1.1.1, 1111)发送报文,报文到达路由器,路由器在自己的公网ip上开辟一个端口800,从而建立了一个隐射关系(8.8.8.8, 800)<--->(192.168.0.3, 100), 建立映射关系后,所以(192.168.0.3, 100)和(1.1.1.1, 1111)之间的报文都通过这个映射关系进行转发。

NAT之间主要的区别分两种情况讨论

1:

client(192.168.0.3, 100)和server(1.1.1.1, 1111)在路由器上建立好映射关系后,如果client(192.168.0.3, 100)又给(2.2.2.2, 2222)发送数据,路由器该怎么处理呢?

1, 复用旧的映射关系(8.8.8.8, 800)<--->(192.168.0.3, 100)和(2.2.2.2, 2222)通信, 这就是锥型(Cone) NAT

2, 创建新的映射关系(8.8.8.8, 801)<--->(192.168.0.3, 100)和(2.2.2.2, 2222)通信, 这就是对称型NAT

注: (8.8.8.8, 801)只是举例,到底用什么端口取决于路由器的端口管理策略,总之是另外的一个端口,有的路由器有多个公网IP,不同的IP也会参与到这个映射关系中。

2:

client(192.168.0.3, 100)和server(1.1.1.1, 1111)在路由器上建立好映射关系后,如果这个时候路由器(8.8.8.8)在800端口上收到从另外一台server(2.2.2.2, 2222)发来的数据,是不是应该转发给(192.168.0.3, 100)呢?

有四种情况:

1, 无条件转发给(192.168.0.3, 100), 这就是全锥型(Full Cone)NAT。

2, 如果(192.168.0.3, 100)之前给(2.2.2.2)发送过数据,则转发, 这就是受限锥型(Restricted Cone)。

3, 如果(192.168.0.3, 100)之前给(2.2.2.2, 2222)发送过数据,则转发, 这就是端口受限锥型(Port Restricted Cone)。

4, 丢弃报文,拒绝转发, 这就是对称型NAT。

从上面也描述也可以看出,安全性系数, 对称型 > 端口受限锥型 > 受限锥型 > 全锥型

学 习 记 录