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():
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。
上一篇: 基于Qt、FFMpeg的音视频播放器设计二(FFMpeg视频处理)
下一篇: ResNet解析