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<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():
而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中列出了所有派生類。
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=3537if (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研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TURN协议简要介绍
- 下一篇: form表单样式