Merge pull request #22383 from Faless/ws_close

Implement WebSocket close frame handling
This commit is contained in:
Max Hilbrunner 2018-09-24 06:29:30 +02:00 committed by GitHub
commit aaef640b8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 179 additions and 48 deletions

View file

@ -31,8 +31,12 @@
<method name="disconnect_from_host">
<return type="void">
</return>
<argument index="0" name="code" type="int" default="1000">
</argument>
<argument index="1" name="reason" type="String" default="&quot;&quot;">
</argument>
<description>
Disconnect from the server if currently connected.
Disconnect this client from the connected host. See [method WebSocketPeer.close] for more info.
</description>
</method>
</methods>
@ -43,8 +47,10 @@
</members>
<signals>
<signal name="connection_closed">
<argument index="0" name="was_clean_close" type="bool">
</argument>
<description>
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.
</description>
</signal>
<signal name="connection_error">
@ -64,6 +70,15 @@
Emitted when a WebSocket message is received. Note: This signal is NOT emitted when used as high level multiplayer peer.
</description>
</signal>
<signal name="server_close_request">
<argument index="0" name="code" type="int">
</argument>
<argument index="1" name="reason" type="String">
</argument>
<description>
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.
</description>
</signal>
</signals>
<constants>
</constants>

View file

@ -15,8 +15,14 @@
<method name="close">
<return type="void">
</return>
<argument index="0" name="code" type="int" default="1000">
</argument>
<argument index="1" name="reason" type="String" default="&quot;&quot;">
</argument>
<description>
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.
</description>
</method>
<method name="get_connected_host" qualifiers="const">

View file

@ -18,8 +18,12 @@
</return>
<argument index="0" name="id" type="int">
</argument>
<argument index="1" name="code" type="int" default="1000">
</argument>
<argument index="2" name="reason" type="String" default="&quot;&quot;">
</argument>
<description>
Disconnects the given peer.
Disconnects the peer identified by [code]id[/code] from the server. See [method WebSocketPeer.close] for more info.
</description>
</method>
<method name="get_peer_address" qualifiers="const">
@ -68,7 +72,7 @@
<description>
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.
</description>
</method>
<method name="stop">
@ -80,6 +84,17 @@
</method>
</methods>
<signals>
<signal name="client_close_request">
<argument index="0" name="id" type="int">
</argument>
<argument index="1" name="code" type="int">
</argument>
<argument index="2" name="reason" type="String">
</argument>
<description>
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.
</description>
</signal>
<signal name="client_connected">
<argument index="0" name="id" type="int">
</argument>
@ -92,8 +107,10 @@
<signal name="client_disconnected">
<argument index="0" name="id" type="int">
</argument>
<argument index="1" name="was_clean_close" type="bool">
</argument>
<description>
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.
</description>
</signal>
<signal name="data_received">

View file

@ -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<EMWSClient *>(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 {

View file

@ -48,7 +48,7 @@ public:
Error connect_to_host(String p_host, String p_path, uint16_t p_port, bool p_ssl, PoolVector<String> p_protocol = PoolVector<String>());
Ref<WebSocketPeer> 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;

View file

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

View file

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

View file

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

View file

@ -48,7 +48,7 @@ public:
Ref<WebSocketPeer> 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<String> get_protocols() const;

View file

@ -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<LWSPeer>();
};

View file

@ -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<String> p_protocol = PoolVector<String>());
Ref<WebSocketPeer> 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;

View file

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

View file

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

View file

@ -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<LWSPeer> 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<LWSPeer> peer = _peer_map[id];
peer->send_close_status(wsi);
}
return -1;
}
if (_peer_map.has(id))
static_cast<Ref<LWSPeer> >(_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() {

View file

@ -54,7 +54,7 @@ public:
Ref<WebSocketPeer> 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();

View file

@ -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<String>()), 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"));
}

View file

@ -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<String> p_protocol = PoolVector<String>()) = 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();

View file

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

View file

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

View file

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

View file

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