pion ice项目源码分析

前言

git 地址https://github.com/pion/ice
ice 流程的介绍博客https://www.rongcloud.cn/blog/?p=4178
整个源码分析会直接根据ice中的example来走,该博文是建立在你对ice流程有一定的了解后,对ice的实现的简介。

创建Agent

以下是比较重要的代码

func NewAgent(config *AgentConfig) (*Agent, error) {
     
    ...
    startedCtx, startedFn := context.WithCancel(context.Background())
    ...
    //开启任务循环
    go a.taskLoop()
    //开启状态该改变
    a.startOnConnectionStateChangeRoutine()

    // 第一次初始化agent对象
    if err := a.Restart(config.LocalUfrag, config.LocalPwd); err != nil {
     
        closeMDNSConn()
        _ = a.Close()
        return nil, err
    }
}

startedCtx和startedFn会用来标志开始进行联通行检查,这个阶段暂时用不到。
会开启一个taskLoop协程,用来让所有需要序列化执行的任务放到这个任务队列中去执行。

func (a *Agent) taskLoop() {
     
    //用于执行一些在任务执行完成后的函数
    after := func() {
     
        for {
     
            // Get and run func registered by afterRun().
            fns := a.getAfterRunFn()
            ...
            for _, fn := range fns {
     
                fn(a.context())
            }
        }
    }
    //在退出这个函数后删除所有的元素
    defer func() {
     
        a.deleteAllCandidates()
        ....
    }()

    for {
     
        select {
     
        //及时关闭
        case <-a.done:
            return
        case t := <-a.chanTask:
            //获得执行任务,并且执行
            t.fn(a.context(), a)
            close(t.done)
            after()
        }
    }
}

startOnConnectionStateChangeRoutine用于开启一些状态修改的协程

func (a *Agent) startOnConnectionStateChangeRoutine() {
     
    go func() {
     
        for {
     
            p, isOpen := <-a.chanCandidatePair
            ...
            //用于通知选择的CandidatePair发生变化
            a.onSelectedCandidatePairChange(p)
        }
    }()
    go func() {
     
        for {
     
            select {
     
            case s, isOpen := <-a.chanState:
                ...
                //通知有状态变化
                a.onConnectionStateChange(s)
            case c, isOpen := <-a.chanCandidate:
                ...
                //通知有Candidate收集
                a.onCandidate(c)
            }
        }
    }()
}

最后调用Restart用于初始化agent对象

func (a *Agent) Restart(ufrag, pwd string) error {
     
    ...
    //启动一个任务,run函数就是将一个任务提交一开始启动的taskloop中序列化执行
    //任务中就是初始化一些变量
    if runErr := a.run(a.context(), func(ctx context.Context, agent *Agent) {
     
        ...
        
        agent.localUfrag = ufrag
        agent.localPwd = pwd
        agent.remoteUfrag = ""
        agent.remotePwd = ""
        a.gatheringState = GatheringStateNew
        a.checklist = make([]*CandidatePair, 0)
        a.pendingBindingRequests = make([]bindingRequest, 0)
        a.setSelectedPair(nil)
        a.deleteAllCandidates()
        ...
    }); runErr != nil {
     
        return runErr
    }
    return err
}

开始收集本地的Candidate

调用GatherCandidates后比较简单,开启gatherCandidates协程收集本地所有类型的Candidate

func (a *Agent) GatherCandidates() error {
     
    var gatherErr error
    if runErr := a.run(a.context(), func(ctx context.Context, agent *Agent) {
     
        //一些错误判断
        ...
        ctx, cancel := context.WithCancel(ctx)
        a.gatherCandidateCancel = cancel
        //开启收集自己的Candidate
        go a.gatherCandidates(ctx)
    }); runErr != nil {
     
        return runErr
    }
    return gatherErr
}

在gatherCandidates中,收集配置所有类型的Candidate

