标签 Golang 下的文章

2024年Go语言盘点:排名历史新高,团队新老传承

本文永久链接 – https://tonybai.com/2024/01/06/the-2024-review-of-go-programming-language

2024年底,由于感染了甲流,我在家卧床休息了两天,原定于2024年进行的Go语言盘点写作因此被迫推迟。不过,我始终相信:迟到但不会缺席。在2025年元旦的第一天,我终于开始了这篇博客的撰写。

时间过得真快,《2023年Go语言盘点:稳中求新,稳中求变》依然历历在目。转眼之间,一年365天过去了,发生了许多事情,甚至有些记忆已在脑海中模糊或消逝。在这里,我将带你盘点那些关于Go的重要时刻,唤起你对Go的美好回忆。

回顾整个2024年,如果非要用一句话来形容Go语言的状态,我会选择:Go完成了技术成熟度曲线中的“稳步爬升复苏期”,开始进入“生产成熟期”。这一点在Go的排名中得到了直接体现,并在Go社区的活跃度方面得到了间接的印证。而Go的年中换帅似乎也预示着这是一个新的起点!在过去一年中,得益于Go团队和社区的共同努力,Go发布了许多值得关注的新特性。

接下来,我将为大家逐一详细介绍!

1. Go排名创历史新高

说到编程语言排名,程序员们首先想到的就是TIOBE!在2024年的TIOBE排行榜上,尽管Go语言没有像AI时代的霸主语言Python那样耀眼,但跻身前十并站稳第七名这一成绩也足以让其他语言羡慕不已!


图:2024年12月TIOBE排名TOP 10

而从2009年开源至今,Go在TIOBE排名走势如下:


图:2010年-2024年TIOBE排行榜Go语言走势

了解Go历史的朋友都知道,Go语言真正具备生产级成熟度是从2015年的Go 1.5版本开始的。按照技术成熟度曲线的划分,2015年之前及其后的一段时间可以视为技术萌芽期。从曲线中可以看出,2017年时达到了期望膨胀期的峰值。此后,Go经历了一段“漫长”的泡沫破裂低谷期以及稳步爬升的复苏期。从2023年开始,到2024年末,Go语言复苏的速度日益加快!目前来看,如无意外,Go将进入技术成熟度曲线的下一阶段:生产成熟期!我曾提到过:绝大多数主流编程语言将在其诞生后的第15至第20年间大步前进。按照这个编程语言的一般规律,刚刚迈过开源第15个年头的Go刚刚迈进自己的黄金5-10年。

当然,单看TIOBE单一榜单似乎说服力不足,我们再来看看今年的Github octoverse报告。在这份报告中,Go依旧稳居github热门编程语言前10(如下图),这一位置已经保持了三年多了!


图:2024年Github最热门编程语言排行榜

此外,在2024年年中发布的“IEEE Spectrum 2024编程语言排行榜”中,Go在Spectrum排名和Trending排名中分列第8位和第7位。

除了排行榜之外,通过Reddit中编程语言论坛的活跃度也可以看出Go语言在全球的受欢迎程度和用户广度。以下是2025年1月1日Reddit上最活跃的9门编程语言子论坛的实时状态截图:


图:2025.1.1 Reddit编程语言子论坛状态对比

我们看到Go子论坛在成员数量和某一时刻的在线人数上都表现良好。此外,如果你是长期关注Reddit Go论坛的Gopher,一定注意到自2024年初以来,Go论坛的人气迅速增长,日均帖子数相比前两年显著增加,其中很多都是新加入Go阵营的初学者!

注:Rust的人气是真高啊,online人数断崖领先!

编程语言技术大会是衡量语言流行度和受欢迎程度的另一重要风向标。自从全球从新冠疫情中恢复后,GopherCon逐渐在各地线下恢复,到了2024年基本回到了疫情前的状态,甚至在一些地方的GopherCon还超越了以往的受欢迎程度。例如,2024年GopherCon欧洲大会破例举办了两次。此外,首届在非洲举行的GopherCon Africa也于2024年10月份在肯尼亚首都内罗毕成功举行!唯一的遗憾是GopherChina在2024年缺席,这或许与国内的经济形势有关。

Go的增长趋势来的有些快,不知道是否是得益于AI应用的快速发展!但就像Go团队前成员Jaana Dogan(Rakyll)所说的那样:

