如何进行 NAT 穿透:技术原理和企业实践
公众号关注 「奇妙的 Linux 世界」
设为「星标」,每天带你玩转 Linux !
译者序
本文翻译自 2020 年的一篇英文博客:How NAT traversal works[1]。
设想这样一个问题:在北京和上海各有一台局域网的机器(例如一台是家里的台式机,一 台是连接到星巴克 WiFi 的笔记本),二者都是私网 IP 地址,但可以访问公网, 如何让这两台机器通信呢?
既然二者都能访问公网,那最简单的方式当然是在公网上架设一个中继服务器:两台机器分别连接到中继服务,后者完成双向转发。这种方式显然有很大的性能开销,而 且中继服务器很容易成为瓶颈。
有没有办法不用中继,让两台机器直接通信呢?
如果有一定的网络和协议基础,就会明白这事儿是可能的。Tailscale 的这篇史诗级长文由浅入深地展示了这种“可能”,如果完全实现本文所 介绍的技术,你将得到一个企业级的 NAT/防火墙穿透工具。此外,如作者所说,去中心化软件领域中的许多有趣想法,简化之后其实都变成了 跨过公网(互联网)实现端到端直连 这一问题,因此本文的意义并不仅限于 NAT 穿透本身。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
在前一篇文章 How Tailscale Works[2] 中, 我们已经用较长篇幅介绍了 Tailscale 是如何工作的。但其中并没有详细描述我们是 如何穿透 NAT 设备,从而实现终端设备直连的 —— 不管这些终端之间 有什么设备(防火墙、NAT 等),以及有多少设备。本文试图补足这一内容。
1 引言
1.1 背景:IPv4 地址短缺,引入 NAT
全球 IPv4 地址早已不够用,因此人们发明了 NAT(网络地址转换)来缓解这个问题。
简单来说,大部分机器都使用私有 IP 地址,如果它们需要访问公网服务,那么,
出向流量:需要经过一台 NAT 设备,它会对流量进行 SNAT,将私有 srcIP+Port 转 换成 NAT 设备的公网 IP+Port(这样应答包才能回来),然后再将包发出去;
应答流量(入向):到达 NAT 设备后进行相反的转换,然后再转发给客户端。
整个过程对双方透明。
更多关于 NAT 的内容,可参考 (译) NAT - 网络地址转换(2016)[3]。译注。
以上是本文所讨论问题的基本背景。
1.2 需求:两台经过 NAT 的机器建立点对点连接
在以上所描述的 NAT 背景下,我们从最简单的问题开始:如何在两台经过 NAT 的机器之间建立 点对点连接(直连)。如下图所示:
直接用机器的 IP 互连显然是不行的,因为它们都是私有 IP(例如 192.168.1.x
)。在 Tailscale 中,我们会建立一个 WireGuard® 隧道 来解决这个问题 —— 但这并不是太重要,因为我们将过去几代人努力都整合到了一个工具集, 这些技术广泛适用于各种场景。例如,
WebRTC[4] 使用这些技术在浏览器之间完成 peer-to-peer 语音、视频和数据传输,
VoIP 电话和一些视频游戏也使用类似机制,虽然不是所有情况下都很成功。
接下来,本文将在一般意义上讨论这些技术,并在合适的地方拿 Tailscale 和其他一些东西作为例子。
1.3 方案:NAT 穿透
1.3.1 两个必备前提:UDP + 能直接控制 socket
如果想设计自己的协议来实现 NAT 穿透,那必须满足以下两个条件:
-
协议应该基于 UDP。
理论上用 TCP 也能实现,但它会给本已相当复杂的问题再增加一层复杂性, 甚至还需要定制化内核 —— 取决于你想实现到什么程度。本文接下来都将关注在 UDP 上。
如果考虑 TCP 是想在 NAT 穿透时获得面向流的连接( stream-oriented connection),可以考虑用 QUIC 来替代,它构 建在 UDP 之上,因此我们能将关注点放在 UDP NAT 穿透,而仍然能获得一个 很好的流协议(stream protocol)。
-
对收发包的 socket 有直接控制权。
例如,从经验上来说,无法基于某个现有的网络库实现 NAT 穿透,因为我们 必须在使用的“主要”协议之外,发送和接收额外的数据包。
某些协议(例如 WebRTC)将 NAT 穿透与其他部分紧密集成。但如果你在构建自己的协议, 建议将 NAT 穿透作为一个独立实体,与主协议并行运行,二者仅 仅是共享 socket 的关系,如下图所示,这将带来很大帮助:
1.3.2 保底方式:中继
在某些场景中,直接访问 socket 这一条件可能很难满足。
退而求其次的一个方式是设置一个 local proxy(本地代理),主协议与这个 proxy 通信 ,后者来完成 NAT 穿透,将包中继(relay)给对端。这种方式增加了一个额外的间接层 ,但好处是:
仍然能获得 NAT 穿透,
不需要对已有的应用程序做任何改动。
1.4 挑战:有状态防火墙和 NAT 设备
有了以上铺垫,下面就从最基本的原则开始,一步步看如何实现一个企业级的 NAT 穿透方案。
我们的目标是:在两个设备之间通过 UDP 实现双向通信, 有了这个基础,上层的其他协议(WireGuard, QUIC, WebRTC 等)就能做一些更酷的事情。
但即便这个看似最基本的功能,在实现上也要解决两个障碍:
有状态防火墙
NAT 设备
2 穿透防火墙
有状态防火墙是以上两个问题中相对比较容易解决的。实际上,大部分 NAT 设备都自带了一个有状态防火墙, 因此要解决第二个问题,必须先解决有第一个问题。
有状态防火墙具体有很多种类型,有些你可能见过:
Windows Defender firewall
Ubuntu’s ufw (using iptables/nftables)
BSD/macOS
pf
AWS Security Groups(安全组)
2.1 有状态防火墙
2.1.1 默认行为(策略)
以上防火墙的配置都是很灵活的,但大部分配置默认都是如下行为:
允许所有出向连接(allows all “outbound” connections)
禁止所有入向连接(blocks all “inbound” connections)
可能有少量例外规则,例如 allowing inbound SSH。
2.1.2 如何区分入向和出向包
连接(connection)和方向(direction)都是协议设计者头脑中的概念,到了 物理传输层,每个连接都是双向的;允许所有的包双向传输。那防火墙是如何区分哪些是入向包、哪些是出向包的呢?这就要回到**“有状态”(stateful)**这三个字了:有状态防火墙会记录它 看到的每个包,当收到下一个包时,会利用这些信息(状态)来判断应该做什么。
对 UDP 来说,规则很简单:如果防火墙之前看到过一个出向包(outbound),就会允许 相应的入向包(inbound)通过,以下图为例:
笔记本电脑中自带了一个防火墙,当该防火墙看到从这台机器出去的 2.2.2.2:1234 -> 5.5.5.5:5678
包时,就会记录一下:5.5.5.5:5678 -> 2.2.2.2:1234
入向包应该放行。这里的逻辑是:我们信任的世界(即笔记本)想主动与 5.5.5.5:5678
通信,因此应该放行(allow)其回包路径。
某些非常宽松的防火墙只要看到有从
2.2.2.2:1234
出去的包,就 会允许所有从外部进入2.2.2.2:1234
的流量。这种防火墙对我们的 NAT 穿透来说非 常友好,但已经越来越少见了。
2.2 防火墙朝向(face-off)与穿透方案
2.2.1 防火墙朝向相同
场景特点:服务端 IP 可直接访问
在 NAT 穿透场景中,以上默认规则对 UDP 流量的影响不大 —— 只要路径上所有防火墙的“朝向”是一样的。一般来说,从内网访问公网上的某个服务器都属于这种情况。
我们唯一的要求是:连接必须是由防火墙后面的机器发起的。这是因为 在它主动和别人通信之前,没人能主动和它通信,如下图所示:
穿透方案:客户端直连服务端,或 hub-and-spoke 拓扑
但上图是假设了通信双方中,其中一端**(服务端)是能直接访问到的**。在 VPN 场景中,这就形成了所谓的 hub-and-spoke 拓扑:中心的 hub 没有任何防火墙策略,谁都能访问到;防火墙后面的 spokes 连接到 hub。如下图所示:
2.2.2 防火墙朝向不同
场景特点:服务端 IP 不可直接访问
但如果两个“客户端”想直连,以上方式就不行了,此时两边的防火墙相向而立,如下图所示:
根据前面的讨论,这种情况意味着:两边要同时发起连接请求,但也意味着 两边都无法发起有效请求,因为对方先发起请求才能在它的防火墙上打开一条缝让我们进去!如何破解这个问题呢?一种方式是让用户重新配置一边或两边的防火墙,打开一个端口, 允许对方的流量进来。
这显然对用户不友好,在像 Tailscale 这样的 mesh 网络中的扩展性也不好,在 mesh 网络中,我们假设对端会以一定的粒度在公网上移动。
此外,在很多情况下用户也没有防火墙的控制权限:例如在咖啡馆或机场中,连接的路 由器是不受你控制的(否则你可能就有麻烦了)。
因此,我们需要寻找一种不用重新配置防火墙的方式。
穿透方案:两边同时主动建连,在本地防火墙为对方打开一个洞
解决的思路还是先重新审视前面提到的有状态防火墙规则:
对于 UDP,其规则(逻辑)是:包必须先出去才能进来(packets must flow out before packets can flow back in)。
注意,这里除了要满足包的 IP 和端口要匹配这一条件之外,并没有要求包必须是相关的(related)。换句话说,只要某些包带着正确的源和目的地址出去了,任何看起来像是响应的包都会被防火墙放进来 —— 即使对端根本没收到你发出去的包。
因此,要穿透这些有状态防火墙,我们只需要共享一些信息:让两端提前知道对方使用的 ip:port:
手动静态配置是一种方式,但显然扩展性不好;
我们开发了一个 coordination server[5], 以灵活、安全的方式来同步
ip:port
信息。
有了对方的 ip:port
信息之后,两端开始给对方发送 UDP 包。在这个过程中,我们预 料到某些包将会被丢弃。因此,双方必须要接受某些包会丢失的事实, 因此如果是重要信息,你必须自己准备好重传。对 UDP 来说丢包是可接受的,但这里尤其需要接受。
来看一下具体建连(穿透)过程:
-
如图所示,笔记本出去的第一包,
2.2.2.2:1234 -> 7.7.7.7:5678
,穿过 Windows Defender 防火墙进入到公网。对方的防火墙会将这个包拦截掉,因为它没有
7.7.7.7:5678 -> 2.2.2.2:1234
的流量记录。但另一方面,Windows Defender 此时已经记录了出向连接,因此会允许7.7.7.7:5678 -> 2.2.2.2:1234
的应答包进来。 -
接着,第一个
7.7.7.7:5678 -> 2.2.2.2:1234
穿过它自己的防火墙到达公网。到达客户端侧时,Windows Defender 认为这是刚才出向包的应答包,因此就放行它进入了! 此外,右侧的防火墙此时也记录了:
2.2.2.2:1234 -> 7.7.7.7:5678
的包应该放行。 -
笔记本收到服务器发来的包之后,发送一个包作为应答。这个包穿过 Windows Defender 防火墙 和服务端防火墙(因为这是对服务端发送的包的应答包),达到服务端。
成功!这样我们就建立了一个穿透两个相向防火墙的双向通信连接。而初看之下,这项任务似乎是不可能完成的。
2.3 关于穿透防火墙的一些思考
穿透防火墙并非永远这么轻松,有时会受一些第三方系统的间接影响,需要仔细处理。那穿透防火墙需要注意什么呢?重要的一点是:通信双方必须几乎同时发起通信, 这样才能在路径上的防火墙打开一条缝,而且两端还都是活着的。
2.3.1 双向主动建连:旁路信道
如何实现“同时”呢?一种方式是两端不断重试,但显然这种方式很浪费资源。假如双方都 知道何时开始建连就好了。
这听上去是鸡生蛋蛋生鸡的问题了:双方想要通信,必须先提前通个信。
-
但实际上,我们可以通过旁路信道(side channel)来达到这个目的 ,并且这个旁路信道并不需要很 fancy:它可以有几秒钟的延迟、只需要传送几 KB 的 信息,因此即使是一个配置非常低的虚拟机,也能为几千台机器提供这样的旁路通信服务。
在遥远的过去,我曾用 XMPP 聊天消息作为旁路,效果非常不错。
另一个例子是 WebRTC,它需要你提供一个自己的“信令信道”(signalling channel, 这个词也暗示了 WebRTC 的 IP telephony ancestry),并将其配置到 WebRTC API。
在 Tailscale,我们的协调服务器(coordination server)和 DERP (Detour Encrypted Routing Protocol) 服务器集群是我们的旁路信道。
2.3.2 非活跃连接被防火墙清理
有状态防火墙内存通常比较有限,因此会定期清理不活跃的连接(UDP 常见的是 30s), 因此要保持连接 alive 的话需要定期通信,否则就会被防火墙关闭,为避免这个问题, 我们,
要么定期向对方发包来 keepalive,
要么有某种带外方式来按需重建连接。
2.3.3 问题都解决了?不,挑战刚刚开始
对于防火墙穿透来说, 我们并不需要关心路径上有几堵墙 —— 只要它们是有状态防火墙且允许出 向连接,这种同时发包(simultaneous transmission)机制就能穿透任意多层防火墙。这一点对我们来说非常友好,因为只需要实现一个逻辑,然后能适用于任何地方了。
…对吗?
其实,不完全对。这个机制有效的前提是:我们能提前知道对方的 ip:port。而这就涉及到了我们今天的主题:NAT,它会使前面我们刚获得的一点满足感顿时消失。
下面,进入本文正题。
3 NAT 的本质
3.1 NAT 设备与有状态防火墙
可以认为 NAT 设备是一个增强版的有状态防火墙,虽然它的增强功能 对于本文场景来说并不受欢迎:除了前面提到的有状态拦截/放行功能之外,它们还会在数据包经过时修改这些包。
3.2 NAT 穿透与 SNAT/DNAT
具体来说,NAT 设备能完成某种类型的网络地址转换,例如,替换源或目的 IP 地址或端口。
讨论连接问题和 NAT 穿透问题时,我们只会受 source NAT —— SNAT 的影响。
DNAT 不会影响 NAT 穿透。
3.3 SNAT 的意义:解决 IPv4 地址短缺问题
SNAT 最常见的使用场景是将很多设备连接到公网,而只使用少数几个公网 IP。例如对于消费级路由器,会将所有设备的(私有) IP 地址映射为单个连接到公网的 IP 地址。
这种方式存在的意义是:我们有远多于可用公网 IP 数量的设备需要连接到公网,(至少 对 IPv4 来说如此,IPv6 的情况后面会讨论)。NAT 使多个设备能共享同一 IP 地址,因 此即使面临 IPv4 地址短缺的问题,我们仍然能不断扩张互联网的规模。
3.4 SNAT 过程:以家用路由器为例
假设你的笔记本连接到家里的 WiFi,下面看一下它连接到公网某个服务器时的情形:
-
笔记本发送 UDP packet
192.168.0.20:1234 -> 7.7.7.7:5678
。这一步就好像笔记本有一个公网 IP 一样,但源地址
192.168.0.20
是私有地址, 只能出现在私有网络,公网不认,收到这样的包时它不知道如何应答。 -
家用路由器出场,执行 SNAT。
包经过路由器时,路由器发现这是一个它没有见过的新会话(session)。它知道
192.168.0.20
是私有 IP,公网无法给这样的地址回包,但它有办法解决:在它自己的公网 IP 上挑一个可用的 UDP 端口,例如
2.2.2.2:4242
,然后创建一个 NAT mapping:
192.168.0.20:1234
<-->
2.2.2.2:4242
,然后将包发到公网,此时源地址变成了
2.2.2.2:4242
而不是原来的192.168.0.20:1234
。因此服务端看到的是转换之后地址,接下来,每个能匹配到这条映射规则的包,都会被路由器改写 IP 和 端口。
反向路径是类似的,路由器会执行相反的地址转换,将
2.2.2.2:4242
变回192.168.0.20:1234
。对于笔记本来说,它根本感知不知道这正反两次变换过程。
这里是拿家用路由器作为例子,但办公网的原理是一样的。不同之处在 于,办公网的 NAT 可能有多台设备组成(高可用、容量等目的),而且它们有不止一个公 网 IP 地址可用,因此在选择可用的公网 ip:port
来做映射时,选择空间更大,能支持 更多客户端。
3.5 SNAT 给穿透带来的挑战
现在我们遇到了与前面有状态防火墙类似的情况,但这次是 NAT 设备:通信双方 不知道对方的 ip:port 是什么,因此无法主动建连,如下图所示:
但这次比有状态防火墙更糟糕,严格来说,在双方发包之前,根本无法确定(自己及对方的)ip:port 信息,因为 只有出向包经过路由器之后才会产生 NAT mapping(即,可以被对方连接的 ip:port
信息)。
因此我们又回到了与防火墙遇到的问题,并且情况更糟糕:双方都需要主动和对 方建连,但又不知道对方的公网地址是多少,只有当对方先说话之后,我们才能拿到它的地址信息。
如何破解以上死锁呢?这就轮到 STUN[6] 登场了。
4 穿透 “NAT+防火墙”:STUN (Session Traversal Utilities for NAT) 协议
STUN[7] 既是一些对 NAT 设备行为的详细研究,也是一种协助 NAT 穿透的协议。本文主要关注 STUN 协议。
4.1 STUN 原理
STUN 基于一个简单的观察:从一个会被 NAT 的客户端访问公网服务器时, 服务器看到的是 NAT 设备的公网 ip:port 地址,而非该 客户端的局域网 ip:port 地址。
也就是说,服务器能告诉客户端它看到的客户端的 ip:port 是什么。因此,只要将这个信息以某种方式告诉通信对端(peer),后者就知道该和哪个地址建连了!这样就又简化为前面的防火墙穿透问题了。
本质上这就是 STUN 协议的工作原理,如下图所示:
笔记本向 STUN 服务器发送一个请求:“从你的角度看,我的地址什么?”
STUN 服务器返回一个响应:“我看到你的 UDP 包是从这个地址来的:
ip:port
”。
The STUN protocol has a bunch more stuff in it — there’s a way of obfuscating the
ip:port
in the response to stop really broken NATs from mangling the packet’s payload, and a whole authentication mechanism that only really gets used by TURN and ICE, sibling protocols to STUN that we’ll talk about in a bit. We can ignore all of that stuff for address discovery.
4.2 为什么 NAT 穿透逻辑和主协议要共享同一个 socket
理解了 STUN 原理,也就能理解为什么我们在文章开头说,如果 要实现自己的 NAT 穿透逻辑和主协议,就必须让二者共享同一个 socket:
每个 socket 在 NAT 设备上都对应一个映射关系(私网地址 -> 公网地址),
STUN 服务器只是辅助穿透的基础设施,
与 STUN 服务器通信之后,在 NAT 及防火墙设备上打开了一个连接,允许入向包进来(回忆前面内容, 只要目的地址对,UDP 包就能进来,不管这些包是不是从 STUN 服务器来的),
因此,接下来只要将这个地址告诉我们的通信对端(peer),让它往这个地址发包,就能实现穿透了。
4.3 STUN 的问题:不能穿透所有 NAT 设备(例如企业级 NAT 网关)
有了 STUN,我们的穿透目的似乎已经实现了:每台机器都通过 STUN 来获取自己的私网 socket 对应的公网 ip:port
,然后把这个信息告诉对端,然后两端 同时发起穿透防火墙的尝试,后面的过程就和上一节介绍的防火墙穿透一样了,对吗?
答案是:看情况。某些情况下确实如此,但有些情况下却不行。通常来说,
对于大部分家用路由器场景,这种方式是没问题的;
但对于一些企业级 NAT 网关来说,这种方式无法奏效。
NAT 设备的说明书上越强调它的安全性,STUN 方式失败的可能性就越高。(但注意,从实际意义上来说, NAT 设备在任何方面都并不会增强网络的安全性,但这不是本文重点,因此不展开。)
4.4 重新审视 STUN 的前提
再次审视前面关于 STUN 的假设:当 STUN 服务器告诉客户端在公网看来它的地址是 2.2.2.2:4242
时,那所有目的地址是 2.2.2.2:4242
的包就都能穿透防火墙到达该客户端。
这也正是问题所在:这一点并不总是成立。
-
某些 NAT 设备的行为与我们假设的一致,它们的有状态防火墙组件只要看到有客户端自己 发起的出向包,就会允许相应的入向包进入;因此只要利用 STUN 功能,再加上两端同时 发起防火墙穿透,就能把连接打通;
in theory, there are also NAT devices that are super relaxed, and don’t ship with stateful firewall stuff at all. In those, you don’t even need simultaneous transmission, the STUN request gives you an internet
ip:port
that anyone can connect to with no further ceremony. If such devices do still exist, they’re increasingly rare. -
另外一些 NAT 设备就要困难很多了,它会针对每个目的地址来生成一条相应的映射关系。在这样的设备上,如果我们用相同的 socket 来分别发送数据包到
5.5.5.5:1234
and7.7.7.7:2345
,我们就会得到2.2.2.2
上的两个不同的端口,每个目的地址对应一个。如果反向包的端口用的不对,包就无法通过防火墙。如下图所示:
5 中场补课:NAT 正式术语
知道 NAT 设备的行为并不是完全一样之后,我们来引入一些正式术语。
5.1 早期术语
如果之前接触过 NAT 穿透,可能会听说过下面这些名词:
“Full Cone”
“Restricted Cone”
“Port-Restricted Cone”
“Symmetric” NATs
这些都是 NAT 穿透领域的早期术语。
但其实这些术语相当让人困惑。我每次都要 查一下 Restricted Cone NAT 是什么意思。从实际经验来看,我并不是唯一对此感到困惑的人。例如,如今互联网上将 “easy” NAT 归类为 Full Cone,而实际上它们更应该归类为 Port-Restricted Cone。
5.2 近期研究与新术语
最近的一些研究和 RFC 已经提出了一些更准确的术语。
首先,它们明确了如下事实:NAT 设备的行为差异表现在多个维度, 而并非只有早期研究中所说的 “cone” 这一个维度,因此基于 “cone” 来划分类别并不是很有帮助。
其次,新研究和新术语能更准确地描述 NAT 在做什么。
前面提到的所谓 "easy" 和 "hard" NAT,只在一个维度有不同:NAT 映射是否考虑到目的地址信息。RFC 4787[8] 中,
-
将 easy NAT 及其变种称为 “Endpoint-Independent Mapping” (EIM,终点无关的映射)
但是,从**“命名很难”**这一程序员界的伟大传统来说,EIM 这个词其实 也并不是 100% 准确,因为这种 NAT 仍然依赖 endpoint,只不过依赖的是源 endpoint:每个 source
ip:port
对应一个映射 —— 否则你的包就会和别人的包混在一起,导致混乱。严格来说,EIM 应该称为 “Destination Endpoint Independent Mapping” (DEIM?), 但这个名字太拗口了,而且按照惯例,Endpoint 永远指的是 Destination Endpoint。
-
将 hard NAT 以及变种称为 “Endpoint-Dependent Mapping”(EDM,终点相关的映射) 。
EDM 中还有一个子类型,依据是只根据 dst_ip 做映射,还是根据 dst_ip + dst_port 做映射。对于 NAT 穿透来说,这种区分对来说是一样的:它们都会导致 STUN 方式不可用。
5.3 老的 cone 类型划分
你可能会有疑问:根据是否依赖 endpoint 这一条件,只能组合出两种可能,那为什么传 统分类中会有四种 cone 类型呢?答案是 cone 包含了两个正交维度的 NAT 行为:
NAT 映射行为:前面已经介绍过了,
有状态防火墙行为:与前者类似,也是分为与 endpoint 相关还是无关两种类型。
因此最终组合如下:
NAT Cone Types
Endpoint 无关 NAT mapping | Endpoint 相关 NAT mapping (all types) | |
---|---|---|
Endpoint 无关防火墙 | Full Cone NAT | N/A* |
Endpoint 相关防火墙 (dst. IP only) | Restricted Cone NAT | N/A* |
Endpoint 相关防火墙 (dst. IP+port) | Port-Restricted Cone NAT | Symmetric NAT |
分解到这种程度之后就可以看出,cone 类型对 NAT 穿透场景来说并没有什么意义。我们关心的只有一点:是否是 Symmetric —— 换句话说,一个 NAT 设备是 EIM 还是 EDM 类型的。
5.4 针对 NAT 穿透场景:简化 NAT 分类
以上讨论可知,虽然理解防火墙的具体行为很重要,但对于编写 NAT 穿透代码来说,这一点并不重要。我们的两端同时发包方式(simultaneous transmission trick)能 有效穿透以上三种类型的防火墙。在真实场景中, 我们主要在处理的是 IP-and-port endpoint-dependent 防火墙。
因此,对于实际 NAT 穿透实现,我们可以将以上分类简化成:
Endpoint-Independent NAT mapping | Endpoint-Dependent NAT mapping (dst. IP only) | |
---|---|---|
Firewall is yes | Easy NAT | Hard NAT |
5.5 更多 NAT 规范(RFC)
想了解更多新的 NAT 术语,可参考
RFC 4787[9] (NAT Behavioral Requirements for UDP)
RFC 5382[10] (for TCP)
RFC 5508[11] (for ICMP)
如果自己实现 NAT,那应该(should)遵循这些 RFC 的规范,这样才能使你的 NAT 行为符合业界惯例,与其他厂商的设备或软件良好兼容。
6 穿透 NAT+防火墙:STUN 不可用时,fallback 到中继模式
6.1 问题回顾与保底方式(中继)
补完基础知识(尤其是定义了什么是 hard NAT)之后,回到我们的 NAT 穿透主题。
第 1~4 节已经解决了 STUN 和防火墙穿透的问题,
但 hard NAT 对我们来说是个大问题,只要路径上出现一个这种设备,前面的方案就行不通了。
准备放弃了吗?这才进入 NAT 真正有挑战的部分:如果已经试过了前面介绍的所有方式 仍然不能穿透,我们该怎么办呢?
实际上,确实有很多 NAT 实现在这种情况下都会选择放弃,向用户报一个**“无法连接”**之类的错误。
但对我们来说,这么快就放弃显然是不可接受的 —— 解决不了连通性问题,Tailscale 就没有存在的意义。
我们的保底解决方式是:创建一个中继连接(relay)实现双方的无障碍地通信。但是,中继方式性能不是很差吗?这要看具体情况:
如果能直连,那显然没必要用中继方式;
但如果无法直连,而中继路径又非常接近双方直连的真实路径,并且带宽足够大,那中 继方式并不会明显降低通信质量。延迟肯定会增加一点,带宽会占用一些,但 相比完全连接不上,还是更能让用户接受的。
不过要注意:我们只有在无法直连时才会选择中继方式。实际场景中,
对于大部分网络,我们都能通过前面介绍的方式实现直连,
剩下的长尾用中继方式来解决,并不算一个很糟的方式。
此外,某些网络会阻止 NAT 穿透,其影响比这种 hard NAT 大多了。例如,我们观察到 UC Berkeley guest WiFi 禁止除 DNS 流量之外的所有 outbound UDP 流量。不管用什么 NAT 黑科技,都无法绕过这个拦截。因此我们终归还是需要一些可靠的 fallback 机制。
6.2 中继协议:TURN、DERP
有多种中继实现方式。
-
TURN (Traversal Using Relays around NAT):经典方式,核心理念是
Tailscale 并不使用 TURN。这种协议用起来并不是很好,而且与 STUN 不同, 它没有真正的交互性,因为互联网上并没有公开的 TURN 服务器。
用户(人)先去公网上的 TURN 服务器认证,成功后后者会告诉你:“我已经为你分配了 ip:port,接下来将为你中继流量”,
然后将这个 ip:port 地址告诉对方,让它去连接这个地址,接下去就是非常简单的客户端/服务器通信模型了。
-
DERP (Detoured Encrypted Routing Protocol)
这是我们创建的一个协议,DERP[12],
前面也简单提到过,DERP 既是我们在 NAT 穿透失败时的保底通信方式(此时的角色 与 TURN 类似),也是在其他一些场景下帮助我们完成 NAT 穿透的旁路信道。换句话说,它既是我们的保底方式,也是有更好的穿透链路时,帮助我们进行连接升 级(upgrade to a peer-to-peer connection)的基础设施。
它是一个通用目的包中继协议,运行在 HTTP 之上,而大部分网络都是允许 HTTP 通信的。
它根据目的公钥(destination’s public key)来中继加密的流量(encrypted payloads)。
6.3 小结
有了“中继”这种保底方式之后,我们穿透的成功率大大增加了。如果此时不再阅读本文接下来的内容,而是把上面介绍的穿透方式都实现了,我预计:
90% 的情况下,你都能实现直连穿透;
剩下的 10% 里,用中继方式能穿透一些(some);
这已经算是一个“足够好”的穿透实现了。
7 穿透 NAT+防火墙:企业级改进
如果你并不满足于“足够好”,那我们可以做的事情还有很多!
本节将介绍一些五花八门的 tricks,在某些特殊场景下会帮到我们。单独使用这项技术都 无法解决 NAT 穿透问题,但将它们巧妙地组合起来,我们能更加接近 100% 的穿透成功率。
7.1 穿透 hard NAT:暴力端口扫描
回忆 hard NAT 中遇到的问题,如下图所示,关键问题是:easy NAT 不知道该往 hard NAT 方的哪个 ip:port
发包。
但必须要往正确的 ip:port
发包,才能穿透防火墙,实现双向互通。怎么办呢?
-
首先,我们能知道 hard NAT 的一些
ip:port
,因为我们有 STUN 服务器。这里先假设我们获得的这些 IP 地址都是正确的(这一点并不总是成立,但这里先这么假 设。而实际上,大部分情况下这一点都是成立的,如果对此有兴趣,可以参考 REQ-2 in RFC 4787[13])。
-
IP 地址确定了,剩下的就是端口了。总共有 65535 中可能,我们能遍历这个端口范围吗?
如果发包速度是 100 packets/s,那最坏情况下,需要 10 分钟来找到正确的端口。还是那句话,这虽然不是最优的,但总比连不上好。
这很像是端口扫描(事实上,确实是),实际中可能会触发对方的网络入侵检测软件。
7.2 基于生日悖论改进暴力扫描:hard side 多开端口 + easy side 随机探测
利用 birthday paradox[14] 算法, 我们能对端口扫描进行改进。
上一节的基本前提是:hard side 只打开一个端口,然后 easy side 暴力扫描 65535 个端口来寻找这个端口;
这里的改进是:在 hard size 开多个端口,例如 256 个(即同时打开 256 个 socket,目的地址都是 easy side 的
ip:port
), 然后 easy side 随机探测这边的端口。
这里省去算法的数学模型,如果你对实现干兴趣,可以看看我写的 python calculator[15]。计算过程是“经典”生日悖论的一个小变种。下面是随着 easy side random probe 次数(假设 hard size 256 个端口)的变化,两边打开的端口有重合(即通信成功)的概率:
随机探测次数 | 成功概率 |
---|---|
174 | 50% |
256 | 64% |
1024 | 98% |
2048 | 99.9% |
根据以上结果,如果还是假设 100 ports/s 这样相当温和的探测速率,那 2 秒钟就有约 50% 的成功概率。即使非常不走运,我们仍然能在 20s 时几乎 100% 穿透成功,而此时只探测了总端口空间的 4%。
非常好!虽然这种 hard NAT 给我们带来了严重的穿透延迟,但最终结果仍然是成功的。那么,如果是两个 hard NAT,我们还能处理吗?
7.3 双 hard NAT 场景
这种情况下仍然可以用前面的 多端口+随机探测 方式,但成功概率要低很多了:
每次通过一台 hard NAT 去探测对方的端口(目的端口)时,我们自己同时也生成了一个随机源端口,
这意味着我们的搜索空间变成了二维
{src port, dst port}
对,而不再是之前的一维 dst port 空间。
这里我们也不就具体计算展开,只告诉结果:仍然假设目的端打开 256 个端口,从源端发起 2048 次(20 秒), 成功的概率是:0.01%。
如果你之前学过生日悖论,就并不会对这个结果感到惊讶。理论上来说,
要达到 99.9% 的成功率,我们需要两边各进行170,000 次探测 —— 如果还是以 100 packets/sec 的速度,就需要 28 分钟。
要达到 50% 的成功率,“只”需要 54,000 packets,也就是 9 分钟。
如果不使用生日悖论方式,而且暴力穷举,需要 1.2 年时间!
对于某些应用来说,28 分钟可能仍然是一个可接受的时间。用半个小时暴力穿透 NAT 之后, 这个连接就可以一直用着 —— 除非 NAT 设备重启,那样就需要再次花半个小时穿透建个新连接。但对于 交互式应用来说,这样显然是不可接受的。
更糟糕的是,如果去看常见的办公网路由器,你会震惊于它的 active session low limit 有多么低。例如,一台 Juniper SRX 300 最多支持 64,000 active sessions。也就是说,
如果我们想创建一个成功的穿透连接,就会把它的整张 session 表打爆 (因为我们要暴力探测 65535 个端口,每次探测都是一条新连接记录)!这显然要求这台路由器能从容优雅地处理过载的情况。
这只是创建一条连接带来的影响!如果 20 台机器同时对这台路由器发起穿透呢?绝对的灾难!
至此,我们通过这种方式穿透了比之前更难一些的网络拓扑。这是一个很大的成就,因为 家用路由器一般都是 easy NAT,hard NAT 一般都是办公网路由器或云 NAT 网关。这意味着这种方式能帮我们解决
home-to-office(家->办公室)
home-to-cloud (家->云)
的场景,以及一部分
office-to-cloud (办公室->云)
cloud-to-cloud (云->办公室)
场景。
7.4 控制端口映射(port mapping)过程:UPnP/NAT-PMP/PCP 协议
如果我们能让 NAT 设备的行为简单点,不要把事情搞这么复杂,那建 立连接(穿透)就会简单很多。真有这样的好事吗?还真有,有专门的一种协议叫 端口映射协议(port mapping protocols)。通过这种协议禁用掉前面 遇到的那些乱七八糟的东西之后,我们将得到一个非常简单的“请求-响应”。
下面是三个具体的端口映射协议:
-
UPnP IGD[16] (Universal Plug’n’Play Internet Gateway Device)
最老的端口控制协议, 诞生于 1990s 晚期,因此使用了很多上世纪 90 年代的技术 (XML、SOAP、multicast HTTP over UDP —— 对,HTTP over UDP ),而且很难准确和安全地实现这个协议。但以前很多路由器都内置了 UPnP 协议, 现在仍然很多。
请求和响应:
“你好,请将我的
lan-ip:port
转发到公网(WAN)”,“好的,我已经为你分配了一个公网映射
wan-ip:port
”。
NAT-PMP
UPnP IGD 出来几年之后,Apple 推出了一个功能类似的协议,名为 NAT-PMP[17] (NAT Port Mapping Protocol)。
但与 UPnP 不同,这个协议只做端口转发,不管是在客户端还是服务端,实现起来都非常简单。
PCP
稍后一点,又出现了 NAT-PMP v2 版,并起了个新名字PCP[18] (Port Control Protocol)。
因此要更好地实现穿透,可以
先判断本地的默认网关上是否启用了 UPnP IGD, NAT-PMP and PCP,
-
如果探测发现其中任何一种协议有响应,我们就申请一个公网端口映射,
可以将这理解为一个加强版 STUN:我们不仅能发现自己的公网
ip:port
,而且能指示我们的 NAT 设备对我们的通信对端友好一些 —— 但并不是为这个端口修改或添加防火墙规则。 接下来,任何到达我们 NAT 设备的、地址是我们申请的端口的包,都会被设备转发到我们。
但我们不能假设这个协议一定可用:
本地 NAT 设备可能不支持这个协议;
设备支持但默认禁用了,或者没人知道还有这么个功能,因此从来没开过;
-
安全策略要求关闭这个特性。
这一点非常常见,因为 UPnP 协议曾曝出一些高危漏洞(后面都修复了,因此如果是较新的设备,可以安全地使用 UPnP —— 如果实现没问题)。不幸的是,某些设备的配置中,UPnP, NAT-PMP,PCP 是放在一个开关里的(可能 统称为 “UPnP” 功能),一开全开,一关全关。因此如果有人担心 UPnP 的安全性,他连另 外两个也用不了。
最后,终归来说,只要这种协议可用,就能有效地减少一次 NAT,大大方便建连过程。但接下来看一些不常见的场景。
7.5 多 NAT 协商(Negotiating numerous NATs)
目前为止,我们看到的客户端和服务端都各只有一个 NAT 设备。如果有多个 NAT 设备会 怎么样?例如下面这种拓扑:
这个例子比较简单,不会给穿透带来太大问题。包从客户端 A 经过多次 NAT 到达公网的过程,与前面分析的穿过多层有状态防火墙是一样的:
额外的这层(NAT 设备)对客户端和服务端来说都不可见,我们的穿 透技术也不关心中间到底经过了多少层设备。
真正有影响的其实只是最后一层设备,因为对端需要在这一层设备上 找到入口让包进来。
具体来说,真正有影响的是端口转发协议。
客户端使用这种协议分配端口时,为我们分配端口的是最靠近客户端的这层 NAT 设备;
而我们期望的是让最离客户端最远的那层 NAT 来分配,否则我们得到的就是一个网络中间层分配的
ip:port
,对端是用不了的;不幸的是,这几种协议都不能递归地告诉我们下一层 NAT 设备是多少 —— 虽然可以用 traceroute 之类的工具来探测网络路径,再加上 猜路上的设备是不是 NAT 设备(尝试发送 NAT 请求) —— 但这个就看运气了。
这就是为什么互联网上充斥着大量的文章说 double-NAT 有多糟糕,以 及警告用户为保持后向兼容不要使用 double-NAT。但实际上,double-NAT 对于绝大部分 互联网应用来说都是不可见的(透明的),因为大部分应用并不需要主动地做这种 NAT 穿 透。
但我也绝不是在建议你在自己的网络中设置 double-NAT。
破坏了端口映射协议之后,某些视频游戏的多人(multiplayer)模式就会无法使用,
也可能会使你的 IPv6 网络无法派上用场,后者是不用 NAT 就能双向直连的一个好方案。
但如果 double-NAT 并不是你能控制的,那除了不能用到这种端口映射协议之外,其他大部分东西都是不受影响的。
double-NAT 的故事到这里就结束了吗?—— 并没有,而且更大型的 double-NAT 场景将展现在我们面前。
7.6 运营商级 NAT 带来的问题
即使用 NAT 来解决 IPv4 地址不够的问题,地址仍然是不够用的,ISP(互联网服务提供商) 显然 无法为每个家庭都分配一个公网 IP 地址。那怎么解决这个问题呢?ISP 的做法是不够了就再嵌套一层 NAT:
家用路由器将你的客户端 SNAT 到一个 “intermediate” IP 然后发送到运营商网络,
ISP’s network 中的 NAT 设备再将这些 intermediate IPs 映射到少量的公网 IP。
后面这种 NAT 就称为“运营商级 NAT”(carrier-grade NAT,或称电信级 NAT),缩写 CGNAT。如下图所示:
CGNAT 对 NAT 穿透来说是一个大麻烦。
在此之前,办公网用户要快速实现 NAT 穿透,只需在他们的路由器上手动设置端口映射就行了。
但有了 CGNAT 之后就不管用了,因为你无法控制运营商的 CGNAT!
好消息是:这其实是 double-NAT 的一个小变种,因此前面介绍的解决方式大部分还仍然是适用的。某些东西可能会无法按预期工作,但只要肯给 ISP 交钱,这些也都能解决。除了 port mapping protocols,其他我们已经介绍的所有东西在 CGNAT 里都是适用的。
新挑战:同一 CGNAT 侧直连,STUN 不可用
但我们确实遇到了一个新挑战:如何直连两个在同一 CGNAT 但不同家用路由器中的对端呢?如下图所示:
在这种情况下,STUN 就无法正常工作了:STUN 看到的是客户端在公网(CGNAT 后面)看到的地址, 而我们想获得的是在 “middle network” 中的 ip:port
,这才是对端真正需要的地址,
解决方案:如果端口映射协议能用:一端做端口映射
怎么办呢?
如果你想到了端口映射协议,那恭喜,答对了!如果 peer 中任何一个 NAT 支持端口映射协议, 对我们就能实现穿透,因为它分配的 ip:port
正是对端所需要的信息。
这里讽刺的是:double-NAT(指 CGNAT)破坏了端口映射协议,但在这里又救了我们!当然,我们假设这些协议一定可用,因为 CGNAT ISP 倾向于在它们的家用路由器侧关闭 这些功能,已避免软件得到“错误的”结果,产生混淆。
解决方案:如果端口映射协议不能用:NAT hairpin 模式
如果不走运,NAT 上没有端口映射功能怎么办?
让我们回到基于 STUN 的技术,看会发生什么。两端在 CGNAT 的同一侧,假设 STUN 告诉我们 A 的地址是 2.2.2.2:1234
,B 的地址是 2.2.2.2:5678
。
那么接下来的问题是:如果 A 向 2.2.2.2:5678
发包会怎么样?期望的 CGNAT 行为是:
执行 A 的 NAT 映射规则,即对
2.2.2.2:1234 -> 2.2.2.2:5678
进行 SNAT。注意到目的地址
2.2.2.2:5678
匹配到的是 B 的入向 NAT 映射,因此接着对这个包执行 DNAT,将目的 IP 改成 B 的私有地址。通过 CGNAT 的 internal 接口(而不是 public 接口,对应公网)将包发给 B。
这种 NAT 行为有个专门的术语,叫 hairpinning(直译为发卡,意思 是像发卡一样,沿着一边上去,然后从另一边绕回来),
大家应该猜到的一个事实是:不是所以 NAT 都支持 hairpin 模式。实际上,大量 well-behaved NAT 设备都不支持 hairpin 模式,
因为它们都有 “只有 src_ip 是私有地址且 dst_ip 是公网地址的包才会经过我” 之类的假设。
因此对于这种目的地址不是公网、需要让路由器把包再转回内网的包,它们会直接丢弃。
这些逻辑甚至是直接实现在路由芯片中的,因此除非升级硬件,否则单靠软件编程无法改变这种行为。
Hairpin 是所有 NAT 设备的特性(支持或不支持),并不是 CGNAT 独有的。
-
在大部分情况下,这个特性对我们的 NAT 穿透目的来说都是无所谓的,因为我们期望中 两个 LAN NAT 设备会直接通信,不会再向上绕到它们的默认网关 CGNAT 来解决这个问题。
Hairpin 特性可有可无这件事有点遗憾,这可能也是为什么 hairpin 功能经常 broken 的原因。
-
一旦必须涉及到 CGNAT,那 hairpinning 对连接性来说就至关重要了。
Hairpinning 使内网连接的行为与公网连接的行为完成一致,因此我们无需关心目的 地址类型,也不用知晓自己是否在一台 CGNAT 后面。
如果 hairpinning 和 port mapping protocols 都不可用,那只能降级到中继模式了。
7.7 全 IPv6 网络:理想之地,但并非问题全无
行文至此,一些读者可能已经对着屏幕咆哮:不要再用 IPv4 了! 花这么多时间精力解决这些没意义的东西,还不如直接换成 IPv6!
的确,之所以有这些乱七八糟的东西,就是因为 IPv4 地址不够了,我们一直在用越来越复杂的 NAT 来给 IPv4 续命。
如果 IP 地址够用,无需 NAT 就能让世界上的每个设备都有一个自己的公网 IP 地址,这些问题不就解决了吗?
简单来说,是的,这也正是 IPv6 能做的事情。但是,也只说对了一半:在理想的全 IPv6 世界中,所有这些东西会变得更加简单,但我们面临的问题并不会完全消失 —— 因为有状态防火墙仍然还是存在的。
办公室中的电脑可能有一个公网 IPv6 地址,但你们公司肯定会架设一个防火墙,只允许 你的电脑主动访问公网,而不允许反向主动建连。
其他设备上的防火墙也仍然存在,应用类似的规则。
因此,我们仍然会用到
本文最开始介绍的防火墙穿透技术,以及
帮助我们获取自己的公网
ip:port
信息的旁路信道仍然需要在某些场景下 fallback 到中继模式,例如 fallback 到最通用的 HTTP 中继 协议,以绕过某些网络禁止 outbound UDP 的问题。
但我们现在可以抛弃 STUN、生日悖论、端口映射协议、hairpin 等等东西了。这是一个好消息!
全球 IPv4/IPv6 部署现状
另一个更加严峻的现实问题是:当前并不是一个全 IPv6 世界。目前世界上
大部分还是 IPv4,
大约 33% 是 IPv6[19],而且分布极度不均匀,因此某些 通信对所在的可能是 100% IPv6,也可能是 0%,或二者之间。
不幸的是,这意味着,IPv6 还无法作为我们的解决方案。就目前来说,它只是我们的工具箱中的一个备选。对于某些 peer 来说,它简直是完美工 具,但对其他 peer 来说,它是用不了的。如果目标是“任何情况下都能穿透(连接) 成功”,那我们就仍然需要 IPv4+NAT 那些东西。
新场景:NAT64/DNS64
IPv4/IPv6 共存也引出了一个新的场景:NAT64 设备。
前面介绍的都是 NAT44 设备:它们将一个 IPv4 地址转换成另一 IPv4 地址。NAT64 从名字可以看出,是将一个内侧 IPv6 地址转换成一个外侧 IPv4 地址。利用 DNS64 设备,我们能将 IPv4 DNS 应答给 IPv6 网络,这样对终端来说,它看到的就是一个 全 IPv6 网络,而仍然能访问 IPv4 公网。
Incidentally, you can extend this naming scheme indefinitely. There have been some experiments with NAT46; you could deploy NAT66 if you enjoy chaos; and some RFCs use NAT444 for carrier-grade NAT.
如果需要处理 DNS 问题,那这种方式工作良好。例如,如果连接到 google.com,将这个域名解析成 IP 地址的过程会涉及到 DNS64 设备,它又会进一步 involve NAT64 设备,但后一步对用户来说是无感知的。
但对于 NAT 和防火墙穿透来说,我们会关心每个具体的 IP 地址和端口。
解决方案:CLAT (Customer-side transLATor)
如果设备支持 CLAT (Customer-side translator — from Customer XLAT),那我们就很幸运:
CLAT 假装操作系统有直接 IPv4 连接,而背后使用的是 NAT64,以对应用程序无感知。在有 CLAT 的设备上,我们无需做任何特殊的事情。
CLAT 在移动设备上非常常见,但在桌面电脑、笔记本和服务器上非常少见, 因此在后者上,必须自己做 CLAT 做的事情:检测 NAT64+DNS64 的存在,然后正确地使用它们。
解决方案:CLAT 不存在时,手动穿透 NAT64 设备
-
首先检测是否存在 NAT64+DNS64。
方法很简单:向
ipv4only.arpa.
发送一个 DNS 请求。这个域名会解析 到一个已知的、固定的 IPv4 地址,而且是纯 IPv4 地址。如果得到的 是一个 IPv6 地址,就可以判断有 DNS64 服务器做了转换,而它必然会用到 NAT64。这样 就能判断出 NAT64 的前缀是多少。 此后,要向 IPv4 地址发包时,发送格式为
{NAT64 prefix + IPv4 address}
的 IPv6 包。类似地,收到来源格式为{NAT64 prefix + IPv4 address}
的包时,就是 IPv4 流量。接下来,通过 NAT64 网络与 STUN 通信来获取自己在 NAT64 上的公网
ip:port
,接 下来就回到经典的 NAT 穿透问题了 —— 除了需要多做一点点事情。
幸运的是,如今的大部分 v6-only 网络都是移动运营商网络,而几乎所有手机都支持 CLAT。运营 v6-only 网络的 ISPs 会在他们给你的路由器上部署 CLAT,因此最后你其实不需要做什么事情。但如果想实现 100% 穿透,就需要解决这种边边角角的问题,即必须显式支持从 v6-only 网络连接 v4-only 对端。
7.8 将所有解决方式集成到 ICE 协议
针对具体场景,该选择哪种穿透方式?
至此,我们的 NAT 穿透之旅终于快结束了。我们已经覆盖了有状态防火墙、简单和高级 NAT、IPv4 和 IPv6。只要将以上解决方式都实现了,NAT 穿透的目的就达到了!
但是,
对于给定的 peer,如何判断改用哪种方式呢?
如何判断这是一个简单有状态防火墙的场景,还是该用到生日悖论算法,还是需要手动处理 NAT64 呢?
还是通信双方在一个 WiFi 网络下,连防火墙都没有,因此不需要任何操作呢?
早期 NAT 穿透比较简单,能让我们精确判断出 peer 之间的路径特点,然后针对性地采用相应的解决方式。但后面,网络工程师和 NAT 设备开发工程师引入了一些新理念,给路径判断造成很大困难。因此 我们需要简化客户端侧的思考(判断逻辑)。
这就要提到 Interactive Connectivity Establishment (ICE,交换式连接建立) 协议了。与 STUN/TURN 类似,ICE 来自电信领域,因此其 RFC 充满了 SIP、SDP、信令会话、拨号等等电话术语。但如果忽略这些领域术语,我们会看到它描述了一个极其优雅的判断最佳连接路径的算法。
真的?这个算法是:每种方法都试一遍,然后选择最佳的那个方法。就是这个算法,惊喜吗?
来更深入地看一下这个算法。
ICE (Interactive Connectivity Establishment) 算法
这里的讨论不会严格遵循 ICE spec,因此如果是在自己实现一个可互操作的 ICE 客户端,应该通读RFC 8445[20], 根据它的描述来实现。这里忽略所有电信术语,只关注核心的算法逻辑, 并提供几个在 ICE 规范允许范围的灵活建议。
-
为实现和某个 peer 的通信,首先需要确定我们自己用的(客户端侧)这个 socket 的地址, 这是一个列表,至少应该包括:
我们自己的 IPv6
ip:ports
我们自己
上一篇: P2P 技术 (2) - NAT 渗透率
推荐阅读
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
NAT 穿透是如何工作的?技术原理与企业实践》(Tailscale,2020 年) 取自 https://arthurchiao.art/blog/how-nat-traversal-works-zh/ NAT 如何渗透:技术原理和企业级实践》(Tailscale,2020 年)
-
如何进行 NAT 穿透:技术原理和企业实践
-
如何进行 NAT 穿透:技术原理和企业实践
-
如何进行 NAT 穿透:技术原理和企业实践
-
像首席技术官一样思考:如何高效管理 30 人的研发团队?-管理越多越轻松。好的研发团队,应该是上拨下用,即下级对上级的向上管理;而不是反过来,总是向下管理,甚至是 CTO 做经理的事,经理做工程师的事,工程师最终会被当成实习生。如果是这样,就会越管越累,不仅团队无法成长,而且团队整天很忙还效率低下,问题一大堆。 有这样一个小故事:一位高级经理下班后帮忙倒垃圾,结果被老板训斥了一顿。这就好比首席技术官做了实习生自己该做的事。事情本身没有对错之分,只是从不同的角度有不同的理解。 古人云:"用人不疑,疑人不用"。在面对自己的研发团队时,应该相信他们能做好,授权一线开发人员充分发挥专业特长,不要限制他们的工作。但在相信他们的同时,也要进行二次确认,始终秉持 "我相信,但我要确认 "的原则和严谨的精神。因为每个人都会犯错和疏忽,通过发挥团队的智慧,团队犯错的机会就会大大减少。比如回归测试、代码审查、开发演示、变更审批等等。 如前所述,每个人都难免会犯错。但作为管理者,你所设计和商定的流程不能出错。管理者的每一个决定和沟通都应该经过深思熟虑。就像红绿灯的交通设计,某辆车不小心闯红灯可能会扣分,但红绿灯的设计一定要正确、人性化、统一。再比如,开发人员可能会因为疏忽大意写出 bug,但研发流程的设计和上线流程的发布不能有任何差错。因此,流程体系的设计,一方面要结合当前团队规模、业务特点和需要重点解决的问题来设计,另一方面也要在人员防错、效率提升、发挥团队集体智慧等维度进行综合考量。应该站在更高更抽象的角度去思考,不断思考一个倍受欢迎的园区应该如何设计,思考一个灵动、经典、永恒的建筑应该遵循怎样的模式,思考一个成功、优秀、卓越的研发团队应该需要怎样的流程和制度。 最后,反馈很重要。向上汇报很重要,向下反馈也很重要。能够保持顺畅的双向反馈和闭环管理,对研发团队的协作和沟通有着非常明显的积极作用。在向上汇报方面,要培养团队在正式汇报、会议汇报、私下沟通、书面总结、非正式场合等方面的沟通能力,提醒下属报喜也要报忧。凡事先记录,再跟进,最后反馈。反馈很重要,主动汇报更难得。 另一方面,同时也不要忽视向下反馈。好的爱,是双向的。团队也是如此,没有严格的上下级之分,只是分工和角色不同而已。作为管理者,不必总保持一种 "神秘感",让人 "捉摸不透 "才是牛。当团队做得好或有人做得好时,要记得在公开或私下场合给予肯定和赞许。业务有增长、业绩有提升时,别忘了给团队一些鼓励,或者安排一次下午茶或聚餐。在例会或正式会议上,也可以同步向大家传达一些重要信息和高层指示。"欲速则不达,欲远则同行"。 当向上汇报、向下反馈的沟通闭环形成后,同时结合前面研发过程的管理闭环,双管齐下,就能形成良性循环。如此反复,持之以恒,优秀卓越的研发团队,必将呈现。 能力、产出和效率 接下来,继续重复关于能力、产出和效率的话题。 站在不同的角色,以及一个企业经营、生存和发展所需要的基础上,我把研发生产力分为三个层次,分别是:一线员工关心的研发能力、管理层关心的软件产出和操作人员关心的企业生产效率。简单概括就是:既要把工作做好,又要能出成果,还要能帮企业赚钱。
-
openEuler郑州用户组成立!openEuler与hyperfusion携手共建河南地区用户生态 - 开幕致辞 超融合操作系统业务总经理、openEuler委员会成员蒋振华先生为本次活动致辞。 在本次活动的致辞中,他提到,作为openEuler社区早期的成员,超融合见证了openEuler从成立到在各行业商业落地,再到跨越生态拐点的过程,感谢openEuler提供了一个全产业链共同创新的平台,共同推动创新技术的商业落地。 同时,本次活动得到了郑州市郑东新区大数据管理局、郑州中原科技城投资服务局的大力支持。 郑东新区大数据管理局曹光远 在活动致辞中表示,openEuler的应用和*应用设施的深度优化,为郑东新区数字化转型提供了安全、可靠、高性能的技术基础;郑州中原科技城招商服务局王林表示,郑东新区欢迎所有openEuler生态相关企业扎根当地,围绕openEuler社区共同发展,形成合力。 openEuler社区及运维功能介绍 openEuler技术委员会委员胡峰 openEuler技术委员会委员胡峰先生在本次活动中介绍了openEuler社区目前发展的整体情况,并重点从技术层面介绍了openEuler的运维功能。 openEuler 晚会 胡峰先生介绍智能运维工具 A-Ops 和 openEuler gala、 阿波罗 Apollo、智能漏洞管理解决方案等新功能,以及涵盖各种运维场景的精品运维组件。在*交流环节,许多用户就目前使用的 openEuler 在*交流环节,许多用户就自己在使用openEuler过程中遇到的一些问题与胡峰先生进行了进一步的交流。 软硬结合,构建多样化算力操作系统 Hyperfusion 基于 openEuler 的基础上,结合自身软硬件技术积累,推出了富讯服务器操作系统 FusionOS FusionOS. FusionOS 首席架构师张海亮 分享了 FusionOS FusionOS首席架构师张海亮分享了FusionOS的软硬件协同优势、卓越的性能和可靠性,以及FusionOS在金融、运营商、*、互联网等行业的实践案例,引起了众多用户的兴趣,分享结束后,不少参会者就FusionOS的特点向讲师提问并进行了交流。
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
反传销网8月30日发布:视频区块链里的骗子,币里的韭菜,杜子建骂人了!金融大V周召说区块链!——“一小帮骗子玩一大帮小白,被割韭菜,小白还轮流被割,割的就是你!” 什么区块链,统统是骗子 作者:周召(知乎金融领域大V,毕业于上海财经大学,目前任职上海某股权投资基金合伙人) 有人问我,区块链现在这么火,到底是不是骗局? 我的回答是: 是骗局。而且我并不是说数字货币是骗局,而是说所有搞区块链的都是骗局。 -01- 区块链是一种鸡肋技术 人类社会任何技术的发明应用,本质都是为了提高社会的生产效率。而所谓区块链技术本质不过是几种早已成熟的技术的大杂烩,冗余且十分低效,除了提高了洗钱和诈骗的效率以外,对人类社会的进步毫无贡献。 真正意义上的区块链得包含三个要素:分布式系统(包括记账和存储),无法篡改的数据结构,以及共识算法,三者互为基础和因果,就像三体世界一样。看上去挺让人不明觉厉的,而经过几年的瞎折腾,稍微懂点区块链的碰了几次壁后都已经渐渐明白区块链其实并没有什么卵用,区块链技术已经名存实亡,沦为了营销工具和传销组织的画皮。 因为符合上述定义的、以比特币为代表的原教旨区块链技术,是反效率的,从经济学角度来说,不但不是一种帕累托改进,甚至还可以说是一种帕累托倒退。 原教旨区块链技术的效率十分低下,因为要遍历所有节点,只能做非常轻量级的数据应用,一旦涉及到大量的数据传输与更新,区块链就瞎了。 一方面整条链交易速度会极慢,另一方面数据库容量极速膨胀,考虑到人手一份的存储机制,区块链其实是对存储资源和能源的一种极大的浪费。 这里还没有加上为了取得所谓的共识和挖矿消耗的巨大的能源,如果说区块链技术是屎,那么这波区块链投机浪潮可谓人类历史上最大规模的搅屎运动。 区块链也验证不了任何东西。 所谓的智能合约,即不智能,也非合约。我看有人还说,如果有了智能合约,就可以跟老板签一份放区块链上,如果明年销售业绩提升30%,就加薪10%,由于区块链不能篡改,不能抵赖,所以老板必须得执行,说得有板有眼,不懂行的愣一看,好像还真是那么回事。 但仔细一想,问题就来了。首先,在区块链上如何证明你真的达到了30%业绩提升?即便真的达到老板耍赖如何执行? 也就是说,如果区块链真这么厉害,要法院和仲裁干什么。 人类社会真正的符合成本效益原则的是代理制度。之前有人说要用区块链改造注册会计师行业,我不知道他准备怎么设计,我猜想他思路大概是这样的,首先肯定搞去中心化,让所有会计师到链上来,然后一个新人要成为注册会计师就要所有会计师同意并记录在链上。 那我就请问了,我每天上班累死累活,为什么还要花时间去验证一个跟我无关的的人的专业能力?最优做法当然是组织一个委员会,让专门的人来负责,这不就是现在注册会师协会干的事儿吗?区块链的逻辑相当于什么事情都要拿出来公投,这个绝对是扯淡的。 当然这么说都有点抬举区块链了,区块链技术本身根本没有判断是非能力,如果这么高级的人工智能,靠一个无脑分布式记账就能实现的话,我们早就进入共产主义社会了。 虽然EOS等数字货币采用了超级节点,通过再中心化的方式提高效率,有点行业协会的意思,是对区块链原教旨主义的一种修正,但是依然无法突破区块链技术最本质的局限性。有人说,私有链和联盟链是区块链技术的未来,也是扯淡,因为区块链技术没有未来。如果有,说明他是包装成区块链的伪区块链技术。 区块链所涉及的所有底层技术,不管是分布式数据库技术,加密技术,还是点对点传输技术等,基本都是早已存在没什么秘密可言的技术。 比特币系统最重要的特性是封闭性和自洽性,他验证不了任何系统自身以外产生的信息的真实性。 所谓系统自身产生的信息,就是数据库数据的变动信息,有价值的基本上有且只有交易信息。所以说比特币最初不过是中本聪一种炫技的产物,来证明自己对几种技术的掌握,你看我多牛逼,设计出了一个像三体一样的系统。因此,数字货币很有可能是区块链从始至终唯一的杀手应用。 比特币和区块链概念从诞生到今天已经快10年了,很多人说区块链技术在爆发的前夜,但这个前夜好像是不是有点过长了啊朋友,跟三体里的长夜有一拼啊。都说区块链技术像是90年代初的互联网,可是90年代初的互联网在十年发展后,已经出现了一大批伟大的公司,阿里巴巴在99年都成立了,区块链怎么除了币还是币呢? 正规的数字货币未来发展的形式无外乎几种,要么就是论坛币形式,或者类似股票的权益凭证等。问题是论坛币和股票之前,本来也都电子化了,区块链来了到底改变了什么呢? 所有想把TOKEN和应用场景结合起来的人最后都很痛苦,最后他们会发现区块链技术就是脱裤子放屁,自己辛苦搞半天,干嘛不自己作为中心关心门来收钱?最后这些人都产生了价值的虚无感,最终精神崩溃,只能发币疯狂收割韭菜,一边嘴里还说着我是个好人之类的奇怪的话。 因此,之前币圈链圈还泾渭分明,互相瞧不起,但这两年链圈逐渐坐不住了,想着是不是趁着泡沫没彻底破灭之前赶快收割一波,不然可能什么都捞不着了。 前段时间和一个名校毕业的链圈朋友瞎聊天,他说他们“致力于用区块链技术解决数字版权保护问题”,我就问他一个问题,你们如何保证你链的版权所有权声明是真实的,万一盗版者抢先一步把数据放在链上怎么办。他说他们的解决方案是连入国家数字版权保护中心的数据库进行验证…… 所以说区块链技术就是个鸡肋,研究到最后都会落入效率与真实性的黑洞,很多人一头扎进链圈后才发现,真正意义上的区块链技术,其实什么都干不了。 -02- 不是蠢就是坏的区块链媒体 空气币和区块链的造富神话,让区块链自媒体也开始迎风乱扭。一群群根本不知道区块链为何物的妖魔鬼怪纷纷进驻区块链自媒体战场,开始大放厥词胡编乱造。 任何东西,但凡只要和区块,链,分,分布式,记账,加密,验证,可追溯等等这些个关键词沾到哪怕一点点,这些所谓的区块链媒体人就会像狗闻到了屎了一样疯狂地把区块链概念往上套。 这让我想起曾经一度也是热闹非凡的物联网,我曾经去看过江苏一家号称要改变世界的“物联网”企业,过去一看是生产路由器的,我黑人问号脸,对方解释说没有路由器万物怎么互联,我觉得他说得好有道理,竟无言以对。 好,下面让我们进入奇葩共赏析时间,来看看区城链媒体经常有哪些危言耸听的奇谈怪论 区块链(分布式记账)的典型应用是*?? 正如前面所说,真正意义上的区块链分布式记账,不光包括“记”这个动作,还包括分布式存储和共识机制等。而*诞生远远早于区块链这个词的出现,勉强算是“分布式编辑”吧,就被很多区块链媒体拿来强行充当区块链技术应用的典范。 其实事实恰恰相反,*恰恰是去中心化失败的典范,现在如果没有精英和专业人士的编辑和维护,*早就没法看了。 区块链会促进社会分工?? 罗振宇好像就说过类似的话,虽然罗振宇说过很多没有逻辑的话,但这句话绝对是最没逻辑思维的。很多区块链自媒体也常常用这句话来忽悠老百姓,说分工代表效率提高社会进步,而区块链“无疑”会促进分工,他们的理由仅仅是分工和分布式记账都共用一个“分”字,就强行把他们扯到一起。 实际情况恰恰相反,区块链是逆分工的,区块链精神是号召所有人积极地参与到他不擅长也不想掺合的事情里面去。 区块链不能像上帝一样许诺他的子民死后上天国,只能给他们许诺你们是六度人脉中的第一级,我可以赚后面五级人的钱,你处于金字塔的顶端。
-
视频会议场景中的空间音频--为何选择空间音频这一主题? 首先,为什么选择空间音频这一主题?我在视频会议领域工作了近二十年,我们的目标一直是让声音更清晰、视频更清晰。但在过去的 20 年中,视频会议的产品形态并没有发生本质的变化。去年元宇宙比较火,微软、Facebook都在做基于VR和元宇宙的企业协作研究,我们也进行了这方面的探索。 一开始,我们想从纯技术角度研究空间音频技术如何应用于视频会议场景,但在研究过程中,我们发现这是一个非常复杂的场景。因为视频会议本质上是人与人之间的交流。人与人之间的沟通是多维度的信息传递,声音、图像、眼神、肢体语言、触觉都是人与人之间沟通的要素,音频只是其中之一。本次分享从沟通与交流的角度,从视频会议的应用场景出发,分析视频会议产品需要什么样的空间音频技术以及如何实现。 02 空间音频与沉浸式交流