From dd2ba9a5ba9d66fc57b0cfc98569d2ac288b3e63 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Wed, 28 Sep 2022 22:43:58 +0200 Subject: [PATCH] [Net] Update & refactor WebSocket Chat demo. Uses new unified StreamPeer, dropped the multiplayer part (in favor of the dedicated WebSocket demo), add reference WebSocketClient and WebSocketServer signal-based implementations that can be used as drop-in nodes in any project. Might be worth maintaning it as a separate addon. --- networking/websocket_chat/chat.tscn | 85 +++++++++ networking/websocket_chat/client.gd | 47 +++++ networking/websocket_chat/client.tscn | 59 +++++++ networking/websocket_chat/client/client.gd | 84 --------- networking/websocket_chat/client/client.tscn | 89 ---------- networking/websocket_chat/client/client_ui.gd | 62 ------- networking/websocket_chat/combo.tscn | 56 ++++++ networking/websocket_chat/combo/combo.tscn | 53 ------ networking/websocket_chat/icon.png.import | 29 ++-- networking/websocket_chat/project.godot | 25 ++- networking/websocket_chat/server.gd | 51 ++++++ networking/websocket_chat/server.tscn | 61 +++++++ networking/websocket_chat/server/server.gd | 84 --------- networking/websocket_chat/server/server.tscn | 88 ---------- networking/websocket_chat/server/server_ui.gd | 70 -------- networking/websocket_chat/utils.gd | 17 -- .../websocket/WebSocketClient.gd | 73 ++++++++ .../websocket/WebSocketServer.gd | 162 ++++++++++++++++++ 18 files changed, 627 insertions(+), 568 deletions(-) create mode 100644 networking/websocket_chat/chat.tscn create mode 100644 networking/websocket_chat/client.gd create mode 100644 networking/websocket_chat/client.tscn delete mode 100644 networking/websocket_chat/client/client.gd delete mode 100644 networking/websocket_chat/client/client.tscn delete mode 100644 networking/websocket_chat/client/client_ui.gd create mode 100644 networking/websocket_chat/combo.tscn delete mode 100644 networking/websocket_chat/combo/combo.tscn create mode 100644 networking/websocket_chat/server.gd create mode 100644 networking/websocket_chat/server.tscn delete mode 100644 networking/websocket_chat/server/server.gd delete mode 100644 networking/websocket_chat/server/server.tscn delete mode 100644 networking/websocket_chat/server/server_ui.gd delete mode 100644 networking/websocket_chat/utils.gd create mode 100644 networking/websocket_chat/websocket/WebSocketClient.gd create mode 100644 networking/websocket_chat/websocket/WebSocketServer.gd diff --git a/networking/websocket_chat/chat.tscn b/networking/websocket_chat/chat.tscn new file mode 100644 index 00000000..21dac2e5 --- /dev/null +++ b/networking/websocket_chat/chat.tscn @@ -0,0 +1,85 @@ +[gd_scene format=3 uid="uid://cyvrywci15kev"] + +[node name="Chat" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Panel" type="Panel" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="Panel"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Listen" type="HBoxContainer" parent="Panel/VBoxContainer"] +offset_right = 1152.0 + +[node name="Connect" type="HBoxContainer" parent="Panel/VBoxContainer"] +offset_top = 4.0 +offset_right = 1152.0 +offset_bottom = 35.0 + +[node name="Host" type="LineEdit" parent="Panel/VBoxContainer/Connect"] +offset_right = 930.0 +offset_bottom = 31.0 +size_flags_horizontal = 3 +text = "ws://localhost:8000/test/" +placeholder_text = "ws://my.server/path/" + +[node name="Connect" type="Button" parent="Panel/VBoxContainer/Connect"] +offset_left = 934.0 +offset_right = 1006.0 +offset_bottom = 31.0 +toggle_mode = true +text = "Connect" + +[node name="Port" type="SpinBox" parent="Panel/VBoxContainer/Connect"] +offset_left = 1010.0 +offset_right = 1093.0 +offset_bottom = 31.0 +min_value = 1.0 +max_value = 65535.0 +value = 8000.0 + +[node name="Listen" type="Button" parent="Panel/VBoxContainer/Connect"] +offset_left = 1097.0 +offset_right = 1152.0 +offset_bottom = 31.0 +toggle_mode = true +text = "Listen" + +[node name="Send" type="HBoxContainer" parent="Panel/VBoxContainer"] +offset_top = 39.0 +offset_right = 1152.0 +offset_bottom = 70.0 + +[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/Send"] +offset_right = 1101.0 +offset_bottom = 31.0 +size_flags_horizontal = 3 +placeholder_text = "Enter some text to send..." + +[node name="Send" type="Button" parent="Panel/VBoxContainer/Send"] +offset_left = 1105.0 +offset_right = 1152.0 +offset_bottom = 31.0 +text = "Send" + +[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"] +offset_top = 74.0 +offset_right = 1152.0 +offset_bottom = 648.0 +size_flags_vertical = 3 diff --git a/networking/websocket_chat/client.gd b/networking/websocket_chat/client.gd new file mode 100644 index 00000000..5ebb0a8f --- /dev/null +++ b/networking/websocket_chat/client.gd @@ -0,0 +1,47 @@ +extends Control + +@onready var _client : WebSocketClient = $WebSocketClient +@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel +@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit +@onready var _host = $Panel/VBoxContainer/Connect/Host + +func info(msg): + print(msg) + _log_dest.add_text(str(msg) + "\n") + + +# Client signals +func _on_web_socket_client_connection_closed(): + var ws = _client.get_socket() + info("Client just disconnected with code: %s, reson: %s" % [ws.get_close_code(), ws.get_close_reason()]) + + +func _on_web_socket_client_connected_to_server(): + info("Client just connected with protocol: %s" % _client.get_socket().get_selected_protocol()) + + +func _on_web_socket_client_message_received(message): + info("%s" % message) + + +# UI signals. +func _on_send_pressed(): + if _line_edit.text == "": + return + + info("Sending message: %s" % [_line_edit.text]) + _client.send(_line_edit.text) + _line_edit.text = "" + + +func _on_connect_toggled(pressed): + if not pressed: + _client.close() + return + if _host.text == "": + return + info("Connecting to host: %s." % [_host.text]) + var err = _client.connect_to_url(_host.text) + if err != OK: + info("Error connecting to host: %s" % [_host.text]) + return diff --git a/networking/websocket_chat/client.tscn b/networking/websocket_chat/client.tscn new file mode 100644 index 00000000..e8928c2c --- /dev/null +++ b/networking/websocket_chat/client.tscn @@ -0,0 +1,59 @@ +[gd_scene load_steps=4 format=3 uid="uid://ph5ghsflqegf"] + +[ext_resource type="PackedScene" uid="uid://cyvrywci15kev" path="res://chat.tscn" id="1_cfcun"] +[ext_resource type="Script" path="res://websocket/WebSocketClient.gd" id="2_m4g4y"] +[ext_resource type="Script" path="res://client.gd" id="2_opbid"] + +[node name="Client" instance=ExtResource("1_cfcun")] +script = ExtResource("2_opbid") + +[node name="WebSocketClient" type="Node" parent="." index="0"] +script = ExtResource("2_m4g4y") +supported_protocols = PackedStringArray("demo-chat") + +[node name="Panel" parent="." index="1"] +layout_mode = 1 + +[node name="VBoxContainer" parent="Panel" index="0"] +layout_mode = 1 + +[node name="Listen" parent="Panel/VBoxContainer" index="0"] +layout_mode = 2 + +[node name="Connect" parent="Panel/VBoxContainer" index="1"] +layout_mode = 2 + +[node name="Host" parent="Panel/VBoxContainer/Connect" index="0"] +layout_mode = 2 +offset_right = 1076.0 + +[node name="Connect" parent="Panel/VBoxContainer/Connect" index="1"] +layout_mode = 2 +offset_left = 1080.0 +offset_right = 1152.0 + +[node name="Port" parent="Panel/VBoxContainer/Connect" index="2"] +visible = false +layout_mode = 2 + +[node name="Listen" parent="Panel/VBoxContainer/Connect" index="3"] +visible = false +layout_mode = 2 + +[node name="Send" parent="Panel/VBoxContainer" index="2"] +layout_mode = 2 + +[node name="LineEdit" parent="Panel/VBoxContainer/Send" index="0"] +layout_mode = 2 + +[node name="Send" parent="Panel/VBoxContainer/Send" index="1"] +layout_mode = 2 + +[node name="RichTextLabel" parent="Panel/VBoxContainer" index="3"] +layout_mode = 2 + +[connection signal="connected_to_server" from="WebSocketClient" to="." method="_on_web_socket_client_connected_to_server"] +[connection signal="connection_closed" from="WebSocketClient" to="." method="_on_web_socket_client_connection_closed"] +[connection signal="message_received" from="WebSocketClient" to="." method="_on_web_socket_client_message_received"] +[connection signal="toggled" from="Panel/VBoxContainer/Connect/Connect" to="." method="_on_connect_toggled"] +[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_send_pressed"] diff --git a/networking/websocket_chat/client/client.gd b/networking/websocket_chat/client/client.gd deleted file mode 100644 index 8a40dc40..00000000 --- a/networking/websocket_chat/client/client.gd +++ /dev/null @@ -1,84 +0,0 @@ -extends Node - -@onready var _log_dest = get_parent().get_node(^"Panel/VBoxContainer/RichTextLabel") - -var _client = WebSocketClient.new() -var _write_mode = WebSocketPeer.WRITE_MODE_BINARY -var _use_multiplayer = true -var last_connected_client = 0 - -func _init(): - _client.connect(&"connection_established", self._client_connected) - _client.connect(&"connection_error", self._client_disconnected) - _client.connect(&"connection_closed", self._client_disconnected) - _client.connect(&"server_close_request", self._client_close_request) - _client.connect(&"data_received", self._client_received) - - _client.connect(&"peer_packet", self._client_received) - _client.connect(&"peer_connected", self._peer_connected) - _client.connect(&"connection_succeeded", self._client_connected, ["multiplayer_protocol"]) - _client.connect(&"connection_failed", self._client_disconnected) - - -func _client_close_request(code, reason): - Utils._log(_log_dest, "Close code: %d, reason: %s" % [code, reason]) - - -func _peer_connected(id): - Utils._log(_log_dest, "%s: Client just connected" % id) - last_connected_client = id - - -func _exit_tree(): - _client.disconnect_from_host(1001, "Bye bye!") - - -func _process(_delta): - if _client.get_connection_status() == WebSocketClient.CONNECTION_DISCONNECTED: - return - - _client.poll() - - -func _client_connected(protocol): - Utils._log(_log_dest, "Client just connected with protocol: %s" % protocol) - _client.get_peer(1).set_write_mode(_write_mode) - - -func _client_disconnected(clean=true): - Utils._log(_log_dest, "Client just disconnected. Was clean: %s" % clean) - - -func _client_received(_p_id = 1): - if _use_multiplayer: - var peer_id = _client.get_packet_peer() - var packet = _client.get_packet() - Utils._log(_log_dest, "MPAPI: From %s Data: %s" % [str(peer_id), Utils.decode_data(packet, false)]) - else: - var packet = _client.get_peer(1).get_packet() - var is_string = _client.get_peer(1).was_string_packet() - Utils._log(_log_dest, "Received data. BINARY: %s: %s" % [not is_string, Utils.decode_data(packet, is_string)]) - - -func connect_to_url(host, protocols, multiplayer): - _use_multiplayer = multiplayer - if _use_multiplayer: - _write_mode = WebSocketPeer.WRITE_MODE_BINARY - return _client.connect_to_url(host, protocols, multiplayer) - - -func disconnect_from_host(): - _client.disconnect_from_host(1000, "Bye bye!") - - -func send_data(data, dest): - _client.get_peer(1).set_write_mode(_write_mode) - if _use_multiplayer: - _client.set_target_peer(dest) - _client.put_packet(Utils.encode_data(data, _write_mode)) - else: - _client.get_peer(1).put_packet(Utils.encode_data(data, _write_mode)) - - -func set_write_mode(mode): - _write_mode = mode diff --git a/networking/websocket_chat/client/client.tscn b/networking/websocket_chat/client/client.tscn deleted file mode 100644 index d08d2ee2..00000000 --- a/networking/websocket_chat/client/client.tscn +++ /dev/null @@ -1,89 +0,0 @@ -[gd_scene load_steps=3 format=2] - -[ext_resource path="res://client/client_ui.gd" type="Script" id=1] -[ext_resource path="res://client/client.gd" type="Script" id=2] - -[node name="Client" type="Control"] -anchor_right = 1.0 -anchor_bottom = 1.0 -script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Panel" type="Panel" parent="."] -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="VBoxContainer" type="VBoxContainer" parent="Panel"] -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="Connect" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_right = 1024.0 -offset_bottom = 24.0 - -[node name="Host" type="LineEdit" parent="Panel/VBoxContainer/Connect"] -offset_right = 956.0 -offset_bottom = 24.0 -size_flags_horizontal = 3 -text = "ws://localhost:8000/test/" -placeholder_text = "ws://my.server/path/" - -[node name="Connect" type="Button" parent="Panel/VBoxContainer/Connect"] -offset_left = 960.0 -offset_right = 1024.0 -offset_bottom = 24.0 -toggle_mode = true -text = "Connect" - -[node name="Settings" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_top = 28.0 -offset_right = 1024.0 -offset_bottom = 52.0 - -[node name="Mode" type="OptionButton" parent="Panel/VBoxContainer/Settings"] -offset_right = 29.0 -offset_bottom = 24.0 - -[node name="Multiplayer" type="CheckBox" parent="Panel/VBoxContainer/Settings"] -offset_left = 33.0 -offset_right = 159.0 -offset_bottom = 24.0 -pressed = true -text = "Multiplayer API" - -[node name="Destination" type="OptionButton" parent="Panel/VBoxContainer/Settings"] -offset_left = 163.0 -offset_right = 192.0 -offset_bottom = 24.0 - -[node name="Send" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_top = 56.0 -offset_right = 1024.0 -offset_bottom = 80.0 - -[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/Send"] -offset_right = 977.0 -offset_bottom = 24.0 -size_flags_horizontal = 3 -placeholder_text = "Enter some text to send..." - -[node name="Send" type="Button" parent="Panel/VBoxContainer/Send"] -offset_left = 981.0 -offset_right = 1024.0 -offset_bottom = 24.0 -text = "Send" - -[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"] -offset_top = 84.0 -offset_right = 1024.0 -offset_bottom = 600.0 -size_flags_vertical = 3 - -[node name="Client" type="Node" parent="."] -script = ExtResource( 2 ) - -[connection signal="toggled" from="Panel/VBoxContainer/Connect/Connect" to="." method="_on_Connect_toggled"] -[connection signal="item_selected" from="Panel/VBoxContainer/Settings/Mode" to="." method="_on_Mode_item_selected"] -[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_Send_pressed"] diff --git a/networking/websocket_chat/client/client_ui.gd b/networking/websocket_chat/client/client_ui.gd deleted file mode 100644 index 6607c97c..00000000 --- a/networking/websocket_chat/client/client_ui.gd +++ /dev/null @@ -1,62 +0,0 @@ -extends Control - -@onready var _client = $Client -@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel -@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit -@onready var _host = $Panel/VBoxContainer/Connect/Host -@onready var _multiplayer = $Panel/VBoxContainer/Settings/Multiplayer -@onready var _write_mode = $Panel/VBoxContainer/Settings/Mode -@onready var _destination = $Panel/VBoxContainer/Settings/Destination - -func _ready(): - _write_mode.clear() - _write_mode.add_item("BINARY") - _write_mode.set_item_metadata(0, WebSocketPeer.WRITE_MODE_BINARY) - _write_mode.add_item("TEXT") - _write_mode.set_item_metadata(1, WebSocketPeer.WRITE_MODE_TEXT) - - _destination.add_item("Broadcast") - _destination.set_item_metadata(0, 0) - _destination.add_item("Last connected") - _destination.set_item_metadata(1, 1) - _destination.add_item("All But last connected") - _destination.set_item_metadata(2, -1) - _destination.select(0) - - -func _on_Mode_item_selected(_id): - _client.set_write_mode(_write_mode.get_selected_metadata()) - - -func _on_Send_pressed(): - if _line_edit.text == "": - return - - var dest = _destination.get_selected_metadata() - if dest > 0: - dest = _client.last_connected_client - elif dest < 0: - dest = -_client.last_connected_client - - Utils._log(_log_dest, "Sending data %s to %s" % [_line_edit.text, dest]) - _client.send_data(_line_edit.text, dest) - _line_edit.text = "" - - -func _on_Connect_toggled( pressed ): - if pressed: - var multiplayer = _multiplayer.pressed - if multiplayer: - _write_mode.disabled = true - else: - _destination.disabled = true - _multiplayer.disabled = true - if _host.text != "": - Utils._log(_log_dest, "Connecting to host: %s" % [_host.text]) - var supported_protocols = PackedStringArray(["my-protocol2", "my-protocol", "binary"]) - _client.connect_to_url(_host.text, supported_protocols, multiplayer) - else: - _destination.disabled = false - _write_mode.disabled = false - _multiplayer.disabled = false - _client.disconnect_from_host() diff --git a/networking/websocket_chat/combo.tscn b/networking/websocket_chat/combo.tscn new file mode 100644 index 00000000..053ec4e7 --- /dev/null +++ b/networking/websocket_chat/combo.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=3 format=3 uid="uid://dye16x7udqrxg"] + +[ext_resource type="PackedScene" uid="uid://qvg4q16blgx5" path="res://server.tscn" id="1_0srxc"] +[ext_resource type="PackedScene" uid="uid://ph5ghsflqegf" path="res://client.tscn" id="2_percb"] + +[node name="Combo" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 + +[node name="Box" type="HBoxContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Server" parent="Box" instance=ExtResource("1_0srxc")] +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_right = 574.0 +offset_bottom = 648.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="Box"] +offset_left = 578.0 +offset_right = 1152.0 +offset_bottom = 648.0 +size_flags_horizontal = 3 + +[node name="Client" parent="Box/VBoxContainer" instance=ExtResource("2_percb")] +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_right = 574.0 +offset_bottom = 213.0 + +[node name="Client2" parent="Box/VBoxContainer" instance=ExtResource("2_percb")] +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_top = 217.0 +offset_right = 574.0 +offset_bottom = 430.0 + +[node name="Client3" parent="Box/VBoxContainer" instance=ExtResource("2_percb")] +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_top = 434.0 +offset_right = 574.0 +offset_bottom = 648.0 diff --git a/networking/websocket_chat/combo/combo.tscn b/networking/websocket_chat/combo/combo.tscn deleted file mode 100644 index 5de9c4d5..00000000 --- a/networking/websocket_chat/combo/combo.tscn +++ /dev/null @@ -1,53 +0,0 @@ -[gd_scene load_steps=3 format=2] - -[ext_resource path="res://server/server.tscn" type="PackedScene" id=1] -[ext_resource path="res://client/client.tscn" type="PackedScene" id=2] - -[node name="Combo" type="Control"] -anchor_right = 1.0 -anchor_bottom = 1.0 -mouse_filter = 1 - -[node name="Box" type="HBoxContainer" parent="."] -anchor_right = 1.0 -anchor_bottom = 1.0 -custom_constants/separation = 20 - -[node name="ServerControl" parent="Box" instance=ExtResource( 1 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -offset_right = 502.0 -offset_bottom = 600.0 -size_flags_horizontal = 3 - -[node name="VBoxContainer" type="VBoxContainer" parent="Box"] -offset_left = 522.0 -offset_right = 1024.0 -offset_bottom = 600.0 -size_flags_horizontal = 3 - -[node name="Client" parent="Box/VBoxContainer" instance=ExtResource( 2 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -offset_right = 502.0 -offset_bottom = 197.0 -size_flags_horizontal = 3 -size_flags_vertical = 3 - -[node name="Client2" parent="Box/VBoxContainer" instance=ExtResource( 2 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -offset_top = 201.0 -offset_right = 502.0 -offset_bottom = 398.0 -size_flags_horizontal = 3 -size_flags_vertical = 3 - -[node name="Client3" parent="Box/VBoxContainer" instance=ExtResource( 2 )] -anchor_right = 0.0 -anchor_bottom = 0.0 -offset_top = 402.0 -offset_right = 502.0 -offset_bottom = 600.0 -size_flags_horizontal = 3 -size_flags_vertical = 3 diff --git a/networking/websocket_chat/icon.png.import b/networking/websocket_chat/icon.png.import index 889af9df..82c15acb 100644 --- a/networking/websocket_chat/icon.png.import +++ b/networking/websocket_chat/icon.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture2D" -path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +type="CompressedTexture2D" +uid="uid://db0a1leye11ap" +path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" metadata={ "vram_texture": false } @@ -10,26 +11,24 @@ metadata={ [deps] source_file="res://icon.png" -dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"] +dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] [params] compress/mode=0 compress/lossy_quality=0.7 -compress/hdr_mode=0 +compress/hdr_compression=1 compress/bptc_ldr=0 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false process/normal_map_invert_y=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/networking/websocket_chat/project.godot b/networking/websocket_chat/project.godot index fe27c2ae..3fa45e5f 100644 --- a/networking/websocket_chat/project.godot +++ b/networking/websocket_chat/project.godot @@ -6,19 +6,32 @@ ; [section] ; section goes between [] ; param=value ; assign values to parameters -config_version=4 +config_version=5 + +_global_script_classes=[{ +"base": "Node", +"class": &"WebSocketClient", +"language": &"GDScript", +"path": "res://websocket/WebSocketClient.gd" +}, { +"base": "Node", +"class": &"WebSocketServer", +"language": &"GDScript", +"path": "res://websocket/WebSocketServer.gd" +}] +_global_script_class_icons={ +"WebSocketClient": "", +"WebSocketServer": "" +} [application] config/name="WebSocket Chat Demo" config/description="This is a demo of a simple chat implemented using WebSockets, showing both how to host a websocket server from Godot and how to connect to it." -run/main_scene="res://combo/combo.tscn" +run/main_scene="res://combo.tscn" +config/features=PackedStringArray("4.0") config/icon="res://icon.png" -[autoload] - -Utils="*res://utils.gd" - [gdnative] singletons=[] diff --git a/networking/websocket_chat/server.gd b/networking/websocket_chat/server.gd new file mode 100644 index 00000000..7ef79e6b --- /dev/null +++ b/networking/websocket_chat/server.gd @@ -0,0 +1,51 @@ +extends Control + +@onready var _server : WebSocketServer = $WebSocketServer +@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel +@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit +@onready var _listen_port = $Panel/VBoxContainer/Connect/Port + +func info(msg): + print(msg) + _log_dest.add_text(str(msg) + "\n") + + +# Server signals +func _on_web_socket_server_client_connected(peer_id): + var peer : WebSocketPeer = _server.peers[peer_id] + info("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()]) + _server.send(-peer_id, "[%d] connected" % peer_id) + + +func _on_web_socket_server_client_disconnected(peer_id): + var peer : WebSocketPeer = _server.peers[peer_id] + info("Remote client disconnected: %d. Code: %d, Reason: %s" % [peer_id, peer.get_close_code(), peer.get_close_reason()]) + _server.send(-peer_id, "[%d] disconnected" % peer_id) + + +func _on_web_socket_server_message_received(peer_id, message): + info("Server received data from peer %d: %s" % [peer_id, message]) + _server.send(-peer_id, "[%d] Says: %s" % [peer_id, message]) + + +# UI signals. +func _on_send_pressed(): + if _line_edit.text == "": + return + + info("Sending message: %s" % [_line_edit.text]) + _server.send(0, "Server says: %s" % _line_edit.text) + _line_edit.text = "" + + +func _on_listen_toggled(pressed): + if not pressed: + _server.stop() + info("Server stopped") + return + var port = int(_listen_port.value) + var err = _server.listen(port) + if err != OK: + info("Error listing on port %s" % port) + return + info("Listing on port %s, supported protocols: %s" % [port, _server.supported_protocols]) diff --git a/networking/websocket_chat/server.tscn b/networking/websocket_chat/server.tscn new file mode 100644 index 00000000..356d7113 --- /dev/null +++ b/networking/websocket_chat/server.tscn @@ -0,0 +1,61 @@ +[gd_scene load_steps=4 format=3 uid="uid://qvg4q16blgx5"] + +[ext_resource type="PackedScene" uid="uid://cyvrywci15kev" path="res://chat.tscn" id="1_i673i"] +[ext_resource type="Script" path="res://server.gd" id="1_urpfw"] +[ext_resource type="Script" path="res://websocket/WebSocketServer.gd" id="3_0eqsy"] + +[node name="Server" instance=ExtResource("1_i673i")] +script = ExtResource("1_urpfw") + +[node name="WebSocketServer" type="Node" parent="." index="0"] +script = ExtResource("3_0eqsy") +supported_protocols = PackedStringArray("demo-chat") + +[node name="Panel" parent="." index="1"] +layout_mode = 3 + +[node name="VBoxContainer" parent="Panel" index="0"] +layout_mode = 3 + +[node name="Listen" parent="Panel/VBoxContainer" index="0"] +layout_mode = 3 + +[node name="Connect" parent="Panel/VBoxContainer" index="1"] +layout_mode = 3 + +[node name="Host" parent="Panel/VBoxContainer/Connect" index="0"] +visible = false +layout_mode = 3 +offset_right = 1006.0 + +[node name="Connect" parent="Panel/VBoxContainer/Connect" index="1"] +visible = false +layout_mode = 3 + +[node name="Port" parent="Panel/VBoxContainer/Connect" index="2"] +layout_mode = 3 +offset_left = 0.0 +offset_right = 83.0 + +[node name="Listen" parent="Panel/VBoxContainer/Connect" index="3"] +layout_mode = 3 +offset_left = 87.0 +offset_right = 142.0 + +[node name="Send" parent="Panel/VBoxContainer" index="2"] +layout_mode = 3 + +[node name="LineEdit" parent="Panel/VBoxContainer/Send" index="0"] +layout_mode = 3 + +[node name="Send" parent="Panel/VBoxContainer/Send" index="1"] +layout_mode = 3 + +[node name="RichTextLabel" parent="Panel/VBoxContainer" index="3"] +layout_mode = 3 + +[connection signal="client_connected" from="WebSocketServer" to="." method="_on_web_socket_server_client_connected"] +[connection signal="client_disconnected" from="WebSocketServer" to="." method="_on_web_socket_server_client_disconnected"] +[connection signal="message_received" from="WebSocketServer" to="." method="_on_web_socket_server_message_received"] +[connection signal="toggled" from="Panel/VBoxContainer/Connect/Listen" to="." method="_on_listen_toggled"] +[connection signal="pressed" from="Panel/VBoxContainer/Send/Send" to="." method="_on_send_pressed"] diff --git a/networking/websocket_chat/server/server.gd b/networking/websocket_chat/server/server.gd deleted file mode 100644 index a416acf0..00000000 --- a/networking/websocket_chat/server/server.gd +++ /dev/null @@ -1,84 +0,0 @@ -extends Node - -@onready var _log_dest = get_parent().get_node(^"Panel/VBoxContainer/RichTextLabel") - -var _server = WebSocketServer.new() -var _clients = {} -var _write_mode = WebSocketPeer.WRITE_MODE_BINARY -var _use_multiplayer = true -var last_connected_client = 0 - -func _init(): - _server.connect(&"client_connected", self._client_connected) - _server.connect(&"client_disconnected", self._client_disconnected) - _server.connect(&"client_close_request", self._client_close_request) - _server.connect(&"data_received", self._client_receive) - - _server.connect(&"peer_packet", self._client_receive) - _server.connect(&"peer_connected", self._client_connected, ["multiplayer_protocol"]) - _server.connect(&"peer_disconnected", self._client_disconnected) - - -func _exit_tree(): - _clients.clear() - _server.stop() - - -func _process(_delta): - if _server.is_listening(): - _server.poll() - - -func _client_close_request(id, code, reason): - print(reason == "Bye bye!") - Utils._log(_log_dest, "Client %s close code: %d, reason: %s" % [id, code, reason]) - - -func _client_connected(id, protocol): - _clients[id] = _server.get_peer(id) - _clients[id].set_write_mode(_write_mode) - last_connected_client = id - Utils._log(_log_dest, "%s: Client connected with protocol %s" % [id, protocol]) - - -func _client_disconnected(id, clean = true): - Utils._log(_log_dest, "Client %s disconnected. Was clean: %s" % [id, clean]) - if _clients.has(id): - _clients.erase(id) - - -func _client_receive(id): - if _use_multiplayer: - var peer_id = _server.get_packet_peer() - var packet = _server.get_packet() - Utils._log(_log_dest, "MPAPI: From %s data: %s" % [peer_id, Utils.decode_data(packet, false)]) - else: - var packet = _server.get_peer(id).get_packet() - var is_string = _server.get_peer(id).was_string_packet() - Utils._log(_log_dest, "Data from %s BINARY: %s: %s" % [id, not is_string, Utils.decode_data(packet, is_string)]) - - -func send_data(data, dest): - if _use_multiplayer: - _server.set_target_peer(dest) - _server.put_packet(Utils.encode_data(data, _write_mode)) - else: - for id in _clients: - _server.get_peer(id).put_packet(Utils.encode_data(data, _write_mode)) - - -func listen(port, supported_protocols, multiplayer): - _use_multiplayer = multiplayer - if _use_multiplayer: - set_write_mode(WebSocketPeer.WRITE_MODE_BINARY) - return _server.listen(port, supported_protocols, multiplayer) - - -func stop(): - _server.stop() - - -func set_write_mode(mode): - _write_mode = mode - for c in _clients: - _clients[c].set_write_mode(_write_mode) diff --git a/networking/websocket_chat/server/server.tscn b/networking/websocket_chat/server/server.tscn deleted file mode 100644 index 52730bbf..00000000 --- a/networking/websocket_chat/server/server.tscn +++ /dev/null @@ -1,88 +0,0 @@ -[gd_scene load_steps=3 format=2] - -[ext_resource path="res://server/server_ui.gd" type="Script" id=1] -[ext_resource path="res://server/server.gd" type="Script" id=2] - -[node name="ServerControl" type="Control"] -anchor_right = 1.0 -anchor_bottom = 1.0 -script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="Server" type="Node" parent="."] -script = ExtResource( 2 ) - -[node name="Panel" type="Panel" parent="."] -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="VBoxContainer" type="VBoxContainer" parent="Panel"] -anchor_right = 1.0 -anchor_bottom = 1.0 - -[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_right = 1024.0 -offset_bottom = 24.0 - -[node name="Port" type="SpinBox" parent="Panel/VBoxContainer/HBoxContainer"] -offset_right = 74.0 -offset_bottom = 24.0 -min_value = 1.0 -max_value = 65535.0 -value = 8000.0 - -[node name="Listen" type="Button" parent="Panel/VBoxContainer/HBoxContainer"] -offset_left = 78.0 -offset_right = 129.0 -offset_bottom = 24.0 -toggle_mode = true -text = "Listen" - -[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_top = 28.0 -offset_right = 1024.0 -offset_bottom = 52.0 - -[node name="WriteMode" type="OptionButton" parent="Panel/VBoxContainer/HBoxContainer2"] -offset_right = 29.0 -offset_bottom = 24.0 - -[node name="MPAPI" type="CheckBox" parent="Panel/VBoxContainer/HBoxContainer2"] -offset_left = 33.0 -offset_right = 159.0 -offset_bottom = 24.0 -pressed = true -text = "Multiplayer API" - -[node name="Destination" type="OptionButton" parent="Panel/VBoxContainer/HBoxContainer2"] -offset_left = 163.0 -offset_right = 192.0 -offset_bottom = 24.0 - -[node name="HBoxContainer3" type="HBoxContainer" parent="Panel/VBoxContainer"] -offset_top = 56.0 -offset_right = 1024.0 -offset_bottom = 80.0 - -[node name="LineEdit" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer3"] -offset_right = 977.0 -offset_bottom = 24.0 -size_flags_horizontal = 3 - -[node name="Send" type="Button" parent="Panel/VBoxContainer/HBoxContainer3"] -offset_left = 981.0 -offset_right = 1024.0 -offset_bottom = 24.0 -text = "Send" - -[node name="RichTextLabel" type="RichTextLabel" parent="Panel/VBoxContainer"] -offset_top = 84.0 -offset_right = 1024.0 -offset_bottom = 600.0 -size_flags_vertical = 3 - -[connection signal="toggled" from="Panel/VBoxContainer/HBoxContainer/Listen" to="." method="_on_Listen_toggled"] -[connection signal="item_selected" from="Panel/VBoxContainer/HBoxContainer2/WriteMode" to="." method="_on_WriteMode_item_selected"] -[connection signal="pressed" from="Panel/VBoxContainer/HBoxContainer3/Send" to="." method="_on_Send_pressed"] diff --git a/networking/websocket_chat/server/server_ui.gd b/networking/websocket_chat/server/server_ui.gd deleted file mode 100644 index af626db9..00000000 --- a/networking/websocket_chat/server/server_ui.gd +++ /dev/null @@ -1,70 +0,0 @@ -extends Control - -@onready var _server = $Server -@onready var _port = $Panel/VBoxContainer/HBoxContainer/Port -@onready var _line_edit = $Panel/VBoxContainer/HBoxContainer3/LineEdit -@onready var _write_mode = $Panel/VBoxContainer/HBoxContainer2/WriteMode -@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel -@onready var _multiplayer = $Panel/VBoxContainer/HBoxContainer2/MPAPI -@onready var _destination = $Panel/VBoxContainer/HBoxContainer2/Destination - -func _ready(): - _write_mode.clear() - _write_mode.add_item("BINARY") - _write_mode.set_item_metadata(0, WebSocketPeer.WRITE_MODE_BINARY) - _write_mode.add_item("TEXT") - _write_mode.set_item_metadata(1, WebSocketPeer.WRITE_MODE_TEXT) - _write_mode.select(0) - - _destination.add_item("Broadcast") - _destination.set_item_metadata(0, 0) - _destination.add_item("Last connected") - _destination.set_item_metadata(1, 1) - _destination.add_item("All But last connected") - _destination.set_item_metadata(2, -1) - _destination.select(0) - - -func _on_Listen_toggled(pressed): - if pressed: - var use_multiplayer = _multiplayer.pressed - _multiplayer.disabled = true - var supported_protocols = PackedStringArray(["my-protocol", "binary"]) - var port = int(_port.value) - if use_multiplayer: - _write_mode.disabled = true - _write_mode.select(0) - else: - _destination.disabled = true - _destination.select(0) - if _server.listen(port, supported_protocols, use_multiplayer) == OK: - Utils._log(_log_dest, "Listing on port %s" % port) - if not use_multiplayer: - Utils._log(_log_dest, "Supported protocols: %s" % supported_protocols) - else: - Utils._log(_log_dest, "Error listening on port %s" % port) - else: - _server.stop() - _multiplayer.disabled = false - _write_mode.disabled = false - _destination.disabled = false - Utils._log(_log_dest, "Server stopped") - - -func _on_Send_pressed(): - if _line_edit.text == "": - return - - var dest = _destination.get_selected_metadata() - if dest > 0: - dest = _server.last_connected_client - elif dest < 0: - dest = -_server.last_connected_client - - Utils._log(_log_dest, "Sending data %s to %s" % [_line_edit.text, dest]) - _server.send_data(_line_edit.text, dest) - _line_edit.text = "" - - -func _on_WriteMode_item_selected(_id): - _server.set_write_mode(_write_mode.get_selected_metadata()) diff --git a/networking/websocket_chat/utils.gd b/networking/websocket_chat/utils.gd deleted file mode 100644 index 78be1512..00000000 --- a/networking/websocket_chat/utils.gd +++ /dev/null @@ -1,17 +0,0 @@ -extends Node - -func encode_data(data, mode): - if mode == WebSocketPeer.WRITE_MODE_TEXT: - return data.to_utf8() - return var2bytes(data) - - -func decode_data(data, is_string): - if is_string: - return data.get_string_from_utf8() - return bytes2var(data) - - -func _log(node, msg): - print(msg) - node.add_text(str(msg) + "\n") diff --git a/networking/websocket_chat/websocket/WebSocketClient.gd b/networking/websocket_chat/websocket/WebSocketClient.gd new file mode 100644 index 00000000..7acd8c48 --- /dev/null +++ b/networking/websocket_chat/websocket/WebSocketClient.gd @@ -0,0 +1,73 @@ +extends Node +class_name WebSocketClient + +@export var handshake_headers : PackedStringArray +@export var supported_protocols : PackedStringArray +@export var tls_trusted_certificate : X509Certificate +@export var tls_verify := true + + +var socket = WebSocketPeer.new() +var last_state = WebSocketPeer.STATE_CLOSED + + +signal connected_to_server() +signal connection_closed() +signal message_received(message: Variant) + + +func connect_to_url(url) -> int: + socket.supported_protocols = supported_protocols + socket.handshake_headers = handshake_headers + var err = socket.connect_to_url(url, tls_verify, tls_trusted_certificate) + if err != OK: + return err + last_state = socket.get_ready_state() + return OK + + +func send(message) -> int: + if typeof(message) == TYPE_STRING: + return socket.send_text(message) + return socket.send(var_to_bytes(message)) + + +func get_message() -> Variant: + if socket.get_available_packet_count() < 1: + return null + var pkt = socket.get_packet() + if socket.was_string_packet(): + return pkt.get_string_from_utf8() + return bytes_to_var(pkt) + + +func close(code := 1000, reason := "") -> void: + socket.close(code, reason) + last_state = socket.get_ready_state() + + +func clear() -> void: + socket = WebSocketPeer.new() + last_state = socket.get_ready_state() + + +func get_socket() -> WebSocketPeer: + return socket + + +func poll() -> void: + if socket.get_ready_state() != socket.STATE_CLOSED: + socket.poll() + var state = socket.get_ready_state() + if last_state != state: + last_state = state + if state == socket.STATE_OPEN: + connected_to_server.emit() + elif state == socket.STATE_CLOSED: + connection_closed.emit() + while socket.get_ready_state() == socket.STATE_OPEN and socket.get_available_packet_count(): + message_received.emit(get_message()) + + +func _process(delta): + poll() diff --git a/networking/websocket_chat/websocket/WebSocketServer.gd b/networking/websocket_chat/websocket/WebSocketServer.gd new file mode 100644 index 00000000..587ecaaa --- /dev/null +++ b/networking/websocket_chat/websocket/WebSocketServer.gd @@ -0,0 +1,162 @@ +extends Node +class_name WebSocketServer + +signal message_received(peer_id : int, message) +signal client_connected(peer_id : int) +signal client_disconnected(peer_id : int) + +@export var handshake_headers := PackedStringArray() +@export var supported_protocols : PackedStringArray +@export var handshake_timout := 3000 +@export var use_tls := false +@export var tls_cert : X509Certificate +@export var tls_key : CryptoKey +@export var refuse_new_connections := false : + set(refuse): + if refuse: + pending_peers.clear() + + +class PendingPeer: + var connect_time : int + var tcp : StreamPeerTCP + var connection : StreamPeer + var ws : WebSocketPeer + + func _init(p_tcp: StreamPeerTCP): + tcp = p_tcp + connection = p_tcp + connect_time = Time.get_ticks_msec() + + +var tcp_server := TCPServer.new() +var pending_peers : Array[PendingPeer] = [] +var peers : Dictionary + + +func listen(port : int) -> int: + assert(not tcp_server.is_listening()) + return tcp_server.listen(port) + + +func stop(): + tcp_server.stop() + pending_peers.clear() + peers.clear() + + +func send(peer_id, message) -> int: + var type = typeof(message) + if peer_id <= 0: + # Send to multiple peers, (zero = brodcast, negative = exclude one) + for id in peers: + if id == -peer_id: + continue + if type == TYPE_STRING: + peers[id].send_text(message) + else: + peers[id].put_packet(message) + return OK + + assert(peers.has(peer_id)) + var socket = peers[peer_id] + if type == TYPE_STRING: + return socket.send_text(message) + return socket.send(var_to_bytes(message)) + + +func get_message(peer_id) -> Variant: + assert(peers.has(peer_id)) + var socket = peers[peer_id] + if socket.get_available_packet_count() < 1: + return null + var pkt = socket.get_packet() + if socket.was_string_packet(): + return pkt.get_string_from_utf8() + return bytes_to_var(pkt) + + +func has_message(peer_id) -> bool: + assert(peers.has(peer_id)) + return peers[peer_id].get_available_packet_count() > 0 + + +func _create_peer() -> WebSocketPeer: + var ws = WebSocketPeer.new() + ws.supported_protocols = supported_protocols + ws.handshake_headers = handshake_headers + return ws + + +func poll() -> void: + if not tcp_server.is_listening(): + return + while not refuse_new_connections and tcp_server.is_connection_available(): + var conn = tcp_server.take_connection() + assert(conn != null) + pending_peers.append(PendingPeer.new(conn)) + var to_remove := [] + for p in pending_peers: + if not _connect_pending(p): + if p.connect_time + handshake_timout < Time.get_ticks_msec(): + # Timeout + to_remove.append(p) + continue # Still pending + to_remove.append(p) + for r in to_remove: + pending_peers.erase(r) + to_remove.clear() + for id in peers: + var p : WebSocketPeer = peers[id] + var packets = p.get_available_packet_count() + p.poll() + if p.get_ready_state() != WebSocketPeer.STATE_OPEN: + client_disconnected.emit(id) + to_remove.append(id) + continue + while p.get_available_packet_count(): + message_received.emit(id, get_message(id)) + for r in to_remove: + peers.erase(r) + to_remove.clear() + + +func _connect_pending(p: PendingPeer) -> bool: + if p.ws != null: + # Poll websocket client if doing handshake + p.ws.poll() + var state = p.ws.get_ready_state() + if state == WebSocketPeer.STATE_OPEN: + var id = randi_range(2, 1 << 30) + peers[id] = p.ws + client_connected.emit(id) + return true # Success. + elif state != WebSocketPeer.STATE_CONNECTING: + return true # Failure. + return false # Still connecting. + elif p.tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return true # TCP disconnected. + elif not use_tls: + # TCP is ready, create WS peer + p.ws = _create_peer() + p.ws.accept_stream(p.tcp) + return false # WebSocketPeer connection is pending. + else: + if p.connection == p.tcp: + assert(tls_key != null and tls_cert != null) + var tls = StreamPeerTLS.new() + tls.accept_stream(p.tcp, tls_key, tls_cert) + p.connection = tls + p.connection.poll() + var status = p.connection.get_status() + if status == StreamPeerTLS.STATUS_CONNECTED: + p.ws = _create_peer() + p.ws.accept_stream(p.connection) + return false # WebSocketPeer connection is pending. + if status != StreamPeerTLS.STATUS_HANDSHAKING: + return true # Failure. + return false + + +func _process(delta): + poll()