Go将成为AI时代重要的AI应用开发语言!AI大模型三强:OpenAI、Claude和Google都提供了对Go SDK的官方支持:

  • OpenAI Go SDK – https://github.com/openai/openai-go
  • Claude GO SDK – https://github.com/anthropics/anthropic-sdk-go
  • Google AI Go SDK – https://github.com/google/generative-ai-go

此外,提到Go和AI大模型,我们不得不提及一个重量级的开源项目——Ollama,它可以说是当前私有部署和使用开源大模型的事实标准!在2024年的用户调查报告中,Go团队还特别关注了用户对使用Go开发AI应用的需求,并将AI应用开发视为Go应用的下一个重要赛道。此外,Russ Cox也积极参与这一领域,开源了专用于开源项目运营维护的AI机器人:Oscar,同时探索Go在AI领域的应用。

如果说Go的排名再创新高让Gopher和Go社区对Go充满了更多自信,那么Go团队的换帅则向整个编程语言界展示了团队的传承与发展!

2. Go团队换帅展示团队传承

对于Go团队来说,2024年的最大的事件不是Go 1.22Go 1.23的发布,而是团队换帅

2024年中旬,Go团队的技术负责人Russ Cox宣布,他将于2024年9月1日起卸任Go项目的技术领导职务。自2008年参与Go项目以来,Russ于2012年成为其技术负责人。在过去的12年里,他引领Go语言从一个实验性项目成长为当今最受欢迎的编程语言之一。在他的带领下,Go凭借简洁的语法、高效的并发模型和强大的标准库赢得了众多开发者的青睐,并在云计算、微服务和DevOps等领域得到了广泛应用。

Russ分享了他卸任的想法,表示这一决定是经过深思熟虑的,是自然发展的结果。他认为,尽管长期稳定的领导对大型项目至关重要,但领导层的变动也能为项目注入新的活力和视角。他强调,定期更换领导者是非常重要的,这有助于引入新思想并防止项目陷入停滞。

接替Russ Cox的是Austin Clements,他将成为新的Go技术负责人,同时领导Google的Go团队和整个Go项目。Austin自2014年起就在Google从事与Go相关的工作,拥有丰富的经验和深厚的技术背景。同时,Cherry Mui将接手负责编译器和运行时等“Go核心”领域的工作。Cherry自2016年加入Google,在Go的核心开发领域表现出色。Russ Cox对这两位新领导给予了高度评价,称赞他们具备卓越的判断力以及对Go语言和其运行系统的广泛而深入的理解。

通过9月份到12月份的角色过期期的观察来看,两位“新负责人”的表现是中规中矩,沿袭了Russ Cox之前确定的Go项目管理框架,Cherry Mui在Go core领域表现的十分积极,这从”Go compiler and runtime meeting notes“的记录中可见一斑!

第333期GoTime播客中,两位新leader也初步分享了他们对后续Go演进的一些想法。

Austin强调,虽然Go保持着稳定和简洁,但它必须继续演进。他的首要目标之一是改善Go的可扩展性,无论是在开发过程中还是在背后的工程流程中。他希望通过提高透明度和扩大社区参与度,赋能社区,创建一个能够更好整合用户反馈的平台(可能是一个论坛),使贡献者能够开发与核心团队目标一致的工具和解决方案。在性能改进方面,Austin长期致力于优化Go的垃圾回收系统,目前正在试验一种新算法,幽默地称其为“绿茶”,旨在优化资源使用,进一步提升Go在越来越大系统上的扩展能力。

Cherry则指出,Go的用户基础正在快速增长,而核心团队的资源却有限。她的任务是确保Go平台能够支持这一日益增长的社区,无论是通过构建更好的API还是平台,帮助用户在Go的基础上开发更强大的工具和解决方案。在技术扩展性方面,Cherry也表达了自己的关注。随着计算能力的提升,核心数量和内存容量不断增加,Go需要适应,以高效处理更大的工作负载。Cherry表示,她非常期待与社区中的工程师合作,解决这些挑战,保持Go简单且可扩展的声誉。

从两位领导的想法与目标中,我们可以看到Go团队传承的文化。对于这样的“换帅”,Go社区应充满信心。

注:GoTime博客在完成其第340期内容后,因平台方Changelog的变动宣布停播了!

3. Go Release新特性一览

对于已经过了15个生日的Go来说,其演进的节奏已经非常稳定和成熟了。2024年,Go平稳地发布了两个重要版本:Go 1.22和Go 1.23。下面我们就来简单浏览一下这两个版本的主要新特性。

