【框架学习】netty 实战:内网穿透 - hippowc/hippowc.github.io GitHub Wiki

内网穿透方案

可以有两种方案参考:一种是基于代理转发,一种是基于P2P打洞

代理转发原理

反向代理原理

譬如实现一个场景,将用户的请求转发到百度,首先我们得监听一个端口号,用来接收用户的请求,然后指定一个规则,把我收到的数据,转发到 www.baidu.com的80端口里,然后百度返回的数据,我们再原封不动的通过 浏览器通信的通道 返回给浏览器

P2P 原理

NAT、NAPT

NAT有两大类,基本NAT(Network Address Translator)和NAPT(Network Address/Port Translator),现在基本使用的是NAPT,NAPT分为对称和锥型NAT

锥形NAT
  • 完全圆锥型(full cone nat):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000 ,192.168.0.8可以收到任意外部主机发到1.2.3.4:62000的数据报。
  • 地址限制圆锥型(Address Restricted Cone NAT):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先给服务器C 6.7.8.9发送一个数据报后,192.168.0.8才能收到6.7.8.9发送到1.2.3.4:62000的数据报。
  • 端口限制圆锥型(Port Restricted Cone NAT):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先向外部主机地址端口6.7.8.9:8000发送一个数据报后,192.168.0.8才能收到6.7.8.9:8000发送到1.2.3.4:62000的数据报。
对称NAT

把所有来自相同内部IP地址和端口号,到特定目的IP地址和端口号的请求映射到相同的外部IP地址和端口。如果同一主机使用不同的源地址和端口对,发送的目的地址不同,则使用不同的映射。只有收到了一个IP包的外部主机才能够向该内部主机发送回一个UDP包。

只有收到了一个IP包的外部主机才能够向该内部主机发送回一个UDP包。对称的NAT不保证所有会话中的(私有地址,私有端口)和(公开IP,公开端口)之间绑定的一致性。相反,它为每个新的会话分配一个新的端口号。

对称NAT是一个请求对应一个端口,非对称NAT是多个请求对应一个端口(象锥形,所以叫Cone NAT)。

NAT检测

当一个接收客户端(Endpoint-Receiver ,简称 EP-R)需要接收文件信息时,在其向连接服务器发送文件请求的同时紧接着向检测服务器发送NAT检测请求。此处再次强调是“紧接着”,因为对于对称型NAT来说,这个操作可以直接算出其地址分配的增量

UDP打洞

P2P实现条件
  • 存在有独立IP的中间服务器用于:保存信息、并能发出建立UDP隧道的命令
  • 网关均要求为Cone NAT类型,也就是锥形NAT,对称性无法建立
UDP打洞原理
  • 双方都通过UDP与服务器通讯后,网关默认就是做了一个外网IP和端口号与你内网IP与端口号的映射,这个无需设置的,服务器也不需要知道客户的真正内网IP
  • 用户A先通过服务器知道用户B的外网地址与端口
  • 用户A向用户B的外网地址与端口发送消息
  • 在这一次发送中,用户B的网关会拒收这条消息,因为它的映射中并没有这条规则。
  • 但是用户A的网关就会增加了一条允许规则,允许接收从B发送过来的消息
  • 服务器要求用户B发送一个消息到用户A的外网IP与端口号
  • 用户B发送一条消息,这时用户A就可以接收到B的消息,而且网关B也增加了允许规则
  • 之后,由于网关A与网关B都增加了允许规则,所以A与B都可以向对方的外网IP和端口号发送消息
TCP打洞

UDP的socket允许多个socket绑定到同一个本地端口,而TCP的socket则不允许。

  • A B要连接到S,肯定首先A B双方都会在本地创建一个socket,去连接S上的socket。
  • 创建一个socket必然会绑定一个本地端口,打洞则需要A和B分别发送数据包到对方的公网IP。如果是UDP的socket,A B可以分别再创建socket,然后将socket绑定到8888,但是如果是TCP的socket,则不能再创建socket并绑定到8888了
  • 也就是说,需要不同的socket复用同一个ip和端口号,tcp不可以

tcp的打洞流程和udp的基本一样,但tcp的api决定了tcp打洞的实现过程和udp不一样

  • nat后的两个peer,A和B,A和B都bind自己listen的端口,向对方发起连接(connect),即使用相同的端口同时连接和等待连接。
  • 因为A和B发出连接的顺序有时间差,假设A的syn包到达B的nat时,B的syn包还没有发出,那么B的nat映射还没有建立,会导致A的连接请求失败(连接失败或无法连接,如果nat返回RST或者icmp差错,api上可能表现为被RST;有些nat不返回信息直接丢弃syn包(反而更好)),(应用程序发现失败时,不能关闭socket,closesocket()可能会导致NAT删除端口映射;隔一段时间(1-2s)后未连接还要继续尝试)
  • 但后发B的syn包在到达A的nat时,由于A的nat已经建立的映射关系,B的syn包会通过A的nat,被nat转给A的listen端口,从而进去三次握手,完成tcp连接。

实践问题

客户端Cannot assign requested address

报错表象指的是,ip绑定不了,或者端口被占用。我遇到的问题就是,我的端口号配置错了,很傻的一个问题。不过这个不是什么诡异的问题,就端口或者ip的问题

sync的作用
bootstrap.connect().sync(); //直到连接返回,才会退出当前线程
future.channel().closeFuture().sync() //直到channel关闭,才会退出当前线程
Unpooled.copiedBuffer

实际是创建了一个非池化的堆内存,并把字节复制进去,可以不用关心内存释放问题,其实不使用这个ByteBuf直接发送字节或者字符串也是没问题的

netty发送消息--writeAndFlush

ctx.channel().writeAndFlush 将从 Pipeline 的尾部开始往前找 OutboundHandler 。 ctx.writeAndFlush 会从当前 handler 往前找 OutboundHandler。

ctx.fireChannelRead

需要显示调用fire方法才能够触发pipeline继续向下执行