func (a *Agent) gatherCandidates(ctx context.Context) {
     
    //获取配置中所有的Candidate类型
    for _, t := range a.candidateTypes {
     
        switch t {
     
        case CandidateTypeHost:
            wg.Add(1)
            go func() {
     
                a.gatherCandidatesLocal(ctx, a.networkTypes)
                wg.Done()
            }()
        case CandidateTypeServerReflexive:
            wg.Add(1)
            go func() {
     
                a.gatherCandidatesSrflx(ctx, a.urls, a.networkTypes)
                wg.Done()
            }()
            if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeServerReflexive {
     
                wg.Add(1)
                go func() {
     
                    a.gatherCandidatesSrflxMapped(ctx, a.networkTypes)
                    wg.Done()
                }()
            }
        case CandidateTypeRelay:
            wg.Add(1)
            go func() {
     
                a.gatherCandidatesRelay(ctx, a.urls)
                wg.Done()
            }()
        case CandidateTypePeerReflexive, CandidateTypeUnspecified:
        }
    }
    // 等待所有协程返回
    wg.Wait()

gatherCandidatesLocal会检查是否有端口复用,MDNS地址,自定义地址。
udp端口复用是所有连接走一个端口,对于webrtc sfu服务器架构来说是一个很好的方案。
MDNS地址是一种局域网内的组播DNS技术。
自定义地址是对于公网服务器。
最后调用addCandidate将收集到的Candidate和创建的PacketConn传入

func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []NetworkType) {
      //nolint:gocognit
    // udpMux是一个端口复用,他可以让所有的Candidate都走一个端口,这如果对webrtc中SFU是非常有用的
    if a.udpMux != nil {
     
        //获取端口复用的Candidate
        if err := a.gatherCandidatesLocalUDPMux(ctx); err != nil {
     
            a.log.Warnf("could not create host candidate for UDPMux")
        }
        //删除udp,后面不需要再去获取udp的Candidate
        delete(networks, udp)
    }

    //获取本地地址
    localIPs, err := localInterfaces(a.net, a.interfaceFilter, networkTypes)
    if err != nil {
     
        a.log.Warnf("failed to iterate local interfaces, host candidates will not be gathered %s", err)
        return
    }

    //遍历所有收集到的本地地址
    for _, ip := range localIPs {
     
        mappedIP := ip
        //检查是否有自定义的ip
        if a.mDNSMode != MulticastDNSModeQueryAndGather && a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost {
     
            if _mappedIP, err := a.extIPMapper.findExternalIP(ip.String()); err == nil {
     
                mappedIP = _mappedIP
            } else {
     
                a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s\n", ip.String())
            }
        }

        address := mappedIP.String()
        //检查是否使用MDNS地址
        if a.mDNSMode == MulticastDNSModeQueryAndGather {
     
            address = a.mDNSName
        }

        //观察是否需要udp或者tcp,注意如果前面使用了udpmux这边是没有udp
        for network := range networks {
     
            var port int
            var conn net.PacketConn
            var err error
            var tcpType TCPType

            switch network {
     
            case tcp:
                ...
                //所有tcp都是走的端口复用
                conn, err = a.tcpMux.GetConnByUfrag(a.localUfrag)
                port = conn.LocalAddr().(*net.TCPAddr).Port
                tcpType = TCPTypePassive
                ...
            case udp:
                //如果是udp,开始监听对应的端口
                conn, err = listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{
     IP: ip, Port: 0})
                ...
                port = conn.LocalAddr().(*net.UDPAddr).Port
            }
            //创建Candidate Host 配置
            hostConfig := CandidateHostConfig{
     
                Network:   network,
                Address:   address,
                Port:      port,
                Component: ComponentRTP,
                TCPType:   tcpType,
            }

            //创建一个完整的Candidate
            c, err := NewCandidateHost(&hostConfig)
            if err != nil {
     
                closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err))
                continue
            }

            //添加完整的Candidate,并且加入刚刚监听的链接
            if err := a.addCandidate(ctx, c, conn); err != nil {
     
                if closeErr := c.close(); closeErr != nil {
     
                    a.log.Warnf("Failed to close candidate: %v", closeErr)
                }
                a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
            }
        }
    }
}

在addCandidate中
调用start开始处理这个Candidate所有接受到的消息
并且配对上所有的remote candidate,进行连通性检查

func (a *Agent) addCandidate(ctx context.Context, c Candidate, candidateConn net.PacketConn) error {
     
    return a.run(ctx, func(ctx context.Context, agent *Agent) {
     
        //开始处理这个Candidate收到的所有消息
        c.start(a, candidateConn, a.startedCh)
        set := a.localCandidates[c.NetworkType()]
        ...
        set = append(set, c)
        a.localCandidates[c.NetworkType()] = set
        //给新加入的LocalCandidate的配对remote candidate
        //并且马上进行连通性检查请求
        if remoteCandidates, ok := a.remoteCandidates[c.NetworkType()]; ok {
     
            for _, remoteCandidate := range remoteCandidates {
     
                a.addPair(c, remoteCandidate)
            }
        }
        //因为有新的Pair加入,请求进行联通性检查
        a.requestConnectivityCheck() 
        //chanCandidate通知有新的Candidate加入
        a.chanCandidate <- c
    })
}