3.1 Go 1.22主要新特性

语言特性

  • loopvar语义修正:for循环中通过短声明定义的循环变量,由整个循环共享一个实例变为每次迭代定义一个实例。这是 Go 语言发展历史上第一次真正的填语义层面的“坑”。
  • for range支持整型表达式:for range循环可以遍历整型范围,如for i := range 10。

编译器和运行时

  • PGO优化增强:基于PGO的构建可以实现更高比例的调用去虚拟化(devirtualize),带来性能提升。
  • 编译器优化:编译器可以更多地运用devirtualize和inline技术进行优化。
  • 运行时优化:运行时可以使基于类型的垃圾收集的元数据更接近每个堆对象,从而降低CPU和内存开销。

工具链

  • go work支持vendor:go work命令可以管理vendor目录,并且支持使用go build -mod=vendor构建。
  • go mod init改进:不再尝试导入其他vendor工具(比如Gopkg)的配置文件。
  • go test -cover改进: 对于没有测试文件的包,会报告覆盖率为0.0%。

标准库

  • math/rand/v2: 标准库第一个V2版本包。
  • 增强http.ServeMux的表达能力: 新版ServeMux支持静态路由、通配符、主机匹配和变量捕获。

3.2 Go 1.23 主要新特性

语言特性

  • 自定义函数迭代器:for range语句支持遍历用户自定义的集合类型,需要定义满足特定签名的迭代器函数。
  • 别名中增加泛型参数:支持在类型别名定义中使用类型参数,如:
type MySlice[T any] = []T

编译器与运行时

  • PGO构建速度提升: 该版本优化后,PGO带来的编译开销显著降低。
  • 限制对linkname的使用: Go 1.23禁止使用linkname指令引用标准库中未标记的内部符号。

工具链

  • Telemetry (遥测): go工具链程序收集性能和使用数据的系统,且支持go telemetry on|off|local命令。
  • go env -changed: go env子命令增加-changed选项,可以查看当前Go环境中设置的Go环境变量值与默认值有差异的项的值。
  • go mod tidy -diff: go mod tidy增加-diff选项,只打印更新信息但不做实际更新。
  • go.mod中增加godebug指示符: 可以通过该指示符设置特定的GODEBUG选项。

标准库

  • Timer/Ticker变化: Timer和Ticker的GC不再需要Stop方法,Stop/Reset后不再接收旧值。
  • structs包: 添加一个零size的类型HostLayout,用于控制编译器对结构体类型的布局方式。
  • unique包: 新增了unique包,用于处理唯一值的集合。
  • iter包: 新增了iter包,并增加了函数迭代器相关的实用函数到maps、slices等包中。

更多更详细关于Go新特性的内容,请阅读《Go 1.22中值得关注的几个变化》和《Go 1.23中值得关注的几个变化》。

4. 2025展望

按照Go演进的一贯风格,我本不该对Go抱有过多期待^_^,但还是忍不住想说几句。

Go已经稳稳地占据了云计算领域的头部后端编程语言地位,在多个编程语言排行榜上名列前茅,Go社区也在健康快速地发展。然而,机遇与风险总是并存。

虽然Go在云原生、Web服务、微服务、API和CLI开发方面拥有明显优势,但也面临着来自Rust等语言的挑战。Go需要进一步巩固其在这些优势领域的地位,同时探索一些能够发挥自身优势的新方向,例如AI应用开发等。

同时,我们期待新一代Go团队领导者,尤其是来自Go编译器和运行时组的领导者们,能够深入打磨和优化Go语言的编译器、运行时性能以及语言互操作性。毕竟,谁不喜欢那种因性能自然增长而带来的愉悦感,以及借助其他语言优势生态快速完成功能的灵活性呢!

最后,感谢Go团队和Go社区在Go语言演进发展上做出的贡献,希望Go越走越好!


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

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

探索基于pion开发的WebRTC应用的建连过程

本文永久链接 – https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion

在《WebRTC第一课:从信令、ICE到NAT穿透的连接建立全流程》一文中,我们从理论层面全面细致地了解了WebRTC连接建立的完整流程。这个流程大致可以分为以下几个阶段:

  • 与信令服务器的交互
  • ICE候选项的采集、交换与排序
  • 形成ICE候选检查表、进行连通性检查,并最终确定最优候选路径

