git 地址https://github.com/pion/ice
ice 流程的介绍博客https://www.rongcloud.cn/blog/?p=4178
整个源码分析会直接根据ice中的example来走,该博文是建立在你对ice流程有一定的了解后,对ice的实现的简介。
以下是比较重要的代码
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
}
调用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()函数解读,判断的流程大概如下:
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