美文网首页WebRTC
WebRTC之STUN、TURN和ICE研究

WebRTC之STUN、TURN和ICE研究

作者: 云上听风 | 来源:发表于2018-04-15 17:34 被阅读38次

    准备查看WebRTC源码对应以下这些文章中的协议格式深入研究一下ICE。

    这三篇文章是目前我看过的最好的ICE文章:
    P2P通信标准协议(一)之STUN
    P2P通信标准协议(二)之TURN
    P2P通信标准协议(三)之ICE

    这个可以做为补充:
    P2P技术详解(三):P2P技术之STUN、TURN、ICE详解


    先学习上面文章的基础知识,然后开始分析WebRTC创建PeerConnection直到连接Stun和Turn的流程:

    bool PeerConnection::InitializePortAllocator_n(
    ...
      if (ParseIceServers(configuration.servers, &stun_servers, &turn_servers) !=
    ...
    port_allocator_->SetConfiguration(
          stun_servers, turn_servers, configuration.ice_candidate_pool_size,
          configuration.prune_turn_ports, configuration.turn_customizer,
          configuration.stun_candidate_keepalive_interval);
    ...
    }
    
    bool PortAllocator::SetConfiguration(
    ...
      stun_servers_ = stun_servers;
      turn_servers_ = turn_servers;
    ...
    // If |candidate_pool_size_| is greater than the number of pooled sessions,
      // create new sessions.
      while (static_cast<int>(pooled_sessions_.size()) < candidate_pool_size_) {
        PortAllocatorSession* pooled_session = CreateSessionInternal("", 0, "", "");
        pooled_session->StartGettingPorts();
        pooled_sessions_.push_back(
            std::unique_ptr<PortAllocatorSession>(pooled_session));
      }
      return true;
    }
    

    PeerConnection在初始化时创建了port_allocator_,同时调用了PortAllocator::SetConfiguration把stun_servers和turn_servers存储起来。
    并且调用了BasicPortAllocatorSession::StartGettingPorts()


    void BasicPortAllocatorSession::StartGettingPorts() {
    ...
    network_thread_->Post(RTC_FROM_HERE, this, MSG_CONFIG_START);
    ...
    }
    
    void BasicPortAllocatorSession::OnMessage(rtc::Message *message) {
      switch (message->message_id) {
      case MSG_CONFIG_START:
        RTC_DCHECK(rtc::Thread::Current() == network_thread_);
        GetPortConfigurations();
    ...
    }
    
    
    void BasicPortAllocatorSession::GetPortConfigurations() {
      PortConfiguration* config = new PortConfiguration(allocator_->stun_servers(),
                                                        username(),
                                                        password());
    
      for (const RelayServerConfig& turn_server : allocator_->turn_servers()) {
        config->AddRelay(turn_server);
      }
      ConfigReady(config);
    }
    
    void BasicPortAllocatorSession::ConfigReady(PortConfiguration* config) {
      network_thread_->Post(RTC_FROM_HERE, this, MSG_CONFIG_READY, config);
    }
    
    void BasicPortAllocatorSession::OnMessage(rtc::Message *message) {
      switch (message->message_id) {
    ...
      case MSG_CONFIG_READY:
        RTC_DCHECK(rtc::Thread::Current() == network_thread_);
        OnConfigReady(static_cast<PortConfiguration*>(message->pdata));
        break;
    ...
    }
    
    // Adds a configuration to the list.
    void BasicPortAllocatorSession::OnConfigReady(PortConfiguration* config) {
      if (config) {
        configs_.push_back(config);
      }
    
      AllocatePorts();
    }
    

    ...

    // For each network, see if we have a sequence that covers it already.  If not,
    // create a new sequence to create the appropriate ports.
    void BasicPortAllocatorSession::DoAllocate(bool disable_equivalent) {
    ...
     AllocationSequence* sequence =
              new AllocationSequence(this, networks[i], config, sequence_flags);
          sequence->SignalPortAllocationComplete.connect(
              this, &BasicPortAllocatorSession::OnPortAllocationComplete);
          sequence->Init();
          sequence->Start();
          sequences_.push_back(sequence);
    ...
    }
    

    一路把PortConfiguration *config传进来,创建AllocationSequence* sequence,并且调用了Start()方法


    void AllocationSequence::Start() {
      state_ = kRunning;
      session_->network_thread()->Post(RTC_FROM_HERE, this, MSG_ALLOCATION_PHASE);
      // Take a snapshot of the best IP, so that when DisableEquivalentPhases is
      // called next time, we enable all phases if the best IP has since changed.
      previous_best_ip_ = network_->GetBestIP();
    }
    
    void AllocationSequence::OnMessage(rtc::Message* msg) {
      RTC_DCHECK(rtc::Thread::Current() == session_->network_thread());
      RTC_DCHECK(msg->message_id == MSG_ALLOCATION_PHASE);
    
      const char* const PHASE_NAMES[kNumPhases] = {"Udp", "Relay", "Tcp"};
    
      // Perform all of the phases in the current step.
      RTC_LOG(LS_INFO) << network_->ToString()
                       << ": Allocation Phase=" << PHASE_NAMES[phase_];
    
      switch (phase_) {
        case PHASE_UDP:
          CreateUDPPorts();
          CreateStunPorts();
          break;
    
        case PHASE_RELAY:
          CreateRelayPorts();
          break;
    
        case PHASE_TCP:
          CreateTCPPorts();
          state_ = kCompleted;
          break;
    
        default:
          RTC_NOTREACHED();
      }
    
      if (state() == kRunning) {
        ++phase_;
        session_->network_thread()->PostDelayed(RTC_FROM_HERE,
                                                session_->allocator()->step_delay(),
                                                this, MSG_ALLOCATION_PHASE);
      } else {
        // If all phases in AllocationSequence are completed, no allocation
        // steps needed further. Canceling  pending signal.
        session_->network_thread()->Clear(this, MSG_ALLOCATION_PHASE);
        SignalPortAllocationComplete(this);
      }
    }
    

    phase_默认为0,也就是PHASE_UDP,然后只要state() == kRunning还会++phase不断发送MSG_ALLOCATION_PHASE消息延迟回调自己,也就是 CreateRelayPorts();和 CreateTCPPorts();都会被调用。延迟的时间为session_->allocator()->step_delay(),被设置成kMinimumStepDelay,也就是说只有50毫秒间隔就执行。

    // As per RFC 5245 Appendix B.1, STUN transactions need to be paced at certain
    // internal. Less than 20ms is not acceptable. We choose 50ms as our default.
    const uint32_t kMinimumStepDelay = 50;
    

    有人在代码中注释说这个间隔太短,导致同时有太多的STUN连接:

      // Delay between different candidate gathering phases (UDP, TURN, TCP).
      // Defaults to 1 second, but PeerConnection sets it to 50ms.
      // TODO(deadbeef): Get rid of this. Its purpose is to avoid sending too many
      // STUN transactions at once, but that's already happening if you configure
      // multiple STUN servers or have multiple network interfaces. We should
      // implement some global pacing logic instead if that's our goal.
      uint32_t step_delay() const { return step_delay_; }
      void set_step_delay(uint32_t delay) { step_delay_ = delay; }
    

    CreateUDPPorts();创建了UDPPort,并且调用session_->AddAllocatedPort(port, this, true);
    CreateStunPorts();创建了StunPort,也调用了session_->AddAllocatedPort(port, this, true);
    StunPort继承于UDPPort。
    UDPPort的type_为LOCAL_PORT_TYPE,而StunPort的type为STUN_PORT_TYPE。

    暂时还不知道为什么要创建UDPPort和StunPort,按道理一个UDPPort或者StunPort就可以了。
    补充:仔细看了一下CreateStunPorts():

    void AllocationSequence::CreateStunPorts() {
    ...
     if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET)) {
        return;
      }
    ...
    

    而PeerConnection初始化时设置了PORTALLOCATOR_ENABLE_SHARED_SOCKET,所以CreateStunPorts()其实不会执行,只有UDPPort是有效的。


    void BasicPortAllocatorSession::AddAllocatedPort(Port* port,
                                                     AllocationSequence * seq,
                                                     bool prepare_address) {
      if (!port)
        return;
    
      RTC_LOG(LS_INFO) << "Adding allocated port for " << content_name();
      port->set_content_name(content_name());
      port->set_component(component());
      port->set_generation(generation());
      if (allocator_->proxy().type != rtc::PROXY_NONE)
        port->set_proxy(allocator_->user_agent(), allocator_->proxy());
      port->set_send_retransmit_count_attribute(
          (flags() & PORTALLOCATOR_ENABLE_STUN_RETRANSMIT_ATTRIBUTE) != 0);
    
      PortData data(port, seq);
      ports_.push_back(data);
    
      port->SignalCandidateReady.connect(
          this, &BasicPortAllocatorSession::OnCandidateReady);
      port->SignalPortComplete.connect(this,
          &BasicPortAllocatorSession::OnPortComplete);
      port->SignalDestroyed.connect(this,
          &BasicPortAllocatorSession::OnPortDestroyed);
      port->SignalPortError.connect(
          this, &BasicPortAllocatorSession::OnPortError);
      RTC_LOG(LS_INFO) << port->ToString()
                       << ": Added port to allocator";
    
      if (prepare_address)
        port->PrepareAddress();
    }
    

    UDPPort和StunPort的PrepareAddress()都调用了UDPPort::SendStunBindingRequests()

    
    void UDPPort::SendStunBindingRequests() {
      // We will keep pinging the stun server to make sure our NAT pin-hole stays
      // open until the deadline (specified in SendStunBindingRequest).
      RTC_DCHECK(requests_.empty());
    
      for (ServerAddresses::const_iterator it = server_addresses_.begin();
           it != server_addresses_.end(); ++it) {
        SendStunBindingRequest(*it);
      }
    }
    
    
    void UDPPort::SendStunBindingRequest(const rtc::SocketAddress& stun_addr) {
    ...
          requests_.Send(
              new StunBindingRequest(this, stun_addr, rtc::TimeMillis()));
    ...
    }
    

    StunBindingRequest继承于StunRequest,其内部有个StunMessage* msg_
    StunMessage封装了Stun客户端协议。
    源码对照P2P通信标准协议(一)之STUN所写的STUN协议格式。

    第一条发出的消息的type_为STUN_BINDING_REQUEST,值为0x0001。


    关于STUN Message Type分解成以下结构

     0                 1
     2  3  4 5 6 7 8 9 0 1 2 3 4 5
    +--+--+-+-+-+-+-+-+-+-+-+-+-+-+
    |M |M |M|M|M|C|M|M|M|C|M|M|M|M|
    |11|10|9|8|7|1|6|5|4|0|3|2|1|0|
    +--+--+-+-+-+-+-+-+-+-+-+-+-+-+
    

    其中显示的位为从最高有效位M11到最低有效位M0,M11到M0表示方法的12位编码。C1和C0两位表示类的编码。比如对于binding方法来说,
    0b00表示request,0b01表示indication,0b10表示success response,0b11表示error response,每一个method都有可能对应不同的传输类别。

    以上0b是二进制前缀(对应16进制的0x),所以0b00也就是C1C0都为0,0b11则C1==1,C0==1。
    所以最多只有4种类别,查看C1C0就可以知道是哪种类别。

    RFC5389文档说明:
    For example, a Binding request has class=0b00 (request) and
    method=0b000000000001 (Binding) and is encoded into the first 16 bits
    as 0x0001. A Binding response has class=0b10 (success response) and
    method=0b000000000001, and is encoded into the first 16 bits as
    0x0101.


    bool StunMessage::Write(ByteBufferWriter* buf) const {
      buf->WriteUInt16(type_);
      buf->WriteUInt16(length_);
      if (!IsLegacy())
        buf->WriteUInt32(stun_magic_cookie_);
      buf->WriteString(transaction_id_);
    
      for (const auto& attr : attrs_) {
        buf->WriteUInt16(attr->type());
        buf->WriteUInt16(static_cast<uint16_t>(attr->length()));
        if (!attr->Write(buf)) {
          return false;
        }
      }
    
      return true;
    }
    

    StunMessage::Write封装了要发出的数据包。

    StunRequest::StunRequest(){
    ...
    msg_->SetTransactionID(
          rtc::CreateRandomString(kStunTransactionIdLength));
    ...
    }
    

    transaction_id_是事务ID, 可以看出transaction_id_的值是个随机字符串。
    写入transaction_id_后再写入所有STUN属性。
    STUN属性的基类为StunAttribute,其派生了好几个类。
    StunAttribute::Create中列出了所有派生类。

    StunAttribute* StunAttribute::Create(StunAttributeValueType value_type,
                                         uint16_t type,
                                         uint16_t length,
                                         StunMessage* owner) {
      switch (value_type) {
        case STUN_VALUE_ADDRESS:
          return new StunAddressAttribute(type, length);
        case STUN_VALUE_XOR_ADDRESS:
          return new StunXorAddressAttribute(type, length, owner);
        case STUN_VALUE_UINT32:
          return new StunUInt32Attribute(type);
        case STUN_VALUE_UINT64:
          return new StunUInt64Attribute(type);
        case STUN_VALUE_BYTE_STRING:
          return new StunByteStringAttribute(type, length);
        case STUN_VALUE_ERROR_CODE:
          return new StunErrorCodeAttribute(type, length);
        case STUN_VALUE_UINT16_LIST:
          return new StunUInt16ListAttribute(type, length);
        default:
          return NULL;
      }
    }
    

    UDPPort::OnReadPacket处理服务器返回的STUN消息。
    调用基类方法Port::OnReadPacket验证如果是有效的STUN回包则解包到IceMessage。
    IceMessage派生于StunMessage。


    事实上在查看STUN客户端的代码过程中可以看出不仅仅有STUN客户端功能,同时还有ICE功能。当客户端双方Offer-Answer得到sdp,并且交换Candidate之后,就会启用ICE功能,双方使用STUN协议来打通NAT,此时跟外部的STUN服务器已经没有关系了。


    下面继续分析TURN客户端代码,从CreateRelayPorts()开始:
    void AllocationSequence::CreateRelayPorts() {
    ...
      for (RelayServerConfig& relay : config_->relays) {
        if (relay.type == RELAY_GTURN) {
          CreateGturnPort(relay);
        } else if (relay.type == RELAY_TURN) {
          CreateTurnPort(relay);
        } else {
          RTC_NOTREACHED();
        }
      }
    }
    

    CreateGturnPort针对的是google自己的TURN服务,CreateTurnPort针对的是标准的TURN服务。以下只分析CreateTurnPort。

    void AllocationSequence::CreateTurnPort(const RelayServerConfig& config) {
      PortList::const_iterator relay_port;
      for (relay_port = config.ports.begin();
           relay_port != config.ports.end(); ++relay_port) {
        // Skip UDP connections to relay servers if it's disallowed.
        if (IsFlagSet(PORTALLOCATOR_DISABLE_UDP_RELAY) &&
            relay_port->proto == PROTO_UDP) {
          continue;
        }
    
        // Do not create a port if the server address family is known and does
        // not match the local IP address family.
        int server_ip_family = relay_port->address.ipaddr().family();
        int local_ip_family = network_->GetBestIP().family();
        if (server_ip_family != AF_UNSPEC && server_ip_family != local_ip_family) {
          RTC_LOG(LS_INFO)
              << "Server and local address families are not compatible. "
                 "Server address: " << relay_port->address.ipaddr().ToString()
              << " Local address: " << network_->GetBestIP().ToString();
          continue;
        }
    
        CreateRelayPortArgs args;
        args.network_thread = session_->network_thread();
        args.socket_factory = session_->socket_factory();
        args.network = network_;
        args.username = session_->username();
        args.password = session_->password();
        args.server_address = &(*relay_port);
        args.config = &config;
        args.origin = session_->allocator()->origin();
        args.turn_customizer = session_->allocator()->turn_customizer();
    
        std::unique_ptr<cricket::Port> port;
        // Shared socket mode must be enabled only for UDP based ports. Hence
        // don't pass shared socket for ports which will create TCP sockets.
        // TODO(mallinath) - Enable shared socket mode for TURN ports. Disabled
        // due to webrtc bug https://code.google.com/p/webrtc/issues/detail?id=3537
        if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET) &&
            relay_port->proto == PROTO_UDP && udp_socket_) {
          port = session_->allocator()->relay_port_factory()->Create(
              args, udp_socket_.get());
    
          if (!port) {
            RTC_LOG(LS_WARNING)
                << "Failed to create relay port with "
                << args.server_address->address.ToString();
            continue;
          }
    
          relay_ports_.push_back(port.get());
          // Listen to the port destroyed signal, to allow AllocationSequence to
          // remove entrt from it's map.
          port->SignalDestroyed.connect(this, &AllocationSequence::OnPortDestroyed);
        } else {
          port = session_->allocator()->relay_port_factory()->Create(
              args,
              session_->allocator()->min_port(),
              session_->allocator()->max_port());
    
          if (!port) {
            RTC_LOG(LS_WARNING)
                << "Failed to create relay port with "
                << args.server_address->address.ToString();
            continue;
          }
        }
        RTC_DCHECK(port != NULL);
        session_->AddAllocatedPort(port.release(), this, true);
      }
    }
    

    主要在于:

     port = session_->allocator()->relay_port_factory()->Create(
              args, udp_socket_.get());
    
    std::unique_ptr<Port> TurnPortFactory::Create(
        const CreateRelayPortArgs& args,
        int min_port,
        int max_port) {
    
      TurnPort* port = TurnPort::Create(
          args.network_thread,
          args.socket_factory,
          args.network,
          min_port,
          max_port,
          args.username,
          args.password,
          *args.server_address,
          args.config->credentials,
          args.config->priority,
          args.origin,
          args.config->tls_alpn_protocols,
          args.config->tls_elliptic_curves,
          args.turn_customizer);
      port->SetTlsCertPolicy(args.config->tls_cert_policy);
      return std::unique_ptr<Port>(port);
    }
    

    TurnPortFactory创建TurnPort,同样AddAllocatedPort,然后TurnPort::PrepareAddress()

    void TurnPort::PrepareAddress() {
      if (credentials_.username.empty() ||
          credentials_.password.empty()) {
        RTC_LOG(LS_ERROR) << "Allocation can't be started without setting the"
                             " TURN server credentials for the user.";
        OnAllocateError();
        return;
      }
    
      if (!server_address_.address.port()) {
        // We will set default TURN port, if no port is set in the address.
        server_address_.address.SetPort(TURN_DEFAULT_PORT);
      }
    
      if (server_address_.address.IsUnresolvedIP()) {
        ResolveTurnAddress(server_address_.address);
      } else {
        // If protocol family of server address doesn't match with local, return.
        if (!IsCompatibleAddress(server_address_.address)) {
          RTC_LOG(LS_ERROR) << "IP address family does not match. server: "
                            << server_address_.address.family()
                            << " local: " << Network()->GetBestIP().family();
          OnAllocateError();
          return;
        }
    
        // Insert the current address to prevent redirection pingpong.
        attempted_server_addresses_.insert(server_address_.address);
    
        RTC_LOG(LS_INFO) << ToString()
                         << ": Trying to connect to TURN server via "
                         << ProtoToString(server_address_.proto) << " @ "
                         << server_address_.address.ToSensitiveString();
        if (!CreateTurnClientSocket()) {
          RTC_LOG(LS_ERROR) << "Failed to create TURN client socket";
          OnAllocateError();
          return;
        }
        if (server_address_.proto == PROTO_UDP) {
          // If its UDP, send AllocateRequest now.
          // For TCP and TLS AllcateRequest will be sent by OnSocketConnect.
          SendRequest(new TurnAllocateRequest(this), 0);
        }
      }
    }
    

    可以看出必须要有username和password,这也就是coTurn中所谓的要支持WebRTC必须开启long-term credentials。

    TurnAllocateRequest也是派生于StunRequest,但是其内部的msg_为TurnMessage。
    看了半天TurnMessage,原来目前的WebRTC版本还不支持turn oauth验证,而那 个W3C WebRTC 1.0: Real-time Communication Between Browsers 只是标准草案,并没有完全实现,WTF。

    相关文章

      网友评论

        本文标题:WebRTC之STUN、TURN和ICE研究

        本文链接:https://www.haomeiwen.com/subject/yptjkftx.html