这个过程的复杂性不言而喻。即便多次阅读全文,读者可能仍难以形成深入的理解。因此,如果能够配上一个真实的示例,相信会更有助于读者全面把握这一过程的细节和原理。

在这篇文章中,我就为大家呈现一个真实的示例,我将使用Go语言开源WebRTC项目pion/webrtc来实现一个基于datachannel的WebRTC演示版程序,通过将pion/webrtc的日志级别设置为TRACE级,输出更多pion/webrtc实现层面的日志,以帮助大家理解WebRTC建连过程。同时,我还会实现一个简易版的基于“Room抽象模型”的信令服务器,供WebRTC通信两端交换信息使用。希望该示例能帮助大家更好的理解WebRTC端到端的建连流程。

按照WebRTC建连的流程,我们先来实现一个简易版的信令服务器。

注:提醒各位读者,本文中所有例子均以演示和帮助大家理解为目的,不建议在生产中使用示例中的代码。

1. 信令服务器(signaling-server)

下面是一个基于WebSocket的WebRTC信令服务器的简化实现,使用WebSocket进行WebRTC信令交换可以提供更快速、更高效和更灵活的通信体验,同时WebSocket生态丰富,可复用的代码库有很多,实现起来也比较简单。

这个信令服务器是基于Room抽象模型的,因此其主要结构是一个Room结构体,代表一个聊天室。我们具体看一下该信令服务器的实现代码:

// webrtc-first-lesson/part2/signaling-server/main.go

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"

    "github.com/gorilla/websocket"
)

type Room struct {
    Clients map[*websocket.Conn]bool
    mu      sync.Mutex
}

var (
    upgrader = websocket.Upgrader{
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }
    rooms  = make(map[string]*Room)
    roomMu sync.Mutex
)

func main() {
    http.HandleFunc("/ws", handleWebSocket)
    log.Println("Signaling server starting on :28080")
    log.Fatal(http.ListenAndServe(":28080", nil))
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("Error upgrading to WebSocket:", err)
        return
    }
    defer conn.Close()

    remoteAddr := conn.RemoteAddr().String()
    log.Println("New WebSocket connection from:", remoteAddr)

    roomID := r.URL.Query().Get("room")
    if roomID == "" {
        roomID = fmt.Sprintf("room_%d", len(rooms)+1)
        log.Printf("Created new room: %s\n", roomID)
    }

    roomMu.Lock()
    room, exists := rooms[roomID]
    if !exists {
        room = &Room{Clients: make(map[*websocket.Conn]bool)}
        rooms[roomID] = room
    }
    roomMu.Unlock()

    room.mu.Lock()
    room.Clients[conn] = true
    room.mu.Unlock()

    log.Printf("Client[%v] joined room %s\n", remoteAddr, roomID)

    for {
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("Error reading message:", err)
            break
        }

        var msg map[string]interface{}
        if err := json.Unmarshal(message, &msg); err != nil {
            log.Println("Error unmarshaling message:", err)
            continue
        }

        msg["roomId"] = roomID
        updatedMessage, _ := json.Marshal(msg)

        room.mu.Lock()
        for client := range room.Clients {
            if client != conn {
                clientAddr := client.RemoteAddr().String()
                if err := client.WriteMessage(messageType, updatedMessage); err != nil {
                    log.Println("Error writing message:", err)
                } else {
                    log.Printf("writing message to client[%v] ok\n", clientAddr)
                }
            }
        }
        room.mu.Unlock()
    }

    room.mu.Lock()
    delete(room.Clients, conn)
    room.mu.Unlock()
    log.Printf("Client[%v] left room %s\n", remoteAddr, roomID)
}

我们看到:Room结构体包含一个WebSocket连接的map和一个互斥锁。演示程序使用全局变量rooms(房间map)和相应的互斥锁管理房间和加入房间的连接,并在房间内进行消息广播,以保证消息能转发到参与通信的所有端(Peer)。当然,如果仅有两端在一个房间中,那么这就变成了一对一的实时通信。

这个信令服务器程序启动后,默认监听28080端口,当客户端连接时,会根据URL参数来将客户端连接加入到某个房间,如果房间号参数为空,则代表该客户端期望创建一个房间。先创建房间并加入的客户端作为answer端,等待offer端的连接。当从某个客户端连接收到消息后,会广播给房间内的其他客户端。当客户端断开连接时,便将其从房间中移除。

