248 lines
7.3 KiB
GDScript
248 lines
7.3 KiB
GDScript
class_name SocketIOClient extends Node
|
|
|
|
enum EngineIOPacketType {
|
|
open = 0,
|
|
close = 1,
|
|
ping = 2,
|
|
pong = 3,
|
|
message = 4,
|
|
upgrade = 5,
|
|
noop = 6,
|
|
}
|
|
|
|
enum SocketIOPacketType {
|
|
CONNECT = 0,
|
|
DISCONNECT = 1,
|
|
EVENT = 2,
|
|
ACK = 3,
|
|
CONNECT_ERROR = 4,
|
|
BINARY_EVENT = 5,
|
|
BINARY_ACK = 6,
|
|
}
|
|
|
|
enum ConnectionState {
|
|
DISCONNECTED,
|
|
CONNECTED,
|
|
RECONNECTING,
|
|
}
|
|
|
|
var _url: String
|
|
var _client: WebSocketPeer = WebSocketPeer.new()
|
|
var _sid: String
|
|
var _pingTimeout: int = 0
|
|
var _pingInterval: int = 0
|
|
var _auth: Variant = null
|
|
var _connection_state: ConnectionState = ConnectionState.DISCONNECTED
|
|
var _reconnect_timer: Timer = null
|
|
|
|
# triggered when engine.io connection is established
|
|
signal on_engine_connected(sid: String)
|
|
# triggered when engine.io connection is closed
|
|
signal on_engine_disconnected(code: int, reason: String)
|
|
# triggered when engine.io message is received
|
|
signal on_engine_message(payload: String)
|
|
|
|
# triggered when socket.io connection is established
|
|
signal on_connect(payload: Variant, name_space: String, error: bool)
|
|
# triggered when socket.io connection is closed
|
|
signal on_disconnect(name_space: String)
|
|
# triggered when socket.io event is received
|
|
signal on_event(event_name: String, payload: Variant, name_space: String)
|
|
|
|
# triggered when lost connection in not-clean way (i.e. service shut down)
|
|
# and now trying to reconnect. When re-connects successfully,
|
|
# the on_reconnected signal is emitted, instead of on_connect
|
|
signal on_connection_lost
|
|
|
|
# triggered when connects again after losing connection
|
|
# it's alternative to on_connect signal, but only emitted
|
|
# after automatically re-connecting with socket.io server
|
|
signal on_reconnected(payload: Variant, name_space: String, error: bool)
|
|
|
|
|
|
func _init(url: String, auth: Variant=null):
|
|
_auth = auth
|
|
url = _preprocess_url(url)
|
|
_url = "%s?EIO=4&transport=websocket" % url
|
|
|
|
func _preprocess_url(url: String) -> String:
|
|
if not url.ends_with("/"):
|
|
url = url + "/"
|
|
if url.begins_with("https"):
|
|
url = "wss" + url.erase(0, len("https"))
|
|
elif url.begins_with("http"):
|
|
url = "ws" + url.erase(0, len("http"))
|
|
return url
|
|
|
|
func _ready():
|
|
_client.connect_to_url(_url)
|
|
|
|
func _process(_delta):
|
|
_client.poll()
|
|
|
|
var state = _client.get_ready_state()
|
|
if state == WebSocketPeer.STATE_OPEN:
|
|
while _client.get_available_packet_count():
|
|
var packet = _client.get_packet()
|
|
var packetString = packet.get_string_from_utf8()
|
|
if len(packetString) > 0:
|
|
_engineio_decode_packet(packetString)
|
|
# TODO: handle binary data?
|
|
|
|
elif state == WebSocketPeer.STATE_CLOSED:
|
|
set_process(false)
|
|
|
|
var code = _client.get_close_code()
|
|
var reason = _client.get_close_reason()
|
|
|
|
if code == -1:
|
|
# -1 is not-clean disconnect (i.e. Service shut down)
|
|
# we should try to reconnect
|
|
|
|
_connection_state = ConnectionState.RECONNECTING
|
|
|
|
_reconnect_timer = Timer.new()
|
|
_reconnect_timer.wait_time = 1.0
|
|
_reconnect_timer.timeout.connect(_on_reconnect_timer_timeout)
|
|
_reconnect_timer.autostart = true
|
|
add_child(_reconnect_timer)
|
|
|
|
on_connection_lost.emit()
|
|
else:
|
|
_connection_state = ConnectionState.DISCONNECTED
|
|
on_engine_disconnected.emit(code, reason)
|
|
|
|
func _on_reconnect_timer_timeout():
|
|
_client.poll()
|
|
var state = _client.get_ready_state()
|
|
if state == WebSocketPeer.STATE_CLOSED:
|
|
_client.connect_to_url(_url)
|
|
else:
|
|
set_process(true)
|
|
_reconnect_timer.queue_free()
|
|
|
|
func _exit_tree():
|
|
if _connection_state == ConnectionState.CONNECTED:
|
|
_engineio_send_packet(EngineIOPacketType.close)
|
|
|
|
_client.close()
|
|
|
|
func _engineio_decode_packet(packet: String):
|
|
var packetType = int(packet.substr(0, 1))
|
|
var packetPayload = packet.substr(1)
|
|
|
|
match packetType:
|
|
EngineIOPacketType.open:
|
|
var json = JSON.new()
|
|
json.parse(packetPayload)
|
|
_sid = json.data["sid"]
|
|
_pingTimeout = int(json.data["pingTimeout"])
|
|
_pingInterval = int(json.data["pingInterval"])
|
|
on_engine_connected.emit(_sid)
|
|
|
|
EngineIOPacketType.ping:
|
|
_engineio_send_packet(EngineIOPacketType.pong)
|
|
|
|
EngineIOPacketType.message:
|
|
_socketio_parse_packet(packetPayload)
|
|
on_engine_message.emit(packetPayload)
|
|
|
|
func _engineio_send_packet(type: EngineIOPacketType, payload: String=""):
|
|
if len(payload) == 0:
|
|
_client.send_text("%d" % type)
|
|
else:
|
|
_client.send_text("%d%s" % [type, payload])
|
|
|
|
func _socketio_parse_packet(payload: String):
|
|
var packetType = int(payload.substr(0, 1))
|
|
payload = payload.substr(1)
|
|
|
|
var regex = RegEx.new()
|
|
regex.compile("(\\d+)-")
|
|
var regexMatch = regex.search(payload)
|
|
if regexMatch and regexMatch.get_start() == 0:
|
|
payload = payload.substr(regexMatch.get_end())
|
|
push_error("Binary data payload not supported!")
|
|
|
|
var name_space = "/"
|
|
regex.compile("(\\w),")
|
|
regexMatch = regex.search(payload)
|
|
if regexMatch and regexMatch.get_start() == 0:
|
|
payload = payload.substr(regexMatch.get_end())
|
|
name_space = regexMatch.get_string(1)
|
|
|
|
# var ack_id = null
|
|
regex.compile("(\\d+)")
|
|
regexMatch = regex.search(payload)
|
|
if regexMatch and regexMatch.get_start() == 0:
|
|
payload = payload.substr(regexMatch.get_end())
|
|
push_warning("Ignoring acknowledge ID!")
|
|
|
|
var data = null
|
|
if len(payload) > 0:
|
|
var json = JSON.new()
|
|
if json.parse(payload) == OK:
|
|
data = json.data
|
|
|
|
match packetType:
|
|
SocketIOPacketType.CONNECT:
|
|
if _connection_state == ConnectionState.RECONNECTING:
|
|
_connection_state = ConnectionState.CONNECTED
|
|
on_reconnected.emit(data, name_space, false)
|
|
else:
|
|
_connection_state = ConnectionState.CONNECTED
|
|
on_connect.emit(data, name_space, false)
|
|
|
|
SocketIOPacketType.CONNECT_ERROR:
|
|
if _connection_state == ConnectionState.RECONNECTING:
|
|
_connection_state = ConnectionState.CONNECTED
|
|
on_reconnected.emit(data, name_space, true)
|
|
else:
|
|
_connection_state = ConnectionState.CONNECTED
|
|
on_connect.emit(data, name_space, true)
|
|
|
|
SocketIOPacketType.EVENT:
|
|
if typeof(data) != TYPE_ARRAY:
|
|
push_error("Invalid socketio event format!")
|
|
var eventName = data[0]
|
|
var eventData = data[1] if len(data) > 1 else null
|
|
on_event.emit(eventName, eventData, name_space)
|
|
|
|
func _socketio_send_packet(type: SocketIOPacketType, name_space: String, data: Variant=null, binaryData: Array[PackedByteArray]=[], ack_id: Variant=null):
|
|
var payload = "%d" % type
|
|
if binaryData.size() > 0:
|
|
payload += "%d-" % binaryData.size()
|
|
if name_space != "/":
|
|
payload += "%s," % name_space
|
|
if ack_id != null:
|
|
payload += "%d" % ack_id
|
|
if data != null:
|
|
payload += "%s" % JSON.stringify(data)
|
|
|
|
_engineio_send_packet(EngineIOPacketType.message, payload)
|
|
|
|
for binary in binaryData:
|
|
_client.put_packet(binary)
|
|
|
|
# connect to socket.io server by namespace
|
|
func socketio_connect(name_space: String="/"):
|
|
_socketio_send_packet(SocketIOPacketType.CONNECT, name_space, _auth)
|
|
|
|
# disconnect from socket.io server by namespace
|
|
func socketio_disconnect(name_space: String="/"):
|
|
if _connection_state == ConnectionState.CONNECTED:
|
|
# We should ONLY send disconnect packet when we're connected
|
|
_socketio_send_packet(SocketIOPacketType.DISCONNECT, name_space)
|
|
|
|
on_disconnect.emit(name_space)
|
|
|
|
# send event to socket.io server by namespace
|
|
func socketio_send(event_name: String, payload: Variant=null, name_space: String="/"):
|
|
if payload == null:
|
|
_socketio_send_packet(SocketIOPacketType.EVENT, name_space, [event_name])
|
|
else:
|
|
_socketio_send_packet(SocketIOPacketType.EVENT, name_space, [event_name, payload])
|
|
|
|
func get_connection_state() -> ConnectionState:
|
|
return _connection_state
|