欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WebRTC之STUN、TURN和ICE研究

程序员文章站 2022-07-01 17:36:26
...

准备查看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。