当然这仅是一个演示版程序,并未对历史建立的房间进行回收,同时也没有进行身份认证等安全方面的控制。

接下来,我们再来看看借助信令服务器进行端到端实时通信的端侧WebRTC应用的实现。

2. 端侧WebRTC应用(webrtc-peer)

WebRTC应用的代码通常都很“样板化”。在开发WebRTC应用程序时,信令连接、设置本地和远程描述、收集ICE候选以及转发信令消息等步骤都是一些常见且重复性较高的任务。这些步骤在不同的WebRTC应用程序中通常都大同小异。以下是这些重复性任务的一些具体步骤示例:

1) 信令连接处理
– 创建信令通道(如WebSocket连接)
– 监听连接建立、断开等事件
– 通过信令通道交换offer/answer等信令消息

2) 本地和远程描述设置
– 创建c实例
– 设置本地描述(createOffer/createAnswer)
– 设置远程描述(setRemoteDescription)

3) ICE 候选收集与交换
– 监听ICE候选事件,收集本地ICE候选
– 通过信令通道交换ICE候选信息
– 将远程ICE候选添加到RTCPeerConnection实例

4) 信令消息转发
– 接收来自远程的信令消息
– 根据消息类型,转发给本地RTCPeerConnection实例

这些基本步骤在大多数WebRTC应用程序中都是必需的。我们的示例代码也不例外,下面就是webrtc-peer程序源码,有些长,也很繁琐:

// webrtc-first-lesson/part2/webrtc-peer/main.go

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "time"

    "github.com/gorilla/websocket"
    "github.com/pion/logging"
    "github.com/pion/webrtc/v3"
)

type signalMsg struct {
    Type string `json:"type"`
    Data string `json:"data"`
}

var (
    signalingServer string
    roomID          string
)

func init() {
    flag.StringVar(&signalingServer, "server", "ws://localhost:28080/ws", "Signaling server WebSocket URL")
    flag.StringVar(&roomID, "room", "", "Room ID (leave empty to create a new room)")
    flag.Parse()
}