在start中
启动一个协程运行recvLoop,这个recvLoop是用于处理该Candidate收到的消息。例如接收到bind request就是在这个协程里面处理
在recvLoop中会等initializedCh管道有消息才会开启消息循环进入事件处理,initializedCh就是我们前面说的startedCtx.Done()返回的管道。当startedFn被调用时,startedCtx.Done()就会接收到消息。在开始连通性检查时startedFn才会被调用。

func (c *candidateBase) start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{
     }) {
     
    c.currAgent = a
    c.conn = conn
    ...
    go c.recvLoop(initializedCh)
}

func (c *candidateBase) recvLoop(initializedCh <-chan struct{
     }) {
     
    defer func() {
     
        close(c.closedCh)
    }()

    select {
     
    case <-initializedCh: //当agent开始check activity的时候就会打开
    case <-c.closeCh:
        return
    }

    buffer := make([]byte, receiveMTU)
    for {
      //开启check
        n, srcAddr, err := c.conn.ReadFrom(buffer)
        if err != nil {
     
            return
        }
        //对应的ICE流程处理,例如:bind request,bind response等
        handleInboundCandidateMsg(c, c, buffer[:n], srcAddr, log)
    }
}

收集local Host Candidate流程就是这些。local ServerReflexive Candidate流程也是一样的,只不过一个是从Stun服务器获取ip,后续的处理流程都是一样的。

func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*URL, networkTypes []NetworkType) {
     
    //检查所有网络类型
    for _, networkType := range networkTypes {
     
        if networkType.IsTCP() {
     
            //没有做tcp协议的请求
            continue
        }
        //遍历所有Stun的服务器
        for i := range urls {
     
            wg.Add(1)
            go func(url URL, network string) {
     
                defer wg.Done()
                ...
                //监听接收udp port
                conn, err := listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{
     IP: nil, Port: 0})
               ...
                //请求stun服务器,获取到外网ip地址
                xoraddr, err := getXORMappedAddr(conn, serverAddr, stunGatherTimeout)
                ...
                srflxConfig := CandidateServerReflexiveConfig{
     
                    Network:   network,
                    Address:   ip.String(),
                    Port:      port,
                    Component: ComponentRTP,
                    RelAddr:   laddr.IP.String(),
                    RelPort:   laddr.Port,
                }
                //创建具体的Candidate
                c, err := NewCandidateServerReflexive(&srflxConfig)
                ...
                //加入这个Candidate
                if err := a.addCandidate(ctx, c, conn); err != nil {
     
                    if closeErr := c.close(); closeErr != nil {
     
                        a.log.Warnf("Failed to close candidate: %v", closeErr)
                    }
                    a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
                }
            }(*urls[i], networkType.String())
        }
    }
}

选择连接

选择连接有两个函数,根据不同的角色做选择。在调用这两个函数的之前你必须将Remote Candidate通过AddRemoteCandidate添加进去,并且设置好Remote的UFrag和Pwd

func (a *Agent) Dial(ctx context.Context, remoteUfrag, remotePwd string) (*Conn, error) {
     
    return a.connect(ctx, true, remoteUfrag, remotePwd)
}
func (a *Agent) Accept(ctx context.Context, remoteUfrag, remotePwd string) (*Conn, error) {
     
    return a.connect(ctx, false, remoteUfrag, remotePwd)
}

步骤都是跟着标准的ICE流程走的,我们看下当为Controlling角色时候的流程。
会去做请求做连通性检测
当连通性检测完成,并且有连接被选中的时候ctx.Done()管道会收到消息,

func (a *Agent) connect(ctx context.Context, isControlling bool, remoteUfrag, remotePwd string) (*Conn, error) {
     
    ...
    //开始做连通性检查
    err = a.startConnectivityChecks(isControlling, remoteUfrag, remotePwd)
    ...
    // 会阻塞到连通性检测完成,并且有连接被选上的时候,ctx.Done()会收到消息:
    select {
     
    case <-a.done:
        return nil, a.getErr()
    case <-ctx.Done():
        return nil, ErrCanceledByCaller
    case <-a.onConnected:
    }
    //返回结果
    return &Conn{
     
        agent: a,
    }, nil
}

在startConnectivityChecks中
根据不同的角色创建不同的selector
调用startedFn()用于开始连通性检测

