diff --git a/modules/websocket/doc_classes/WebSocketClient.xml b/modules/websocket/doc_classes/WebSocketClient.xml index 9241492623e..b3ea5354950 100644 --- a/modules/websocket/doc_classes/WebSocketClient.xml +++ b/modules/websocket/doc_classes/WebSocketClient.xml @@ -31,8 +31,12 @@ + + + + - Disconnect from the server if currently connected. + Disconnect this client from the connected host. See [method WebSocketPeer.close] for more info. @@ -43,8 +47,10 @@ + + - Emitted when the connection to the server is closed. + Emitted when the connection to the server is closed. [code]was_clean_close[/code] will be [code]true[/code] if the connection was shutdown cleanly. @@ -64,6 +70,15 @@ Emitted when a WebSocket message is received. Note: This signal is NOT emitted when used as high level multiplayer peer. + + + + + + + Emitted when the server requests a clean close. You should keep polling until you get a [signal connection_closed] signal to achieve the clean close. See [method WebSocketPeer.close] for more details. + + diff --git a/modules/websocket/doc_classes/WebSocketPeer.xml b/modules/websocket/doc_classes/WebSocketPeer.xml index 85a08e0c0b1..cc597069160 100644 --- a/modules/websocket/doc_classes/WebSocketPeer.xml +++ b/modules/websocket/doc_classes/WebSocketPeer.xml @@ -15,8 +15,14 @@ + + + + - Close this WebSocket connection, actively disconnecting the peer. + Close this WebSocket connection. [code]code[/code] is the status code for the closure (see RFC6455 section 7.4 for a list of valid status codes). [reason] is the human readable reason for closing the connection (can be any UTF8 string, must be less than 123 bytes). + Note: To achieve a clean close, you will need to keep polling until either [signal WebSocketClient.connection_closed] or [signal WebSocketServer.client_disconnected] is received. + Note: HTML5 export might not support all status codes. Please refer to browsers-specific documentation for more details. diff --git a/modules/websocket/doc_classes/WebSocketServer.xml b/modules/websocket/doc_classes/WebSocketServer.xml index a1061e446b7..ba66fdd89bd 100644 --- a/modules/websocket/doc_classes/WebSocketServer.xml +++ b/modules/websocket/doc_classes/WebSocketServer.xml @@ -18,8 +18,12 @@ + + + + - Disconnects the given peer. + Disconnects the peer identified by [code]id[/code] from the server. See [method WebSocketPeer.close] for more info. @@ -68,7 +72,7 @@ Start listening on the given port. You can specify the desired subprotocols via the "protocols" array. If the list empty (default), "binary" will be used. - You can use this server as a network peer for [MultiplayerAPI] by passing true as "gd_mp_api". Note: [signal data_received] will not be fired and clients other than Godot will not work in this case. + You can use this server as a network peer for [MultiplayerAPI] by passing [code]true[/code] as [code]gd_mp_api[/code]. Note: [signal data_received] will not be fired and clients other than Godot will not work in this case. @@ -80,6 +84,17 @@ + + + + + + + + + Emitted when a client requests a clean close. You should keep polling until you get a [signal client_disconnected] signal with the same [code]id[/code] to achieve the clean close. See [method WebSocketPeer.close] for more details. + + @@ -92,8 +107,10 @@ + + - Emitted when a client disconnects. + Emitted when a client disconnects. [code]was_clean_close[/code] will be [code]true[/code] if the connection was shutdown cleanly. diff --git a/modules/websocket/emws_client.cpp b/modules/websocket/emws_client.cpp index 836b564de8e..8255ed71161 100644 --- a/modules/websocket/emws_client.cpp +++ b/modules/websocket/emws_client.cpp @@ -55,8 +55,9 @@ EMSCRIPTEN_KEEPALIVE void _esws_on_error(void *obj) { EMSCRIPTEN_KEEPALIVE void _esws_on_close(void *obj, int code, char *reason, int was_clean) { EMWSClient *client = static_cast(obj); + client->_on_close_request(code, String(reason)); client->_is_connecting = false; - client->_on_disconnect(); + client->_on_disconnect(was_clean != 0); } } @@ -145,7 +146,7 @@ Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port, if (!Module.IDHandler.has($0)) return; // Godot Object is gone! var was_clean = 0; - if (event.was_clean) + if (event.wasClean) was_clean = 1; ccall("_esws_on_close", "void", @@ -182,9 +183,9 @@ NetworkedMultiplayerPeer::ConnectionStatus EMWSClient::get_connection_status() c return CONNECTION_DISCONNECTED; }; -void EMWSClient::disconnect_from_host() { +void EMWSClient::disconnect_from_host(int p_code, String p_reason) { - _peer->close(); + _peer->close(p_code, p_reason); }; IP_Address EMWSClient::get_connected_host() const { diff --git a/modules/websocket/emws_client.h b/modules/websocket/emws_client.h index 6c2fa23b539..b20633baff2 100644 --- a/modules/websocket/emws_client.h +++ b/modules/websocket/emws_client.h @@ -48,7 +48,7 @@ public: Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_ssl, PoolVector p_protocol = PoolVector()); Ref get_peer(int p_peer_id) const; - void disconnect_from_host(); + void disconnect_from_host(int p_code = 1000, String p_reason = ""); IP_Address get_connected_host() const; uint16_t get_connected_port() const; virtual ConnectionStatus get_connection_status() const; diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp index e0b987b4d78..68f41165eb2 100644 --- a/modules/websocket/emws_peer.cpp +++ b/modules/websocket/emws_peer.cpp @@ -130,15 +130,17 @@ bool EMWSPeer::is_connected_to_host() const { return peer_sock != -1; }; -void EMWSPeer::close() { +void EMWSPeer::close(int p_code, String p_reason) { if (peer_sock != -1) { /* clang-format off */ EM_ASM({ var sock = Module.IDHandler.get($0); - sock.close(); + var code = $1; + var reason = UTF8ToString($2); + sock.close(code, reason); Module.IDHandler.remove($0); - }, peer_sock); + }, peer_sock, p_code, p_reason.utf8().get_data()); /* clang-format on */ } peer_sock = -1; diff --git a/modules/websocket/emws_peer.h b/modules/websocket/emws_peer.h index e06f7252655..a4b2c8f50b3 100644 --- a/modules/websocket/emws_peer.h +++ b/modules/websocket/emws_peer.h @@ -63,7 +63,7 @@ public: virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size); virtual int get_max_packet_size() const { return PACKET_BUFFER_SIZE; }; - virtual void close(); + virtual void close(int p_code = 1000, String p_reason = ""); virtual bool is_connected_to_host() const; virtual IP_Address get_connected_host() const; virtual uint16_t get_connected_port() const; diff --git a/modules/websocket/emws_server.cpp b/modules/websocket/emws_server.cpp index 3eb93e41525..ad4a758c0fc 100644 --- a/modules/websocket/emws_server.cpp +++ b/modules/websocket/emws_server.cpp @@ -68,7 +68,7 @@ int EMWSServer::get_peer_port(int p_peer_id) const { return 0; } -void EMWSServer::disconnect_peer(int p_peer_id) { +void EMWSServer::disconnect_peer(int p_peer_id, int p_code, String p_reason) { } EMWSServer::EMWSServer() { diff --git a/modules/websocket/emws_server.h b/modules/websocket/emws_server.h index 9ec4ce72c81..74b689a29b4 100644 --- a/modules/websocket/emws_server.h +++ b/modules/websocket/emws_server.h @@ -48,7 +48,7 @@ public: Ref get_peer(int p_id) const; IP_Address get_peer_address(int p_peer_id) const; int get_peer_port(int p_peer_id) const; - void disconnect_peer(int p_peer_id); + void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = ""); virtual void poll(); virtual PoolVector get_protocols() const; diff --git a/modules/websocket/lws_client.cpp b/modules/websocket/lws_client.cpp index 09f64220583..cd814760e62 100644 --- a/modules/websocket/lws_client.cpp +++ b/modules/websocket/lws_client.cpp @@ -129,6 +129,7 @@ int LWSClient::_handle_cb(struct lws *wsi, enum lws_callback_reasons reason, voi peer->set_wsi(wsi); peer_data->peer_id = 0; peer_data->force_close = false; + peer_data->clean_close = false; _on_connect(lws_get_protocol(wsi)->name); break; @@ -137,10 +138,18 @@ int LWSClient::_handle_cb(struct lws *wsi, enum lws_callback_reasons reason, voi destroy_context(); return -1; // We should close the connection (would probably happen anyway) + case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: { + int code; + String reason = peer->get_close_reason(in, len, code); + peer_data->clean_close = true; + _on_close_request(code, reason); + return 0; + } + case LWS_CALLBACK_CLIENT_CLOSED: peer->close(); destroy_context(); - _on_disconnect(); + _on_disconnect(peer_data->clean_close); return 0; // We can end here case LWS_CALLBACK_CLIENT_RECEIVE: @@ -150,8 +159,10 @@ int LWSClient::_handle_cb(struct lws *wsi, enum lws_callback_reasons reason, voi break; case LWS_CALLBACK_CLIENT_WRITEABLE: - if (peer_data->force_close) + if (peer_data->force_close) { + peer->send_close_status(wsi); return -1; + } peer->write_wsi(); break; @@ -179,13 +190,12 @@ NetworkedMultiplayerPeer::ConnectionStatus LWSClient::get_connection_status() co return CONNECTION_CONNECTING; } -void LWSClient::disconnect_from_host() { +void LWSClient::disconnect_from_host(int p_code, String p_reason) { if (context == NULL) return; - _peer->close(); - destroy_context(); + _peer->close(p_code, p_reason); }; IP_Address LWSClient::get_connected_host() const { @@ -208,6 +218,7 @@ LWSClient::~LWSClient() { invalidate_lws_ref(); // We do not want any more callback disconnect_from_host(); + destroy_context(); _peer = Ref(); }; diff --git a/modules/websocket/lws_client.h b/modules/websocket/lws_client.h index 8850683cb5a..1bbc19f3524 100644 --- a/modules/websocket/lws_client.h +++ b/modules/websocket/lws_client.h @@ -46,7 +46,7 @@ class LWSClient : public WebSocketClient { public: Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_ssl, PoolVector p_protocol = PoolVector()); Ref get_peer(int p_peer_id) const; - void disconnect_from_host(); + void disconnect_from_host(int p_code = 1000, String p_reason = ""); IP_Address get_connected_host() const; uint16_t get_connected_port() const; virtual ConnectionStatus get_connection_status() const; diff --git a/modules/websocket/lws_peer.cpp b/modules/websocket/lws_peer.cpp index 0989357258b..245b28b608b 100644 --- a/modules/websocket/lws_peer.cpp +++ b/modules/websocket/lws_peer.cpp @@ -178,11 +178,49 @@ bool LWSPeer::is_connected_to_host() const { return wsi != NULL; }; -void LWSPeer::close() { +String LWSPeer::get_close_reason(void *in, size_t len, int &r_code) { + String s; + r_code = 0; + if (len < 2) // From docs this should not happen + return s; + + const uint8_t *b = (const uint8_t *)in; + r_code = b[0] << 8 | b[1]; + + if (len > 2) { + const char *utf8 = (const char *)&b[2]; + s.parse_utf8(utf8, len - 2); + } + return s; +} + +void LWSPeer::send_close_status(struct lws *p_wsi) { + if (close_code == -1) + return; + + int len = close_reason.size(); + ERR_FAIL_COND(len > 123); // Maximum allowed reason size in bytes + + lws_close_status code = (lws_close_status)close_code; + unsigned char *reason = len > 0 ? (unsigned char *)close_reason.utf8().ptrw() : NULL; + + lws_close_reason(p_wsi, code, reason, len); + + close_code = -1; + close_reason = ""; +} + +void LWSPeer::close(int p_code, String p_reason) { if (wsi != NULL) { + close_code = p_code; + close_reason = p_reason; PeerData *data = ((PeerData *)lws_wsi_user(wsi)); data->force_close = true; - lws_callback_on_writable(wsi); // notify that we want to disconnect + data->clean_close = true; + lws_callback_on_writable(wsi); // Notify that we want to disconnect + } else { + close_code = -1; + close_reason = ""; } wsi = NULL; rbw.resize(0); diff --git a/modules/websocket/lws_peer.h b/modules/websocket/lws_peer.h index d7d46e30769..571445db01f 100644 --- a/modules/websocket/lws_peer.h +++ b/modules/websocket/lws_peer.h @@ -53,10 +53,14 @@ private: WriteMode write_mode; bool _was_string; + int close_code; + String close_reason; + public: struct PeerData { uint32_t peer_id; bool force_close; + bool clean_close; }; RingBuffer rbw; @@ -71,7 +75,7 @@ public: virtual Error put_packet(const uint8_t *p_buffer, int p_buffer_size); virtual int get_max_packet_size() const { return PACKET_BUFFER_SIZE; }; - virtual void close(); + virtual void close(int p_code = 1000, String p_reason = ""); virtual bool is_connected_to_host() const; virtual IP_Address get_connected_host() const; virtual uint16_t get_connected_port() const; @@ -83,6 +87,8 @@ public: void set_wsi(struct lws *wsi); Error read_wsi(void *in, size_t len); Error write_wsi(); + void send_close_status(struct lws *wsi); + String get_close_reason(void *in, size_t len, int &r_code); LWSPeer(); ~LWSPeer(); diff --git a/modules/websocket/lws_server.cpp b/modules/websocket/lws_server.cpp index 4a614f69337..58fa0433461 100644 --- a/modules/websocket/lws_server.cpp +++ b/modules/websocket/lws_server.cpp @@ -90,20 +90,36 @@ int LWSServer::_handle_cb(struct lws *wsi, enum lws_callback_reasons reason, voi peer_data->peer_id = id; peer_data->force_close = false; - + peer_data->clean_close = false; _on_connect(id, lws_get_protocol(wsi)->name); break; } + case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: { + if (peer_data == NULL) + return 0; + + int32_t id = peer_data->peer_id; + if (_peer_map.has(id)) { + int code; + Ref peer = _peer_map[id]; + String reason = peer->get_close_reason(in, len, code); + peer_data->clean_close = true; + _on_close_request(id, code, reason); + } + return 0; + } + case LWS_CALLBACK_CLOSED: { if (peer_data == NULL) return 0; int32_t id = peer_data->peer_id; + bool clean = peer_data->clean_close; if (_peer_map.has(id)) { _peer_map[id]->close(); _peer_map.erase(id); } - _on_disconnect(id); + _on_disconnect(id, clean); return 0; // we can end here } @@ -118,10 +134,15 @@ int LWSServer::_handle_cb(struct lws *wsi, enum lws_callback_reasons reason, voi } case LWS_CALLBACK_SERVER_WRITEABLE: { - if (peer_data->force_close) - return -1; - int id = peer_data->peer_id; + if (peer_data->force_close) { + if (_peer_map.has(id)) { + Ref peer = _peer_map[id]; + peer->send_close_status(wsi); + } + return -1; + } + if (_peer_map.has(id)) static_cast >(_peer_map[id])->write_wsi(); break; @@ -164,10 +185,10 @@ int LWSServer::get_peer_port(int p_peer_id) const { return _peer_map[p_peer_id]->get_connected_port(); } -void LWSServer::disconnect_peer(int p_peer_id) { +void LWSServer::disconnect_peer(int p_peer_id, int p_code, String p_reason) { ERR_FAIL_COND(!has_peer(p_peer_id)); - get_peer(p_peer_id)->close(); + get_peer(p_peer_id)->close(p_code, p_reason); } LWSServer::LWSServer() { diff --git a/modules/websocket/lws_server.h b/modules/websocket/lws_server.h index 9e3fb9b7752..346773ebc4f 100644 --- a/modules/websocket/lws_server.h +++ b/modules/websocket/lws_server.h @@ -54,7 +54,7 @@ public: Ref get_peer(int p_id) const; IP_Address get_peer_address(int p_peer_id) const; int get_peer_port(int p_peer_id) const; - void disconnect_peer(int p_peer_id); + void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = ""); virtual void poll() { _lws_poll(); } LWSServer(); diff --git a/modules/websocket/websocket_client.cpp b/modules/websocket/websocket_client.cpp index 7701163085b..f9b94dc5193 100644 --- a/modules/websocket/websocket_client.cpp +++ b/modules/websocket/websocket_client.cpp @@ -107,12 +107,17 @@ void WebSocketClient::_on_connect(String p_protocol) { } } -void WebSocketClient::_on_disconnect() { +void WebSocketClient::_on_close_request(int p_code, String p_reason) { + + emit_signal("server_close_request", p_code, p_reason); +} + +void WebSocketClient::_on_disconnect(bool p_was_clean) { if (_is_multiplayer) { emit_signal("connection_failed"); } else { - emit_signal("connection_closed"); + emit_signal("connection_closed", p_was_clean); } } @@ -127,7 +132,7 @@ void WebSocketClient::_on_error() { void WebSocketClient::_bind_methods() { ClassDB::bind_method(D_METHOD("connect_to_url", "url", "protocols", "gd_mp_api"), &WebSocketClient::connect_to_url, DEFVAL(PoolVector()), DEFVAL(false)); - ClassDB::bind_method(D_METHOD("disconnect_from_host"), &WebSocketClient::disconnect_from_host); + ClassDB::bind_method(D_METHOD("disconnect_from_host", "code", "reason"), &WebSocketClient::disconnect_from_host, DEFVAL(1000), DEFVAL("")); ClassDB::bind_method(D_METHOD("set_verify_ssl_enabled", "enabled"), &WebSocketClient::set_verify_ssl_enabled); ClassDB::bind_method(D_METHOD("is_verify_ssl_enabled"), &WebSocketClient::is_verify_ssl_enabled); @@ -135,6 +140,7 @@ void WebSocketClient::_bind_methods() { ADD_SIGNAL(MethodInfo("data_received")); ADD_SIGNAL(MethodInfo("connection_established", PropertyInfo(Variant::STRING, "protocol"))); - ADD_SIGNAL(MethodInfo("connection_closed")); + ADD_SIGNAL(MethodInfo("server_close_request", PropertyInfo(Variant::INT, "code"), PropertyInfo(Variant::STRING, "reason"))); + ADD_SIGNAL(MethodInfo("connection_closed", PropertyInfo(Variant::BOOL, "was_clean_close"))); ADD_SIGNAL(MethodInfo("connection_error")); } diff --git a/modules/websocket/websocket_client.h b/modules/websocket/websocket_client.h index 6165f37d40b..948f128e9f1 100644 --- a/modules/websocket/websocket_client.h +++ b/modules/websocket/websocket_client.h @@ -53,7 +53,7 @@ public: virtual void poll() = 0; virtual Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_ssl, PoolVector p_protocol = PoolVector()) = 0; - virtual void disconnect_from_host() = 0; + virtual void disconnect_from_host(int p_code = 1000, String p_reason = "") = 0; virtual IP_Address get_connected_host() const = 0; virtual uint16_t get_connected_port() const = 0; @@ -62,7 +62,8 @@ public: void _on_peer_packet(); void _on_connect(String p_protocol); - void _on_disconnect(); + void _on_close_request(int p_code, String p_reason); + void _on_disconnect(bool p_was_clean); void _on_error(); WebSocketClient(); diff --git a/modules/websocket/websocket_peer.cpp b/modules/websocket/websocket_peer.cpp index 61f783e3771..3ecb32ce197 100644 --- a/modules/websocket/websocket_peer.cpp +++ b/modules/websocket/websocket_peer.cpp @@ -42,7 +42,7 @@ void WebSocketPeer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_write_mode", "mode"), &WebSocketPeer::set_write_mode); ClassDB::bind_method(D_METHOD("is_connected_to_host"), &WebSocketPeer::is_connected_to_host); ClassDB::bind_method(D_METHOD("was_string_packet"), &WebSocketPeer::was_string_packet); - ClassDB::bind_method(D_METHOD("close"), &WebSocketPeer::close); + ClassDB::bind_method(D_METHOD("close", "code", "reason"), &WebSocketPeer::close, DEFVAL(1000), DEFVAL("")); ClassDB::bind_method(D_METHOD("get_connected_host"), &WebSocketPeer::get_connected_host); ClassDB::bind_method(D_METHOD("get_connected_port"), &WebSocketPeer::get_connected_port); diff --git a/modules/websocket/websocket_peer.h b/modules/websocket/websocket_peer.h index ad451e9cc73..5918fda3c22 100644 --- a/modules/websocket/websocket_peer.h +++ b/modules/websocket/websocket_peer.h @@ -58,7 +58,7 @@ public: virtual WriteMode get_write_mode() const = 0; virtual void set_write_mode(WriteMode p_mode) = 0; - virtual void close() = 0; + virtual void close(int p_code = 1000, String p_reason = "") = 0; virtual bool is_connected_to_host() const = 0; virtual IP_Address get_connected_host() const = 0; diff --git a/modules/websocket/websocket_server.cpp b/modules/websocket/websocket_server.cpp index 53dd7b51b78..c631ed70d57 100644 --- a/modules/websocket/websocket_server.cpp +++ b/modules/websocket/websocket_server.cpp @@ -46,9 +46,10 @@ void WebSocketServer::_bind_methods() { ClassDB::bind_method(D_METHOD("has_peer", "id"), &WebSocketServer::has_peer); ClassDB::bind_method(D_METHOD("get_peer_address", "id"), &WebSocketServer::get_peer_address); ClassDB::bind_method(D_METHOD("get_peer_port", "id"), &WebSocketServer::get_peer_port); - ClassDB::bind_method(D_METHOD("disconnect_peer", "id"), &WebSocketServer::disconnect_peer); + ClassDB::bind_method(D_METHOD("disconnect_peer", "id", "code", "reason"), &WebSocketServer::disconnect_peer, DEFVAL(1000), DEFVAL("")); - ADD_SIGNAL(MethodInfo("client_disconnected", PropertyInfo(Variant::INT, "id"))); + ADD_SIGNAL(MethodInfo("client_close_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "code"), PropertyInfo(Variant::STRING, "reason"))); + ADD_SIGNAL(MethodInfo("client_disconnected", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::BOOL, "was_clean_close"))); ADD_SIGNAL(MethodInfo("client_connected", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "protocol"))); ADD_SIGNAL(MethodInfo("data_received", PropertyInfo(Variant::INT, "id"))); } @@ -85,13 +86,18 @@ void WebSocketServer::_on_connect(int32_t p_peer_id, String p_protocol) { } } -void WebSocketServer::_on_disconnect(int32_t p_peer_id) { +void WebSocketServer::_on_disconnect(int32_t p_peer_id, bool p_was_clean) { if (_is_multiplayer) { // Send delete to clients _send_del(p_peer_id); emit_signal("peer_disconnected", p_peer_id); } else { - emit_signal("client_disconnected", p_peer_id); + emit_signal("client_disconnected", p_peer_id, p_was_clean); } } + +void WebSocketServer::_on_close_request(int32_t p_peer_id, int p_code, String p_reason) { + + emit_signal("client_close_request", p_peer_id, p_code, p_reason); +} diff --git a/modules/websocket/websocket_server.h b/modules/websocket/websocket_server.h index 64935f8a584..156f25897c2 100644 --- a/modules/websocket/websocket_server.h +++ b/modules/websocket/websocket_server.h @@ -54,11 +54,12 @@ public: virtual IP_Address get_peer_address(int p_peer_id) const = 0; virtual int get_peer_port(int p_peer_id) const = 0; - virtual void disconnect_peer(int p_peer_id) = 0; + virtual void disconnect_peer(int p_peer_id, int p_code = 1000, String p_reason = "") = 0; void _on_peer_packet(int32_t p_peer_id); void _on_connect(int32_t p_peer_id, String p_protocol); - void _on_disconnect(int32_t p_peer_id); + void _on_disconnect(int32_t p_peer_id, bool p_was_clean); + void _on_close_request(int32_t p_peer_id, int p_code, String p_reason); WebSocketServer(); ~WebSocketServer();