admin管理员组

文章数量:1547489

WebRTC之P2P

StoneLiu999 2020-11-19 11:35:39 802 已收藏 4 分类专栏: WebRTC 文章标签: webrtc p2p turn nat stun 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn/momo0853/article/details/109806172 版权

文章目录

    • SDP/STUN/TURN/ICE
      • SDP
      • STUN
        • 服务端实现
        • 客户端实现
        • NAT类型判断
      • TURN
        • TurnServer(TURN服务端)
        • TurnPort(TURN客户端)
      • ICE
    • NAT类型
      • 完全圆锥形NAT(Full cone NAT)
      • 受限圆锥形NAT(Address-Restricted cone NAT)
      • 端口受限圆锥形NAT(Port-Restricted cone NAT)
      • 对称NAT(Symmetric NAT)

SDP/STUN/TURN/ICE

对这几种名称进行简单介绍如下:

  • SDP是一种用于描述媒体信息的标准协议,例如分辨率、编码器、加密等
  • Offer/Answer,我们要和对端交换的描述信息就称为Offer,对端发给我们的描述信息就称为Answer,不同客户端支持的编解码类型是不一样的,所以需要协商
  • STUN是一种获取NAT公网IP,以及NAT类型的协议
  • TURN是在STUN基础上增加转发功能的协议
  • ICE就是把STUN和TURN的结合

SDP

以下SDP内容完全来自维基-Session Description Protocol,阅读原文获取更全面的信息。

SDP是用于描述流媒体通信参数的格式。IETF在1998年4月发布了原始规范作为拟议标,随后在2006年7月发布了修订的规范RFC4566。
SDP用于会话描述通告,会话邀请和参数协商等多媒体通信会话。SDP本身并不传递任何媒体,而是在端点之间用于协商媒体类型,格式和所有相关属性。属性和参数的集合通常称为会话配置文件。
SDP被设计为可扩展的,以支持新的媒体类型和格式。SDP最初是作为会话公告协议(SAP)的组成部分,但发现它与实时传输协议(RTP),实时流协议(RTSP),会话发起协议(SIP)和即使是用于描述多播会话的独立格式。

STUN

以下STUN内容完全来自维基-STUN,阅读原文获取更全面的信息。

STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。该协议由RFC 5389定义。

一旦客户端得知了Internet端的UDP端口,通信就可以开始了。如果NAT是完全圆锥型的,那么双方中的任何一方都可以发起通信。如果NAT是受限圆锥型或端口受限圆锥型,双方必须一起开始传输。

STUN使用下列的算法(取自RFC 3489)来发现NAT gateways类型以及防火墙(firewalls)

一旦路经通过红色箱子的终点时,UDP的沟通是没有可能性的。一旦通过黄色或是绿色的箱子,就有连线的可能。

  • 1.STUN客户端向STUN服务器发送请求,要求得到自身经NAT映射后的地址:

    • a. 收不到服务器回复,则认为UDP被防火墙阻断,不能通信,网络类型:Blocked.
    • b. 收到服务器回复,对比本地地址,如果相同,则认为无NAT设备,进入第2步,否则认为有NAT设备,进入3步.
  • 2.(已确认无NAT设备)STUN客户端向STUN服务器发送请求,要求服务器从其他IP和PORT向客户端回复包:

    • a. 收不到服务器从其他IP地址的回复,认为包被前置防火墙阻断,网络类型:Symmetric UDP Firewall.
    • b. 收到则认为客户端处在一个开放的网络上,网络类型:Opened.
  • 3.(已确认存在NAT设备)STUN客户端向STUN服务器发送请求,要求服务器从其他IP和PORT向客户端回复包:

    • a. 收不到服务器从其他IP地址的回复,认为包被前置NAT设备阻断,进入第4步.
    • b. 收到则认为NAT设备类型为Full Cone,即网络类型:Full Cone NAT.
  • 4.STUN客户端向STUN服务器的另外一个IP地址发送请求,要求得到自身经NAT映射后的地址,并对比之:

    • a. 地址不相同,则网络类型:Symmetric NAT.
    • b. 相同则认为是Restricted NAT,进入第5步,进一步确认类型.
  • 5.(已确认Restricted NAT设备)STUN客户端向STUN服务器发送请求,要求服务器从相同IP的其他PORT向客户端回复包:

    • a. 收不到服务器从其他PORT地址的回复,认为包被前置NAT设备阻断,网络类型:Port Restricted cone NAT.
    • b. 收到则认为网络类型: Restricted cone NAT.