func (a *Agent) startConnectivityChecks(isControlling bool, remoteUfrag, remotePwd string) error {
     
    ...
    return a.run(a.context(), func(ctx context.Context, agent *Agent) {
     
        if isControlling {
     
            a.selector = &controllingSelector{
     agent: a, log: a.log}
        } else {
     
            a.selector = &controlledSelector{
     agent: a, log: a.log}
        }
        if a.lite {
     
            a.selector = &liteSelector{
     pairCandidateSelector: a.selector}
        }

        a.selector.Start()
        a.startedFn()//表示已经开始,这个时候所有在addCandidate时候开启的接受事件都会开始接受
        agent.updateConnectionState(ConnectionStateChecking)
        //请求开始连通性检查
        a.requestConnectivityCheck()
        //开启联通性检查的协程
        go a.connectivityChecks()
    })
}

在connectivityChecks中
会根据不同的状态创建不同时间的定时器,用于触发连通性检查的流程。
触发连通性检查有两种流程1.是定时器到了。2.是调用requestConnectivityCheck()时,会往检查forceCandidateContact管道中输入一个变量,这个时候会停止定时器,然后执行连通性检查流程。
执行contact,会执行不同角色selector的ContactCandidates()函数。

func (a *Agent) connectivityChecks() {
     
    //创建定时器,用于定时发送固定的数据包做连通性检测
    checkingDuration := time.Time{
     }

    contact := func() {
     
        if err := a.run(a.context(), func(ctx context.Context, a *Agent) {
     
            ...
            switch a.connectionState {
     
            case ConnectionStateFailed:
                //连接失败,直接返回
                return
            case ConnectionStateChecking:
                //正在检查中
                if lastConnectionState != a.connectionState {
     
                    checkingDuration = time.Now()
                }
                //已经超时,返回连接失败
                if time.Since(checkingDuration) > a.disconnectedTimeout+a.failedTimeout {
     
                    a.updateConnectionState(ConnectionStateFailed)
                    return
                }
            }
            //正在检测,并且没有超时,调用特定的selector,执行连通性检查
            a.selector.ContactCandidates()
        }); err != nil {
     
            a.log.Warnf("taskLoop failed: %v", err)
        }
    }
    //开启时钟,用于检查联通性
    for {
     
        ...
        //根据不同的状态,更新不同时间
        switch lastConnectionState {
     
        case ConnectionStateNew, ConnectionStateChecking: // While connecting, check candidates more frequently
            updateInterval(a.checkInterval)
        case ConnectionStateConnected, ConnectionStateDisconnected:
            updateInterval(a.keepaliveInterval)
        default:
        }
        //创建定时器
        t := time.NewTimer(interval)
        select {
     
        //强制刷新请求
        case <-a.forceCandidateContact:
            t.Stop()
            contact()
        //到定时时间,开始流程
        case <-t.C:
            contact()
        //选择完成
        case <-a.done:
            t.Stop()
            return
        }
    }
}

我们挑选controlling角色selector的ContactCandidates()函数解读,判断的流程大概如下:

  1. 检查是否已经选中的Pair。
  2. 检查有正在选的Pair。
  3. 没有挑选中的Pair,那么就选出一个最好的,将这个设置为挑选的Pair,发送请求包。
  4. 如果发现这个最优的已经超时了,那么会对所有的Pair发送请求包。
func (s *controllingSelector) ContactCandidates() {
     
    switch {
     
    case s.agent.getSelectedPair() != nil:
        if s.agent.validateSelectedPair() {
     
            s.log.Trace("checking keepalive")
            s.agent.checkKeepalive()
        }
    case s.nominatedPair != nil:
        s.nominatePair(s.nominatedPair)
    default:
        p := s.agent.getBestValidCandidatePair()
        if p != nil && s.isNominatable(p.Local) && s.isNominatable(p.Remote) {
     
            s.log.Tracef("Nominatable pair found, nominating (%s, %s)", p.Local.String(), p.Remote.String())
            p.nominated = true
            s.nominatedPair = p
            s.nominatePair(p)
            return
        }
        s.agent.pingAllCandidates()
    }
}

总结

到这就所有的流程结束了,整个流程就是通过GatherCandidates开始收集local的Candidate,并且会在add的时候开启接受消息的协程。当设置完成remote Candidate后,调用Dial或者Accepte才会开启连通性检查,挑选最合适的Pair

你可能感兴趣的:(webrtc,webrtc,rtc)