func main() {
    // Connect to signaling server
    signalingURL := fmt.Sprintf("%s?room=%s", signalingServer, roomID)
    conn, _, err := websocket.DefaultDialer.Dial(signalingURL, nil)
    if err != nil {
        log.Fatal("Error connecting to signaling server:", err)
    }
    defer conn.Close()
    log.Println("connect to signaling server ok")

    // Create a new RTCPeerConnection
    config := webrtc.Configuration{
        ICEServers: []webrtc.ICEServer{
            {
                URLs: []string{"stun:stun.l.google.com:19302"},
            },
        },
    }

    // 创建一个自定义的日志工厂
    loggerFactory := logging.NewDefaultLoggerFactory()
    loggerFactory.DefaultLogLevel = logging.LogLevelTrace
    //loggerFactory.DefaultLogLevel = logging.LogLevelInfo
    //loggerFactory.DefaultLogLevel = logging.LogLevelDebug

    // Enable detailed logging
    s := webrtc.SettingEngine{}
    s.LoggerFactory = loggerFactory
    s.SetICETimeouts(5*time.Second, 5*time.Second, 5*time.Second)

    api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
    peerConnection, err := api.NewPeerConnection(config)
    if err != nil {
        log.Fatal(err)
    }

    // Create a datachannel
    dataChannel, err := peerConnection.CreateDataChannel("test", nil)
    if err != nil {
        log.Fatal(err)
    }

    dataChannel.OnOpen(func() {
        log.Println("Data channel is open")
        go func() {
            for {
                err := dataChannel.SendText("Hello from " + roomID)
                if err != nil {
                    log.Println(err)
                }
                time.Sleep(5 * time.Second)
            }
        }()
    })

    dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
        log.Printf("Received message: %s\n", string(msg.Data))
    })

    // Set the handler for ICE connection state
    peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
        log.Printf("ICE Connection State has changed: %s\n", connectionState.String())
    })

    // Set the handler for Peer connection state
    peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
        log.Printf("Peer Connection State has changed: %s\n", s.String())
    })

    // Set the handler for Signaling state
    peerConnection.OnSignalingStateChange(func(s webrtc.SignalingState) {
        log.Printf("Signaling State has changed: %s\n", s.String())
    })

    // Register data channel creation handling
    peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
        log.Printf("New DataChannel %s %d\n", d.Label(), d.ID())

        d.OnOpen(func() {
            log.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID())
        })

        d.OnMessage(func(msg webrtc.DataChannelMessage) {
            log.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data))
        })
    })

    // Set the handler for ICE candidate generation
    peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
        if i == nil {
            return
        }

        candidateString, err := json.Marshal(i.ToJSON())
        if err != nil {
            log.Println(err)
            return
        }

        if writeErr := conn.WriteJSON(&signalMsg{
            Type: "candidate",
            Data: string(candidateString),
        }); writeErr != nil {
            log.Println(writeErr)
        }
    })

    // Handle incoming messages from signaling server
    go func() {
        for {
            _, rawMsg, err := conn.ReadMessage()
            if err != nil {
                log.Println("Error reading message:", err)
                return
            }
            log.Println("recv msg from signaling server")

            var msg signalMsg
            if err := json.Unmarshal(rawMsg, &msg); err != nil {
                log.Println("Error parsing message:", err)
                continue
            }
            log.Println("recv msg is", msg)

            switch msg.Type {
            case "offer":
                log.Println("recv a offer msg")
                offer := webrtc.SessionDescription{}
                if err := json.Unmarshal([]byte(msg.Data), &offer); err != nil {
                    log.Println("Error parsing offer:", err)
                    continue
                }

                if err := peerConnection.SetRemoteDescription(offer); err != nil {
                    log.Println("Error setting remote description:", err)
                    continue
                }

                answer, err := peerConnection.CreateAnswer(nil)
                if err != nil {
                    log.Println("Error creating answer:", err)
                    continue
                }

                if err := peerConnection.SetLocalDescription(answer); err != nil {
                    log.Println("Error setting local description:", err)
                    continue
                }

                answerString, err := json.Marshal(answer)
                if err != nil {
                    log.Println("Error encoding answer:", err)
                    continue
                }

                if err := conn.WriteJSON(&signalMsg{
                    Type: "answer",
                    Data: string(answerString),
                }); err != nil {
                    log.Println("Error sending answer:", err)
                }
                log.Println("send answer ok")

            case "answer":
                log.Println("recv a answer msg")
                answer := webrtc.SessionDescription{}
                if err := json.Unmarshal([]byte(msg.Data), &answer); err != nil {
                    log.Println("Error parsing answer:", err)
                    continue
                }

                if err := peerConnection.SetRemoteDescription(answer); err != nil {
                    log.Println("Error setting remote description:", err)
                }
                log.Println("set remote desc for answer ok")

            case "candidate":
                candidate := webrtc.ICECandidateInit{}
                if err := json.Unmarshal([]byte(msg.Data), &candidate); err != nil {
                    log.Println("Error parsing candidate:", err)
                    continue
                }

                if err := peerConnection.AddICECandidate(candidate); err != nil {
                    log.Println("Error adding ICE candidate:", err)
                }
                log.Println("adding ICE candidate:", candidate)
            }
        }
    }()

    // Create an offer if we are the peer to join the room
    if roomID != "" {
        offer, err := peerConnection.CreateOffer(nil)
        if err != nil {
            log.Fatal(err)
        }

        if err := peerConnection.SetLocalDescription(offer); err != nil {
            log.Fatal(err)
        }

        offerString, err := json.Marshal(offer)
        if err != nil {
            log.Fatal(err)
        }

        if err := conn.WriteJSON(&signalMsg{
            Type: "offer",
            Data: string(offerString),
        }); err != nil {
            log.Fatal(err)
        }
        log.Printf("send offer to signaling server ok\n")
    }

    // Wait forever
    select {}
}

通过代码,我们看到:这个使用Go实现的WebRTC对等连接示例程序通过WebSocket与信令服务器通信,创建和管理RTCPeerConnection,处理ICE候选、offer和answer,并实现了数据通道功能。程序支持创建新房间或加入现有房间,展示了完整的WebRTC连接建立流程,包括信令交换和ICE处理。它通过对pion/webrtc的日志级别设置让其具有详细的日志记录能力,这为我们后续通过日志分别WebRTC建连各个阶段奠定了基础。

3. 建立连接流程的日志分析

下面是实验环境的拓扑图:

webrtc-peer分别位于两台服务器上,其中Host A是一台位于NAT后面的内网主机,而HOST B则是一台位于美国的公网主机,信令服务器搭建在HOST B上,stun服务器使用的是Google提供的公网免费stun server。

