[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.
This commit is contained in:
Fabio Alessandrelli
2022-09-28 22:43:58 +02:00
parent ea65d7f896
commit dd2ba9a5ba
18 changed files with 627 additions and 568 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"]

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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=[]

View File

@@ -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])

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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"]

View File

@@ -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())

View File

@@ -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")

View File

@@ -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()

View File

@@ -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()