WebRTC

HTML5、WebSocket、WebRTC

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

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

WebSocket

When the WebSocket protocol was standardized in December 2011, it allowed browsers to create stable and bidirectional connections to a WebSocket server. The protocol is a very powerful tool to send push notifications to browsers, and has been used to implement chats, turn-based games, etc.

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

WebRTC

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

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

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

../../_images/webrtc_signaling.png

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

在 Godot 中使用 WebRTC

WebRTC is implemented in Godot via two main classes WebRTCPeerConnection and WebRTCDataChannel, plus the multiplayer API implementation WebRTCMultiplayerPeer. See section on high-level multiplayer for more details.

备注

These classes are available automatically in HTML5, but require an external GDExtension plugin on native (non-HTML5) platforms. Check out the webrtc-native plugin repository for instructions and to get the latest release.

警告

导出到 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.session_description_created.connect(p1.set_local_description)
  11. # Connect P1 session and ICE created to p2 set remote description and candidates.
  12. p1.session_description_created.connect(p2.set_remote_description)
  13. p1.ice_candidate_created.connect(p2.add_ice_candidate)
  14. # Same for P2
  15. p2.session_description_created.connect(p2.set_local_description)
  16. p2.session_description_created.connect(p1.set_remote_description)
  17. p2.ice_candidate_created.connect(p1.add_ice_candidate)
  18. # Let P1 create the offer
  19. p1.create_offer()
  20. # Wait a second and send message from P1.
  21. await get_tree().create_timer(1).timeout
  22. ch1.put_packet("Hi from P1".to_utf8_buffer())
  23. # Wait a second and send message from P2.
  24. await get_tree().create_timer(1).timeout
  25. ch2.put_packet("Hi from P2".to_utf8_buffer())
  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. extends Node
  2. # An example p2p chat client.
  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.ice_candidate_created.connect(self._on_ice_candidate)
  9. peer.session_description_created.connect(self._on_session)
  10. # Register to the local signaling server (see below for the implementation).
  11. Signaling.register(String(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(String(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(String(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(String(get_path()), " received: ", channel.get_packet().get_string_from_utf8())
  26. func send_message(message):
  27. channel.put_packet(message.to_utf8_buffer())

现在是本地信号服务器:

备注

这个本地信号服务器应该是作为一个 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 peers.size() == 2:
  9. get_node(peers[0]).peer.create_offer()
  10. func _find_other(path):
  11. # Find the other registered peer.
  12. for p in peers:
  13. if p != path:
  14. return p
  15. return ""
  16. func send_session(path, type, sdp):
  17. var other = _find_other(path)
  18. assert(other != "")
  19. get_node(other).peer.set_remote_description(type, sdp)
  20. func send_candidate(path, mid, index, sdp):
  21. var other = _find_other(path)
  22. assert(other != "")
  23. 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. # Wait a second and send message from P1
  10. await get_tree().create_timer(1).timeout
  11. p1.send_message("Hi from %s" % String(p1.get_path()))
  12. # Wait a second and send message from P2
  13. await get_tree().create_timer(1).timeout
  14. p2.send_message("Hi from %s" % String(p2.get_path()))

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

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

使用 WebSocket 进行远程信号传输

A more advanced demo using WebSocket for signaling peers and WebRTCMultiplayerPeer is available in the godot demo projects under networking/webrtc_signaling.