下面是信令服务器和两端peer服务器的编译和启动步骤:

我们先启动信令服务器:

//在Host B上signaling-server目录下
$make
$./signaling-server
2024/08/20 21:45:50 Signaling server starting on :28080

接下来,启动Host A上的webrtc-peer程序:

//在Host A上webrtc-peer目录下
$make
$./webrtc-peer -server ws://206.189.166.16:28080/ws

这时信令服务器就会发现有新的websocket连入,并创建了room_6(这只是多次运行中的某一次的room id罢了):

2024/08/20 21:48:52 New WebSocket connection from: 47.93.3.95:17355
2024/08/20 21:48:52 Created new room: room_6
2024/08/20 21:48:52 Client[47.93.3.95:17355] joined room room_6

然后我们启动Host B上的webrtc-peer程序,将这一端加入到上面创建的room_6中:

//在Host B上webrtc-peer目录下
$make
$./webrtc-peer -room room_6 -server ws://206.189.166.16:28080/ws

这之后,信令服务器也会发现Host B上的webrtc-peer的连接。之后便开始从信令交互开始逐步实现端到端的建连。以下是对各个阶段产生的详细日志的分析:

3.1 信令服务连接(房间加入)和SDP 交互

  1. 信令连接
"2024/08/20 21:45:48 connect to signaling server ok"

以上日志表示成功连接到信令服务器。如果房间号为空,则该peer(answer)先启动并在信令服务器建立房间,然后另一个peer(offer)加入该房间,通过信令服务器交换信息。

  1. SDP交互

下面日志则是表示接收到另一个peer的offer SDP:

"2024/08/20 21:45:55 recv msg is {offer {"type":"offer","sdp":"v=0\r\no=- 2149168073199454578 1724143555 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=msid-semantic:WMS*\r\na=fingerprint:sha-256 A6:D6:AE:F3:30:0D:D8:07:D2:23:C9:A5:69:27:F2:CC:B1:8C:A4:DB:30:79:E7:62:9B:09:87:B7:68:1F:55:A7\r\na=extmap-allow-mixed\r\na=group:BUNDLE 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=setup:actpass\r\na=mid:0\r\na=sendrecv\r\na=sctp-port:5000\r\na=ice-ufrag:TYfjBFmqpgGEtKbh\r\na=ice-pwd:NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB\r\n"}}

其中”recv a offer msg”表示程序识别到收到了offer消息。而”offer := webrtc.SessionDescription{}”及后续代码则是处理offer,创建answer并发送回给另一个peer。

在WebRTC中,信令服务器用于交换SDP(Session Description Protocol)信息,SDP描述了连接的媒体信息,如编解码器、IP 地址、端口等。先启动的peer创建房间,等待offer,后加入的peer发送offer后,等待answer的回复,双方通过信令服务器交换这些信息以建立连接。

接下来,便是两端的ICE流程。

3.2 ice Candidate Gathering、Candidate Priorization以及candidate的排序列表

下面一行日志表示开始收集ICE 候选者,这里是一个host类型的候选者:

"2024/08/20 21:45:55 adding ICE candidate: {candidate:3384150427 1 udp 2130706431 206.189.166.16 52256 typ host 0xc000210230 0xc0002121fe <nil>}"

后续有多个类似的日志,分别添加不同类型的候选者,如 host、srflx(Server Reflexive)等:

2024/08/20 21:45:55 adding ICE candidate: {candidate:604015337 1 udp 2130706431 10.46.0.5 38367 typ host 0xc000210260 0xc000212250 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:3019421960 1 udp 2130706431 2604:a880:2:d0::2094:3001 48394 typ host 0xc000210290 0xc000212298 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2090009598 1 udp 2130706431 10.0.0.1 58895 typ host 0xc0002102d0 0xc0002122e0 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:233762139 1 udp 2130706431 172.17.0.1 58343 typ host 0xc000210300 0xc000212328 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2943811937 1 udp 1694498815 2604:a880:2:d0::2094:3001 40480 typ srflx raddr :: rport 40480 0xc00038c070 0xc00038e050 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2614874796 1 udp 1694498815 206.189.166.16 38534 typ srflx raddr 0.0.0.0 rport 38534 0xc000210760 0xc000212b98 <nil>}

不过,在输出的日志中,我们看到并没有明确输出我们期待的经过 Candidate Priorization(候选者优先级排序)后的候选者排序列表。

注:重温一下ICE(Interactive Connectivity Establishment),这是一种用于在两个peer之间建立连接的协议,通过收集各种类型的候选者(如 host 表示本机地址、srflx 表示通过 NAT 反射得到的地址等),增加连接成功的可能性。

3.3 ice connectivity check的各个子阶段

  1. 子阶段1:输出每一端的角色

在ICE连接中,会确定一个controlling方和一个controlled方,用于决定连接的发起和响应顺序。 下面这行输出日志表示本端不是controlling方:

"ice DEBUG: 21:45:55.401065 agent.go:395: Started agent: isControlling? false, remoteUfrag: "TYfjBFmqpgGEtKbh", remotePwd: "NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB"
  1. 子阶段2:每端输出形成的检查列表(Forming checklist)

这个阶段日志中没有明确输出检查列表,但日志中有大量的“Ping STUN from… to…”表示正在进行连接检查,这些日志汇总在一起可以看成是形成的检查列表。例如:

ice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256。

每一端都会通过发送STUN请求来检查不同候选者之间的连接性。

  1. 子阶段3: 输出针对每个列表项的连接检查结果

日志中有很多类似的日志表示收到了来自特定候选者的成功响应:

"ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115"

根据连接检查的结果,如果发现Peer Reflexive 候选,也会有相应的日志输出,比如:

ice DEBUG: 21:45:25.771665 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61194
ice DEBUG: 21:45:25.772355 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:26408
ice DEBUG: 21:45:25.775320 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:40491
ice DEBUG: 21:45:25.776894 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:5767
ice DEBUG: 21:45:25.777018 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61432
... ...

3.4 NAT穿透尝试并输出最终的最佳候选者对

日志中大量的”Ping STUN”和”Inbound STUN (SuccessResponse)”表示正在进行 NAT 穿透尝试。例如:

ice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256

ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115

通过STUN请求和响应来确定是否能够穿透NAT,如果穿透失败,则将其标记为failed:

ice TRACE: 21:45:56.274839 agent.go:550: Maximum requests reached for pair prio 9151314440652587007 (local, prio 2130706431) udp4 host 172.18.0.1:59520 <-> udp4 host 10.0.0.1:58895 (remote, prio 2130706431), state: in-progress, nominated: false, nominateOnBindingSuccess: false, marking it as failed

如果能够成功穿透,则可以建立连接。下面的日志表示选出了最终的最佳候选者对:

ice TRACE: 21:45:56.656900 agent.go:524: Set selected candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 <-> udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false
ice TRACE: 21:45:56.823017 selection.go:239: Found valid candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 <-> udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false

一旦确定了最佳候选者对,连接就算建立成功了!

接下来,就是打开datachannel通道并进行数据传输了!

3.5 data channel打开以及定时(5秒一次)的数据传输

下面日志表示数据通道已打开:

"Data channel is open"

下面日志表示创建了一个名为“test”的数据通道:

"New DataChannel test 824638605290"

下面日志表示数据通道打开成功:

"Data channel 'test'-'824638605290' open"

示例代码中,启动一个goroutine用于定时向data channel发送数据,当出现下面日志时,表示接收到来自另一个 peer 的数据:

"Message from DataChannel 'test': 'Hello from room_6'"

4. 小结

在这篇文章中,我通过使用Go语言开源项目pion/webrtc实现的webrtc端侧应用,为大家详细展示了WebRTC应用的建连过程。

首先,我实现了一个基于WebSocket的简易信令服务器。这个信令服务器基于Room抽象模型,使用全局变量来管理房间和连接,并进行消息广播。

接下来,我介绍了端侧WebRTC应用的实现。这个应用通过与信令服务器通信,创建RTCPeerConnection,处理ICE候选、offer和answer,以及实现数据通道功能。我还通过设置TRACE日志级别,展示了详细的建连流程。

之后,我在实验环境的实际执行了上述程序,并通过对日志的分析展示了建连过程。这些分析涵盖了信令服务连接和SDP交互、ICE候选收集与优先级排序、ICE 连通性检查各子阶段、NAT穿透尝试及最佳候选者对确定,以及数据通道打开和数据传输。希望这样的分析可以帮助大家更深刻的理解和体会建连过程。

WebRTC网络结构和建连就先讲到这里,后面的系列文章中,我们会开始聚焦WebRTC技术栈的另外一个主要方面:音视频质量,包括编码器以及媒体流处理等。

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


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

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats