【音视频第14天】webRTC协议(1)

协议

ICE

Interactive Connectivity Establishment (ICE)是一个框架,能让web浏览器与对面进行连接。
从a端到b端不能直接建立连接原因有很多。

  1. 它需要绕过会阻止建立连接的防火墙
  2. 如果你的设备没有公共IP地址,那么它需要提供给你唯一的地址
  3. 如果你的路由器不允许直接与对端连接,那么需要通过服务器中转数据。
    ICE使用STUN和/或TURN服务器来完成此任务,如下所述。

STUN

Session Traversal Utilities for NAT (STUN)是一种协议,用于发现您的公共地址,并确定路由器中阻止与对端直接连接的任何限制。
客户端将向Internet上的STUN服务器发送一个请求,STUN服务器将回复客户端的公共地址以及在路由器的NAT之后是否可以访问客户端。
可以了就可以直接访问了。
在这里插入图片描述

NAT

Network Address Translation (NAT)用于为您的设备提供一个公共IP地址。路由器将有一个公共IP地址,连接到路由器的每个设备将有一个私有IP地址。请求将从设备的私有IP转换为路由器的具有唯一端口的公共IP。这样你就不用每个设备都有一个唯一的公共IP,但仍然可以在互联网上被发现。
对于谁能连接上已经在互联网上的设备,路由器对此有限制。这可能意味着,即使我们拥有STUN服务器找到的公共IP地址,也不是任何人都可以创建连接。在这种情况下,我们需要使用TURN。

TURN

一些使用NAT的路由器采用“对称NAT”的限制方法。这意味着路由器将只接受来自你之前连接过的对端的连接。【也就是在对称NAT下,只能连接之前连接了的对端】
Traversal Using Relays around NAT (TURN)意味着绕过对称NAT的限制,通过打开与TURN服务器的连接并通过该服务器中转所有信息。您将创建一个与TURN服务器的连接,并告诉所有对等点向服务器发送数据包,然后这些数据包将被转发给您。这显然会带来一些成本开销,所以只有在没有其他替代方案的情况下才会使用它。
在这里插入图片描述

SDP

Session Description Protocol (SDP)是一种标准,用于描述连接的多媒体内容,如分辨率、格式、编解码器、加密等,以便对等端在数据传输时能够相互理解。从本质上讲,这是描述内容的元数据,而不是媒体内容本身。
从技术上讲,SDP并不是真正的协议,而是一种用于描述设备间共享媒体的连接的数据格式
编写SDP文档远远超出了本文档的范围;然而,这里有一些事情值得注意:

SDP结构

SDP由一行或多行UTF-8文本组成,每行以一个字符类型开头,后面跟着等号(“=”),然后是包含值或描述的结构化文本,其格式取决于类型。以给定字母开头的文本行通常被称为“字母行”。例如,提供媒体描述的行具有“m”类型,因此这些行被称为“m-line”。

Signaling and Connecting

Signaling: How peers find each other in WebRTC

【通过带外的信令,传递SDP】
当WebRTC代理启动时,它不知道它要与谁通信,也不知道他们要通信什么。Signaling解决了这个问题。Signaling用于引导调用,允许两个独立的WebRTC代理开始通信。
Signaling使用一种称为SDP(会话描述协议)的协议。每个SDP消息由键/值对组成,并包含一个“media sections”列表。两个WebRTC代理交换的SDP包含如下细节:

  • 代理可访问的ip和ports
  • 代理希望发送的音频和视频轨道的数量。
  • 每个代理支持的音频和视频编解码器。
  • 连接时使用的值(uFrag/uPwd)。
  • 安全时使用的值(证书指纹)。
    需要注意的是,信令通常发生在“带外out-of-band”,这意味着应用程序通常不使用WebRTC本身来交换信令消息。任何适合于发送消息的体系结构都可以在连接的对等体之间中转sdp,许多应用程序将简单地使用它们现有的基础结构(例如REST端点、WebSocket连接或身份验证代理)来促进适当的客户机之间的sdp交易。

Connecting and NAT Traversal with STUN/TURN

【通过ICE建立连接】
一旦两个WebRTC代理交换了sdp,它们就有了足够的信息来尝试相互连接。为了实现这种连接,WebRTC使用了另一种称为ICE(交互式连接建立)的成熟技术。
ICE是一种早于WebRTC的协议,允许在没有中央服务器的情况下在两个代理之间建立直接连接。这两个代理可能在同一个网络上,也可能在世界的另一端。
ICE可以实现直接连接,但真正连接过程涉及一个叫做“NAT遍历”的概念和STUN/TURN服务器的使用。
当两个代理成功建立ICE连接时,WebRTC将进入下一步;为在它们之间共享音频、视频和数据建立加密传输。

Signaling

当您创建WebRTC代理时,它对另一个对端一无所知。它不知道它将与谁连接,也不知道他们将发送什么。我们使用信令引导调用。在这些值交换之后,WebRTC代理可以直接相互通信。信令信息只是文本。WebRTC并不关心他们是如何被运输的。它们通常通过Websockets共享,但这不是必需的。
WebRTC使用一种称为sdp协议。通过该协议,两个WebRTC端点将共享建立连接所需的所有状态。协议本身易于阅读和理解。复杂性来自于理解WebRTC填充的所有值**。这个协议不是专门针对WebRTC的**。WebRTC实际上只利用了sdp协议的一个子集,所以我们只学习我们需要的部分。在我们理解该协议之后,我们将继续讨论它在WebRTC中的应用。

sdp协议

sdp协议在RFC 8866中定义。它是一个键/值协议,每个值后面都有换行符。它感觉类似于INI文件。一个会话描述包含0个或多个媒体描述。在思想上,你可以把它建模为一个会话描述,其中包含一个媒体描述数组。
媒体描述通常映射到一个单一的媒体流。因此,如果你想用三个视频流和两个音频轨道来描述一个会话,你将有五个媒体描述。
会话描述中的每一行都以一个字符开始,这是你的键。然后它后面会跟着一个等号。等号后面就是值。在该值完成后,将有一个换行符。
会话描述协议定义了所有有效的密钥。您只能使用协议中定义的字母作为密钥。这些键都有重要的意义,后面会解释。以下会话描述:

a=my-sdp-value
a=second-value

你有两条线。第一行值为my-sdp-value,第二行值为second-value。
WebRTC并不使用会话描述协议定义的所有键值。只有在RFC 8829中定义的JavaScript会话建立协议(JSEP)中使用的键是重要的。下面的七个关键是你现在只需要了解的:

v - Version, should be equal to 0.
o - Origin, 包含一个唯一的ID,用于重新协商.
s - Session Name, should be equal to -.
t - Timing, should be equal to 0 0.
m - Media Description (m=<media> <port> <proto> <fmt> ...), described in detail below.
a - Attribute, 一个自由文本字段。这是WebRTC中最常见的一行。
c - Connection Data, should be equal to IN IP4 0.0.0.0.

“会话描述”可以包含无限个“Media Description”。
Media Description定义包含一个格式列表。这些格式映射到RTP有效负载类型。然后,实际的编解码器由Media Description中值为rtpmap的Attribute定义。每个媒体描述可以包含无限数量的属性。
以这段Session Description摘录为例:

v=0
m=audio 4000 RTP/AVP 111
a=rtpmap:111 OPUS/48000/2
m=video 4000 RTP/AVP 96
a=rtpmap:96 VP8/90000
a=my-sdp-value

您有两个媒体描述,一个是fmt 111类型的音频,一个是96格式的视频。第一个媒体描述只有一个属性。此属性将负载类型111映射到Opus。第二个Media Description有两个属性。第一个属性将Payload Type 96映射为VP8,第二个属性是my-sdp-value。
下面将我们讨论过的所有概念结合在一起。这些都是WebRTC使用的会话描述协议的所有特性。

v=0
o=- 0 0 IN IP4 127.0.0.1
s=-
c=IN IP4 127.0.0.1
t=0 0
m=audio 4000 RTP/AVP 111
a=rtpmap:111 OPUS/48000/2
m=video 4002 RTP/AVP 96
a=rtpmap:96 VP8/90000

v, o, s, c, t被定义,但它们不影响WebRTC会话。
你有两个媒体描述。一个是音频类型,一个是视频类型。
每一个都有一个属性。此属性配置RTP管道的详细信息。

WebRTC如何使用sdp

WebRTC使用offer/answer模型。这意味着一个WebRTC代理发出“offer”开始呼叫,如果它愿意接受所提供的内容,WebRTC 另一端“answers”。这使应答者有机会在媒体描述中拒绝不支持的编解码器。这就是两端如何理解它们想交换的格式。
Transceivers 是用于发送和接收的
Transceivers 是WebRTC特定的概念,你将在API中看到。它将“Media Description”暴露给JavaScript API。每个Media Description都成为一个Transceivers。每次你创建一个Transceivers,一个新的“Media Description就会被添加到本地Session Description中。WebRTC中的每个Media Description都有一个方向属性。这允许WebRTC代理声明“我将把这个编解码器发送给你,但我不愿意接受任何回报”。有四个有效值:send recv sendrecv inactive
WebRTC使用的SDP值
在WebRTC代理的会话描述中常见属性的列表。这些值控制着子系统。
group:BUNDLE:Bundling是在一个连接上运行多种类型的流量的行为。一些WebRTC实现为每个媒体流使用专用连接。最好是Bundling。
fingerprint:sha-256:这是对等体用于DTLS的证书的hash。DTLS握手完成后,将其与实际证书进行比较,以确认您正在与期望的对象通信。
**setup:**这控制DTLS代理行为。这决定了在ICE连接之后,它是作为客户机还是服务器运行。可能取值为:

  • setup:active -作为DTLS客户端运行。
  • setup:passive -作为DTLS服务器运行。
  • setup:actpass -让其他WebRTC代理选择。
    **mid:**识别会话描述中的媒体流。
    ice-ufrag:这是ICE Agent的用户分片值。用于对ICE Traffic进行认证。
    ice-pwd:这是 ICE Agent的密码。用于对ICE Traffic进行认证。
    rtpmap:此值用于将特定的编解码器映射到RTP有效负载类型。有效负载类型不是静态的,因此对于每个调用,提供程序决定每个编解码器的有效负载类型。
    fmtp:为一个有效负载类型定义附加值。这对于传达特定的视频配置文件或编码器设置非常有用。
    **candidate:**这是一个来自ICE Agent的ICE Candidate。这是WebRTC Agent 可用的一个可能地址。这些将在下一章详细解释。
    ssrc :一个同步源(SSRC)定义了一个单一的媒体流轨迹。
    label:是这个单独流的ID。mslabel是容器的ID,容器里面其中可以有多个流。

WebRTC会话示例

下面是WebRTC客户端生成的完整会话描述:

v=0
o=- 3546004397921447048 1596742744 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 0F:74:31:25:CB:A2:13:EC:28:6F:6D:2C:61:FF:5D:C2:BC:B9:DB:3D:98:14:8D:1A:BB:EA:33:0C:A4:60:A8:8E
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=setup:active
a=mid:0
a=ice-ufrag:CsxzEWmoKpJyscFj
a=ice-pwd:mktpbhgREmjEwUFSIJyPINPUhgDqJlSd
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=ssrc:350842737 cname:yvKPspsHcYcwGFTw
a=ssrc:350842737 msid:yvKPspsHcYcwGFTw DfQnKjQQuwceLFdV
a=ssrc:350842737 mslabel:yvKPspsHcYcwGFTw
a=ssrc:350842737 label:DfQnKjQQuwceLFdV
a=msid:yvKPspsHcYcwGFTw DfQnKjQQuwceLFdV
a=sendrecv
a=candidate:foundation 1 udp 2130706431 192.168.1.1 53165 typ host generation 0
a=candidate:foundation 2 udp 2130706431 192.168.1.1 53165 typ host generation 0
a=candidate:foundation 1 udp 1694498815 1.2.3.4 57336 typ srflx raddr 0.0.0.0 rport 57336 generation 0
a=candidate:foundation 2 udp 1694498815 1.2.3.4 57336 typ srflx raddr 0.0.0.0 rport 57336 generation 0
a=end-of-candidates
m=video 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=setup:active
a=mid:1
a=ice-ufrag:CsxzEWmoKpJyscFj
a=ice-pwd:mktpbhgREmjEwUFSIJyPINPUhgDqJlSd
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=ssrc:2180035812 cname:XHbOTNRFnLtesHwJ
a=ssrc:2180035812 msid:XHbOTNRFnLtesHwJ JgtwEhBWNEiOnhuW
a=ssrc:2180035812 mslabel:XHbOTNRFnLtesHwJ
a=ssrc:2180035812 label:JgtwEhBWNEiOnhuW
a=msid:XHbOTNRFnLtesHwJ JgtwEhBWNEiOnhuW
a=sendrecv

以下是我们从这条信息中了解到的:
我们有两个媒体部分,一个音频部分和一个视频部分。
它们都是sendrecv收发器。我们得到了两个流,我们可以发送两个回去。
我们有ICE候选人和身份验证详细信息,因此我们可以尝试连接。
我们有一个证书指纹,所以我们可以进行安全通话。

Connecting

为什么WebRTC需要一个专用的子系统来连接?

目前部署的大多数应用程序都建立 client/server连接。客户端/服务器连接要求服务器有一个稳定的已知传输地址。客户端与服务器连接,服务器响应。

WebRTC不使用 client/server 模型,它建立点对点(P2P)连接。在P2P连接中,创建连接的任务平均分配给两端。这是因为WebRTC中的传输地址(IP和端口)不能被假定,甚至可能在会话期间发生变化。WebRTC将尽可能地收集所有信息,并将竭尽全力实现两个WebRTC代理之间的双向通信。

不过,建立点对点连接可能很困难。这些代理可能位于不能直连的不同网络中。在存在直连的情况下,您仍然可能遇到其他问题。在某些情况下,您的客户端不使用相同的网络协议(UDP <-> TCP)或可能使用不同的IP版本(IPv4 <-> IPv6)

尽管在建立P2P连接时存在这些困难,但由于WebRTC提供的以下属性,您获得了优于传统客户端/服务器技术的优势。

  1. Reduced Bandwidth Costs
    因为媒体通信直接发生在对等点之间,你不需要付费,也不需要托管一个单独的服务器来转发媒体。
  2. Lower Latency
    低延迟:Lower Latency
    直连更快捷,当通过服务器运行所有内容时,传输速度会变慢
    3.Secure E2E Communication
    端到端安全通信:直接交流更安全。由于用户不会通过您的服务器路由数据,因此他们甚至不需要相信您不会解密数据

上面描述的过程称为交互式连接建立(ICE)。另一个早于WebRTC的协议。ICE是一种试图找到两个ICE代理之间通信的最佳方式的协议。每个ICE Agent 公布了可以到达的方式,这些被称为candidates。candidates本质上是agent的传输地址,它认为另一个对端可以到达该传输地址。然后,ICE决定candidates的最佳组合。

Networking real-world constraints

ICE就是要克服现实世界网络的限制。在探讨解决方案之前,让我们先谈谈实际问题

  1. 不在同一个网络
    在这里插入图片描述
    对于同一网络中的主机来说,连接非常容易。192.168.0.1 ->192.168.0.2很容易做到。这两台主机可以在没有任何外部帮助的情况下相互连接。
    然而,使用路由器B(routerB)的主机没有办法直接访问路由器a(routerA)后面的任何东西,你如何区分路由器a(routerA)后面的192.168.0.1和路由器B(routerB)后面的相同IP ? 它们是私有ip 。使用路由器B(routerB)的主机可以直接向路由器A(routerA)发送流量,但请求将在那里结束。路由器A(routerA)如何知道它应该把消息转发给哪台主机?
  2. 协议限制
    一些网络根本不允许UDP流量,或者可能他们不允许TCP。一些网络可能有非常低的MTU(最大传输单元)。网络管理员可以改变很多变量,从而使通信变得困难。
  3. 防火墙/IDS规则
    另一个是“深度包检测”和其他智能过滤。一些网络管理员会运行试图处理每个数据包的软件。很多时候,这个软件不理解WebRTC,所以它会阻止它,因为它不知道该做什么,例如,将WebRTC数据包视为任意端口上的可疑UDP数据包,而不是白名单。

NAT Mapping

NAT(网络地址转换)映射是使WebRTC的连通性成为可能。这就是WebRTC允许完全不同子网中的两端通信的方式,解决了上述“不在同一个网络”的问题。虽然Agent 1和Agent 2他们在不同的网络中,但是是可以通信的。
在这里插入图片描述
为了实现这种通信,需要建立一个NAT映射。Agent 1通过7000端口与Agent 2建立WebRTC连接。这将创建192.168.0.1:7000到5.0.0.1:7000的绑定。这允许Agent 2通过发送到5.0.0.1:7000的报文到达Agent 1。这个示例中的NAT映射就像在路由器中进行端口转发的自动版本。

NAT映射的缺点是没有单一形式的映射(例如静态端口转发),并且网络之间的行为不一致。互联网服务提供商和硬件制造商可能以不同的方式做到这一点。【看不太懂NAT映射的缺点】在某些情况下,网络管理员甚至会禁用它。好消息是,所有的行为都是可以理解和观察到的,因此ICE代理能够确认它创建了一个NAT映射,以及映射的属性。描述这些行为的文档是RFC 4787。

创建映射

创建映射是最简单的部分。当你发送一个包到你的网络之外的地址时,一个映射就被创建了。NAT映射只是一个临时的公网IP和端口,由您的NAT分配。出站消息将被重写,其源地址由新的映射地址给出。如果一条消息被发送到映射,它将自动路由回创建它的NAT内的主机。映射的细节是它变得复杂的地方。

映射创建行为

映射创建分为三个不同的类别:

  1. 端点独立映射
    在NAT内部为每个发送方创建一个映射。如果向两个不同的远端地址发送两个报文,NAT映射将被重用。两个远程主机将看到相同的源IP和端口。如果远程主机响应,它将被发送回同一个本地侦听器。
    这是最好的情况。对于工作调用,至少有一方必须是这种类型。
  2. 地址依赖映射
    每次发送一个数据包到一个新地址时都会创建一个新的映射。如果向不同的主机发送两个报文,会产生两个映射。如果向同一个远端主机发送两个数据包,但目的端口不同,则不会创建新的映射。
  3. 地址和端口依赖映射
    如果远端IP或端口不同,则创建新的映射。如果向同一个远端主机发送两个数据包,但目的端口不同,则会创建一个新的映射。

映射过滤行为

映射过滤是关于谁可以使用映射的规则。它们分为三个类似的类别:

  1. 端点独立过滤
    任何人都可以使用映射。您可以与多个其他对等点共享映射,它们都可以向它发送流量。
  2. 地址相关过滤
    只有创建映射的主机可以使用该映射。如果您向主机A发送数据包,则只能从同一主机获得响应。如果主机B试图向该映射发送一个数据包,它将被忽略。
  3. 地址和端口依赖过滤
    只有为其创建映射的主机和端口才能使用该映射。如果你发送一个包到a:5000,你只能从同一个主机和端口得到响应。如果A:5001试图向该映射发送一个数据包,它将被忽略。

映射刷新

如果一个映射5分钟没有使用,建议销毁它。这完全取决于ISP或硬件制造商。

STUN

STUN(NAT 会话传输实用程序)是一种用来配合 NAT 使用的协议。这是 WebRTC(和 ICE!)之前的另一项技术。它由RFC 8489定义,该文件还定义了 STUN 数据包结构。STUN 协议也在 ICE/TURN 中被使用。

STUN 很有用,因为它允许以编程方式创建 NAT 映射。在 STUN 之前,我们能够创建 NAT 映射,但是我们不知道映射的 IP 和端口是什么!STUN 不仅使你能够创建映射,还可以让你获取映射的详细信息,你可以他人分享这些详细信息,然后他们便可以通过你刚刚创建的映射向你传回数据。

让我们从对 STUN 的基本描述开始。稍后,我们再将话题扩展到 TURN 和 ICE 的用法。现在,我们只打算描述请求 / 响应流程来创建映射。然后,我们将讨论如何获取该映射的详细信息以便与他人共享。当你在 ICE URLs 中有一个用于 WebRTC PeerConnection 的 stun: 服务器时,此过程就会发生。简而言之,STUN 向 NAT 外部的 STUN 服务器发送请求,服务器返回其在请求中观察到的内容,STUN 根据这些内容来帮助 NAT 后面的端点找出已创建的映射。

Protocol Structure

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0|     STUN Message Type     |         Message Length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Magic Cookie                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Transaction ID (96 bits)                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             Data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STUN 消息类型
每个 STUN 数据包都有一个类型。目前,我们仅关心以下几种:

  • Binding Request - 0x0001
  • Binding Response - 0x0101
    为了创建一个 NAT 映射,我们发出一个 Binding Request。然后服务器回应一个 Binding Response

消息长度
这就是 Data 段的长度。这一段中包含由消息类型所定义的任意数据。

Magic Cookie
指的是固定值 0x2112A442,以网络字节顺序发送。这个值有助于将 STUN 流量与其他协议区分开。

交互(Transaction)ID
一个 96-bit 的标识符,用于唯一标识一个请求 / 响应对。这可以帮助你配对请求和响应。

数据
数据将包含一个 STUN 属性的列表。一个 STUN 属性具有以下结构:

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Type                  |            Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Value (variable)                ....
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STUN Binding Request 不使用任何属性。这意味着一个STUN Binding Request仅包含 header。

STUN Binding Response 使用一个 XOR-MAPPED-ADDRESS (0x0020)。此属性包含一个 IP 和一个端口。这正是所创建的 NAT 映射的 IP 和端口!

创建 NAT 映射

使用 STUN 创建 NAT 映射只需要发送一个请求。你向 STUN 服务器发送一个 STUN Binding Request。然后,STUN 服务器回应一个 STUN Binding Response。 该 STUN Binding Response 将包含映射地址。映射地址是 STUN 服务器看到你的方式,也是你的 NAT 映射。 如果你希望某人向你发送数据包,那么你应该共享该映射地址。

人们还会将映射地址称为公网 IP 或 Server Reflexive Candidate。

确定 NAT 类型

不幸的是,映射地址可能并非在所有情况下都可用。如果是地址相关的映射,则只有 STUN 服务器才能将流量发送回给你。如果你共享它,那么另一个 peer 尝试向该地址发送的消息将被丢弃。这使得该 peer 无法与别的 peer 交流。如果 STUN 服务器还可以为你将数据包转发给对端 peer,你可能会发现地址相关的映射问题实际上是可以解决的!这也就是下面将要说到的 TURN 解决方案。

RFC 5780定义了一种方法,可以运行一个测试来确定你的 NAT 类型。这很有用,因为你可能会提前知道是否可以进行直接连接。

猜你喜欢

转载自blog.csdn.net/Magic_o/article/details/130193516
今日推荐