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