WebRTC

HTML5、WebSocket、WebRTC

Godot的一大特点是它能够导出到HTML5/WebAssembly平台, 当用户访问您的网页时, 您的游戏可以直接在浏览器中运行.

这对于演示和完整的游戏来说都是一个很好的机会, 但过去有一些限制, 在网络领域, 浏览器过去只支持HTTPRequests, 直到最近, 首先是WebSocket, 然后是WebRTC被提出为标准.

WebSocket

当WebSocket协议在2011年12月被标准化后, 它允许浏览器与WebSocket服务器建立稳定的双向连接. 该协议相当简单, 但却是一个非常强大的向浏览器发送推送通知的工具, 并已被用于实现聊天, 回合制游戏等.

不过,WebSockets仍然使用TCP连接, 这对可靠性有好处, 但对减少延迟没有好处, 所以不适合实时应用, 比如VoIP和快节奏的游戏.

WebRTC

为此, 从2010年开始, 谷歌开始研究一项名为WebRTC的新技术, 后来在2017年, 这项技术成为W3C候选推荐.WebRTC是一套复杂得集合规范, 并且在后台依靠许多其他技术(ICE, DTLS, SDP)来提供两个对等体之间快速, 实时, 安全的通信.

其想法是找到两个对等体之间最快的路线, 并尽可能建立直接通信(尽量避开中继服务器).

然而, 这是有代价的, 那就是在通信开始之前, 两个对等体之间必须交换一些媒介信息(以会话描述协议—SDP字符串的形式). 这通常采取所谓的WebRTC信号服务器的形式.

../../_images/webrtc_signaling.png

对等体连接到信号服务器(例如 WebSocket 服务器)并发送其媒介信息. 然后, 服务器将此信息转发到其他对等体, 允许它们建立所需的直接通信. 这一步完成后, 对等体可以断开与信号服务器的连接, 并保持直接的点对点(P2P)连接打开状态.

在 Godot 中使用 WebRTC

在Godot中,WebRTC是通过两个主要的类来实现的 WebRTCPeerConnectionWebRTCDataChannel, 加上多人游戏API实现 WebRTCMultiplayer. 更多细节请参见 high-level multiplayer 章节.

备注

这些类在HTML5中自动可用, 但 需要在本地(非HTML5)平台上使用外部GDNative插件 . 查看 webrtc-native 插件库 , 以获取说明和最新的 发布 .

警告

当导出到 Android 时,在导出项目或使用一键部署之前,确保在 Android 导出预设中启用 INTERNET 权限。否则,任何形式的网络通信都会被 Android 系统阻止。

最小连接示例

这个例子将向您展示如何在同一应用程序中的两个对等体之间创建WebRTC连接. 这在现实场景中并不是很有用, 但会让你对如何设置WebRTC连接有一个很好的概览.

  1. extends Node
  2. # Create the two peers
  3. var p1 = WebRTCPeerConnection.new()
  4. var p2 = WebRTCPeerConnection.new()
  5. # And a negotiated channel for each each peer
  6. var ch1 = p1.create_data_channel("chat", {"id": 1, "negotiated": true})
  7. var ch2 = p2.create_data_channel("chat", {"id": 1, "negotiated": true})
  8. func _ready():
  9. # Connect P1 session created to itself to set local description
  10. p1.connect("session_description_created", p1, "set_local_description")
  11. # Connect P1 session and ICE created to p2 set remote description and candidates
  12. p1.connect("session_description_created", p2, "set_remote_description")
  13. p1.connect("ice_candidate_created", p2, "add_ice_candidate")
  14. # Same for P2
  15. p2.connect("session_description_created", p2, "set_local_description")
  16. p2.connect("session_description_created", p1, "set_remote_description")
  17. p2.connect("ice_candidate_created", p1, "add_ice_candidate")
  18. # Let P1 create the offer
  19. p1.create_offer()
  20. # Wait a second and send message from P1
  21. yield(get_tree().create_timer(1), "timeout")
  22. ch1.put_packet("Hi from P1".to_utf8())
  23. # Wait a second and send message from P2
  24. yield(get_tree().create_timer(1), "timeout")
  25. ch2.put_packet("Hi from P2".to_utf8())
  26. func _process(_delta):
  27. # Poll connections
  28. p1.poll()
  29. p2.poll()
  30. # Check for messages
  31. if ch1.get_ready_state() == ch1.STATE_OPEN and ch1.get_available_packet_count() > 0:
  32. print("P1 received: ", ch1.get_packet().get_string_from_utf8())
  33. if ch2.get_ready_state() == ch2.STATE_OPEN and ch2.get_available_packet_count() > 0:
  34. print("P2 received: ", ch2.get_packet().get_string_from_utf8())