有了上面的理论以后,我们来看看WebRTC的代码。WebRTC实现了STUN的功能,包括了客户端和服务端以及NAT探测,它使用的是RFC 5389协议。

服务端实现

WebRTC的STUN实现stunserver很简单,收到stun客户端的请求,然后把客户端的最外层地址返回给用户。

// 判断是不是stun格式,如果是拍判断消息类型,目前仅仅支持STUN_BINDING_REQUEST消息
void StunServer::OnPacket(
    rtc::AsyncPacketSocket* socket, const char* buf, size_t size,
    const rtc::SocketAddress& remote_addr,
    const rtc::PacketTime& packet_time) {
  // Parse the STUN message; eat any messages that fail to parse.
  rtc::ByteBufferReader bbuf(buf, size);
  StunMessage msg;
  if (!msg.Read(&bbuf)) {
    return;
  }
  // Send the message to the appropriate handler function.
  switch (msg.type()) {
    case STUN_BINDING_REQUEST:
      OnBindingRequest(&msg, remote_addr);
      break;
    default:
      SendErrorResponse(msg, remote_addr, 600, "Operation Not Supported");
  }
}
// 构建一个stun的response消息,并写入stun客户的外网地址
void StunServer::OnBindingRequest(
    StunMessage* msg, const rtc::SocketAddress& remote_addr) {
  StunMessage response;
  GetStunBindReqponse(msg, remote_addr, &response);
  SendResponse(response, remote_addr);
}
// 返回response数据给客户端
void StunServer::SendResponse(
    const StunMessage& msg, const rtc::SocketAddress& addr) {
  rtc::ByteBufferWriter buf;
  msg.Write(&buf);
  rtc::PacketOptions options;
  if (socket_->SendTo(buf.Data(), buf.Length(), addr, options) < 0)
    LOG_ERR(LS_ERROR) << "sendto";
}

     
     
     
     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
客户端实现

客户端的实现简单来说就是构建一个标准的STUN消息请求并发送给不同的STUN服务器(或许存在多个不同的STUN服务器,我还不能明白存在多个STUN服务器的意义),并收集STUN服务器返回的自身的外网IP,并放到candidates中。虽然代码看着不少,因为代码要考虑健壮性。
CreateStunPorts -> StunPort::Create -> new StunPort -> new UDPPort -> UDPPort::Init -> UDPPort::OnLocalAddressReady -> StunRequestManager::SendDelayed -> UDPPort::OnSendPacket -> UDPPort::OnReadPacket -> StunBindingRequest::OnResponse -> StunRequestManager::CheckResponse -> UDPPort::OnStunBindingRequestSucceeded

// 构建一个StunPort,StunPort是对见Port的简单封装
void AllocationSequence::CreateStunPorts() {
  ...
  StunPort* port = StunPort::Create(
      session_->network_thread(), session_->socket_factory(), network_,
      session_->allocator()->min_port(), session_->allocator()->max_port(),
      session_->username(), session_->password(), config_->StunServers(),
      session_->allocator()->origin());
   ...
}
// 初始化StunPort
static StunPort* Create(rtc::Thread* thread, rtc::PacketSocketFactory* factory,  rtc::Network* network, ...) {
    StunPort* port = new StunPort(thread, factory, network, min_port, max_port, username, password, servers, origin);
    if (!port->Init()) { delete port; port = NULL;  }
}
// StunPort是对UDPPort的子类
StunPort(rtc::Thread* thread, rtc::PacketSocketFactory* factory,  rtc::Network* network, uint16_t min_port,  uint16_t max_port,  ...)
      : UDPPort(thread, factory, network, min_port, max_port, username, password, origin, false) {
    // UDPPort will set these to local udp, updating these to STUN.
    set_type(STUN_PORT_TYPE);
    set_server_addresses(servers);
}
// 创建一个UDP Socket,这个Socket也是WebRTC的封装,不细说,当Socket的状态发生变化的时候会通过Socket的`Signal`信号槽回调出来
bool UDPPort::Init() {
     ...
    socket_ = socket_factory()->CreateUdpSocket(rtc::SocketAddress(Network()->GetBestIP(), 0), min_port(), max_port());
    socket_->SignalReadPacket.connect(this, &UDPPort::OnReadPacket);
    socket_->SignalSentPacket.connect(this, &UDPPort::OnSentPacket);
    socket_->SignalReadyToSend.connect(this, &UDPPort::OnReadyToSend);
    socket_->SignalAddressReady.connect(this, &UDPPort::OnLocalAddressReady);
    requests_.SignalSendPacket.connect(this, &UDPPort::OnSendPacket);
   return true;
}
// Socket地址可用回调此函数,在这里会先收集local candiate,然后调用MaybePrepareStunCandidate获取本机的外网地址
void UDPPort::OnLocalAddressReady(rtc::AsyncPacketSocket* socket, const rtc::SocketAddress& address) {
  rtc::SocketAddress addr = address;
  MaybeSetDefaultLocalAddress(&addr);
  AddAddress(addr, addr, rtc::SocketAddress(), UDP_PROTOCOL_NAME, "", "", LOCAL_PORT_TYPE, ICE_TYPE_PREFERENCE_HOST, 0, "", false);
  MaybePrepareStunCandidate();
}
// 判断是否需要向STUN服务器请求本机外网地址或者判断是否完成了获取外网地址的请求
void UDPPort::MaybePrepareStunCandidate() {
  if (!server_addresses_.empty()) {
    SendStunBindingRequests();
  } else {
    MaybeSetPortCompleteOrError();
  }
}
// 依次向不同的服务器请求外网地址
void UDPPort::SendStunBindingRequests() {
  for (ServerAddresses::const_iterator it = server_addresses_.begin(); it != server_addresses_.end(); ++it) {
    SendStunBindingRequest(*it);
  }
}
// 如果STUN服务器地址可用,那么向此服务器发送一个binding请求
void UDPPort::SendStunBindingRequest(const rtc::SocketAddress& stun_addr) {
  if (stun_addr.IsUnresolvedIP()) {
    ResolveStunAddress(stun_addr);
  } else if (socket_->GetState() == rtc::AsyncPacketSocket::STATE_BOUND) {
    if (IsCompatibleAddress(stun_addr)) {
      requests_.Send(new StunBindingRequest(this, stun_addr, rtc::TimeMillis()));
    } else {
      OnStunBindingOrResolveRequestFailed(stun_addr);
    }
  }
}
// 进一步完成StunRequest的设定,并把此数据发送给stun服务器
void StunRequestManager::SendDelayed(StunRequest* request, int delay) {
  request->set_manager(this);
  RTC_DCHECK(requests_.find(request->id()) == requests_.end());
  request->set_origin(origin_);
  request->Construct();
  requests_[request->id()] = request;
  if (delay > 0) {
    thread_->PostDelayed(RTC_FROM_HERE, delay, request, MSG_STUN_SEND, NULL);
  } else {
    thread_->Send(RTC_FROM_HERE, request, MSG_STUN_SEND, NULL);
  }
}
// 通过SignalSendPacket发送数据,紧接着判断已经发送的次数,以及超时情况
void StunRequest::OnMessage(rtc::Message* pmsg) {
  tstamp_ = rtc::TimeMillis();
  rtc::ByteBufferWriter buf;
  msg_->Write(&buf);
  manager_->SignalSendPacket(buf.Data(), buf.Length(), this);
  OnSent();
  manager_->thread_->PostDelayed(RTC_FROM_HERE, resend_delay(), this, MSG_STUN_SEND, NULL);
}
// 调用udp socket把数据发出去
void UDPPort::OnSendPacket(const void* data, size_t size, StunRequest* req) {
  StunBindingRequest* sreq = static_cast<StunBindingRequest*>(req);
  rtc::PacketOptions options(DefaultDscpValue());
  if (socket_->SendTo(data, size, sreq->server_addr(), options) < 0)
    PLOG(LERROR, socket_->GetError()) << "sendto";
}
// 如果是stun服务器返回的消息,则调用CheckResponse检查是啥消息
void UDPPort::OnReadPacket(rtc::AsyncPacketSocket* socket, const char* data,  size_t size, const rtc::SocketAddress& remote_addr,  const rtc::PacketTime& packet_time) {
  if (server_addresses_.find(remote_addr) != server_addresses_.end()) {
    requests_.CheckResponse(data, size);
    return;
  }

本文标签: webrtcP2P