WebRTC之STUN、TURN和ICE研究

准备查看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(pooled_sessions_.size()) < candidate_pool_size_) {
    PortAllocatorSession* pooled_session = CreateSessionInternal("", 0, "", "");
    pooled_session->StartGettingPorts();
    pooled_sessions_.push_back(
        std::unique_ptr(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(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(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 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 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);
}

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研究)