这将打印:

  1. P1 received: Hi from P1
  2. P2 received: Hi from P2

本地信号示例

这个例子在上一个例子的基础上进行了扩展, 将对等体分离在两个不同的场景中, 并使用 singleton 作为信号服务器.

  1. # An example P2P chat client (chat.gd)
  2. extends Node
  3. var peer = WebRTCPeerConnection.new()
  4. # Create negotiated data channel
  5. var channel = peer.create_data_channel("chat", {"negotiated": true, "id": 1})
  6. func _ready():
  7. # Connect all functions
  8. peer.connect("ice_candidate_created", self, "_on_ice_candidate")
  9. peer.connect("session_description_created", self, "_on_session")
  10. # Register to the local signaling server (see below for the implementation)
  11. Signaling.register(get_path())
  12. func _on_ice_candidate(mid, index, sdp):
  13. # Send the ICE candidate to the other peer via signaling server
  14. Signaling.send_candidate(get_path(), mid, index, sdp)
  15. func _on_session(type, sdp):
  16. # Send the session to other peer via signaling server
  17. Signaling.send_session(get_path(), type, sdp)
  18. # Set generated description as local
  19. peer.set_local_description(type, sdp)
  20. func _process(delta):
  21. # Always poll the connection frequently
  22. peer.poll()
  23. if channel.get_ready_state() == WebRTCDataChannel.STATE_OPEN:
  24. while channel.get_available_packet_count() > 0:
  25. print(get_path(), " received: ", channel.get_packet().get_string_from_utf8())
  26. func send_message(message):
  27. channel.put_packet(message.to_utf8())

现在是本地信号服务器:

备注

这个本地信号服务器应该是作为一个 singleton 来连接同一场景中的两个对等体.

  1. # A local signaling server. Add this to autoloads with name "Signaling" (/root/Signaling)
  2. extends Node
  3. # We will store the two peers here
  4. var peers = []
  5. func register(path):
  6. assert(peers.size() < 2)
  7. peers.append(path)
  8. # If it's the second one, create an offer
  9. if peers.size() == 2:
  10. get_node(peers[0]).peer.create_offer()
  11. func _find_other(path):
  12. # Find the other registered peer.
  13. for p in peers:
  14. if p != path:
  15. return p
  16. return ""
  17. func send_session(path, type, sdp):
  18. var other = _find_other(path)
  19. assert(other != "")
  20. get_node(other).peer.set_remote_description(type, sdp)
  21. func send_candidate(path, mid, index, sdp):
  22. var other = _find_other(path)
  23. assert(other != "")
  24. get_node(other).peer.add_ice_candidate(mid, index, sdp)

然后, 您可以这样使用它:

  1. # Main scene (main.gd)
  2. extends Node
  3. const Chat = preload("res://chat.gd")
  4. func _ready():
  5. var p1 = Chat.new()
  6. var p2 = Chat.new()
  7. add_child(p1)
  8. add_child(p2)
  9. yield(get_tree().create_timer(1), "timeout")
  10. p1.send_message("Hi from %s" % p1.get_path())
  11. # Wait a second and send message from P2
  12. yield(get_tree().create_timer(1), "timeout")
  13. p2.send_message("Hi from %s" % p2.get_path())

将打印出类似这样的内容:

  1. /root/main/@@3 received: Hi from /root/main/@@2
  2. /root/main/@@2 received: Hi from /root/main/@@3

使用 WebSocket 进行远程信号传输

一个更高级的演示,使用 WebSocket 作为信号对等体和 WebRTCMultiplayergodot 演示项目 networking/webrtc_signaling 下提供。