标签 Golang 下的文章

Go 1.24新特性前瞻:语法、编译器与运行时

本文永久链接 – https://tonybai.com/2024/12/16/go-1-24-foresight-part1

自2020年底撰写《Go 1.16版本新特性前瞻》以来,四年转瞬而逝。在这段时间里,每当Go的大版本开发进入新特性冻结(freeze)阶段,我都会为大家带来该版本的特性前瞻,旨在让大家更早地了解和实验这些新特性,从而在版本正式发布时能够准确评估是否应用它们。

11月末,Go 1.24的新特性开发已经冻结,我认为是时候对Go 1.24新特性进行前瞻了。本次前瞻将分为两篇进行,本文,也就是第一篇将讲解语法、编译器与运行时方面的变化,而第二篇将聚焦工具链和标准库。本次前瞻可以引导大家了解即将在明年3月份发布的Go 1.24版本中的重要变化,希望能给大家带去帮助。

注:Go每六个月发布一次。每个发布周期都分为持续约4个月的开发阶段,然后是为期3个月的测试和完善阶段(称为发布冻结期)。当前的发布周期预计于每年一月中旬和七月中旬开始,如下图所示。以Go 1.24为例,2024年7月开始plan,经过4个月开发,11月下旬冻结,再经历3个月的测试完善,预计2025年2月发布。


图来自go.dev/wiki/Go-Release-Cycle

注:大家可以使用Go playground体验dev branch的最新特性,或在本地安装GoTip版本进行体验。2024年12月14日,Go 1.24RC1版本发布,大家也可以直接用go install golang.org/dl/go1.24rc1@latest体验,或到Go官方下载站unstable version中直接下载安装。

1. 语法

Go 1.18引入了泛型Go 1.21版本新增了max、min和clear等预定义函数,而Go 1.23版本则引入了自定义迭代器。与这些创新相比,Go 1.24似乎又回归到了我们熟悉的“静默期”,没有显著的语法特性更新。

唯一一个值得提及的还是Go 1.23版本引入的实验特性:“带有类型参数的type alias”。如果你已经忘记这是一个什么语法特性,下面我就带你简单地回顾一下。

传统的类型别名的形式是这样的:

type P = Q

在《“类型名称”在Go语言规范中的演变》一文中我们介绍过,Q是Named Type,包括Predeclared Type、Anonymous Type、Existing Defined Type以及Existing Alias Type,甚至可以用泛型类型实例化后的类型作为Q,比如:

type MySlice[T any] []T

func main() {
    type P = MySlice[int]  // MySlice[int]作为Q
    var p P
    fmt.Println(len(p)) // 0
}

但P中不能包含类型参数!下面这样的类型别名定义是不合法的:

type P[T any] = []T

不过Go 1.23版本以实验特性(需显式使用GOEXPERIMENT=aliastypeparams)支持了带有类型参数的类型别名,在Go 1.24中,这个实验特性转正了,成为了默认特性。我们看看下面这个示例:

// go1.24-foresight/lang/generic_type_alias.go

package main

import "fmt"

type MySlice[T any] = []T

func main() {
    // 使用int类型实例化MySlice
    intSlice := MySlice[int]{1, 2, 3, 4, 5}
    fmt.Println("Int Slice:", intSlice)

    // 使用string类型实例化MySlice
    stringSlice := MySlice[string]{"hello", "world"}
    fmt.Println("String Slice:", stringSlice)

    // 使用自定义类型实例化MySlice
    type Person struct {
        Name string
        Age  int
    }

    personSlice := MySlice[Person]{
        {Name: "Alice", Age: 30},
        {Name: "Bob", Age: 25},
    }

    fmt.Println("Person Slice:", personSlice)
}

使用Gotip直接运行上面示例,我们可以得到如下结果:

Int Slice: [1 2 3 4 5]
String Slice: [hello world]
Person Slice: [{Alice 30} {Bob 25}]

怎么理解带有类型参数的类型别名呢?在《Go 1.23中值得关注的几个变化》一文中,我们也介绍了Russ Cox给出的理解,即可以将其看成是一种“类型宏”(类似c中的#define):

type MySlice[T any] = []T

就是在任何出现MySlice[T]的地方,将其换成[]T

在Go 1.23以实验特性出现的带类型参数的别名还有一些问题,比如下面这个本不该正常运行的示例(int切片类型是不满足comparable的),在Go 1.23.0版本中是可以正常编译运行的:

// go1.24-examples/lang/strict_alias.go

package main

import "fmt"

type MySlice[T any] = []T
type YourSlice[T comparable] = MySlice[T]

func main() {
    // 使用int类型实例化MySlice
    intSlice := MySlice[int]{1, 2, 3, 4, 5}
    fmt.Println("Int Slice:", intSlice)

    intsliceSlice := YourSlice[[]int]{
        []int{1, 2, 3},
        []int{4, 5, 6},
    }
    fmt.Println("IntSlice Slice:", intsliceSlice)
}

不过在Go 1.24中该问题被修正,如果你使用gotip运行该示例,你将得到类似下面编译错误:

./strict_alias.go:13:29: []int does not satisfy comparable

gotip版go spec(截至2024.12.09)中,对带有类型参数的type alias有如下约束:

type A[P any] = P    // illegal: P is a type parameter

即类型别名声明中的右侧已知类型不能是类型参数自身。但目前的gotip实现似乎忽略了这一条,下面代码在gotip下是可以正常编译运行的:

package main

import "fmt"

type A[P any] = P

func main() {
    var a A[int] = 5 // identical to int
    fmt.Println(a) // 5
}

此外Go 1.23.0中,带有类型参数的别名类型是不能跨包使用的,但Go 1.24中这条限制被取消了,带有类型参数的别名类型可以与常规类型别名一样跨包使用

在Go 1.24中,你也可以通过设置GOEXPERIMENT=noaliastypeparams来禁用这一特性,但该设置将在Go 1.25中被移除。

2. 编译器与运行时

2.1 运行时性能优化

Go 1.24版本在运行时方面实现了多个优化,包括采用基于Swiss Tables的原生map实现(#54766)、更高效的小对象内存分配以及改进的内部互斥锁实现,整体降低了2-3%的CPU开销。

Swiss Table是由Google工程师于2017年开发的一种高效哈希表实现,旨在优化内存使用和提升性能,解决Google内部代码库中广泛使用的std::unordered_map所面临的性能问题。目前,Swiss Table已被应用于多种编程语言,包括C++ Abseil库的flat_hash_map(可替换std::unordered_map)Rust标准库Hashmap的默认实现等。在字节工程师的提案下,Go runtime团队决定替换原生map的底层实现,改为基于Swiss Table。通过基于gotip的实测,大多数测试项中,新版基于swiss table的map的性能都有大幅提升,有些甚至接近50%!之前写过一篇《Go map使用Swiss Table重新实现,性能最高提升近50%》,大家可以移步到那里了解关于基于Swiss Table实现的map的原理的详情,这里就不赘述了。

另外一个重要的性能优化是runtime: improve scaling of lock2中的提案,旨在针对当前runtime.lock2实现的问题进行优化,具体的propsal在design/68578-mutex-spinbit.md文件中。下面简略说一下该优化的背景、方案原理以及取得的效果。

当前runtime.lock2的实现通过三态设计(未锁定、锁定、锁定且有等待线程),在高竞争情况下,多个线程反复轮询mutex的状态字,产生大量缓存一致性流量。每个轮询线程需要从内存中加载状态字,并在更新时触发缓存行失效,这导致性能大幅下降。而每次释放锁时,无论是否已有线程在轮询mutex状态字,都会尝试唤醒一个线程,这进一步增加了系统负载。总之,现有的三态设计不能有效限制线程的忙等待行为。即使锁的临界区操作非常短,线程依然会因为抢占资源而竞争加剧

新提案引入“spinbit”机制,扩展mutex状态字,增加一个”spinning”位,表示是否有线程处于忙等待状态。一个线程可以独占此位,在轮询状态字时拥有优先权。其他线程无需忙等待,直接进入休眠。同时提案优化了唤醒逻辑,当unlock2检测到已有线程正在忙等待时,不再唤醒休眠线程,从而减少不必要的线程切换和上下文切换。

目前该优化提供了基于futex和非futex系统调用的两个实现,基于futex的版本适用于Linux平台,通过精细控制休眠线程的列表,进一步减少竞争。

状态字中使用独立的位分别表示锁定状态、休眠线程存在与否、忙等待标志等,并通过位操作和Xchg8原子操作,确保性能和线程安全。

新方案在高竞争状况下取得了显著的可扩展性提升,新实现的spinbit机制能维持性能稳定,而不是像现有实现那样随线程数增加而急剧下降。基准测试表明,在GOMAXPROCS=20时,性能提升达3倍。大部分线程可以按设计预期那样,直接休眠而非忙等待,减少了电力消耗和处理器资源占用。同时,通过对休眠线程的显式管理,可实现有针对性的唤醒,降低线程长期休眠的风险(避免饿死)。

上述的基于Swiss table的map实现以及lock2优化是实验特性,但都是默认生效的,在Go 1.24中,你可以在构建阶段,通过显式设置GOEXPERIMENT=noswissmap和GOEXPERIMENT=nospinbitmutex关闭这两个实验特性。

2.2 cgo:优化C代码调用

如果你决定不碰cgo,那么你大可略过这节的说明。

传统cgo机制下调用c函数时,Go会保证传递给C函数的go指针指向的对象位于堆上。但如果C函数不保留Go指针的副本,并且不将该指针传递回Go代码,那么这个保证就是没有必要的。Go 1.24增加了下面注解用于显式告诉go编译器:不会有指针通过特定的C函数逃逸。

// #cgo noescape cFunctionName

此外,当Go函数调用C函数时,它默认会为C函数中再调用Go函数做好准备,这当然会有一些额外开销。这对于那些不会调回Go函数的C函数也是没有必要的。在Go 1.24中新增的#cgo nocallback注解就是用于告诉编译器这些准备工作不是必需的:

// #cgo nocallback cFunctionName

更多关于上述cgo优化c代码调用的新机制的说明,请参见cgo增加#cgo noescape和#cgo nocallback注解(#56378)

2.3 编译器禁止为C类型别名添加方法

Go 1.24之前,Go编译器允许在C类型的别名上声明方法,虽然某些时候它可以正常工作,如下面示例:

package main

/*
typedef int foo;
*/
import "C"

type foo = C.foo

func (foo) method() int { return 123 }

func main() {
    var x foo
    println(x.method()) // "123"
}

但这可能引入了潜在的类型安全性以及运行时错误问题,尽管目前为C类型别名添加方法的情形非常少。

Go 1.24通过引入了一个新的编译器检查修复了该问题,该检查利用了isCgoGeneratedFile函数和类型名称的特征(如_Ctype_前缀)来识别C类型别名,并禁止在C类型别名上声明方法。

3. 小结

本文对即将发布的Go 1.24版本的新特性进行了全面的展望。主要内容包括:

语法更新:Go 1.24未显著增加新语法特性,但实验性特性“带有类型参数的类型别名”已转正为默认特性,允许更灵活的类型别名定义。

编译器与运行时优化:

  • 运行时性能优化:引入了基于Swiss Tables的新原生map实现,显著提高了性能。还优化了内部互斥锁的实现,改善了高竞争情况下的性能。
  • cgo改进:新增了#cgo noescape和#cgo nocallback注解,优化C代码调用的效率。
  • 编译器限制:禁止在C类型别名上声明方法,以提高类型安全性。

Go 1.24版本在语法上保持稳定,但在性能和安全性方面进行了多项关键优化,旨在提升开发者的体验和代码的效率。

在接下来的“Go 1.24新特性前瞻:工具链和标准库”一文中,我将继续为大家带来更丰富详尽的Go 1.24新特性,敬请期待!

本文涉及的源码可以在这里下载。

4. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

WebRTC第一课:从信令、ICE到NAT穿透的连接建立全流程

本文永久链接 – https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish

在上一篇文章《WebRTC第一课:网络架构与NAT工作原理》中,我们介绍了WebRTC的网络架构和NAT的基本概念,学习了WebRTC采用端对端(P2P)的通信模型,知道了NAT(网络地址转换)的概念以及给像WebRTC这样的直接P2P通信带来的挑战。

在实际的网络环境中,建立WebRTC这样的端到端连接的确并非易事。因此,在这篇文章中,我将继续上一篇文章的内容,全面探讨一下WebRTC连接建立的全流程,涵盖信令交换、ICE候选信息采集和选择、NAT穿透的各个关键步骤,希望能给大家理解WebRTC技术栈带去帮助。

1. WebRTC连接建立概览

在深入细节之前,我们先用一个时序图来概览WebRTC连接建立的主要步骤:

注:上图由mermaid生成,对应的脚本在webrtc-first-lesson/part2/process-overview.mermaid。

这个过程可以概括为以下几个主要步骤:

  • 信令交换:客户端通过信令服务器交换必要的连接信息,包括会话描述协议(SDP)数据。
  • ICE候选收集:每个客户端收集可能的连接方式(称为ICE候选)。
  • 候选交换:通过信令服务器交换ICE候选信息。
  • 连通性检查:客户端尝试各种可能的连接方式。
  • 建立P2P连接:选择最佳的连接路径,建立直接的端到端连接。

在这个过程中,我们会涉及到几个关键概念:

  • 信令(Signaling):用于协调通信并交换元数据的过程。
  • ICE(Interactive Connectivity Establishment):一种用于在各种网络情况下协助建立端到端连接的框架。
  • NAT穿透(NAT Traversal):克服网络地址转换带来的通信障碍的技术。

接下来,我们将详细探讨这些概念,并基于这些概念详细说明WebRTC建连的全流程。

我们先来看看信令(Signaling)。

2. 信令:WebRTC连接的基础

WebRTC技术栈中唯一没有标准化的就是信令(Signaling),但信令却又是WebRTC连接的基础和必不可少的部分。

信令是WebRTC中用于协调通信过程的“指令”,它负责在对等端之间交换建立连接所需的元数据,但不会直接传输音视频数据。信令的主要作用包括:

  • 交换会话描述协议(SDP)信息
  • 交换网络配置信息(ICE候选)
  • 协调会话的开始和结束
  • 处理错误和会话状态变化

WebRTC本身并未定义信令协议标准,这主要考虑的是信令的设计与实现依赖于具体应用的需求,同时也有兼容性方面的考虑,比如:使WebRTC能够与现有的通信系统集成。在安全性方面,自定义信令也可以允许应用层来控制如何交换敏感信息。

目前用于WebRTC信令协议实现的常见方案主要包括基于WebSocket的自定义协议、SIP协议(Session Initiation Protocol)以及一些像XMPP(eXtensible Messaging and Presence Protocol)这样的成熟的即时通讯协议等。

就我个人而言,SIP和XMPP这样的传统协议都太重了,协议自身理解起来就有门槛!基于WebSocket的自定义协议,既简单又灵活,适合大多数业务不那么复杂的场景,在本文中,我们的信令协议就基于WebSocket自定义协议来实现。

Why Websocket?用WebSocket承载自定义信令协议的主要原因是几乎所有现代浏览器和后端框架都支持WebSocket,并且它是全双工通信,允许服务器和客户端随时发送消息,并且建立连接后,消息交换的开销也很小。

在设计信令语义时,我们通常会采用“Room”这个抽象模型来管理参与通信的客户端,这与WebRTC常用于互联网音视频应用不无关系。Room模型有助于组织和管理多个参与者,控制消息的广播范围,并可以实现更复杂的通信场景(如多人会议系统)。

下面是基于Room模型设计的信令交互的典型流程:

注:上图由mermaid生成,对应的脚本在webrtc-first-lesson/part2/signaling-room-model-flow.mermaid。

下面对图中几个关键流程做一些简要说明:

  • 房间创建

Client1向SignalingServer发送创建房间的请求,SignalingServer创建房间并返回房间ID给Client1。

  • 客户端加入

Client2使用房间ID向SignalingServer发送加入房间的请求,SignalingServer通知Client1有新客户端加入。SignalingServer向Client2确认加入成功,并返回房间信息。重复相同的过程,Client3也如此加入了房间。

  • WebRTC连接建立(以Client1和Client2为例)

Client1创建Offer并通过SignalingServer向Client2发送Offer,SignalingServer将Offer转发给Client2。Client2创建Answer,并通过SignalingServer向Client1转发Answer。之后,Client1和Client2还会以类似的方式互相交换ICE候选信息,通过SignalingServer进行转发。

注:offer是由发起者(通常是调用方)创建的SDP(Session Description Protocol)消息,表示希望建立的媒体会话的描述。answer是由接收者(通常是被叫方)回复的SDP消息,表示其对offer的响应。SDP中通常包含媒体格式、网络信息、编解码器等详细信息,供双方协商和确认,具体可参考我之前的文章《使用Go和WebRTC data channel实现端到端实时通信》。

  • 客户端离开(以Client2离开为例)

Client2向SignalingServer发送离开房间的请求,SignalingServer通知房间内的其他客户端(Client1 和 Client3)有客户端离开。

  • 房间关闭

Client1(假设是房主)向SignalingServer发送关闭房间的请求。SignalingServer通知剩余的客户端(Client3)房间已关闭。

我们看到:这个流程展示了Room模型在WebRTC信令过程中的典型应用:

  • Room作为一个逻辑单元,管理多个参与者之间的通信。
  • SignalingServer负责转发所有的信令消息,包括房间管理消息和WebRTC相关的SDP和ICE候选信息。
  • 客户端可以动态地加入和离开房间,SignalingServer会及时通知房间内的其他客户端。
  • 房间可以由创建者(通常是第一个加入的客户端)来关闭。

由此来看,支持Room模型的信令服务器要支持房间创建、加入房间、转发Offer和Answer、离开房间、房间关闭等关键API。同时我们也能看出这种模型非常适合于实现多人音视频通话、在线教室、游戏大厅等应用场景,它提供了一种结构化的方式来管理复杂的多方实时通信。

有了信令服务器,WebRTC通信两端就可以交换元信息了,这其中就包含用于建立端到端通信的ICE候选信息。接下来,我们就来看看WebRTC端到端建连的关键流程:交互式连接建立(ICE, Interactive Connectivity Establishment),以及这个过程中可能发生的NAT穿透。

3. ICE、NAT穿透与连接最终建立

ICE是一种用于在NAT(网络地址转换)环境中建立对等连接的协议,它允许两个agent(在RFC8445中用AgentL和AgentR指代,如下图)发现彼此的最佳通信路径,进而完成端到端的连接。


图:ICE典型部署场景(from RFC8445)

在这个过程中,我们还会涉及两个概念,一个是STUN(Session Traversal Utilities for NAT)服务器,一个是ICE Candidiate。

STUN服务器是帮助上述agent(AgentL和AgentR)发现其公网IP地址和端口的服务网元,这对于NAT穿透至关重要。而ICE Candidiate则是agent采集并与对端交换的、可能用于通信的潜在端点地址(IP地址和端口的组合)。

为了更直观的理解,下面我们来看一下通过ICE选择最佳通信路径的一般流程:

注:上图由mermaid生成,对应的脚本在webrtc-first-lesson/part2/ice-protocol-sequence.mermaid。

在信令流程发起和转发Offer/Answer之后,两个端都会开启ICE最佳通信路径选择的流程。

3.1 ICE Candidate Gathering

这第一步就是ICE Candidate Gathering,即收集ICE候选者(端点)信息。

在这个过程中,每个agent收集可能的候选者类型包括如下几种:

  • 主机候选者(Host Candidate)

主机候选者,即本地接口的地址。通过直接使用本地网络接口的IP地址和端口即可获得,比如:192.168.1.2:5000。

  • 反射候选者(Server Reflexive Candidate)

反射候选者是通过STUN服务器查询公网IP地址和端口获得的,比如203.0.113.1:6000。

STUN通常是位于公网的一个服务器,比如最知名的公共stun是Google的“stun:stun.l.google.com:19302”。在收集反射候选者时,Agent(客户端)会向STUN服务器发送Binding Request(绑定请求),STUN服务器会响应一个Binding Response(绑定响应),其中包含客户端的公共IP地址和端口信息。

  • 中继候选者(Relay Candidate)

中继候选者是通过TURN服务器(Traversal Using Relays around NAT)获得的端点地址(在上图中未显示),是在开启中继模式的情况下,由客户端向TURN服务器发送请求以获取中继地址。只有在WebRTC通信双方(AgentL和AgentR)无法直连的情况下(通常是NAT穿透失败导致的),才会使用中继候选者,并通过TURN服务器进行数据中继来实现两端的数据通信。

注:在本文中,我们暂不考虑中继模式。

  • 对端反射候选者(Peer Reflexive Candidates)

严格来说,对端反射候选者并非是在这个环节能获取到的候选者。对端反射是在ICE连接检查过程中动态发现的候选者,只有在连接检查过程中才能发现,且不太可预测,取决于网络拓扑和NAT行为。对比反射候选者,反射后选者是通过STUN服务器发现的。当一个端点向STUN服务器发送请求时,STUN服务器会回复该端点在公网上的IP地址和端口。而对端反射候选者是在两个端点尝试直接通信时发现的。当一个端点通过其已知的候选者(如主机候选者或反射候选者)向另一个端点发送数据时,如果成功到达,接收端会发现一个新的、之前未知的远程地址。这个新发现的地址就成为了对端反射候选者。

在ICE候选者信息收集的过程中,两端的Agent还要通过定期发送的STUN Binding请求,确保收集到的ICE反射/中继候选者信息在连接建立期间保持有效。这个过程在RFC 8445中被称为“Keeping Candidates Alive”,它可以帮助检测网络环境的变化,比如IP地址或端口的变化。通过定期的STUN请求,ICE可以确保候选者在NAT设备中的映射保持活跃,避免因长时间没有通信而被关闭。

3.2 Candidate Priorization

两端的Agent在收集完候选者信息后,会通过信令服务器交换他们收集到的候选者信息,这个流程在前面的信令交互流程图中也是有的,是信令协议要支持的功能的一部分。

一旦ICE Candidate Gathering以及candidate交换结束,两端的agent会对自己收集到的candidate以及收到的对端的candidate信息进行”Candidate Priorization”,即对自己收集到候选者集合和交换得到的对端候选者集合分别按优先级进行排序。

RFC8445中给出推荐的候选者的优先级公式如下:

priority = (2^24)*(type preference) +
           (2^8)*(local preference) +
           (2^0)*(256 - component ID)

在公式中有三个名词:type preference、local preference和component ID。下面分别介绍一下这三个名词的含义:

  • type preference

Type preference一个表示候选者类型优先级的值。不同类型的候选者会被赋予不同的type preference值,以反映它们在ICE过程中的相对重要性。

它的取值范围通常是0到126之间的整数,值越大,优先级越高。 RFC8445中常见候选者类型及其推荐值如下:

主机候选者 (Host candidates): 126
反射候选者 (Server Reflexive candidates): 100
对端反射候选者 (Peer Reflexive candidates): 110
中继候选者 (Relay candidates): 0

通过不同类型的候选者的推荐值,我们也能看出:主机候选者 > 对端反射候选者 > 反射候选者 > 中继候选者。

注:Peer Reflexive candidates被赋予比Server Reflexive candidates更高的优先级,前面提过,是因为它不是在ICE候选者收集阶段就能发现的,而是在后面的连接检查阶段才能发现,因此可能代表更直接的连接路径。

  • local preference

local preference是一个表示本地优先级的数值,其取值范围0~65535。这个值由本地ICE agent根据自己的策略来设置。

RFC 8421(Guidelines for Multihomed and IPv4/IPv6 Dual-Stack Interactive Connectivity Establishment (ICE))进一步补充了关于local preference的使用建议,特别是在多宿主和双栈(IPv4/IPv6)环境下。建议为IPv6候选地址分配比IPv4更高的local preference值,比如:IPv6地址可以分配65535(最高优先级),IPv4地址可以分配65535-1=65534(次高优先级)。在多宿主(Multihomed)环境中,可以根据网络接口的特性(如带宽、延迟、成本等)来分配不同的local preference值。

注:多宿主环境(Multihomed)指的是一个设备或系统通过多个网络接口连接到网络的情况。这些接口可以连接到同一个网络,也可以连接到不同的网络。多宿主环境下,每个网络接口都可能产生多个ICE候选者,需要为不同接口的候选者分配合适的优先级,可能需要考虑不同网络接口的特性(如带宽、延迟、成本)。

  • Component ID

Component ID用于区分同一媒体流中的不同组件。在ICE中,一个媒体流可能包含多个组件,例如RTP和RTCP。Component ID通常是从1开始的连续整数。在公式中使用(256 – component ID)是为了确保值较小的component ID得到较高的优先级。RTP组件通常被赋值为1,RTCP组件(如果存在)通常被赋值为2。Component ID在优先级计算中的作用相对较小,主要用于在其他因素相同的情况下,为同一流的不同组件提供细微的优先级区分。

下面我们用一个示例来演示一下候选者计算优先级的过程。示例将展示一个ICE agent如何计算自己的candidates和对端candidates的优先级。我们假设这是一个音频流的情况,涉及RTP组件。假设我们有两个agent:AgentL和AgentR,我们将关注AgentL的视角。

AgentL的收集的候选者集合如下:

Host candidate (IPv4): 192.168.1.10:50000
Server Reflexive candidate: 203.0.113.5:50000
Relay candidate: 198.51.100.1:50000

AgentL通过信令交换获得的AgentR的候选者集合如下:

Host candidate (IPv6): 2001:db8::1:5000
Host candidate (IPv4): 192.168.2.20:5000
Server Reflexive candidate: 203.0.113.10:5000

上面优先级计算公式中各个参数值选择如下:

Type preferences:

- Host: 126
- Server Reflexive: 100
- Relay: 0

Local preferences:

- IPv6: 65535
- IPv4: 65534

Component ID: 1 (RTP)

下面是AgentL的候选者优先级的计算过程:

- Host (IPv4): (2^24) * 126 + (2^8) * 65534 + (256 - 1) = 658871
- Server Reflexive: (2^24) * 100 + (2^8) * 65534 + (256 - 1) = 658195
- Relay: (2^24) * 0 + (2^8) * 65534 + (256 - 1) = 655595

AgentR的候选者优先级(从AgentL的角度计算)计算过程:

- Host (IPv6): (2^24) * 126 + (2^8) * 65535 + (256 - 1) = 658881
- Host (IPv4): (2^24) * 126 + (2^8) * 65534 + (256 - 1) = 658871
- Server Reflexive: (2^24) * 100 + (2^8) * 65534 + (256 - 1) = 658195

最终优先级排序(从高到低):

AgentR: Host (IPv6) - 658881
AgentR: Host (IPv4) - 658871
AgentL: Host (IPv4) - 658871
AgentR: Server Reflexive - 658195
AgentL: Server Reflexive - 658195
AgentL: Relay - 655595

这个优先级排序将用于指导下一阶段的ICE连接检查(ICE Connectivity Checks)顺序,但最终的连接选择还会考虑连接检查的结果。在实际场景中,可能会有更多的候选者,包括不同网络接口的多个Host候选者等等。

3.3 ICE Connectivity Checks

有了两端的候选者集合以及优先级值后,两个Agent就可以进入下一阶段ICE Connectivity Checks(连接检查)了。

连接检查实际也可以划分为三个阶段,我们逐一来看一下。

3.3.1 确定角色(Determining Role)

在 WebRTC 的 ICE(Interactive Connectivity Establishment)连接过程中,角色的确定对于连接检查非常重要。ICE 的角色分为两种:控制方(Controlling)和被控方(Controlled)。这些角色用于决定在多个候选路径中选择哪一条作为最终的连接路径。控制方(Controlling Agent) 负责最终选择使用哪个候选对进行通信,而受控方(Controlled Agent)则需遵循控制方的决定。

在offer/answer的信令模型中,通常发起offer的一方会被指定为控制方,而应答(answer)的一方会成为受控方。有时可能会出现两个agents都认为自己是控制方的情况。ICE提供了解决这种冲突的机制:每个agent生成一个随机数(称为tie-breaker),当发现冲突时,比较tie-breaker,tie-breaker较大的agent成为控制方。

ICE(Interactive Connectivity Establishment)连接检查是由控制方和被控方的两个ICE agent同时进行的。两者会各自发起连接检查,以确保双方能够建立有效的连接。控制方通过在检查中包含USE-CANDIDATE属性来提名(Nomination)候选对。

在某些情况下,角色可能会在ICE过程中切换,比如如果发现角色冲突并解决冲突后,又比如在ICE重启(restart)的特定场景下。

3.3.2 形成检查列表(Forming checklist)

通信的双方,无论是控制端还是被控端都会独立形成自己的检查列表。

检查列表是所有可能的候选者对(Candidate Pair)的组合。让我们结合上面的示例,详细说明这个过程。

在我们的例子中,以AgentL为例,每个本地候选与每个远程候选会形成一对,这里会形成9个候选者对:

(L-Host, R-Host-IPv6)
(L-Host, R-Host-IPv4)
(L-Host, R-Server-Reflexive)
(L-Server-Reflexive, R-Host-IPv6)
(L-Server-Reflexive, R-Host-IPv4)
(L-Server-Reflexive, R-Server-Reflexive)
(L-Relay, R-Host-IPv6)
(L-Relay, R-Host-IPv4)
(L-Relay, R-Server-Reflexive)

根据之前计算的优先级,对候选对对优先级进行计算,并按从高到底进行排序。RFC8445中给出了候选者对优先级计算的公式:

// Let G be the priority for the candidate provided by the controlling agent.
// Let D be the priority for the candidate provided by the controlled agent.
pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)

具体的计算过程这里就不体现了,排序后的检查列表可能如下:

(L-Host, R-Host-IPv6)
(L-Host, R-Host-IPv4)
(L-Server-Reflexive, R-Host-IPv6)
(L-Server-Reflexive, R-Host-IPv4)
(L-Host, R-Server-Reflexive)
(L-Server-Reflexive, R-Server-Reflexive)
(L-Relay, R-Host-IPv6)
(L-Relay, R-Host-IPv4)
(L-Relay, R-Server-Reflexive)

AgentR(被控方) 也会形成自己的检查列表,与AgentL类似,但AgentR并不主动选择最终的路径。

有了排序后的候选者对后,我们接下来便可以执行连接检查了,AgentL和AgentR会各自执行自己的检查。

3.3.3 执行连接检查(Performing Connectivity Checks)

我们以AgentL为例,看看执行连接检查的主要步骤。

AgentL开始按照检查列表的顺序(优先级由高到低)发送STUN Binding请求:

  • AgentL向R-Host-IPv6发送STUN Binding请求;
  • 如果第一个检查失败或超时,AgentL会继续向第二个候选者对的R-Host-IPv4发送STUN Binding请求;
  • 如果失败,继续下一个候选对…,依次类推

同时,AgentR也会执行类似的过程,按照他自己的检查列表发送自己的STUN Binding请求。

当AgentL收到STUN Binding响应时,可能有以下几种可能:

  • 如果是成功的响应,这个候选对被标记为有效。
  • 如果响应来自一个未知地址,创建一个新的Peer Reflexive候选。

之后,AgentL便会更新候选列表,将新候选与所有远程候选配对,形成新的候选对,并根据ICE优先级算法重新排序检查和更新检查列表。AgentL还可能对新形成的候选对立即开始连接性检查。

3.3.4 NAT穿透与最终候选路径的形成

如果两端都在NAT后面,那么Peer Reflexive候选者就是NAT穿透的关键!我们结合下图详细说说ICE过程是如何一步步的选出由新发现的Peer Reflexive组成的最终候选路径的。

注:上图由mermaid生成,对应的脚本在webrtc-first-lesson/part2/ice-nat-traversal-sequence.mermaid

图中使用A、B作为两个端点,通过stun服务器获取的反射候选为A’和B’,通过连接检查阶段发现的对端反射候选(Peer Reflexive)分别为A”和B”。接下来,我们详细说明一下图中流程。

  • ICE候选收集阶段

端点A和B都收集主机候选。A和B都通过各自的NAT向STUN服务器发送请求,获取服务器反射候选(A’和B’)。

  • 候选交换

A和B交换各自的候选列表,包括主机候选和反射候选(A’和B’)。

  • 连接检查开始

A向B的反射候选B’发送STUN绑定请求,这个请求经过A的NAT和B的NAT。

B收到请求后,发现源地址(A”)与A提供的候选(A’)不匹配,因此创建一个新的Peer Reflexive候选A”。

B通过NAT链回复STUN绑定响应。

A收到响应后,从响应中的XOR-MAPPED-ADDRESS字段获知并创建自己的Peer Reflexive候选A”。

注:STUN协议中的XOR-MAPPED-ADDRESS字段可用于帮助对等方(peer)在ICE连接检查阶段找到自己的Peer Reflexive地址。

  • B也向A发送STUN绑定请求

类似的过程在反向发生。A创建B的Peer Reflexive候选B”。B从A的响应中获知并创建自己的Peer Reflexive候选B”。

  • 更新候选对和继续检查

A和B都更新各自的检查列表,包括新的(A”, B”)对。

  • 选择最佳路径

最终,(A”, B”)被选为最佳路径,实现双向NAT穿透。

一旦选定了最佳候选者对,ICE过程就结束了,可以开始实际的数据传输。

3.4 ICE重启

最后我们简单说说ICE重启(restart)。ICE restart提供了一种在不中断现有应用会话的情况下重新建立和优化网络连接的机制。这通常是因为网络条件发生了变化或者需要切换到更优的连接路径。下面序列图展示了ICE重启的基本流程:

注:上图由mermaid生成,对应的脚本在webrtc-first-lesson/part2/ice-restart-sequence.mermaid

ICE restart可能由多种原因触发,如网络变化、切换到更优路径、或解决连接问题。任何一方都可以发起ICE restart。

和前面的ICE流程不同之处在于重启时,发起restart的一方会生成新的ICE ufrag和password。这些新的凭证用于区分新的ICE会话和旧的会话。

之后的流程就和正常的ICE交互选出最优通信路径没有太大区别了!这里也就不重复说明了。

注:ICE restart不一定会改变控制方(controlling)和受控方(controlled)的角色。通常情况下,原有的角色分配会被保持。

4. 小结

在这篇文章中,我们深入探讨了WebRTC连接建立的全流程,涵盖了以下关键概念:

  • 信令:我们讨论了信令的重要性,并了解了基于Room抽象的常见的信令服务器模型;
  • ICE框架:我们学习了ICE候选信息的收集、交换以及连接检查;
  • NAT穿透:我们在ICE连接检查过程中,详细说明了ICE是如何实现NAT穿透并选出最终最优的通信路径的。

在实际生产应用中,我们可能还需要考虑以下几点:

  • 连接建立优化:可以通过使用ICE Lite、预先收集候选信息等方式来加速连接建立过程。
  • 安全性考虑:在生产环境中,应该使用HTTPS和WSS来保护信令通道。
  • 错误处理和重连:实际应用中需要处理各种可能的错误情况,并实现自动重连机制。

在接下来的系列文章中,我将用一个相对完整的演示示例来展示WebRTC应用端到端建连的所有细节(通过TRACE级别日志),希望通过这些细节的分析能帮助大家更好地理解WebRTC的建连过程。

本文涉及的Mermaid源码在这里可以下载到 – https://github.com/bigwhite/experiments/blob/master/webrtc-first-lesson/part2

5. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats