Index: ps/trunk/source/network/NetSession.cpp =================================================================== --- ps/trunk/source/network/NetSession.cpp (revision 24615) +++ ps/trunk/source/network/NetSession.cpp (revision 24616) @@ -1,290 +1,298 @@ /* Copyright (C) 2019 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "NetSession.h" #include "NetClient.h" #include "NetMessage.h" #include "NetServer.h" #include "NetStats.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" constexpr int NETCLIENT_POLL_TIMEOUT = 50; constexpr int CHANNEL_COUNT = 1; CNetClientSession::CNetClientSession(CNetClient& client) : m_Client(client), m_FileTransferer(this), m_Host(nullptr), m_Server(nullptr), m_Stats(nullptr), m_IsLocalClient(false), m_IncomingMessages(16), m_OutgoingMessages(16), m_LoopRunning(false), m_ShouldShutdown(false), m_MeanRTT(0), m_LastReceivedTime(0) { } CNetClientSession::~CNetClientSession() { ENSURE(!m_LoopRunning); delete m_Stats; if (m_Host && m_Server) { // Disconnect immediately (we can't wait for acks) enet_peer_disconnect_now(m_Server, NDR_SERVER_SHUTDOWN); enet_host_destroy(m_Host); m_Host = NULL; m_Server = NULL; } } bool CNetClientSession::Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient) { ENSURE(!m_LoopRunning); ENSURE(!m_Host); ENSURE(!m_Server); // Create ENet host ENetHost* host; if (enetClient != nullptr) host = enetClient; else host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0); if (!host) return false; // Bind to specified host ENetAddress addr; addr.port = port; if (enet_address_set_host(&addr, server.c_str()) < 0) return false; // Initiate connection to server ENetPeer* peer = enet_host_connect(host, &addr, CHANNEL_COUNT, 0); if (!peer) return false; m_Host = host; m_Server = peer; m_IsLocalClient = isLocalClient; m_Stats = new CNetStatsTable(m_Server); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); return true; } void CNetClientSession::RunNetLoop(CNetClientSession* session) { ENSURE(!session->m_LoopRunning); session->m_LoopRunning = true; debug_SetThreadName("NetClientSession loop"); while (!session->m_ShouldShutdown) { ENSURE(session->m_Host && session->m_Server); session->m_FileTransferer.Poll(); session->Poll(); session->Flush(); session->m_LastReceivedTime = enet_time_get() - session->m_Server->lastReceiveTime; session->m_MeanRTT = session->m_Server->roundTripTime; } session->m_LoopRunning = false; // Deleting the session is handled in this thread as it might outlive the CNetClient. SAFE_DELETE(session); } void CNetClientSession::Shutdown() { m_ShouldShutdown = true; } void CNetClientSession::Poll() { ENetEvent event; // Use the timeout to make the thread wait and save CPU time. if (enet_host_service(m_Host, &event, NETCLIENT_POLL_TIMEOUT) <= 0) return; if (event.type == ENET_EVENT_TYPE_CONNECT) { ENSURE(event.peer == m_Server); // Report the server address immediately. char hostname[256] = "(error)"; enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname)); LOGMESSAGE("Net client: Connected to %s:%u", hostname, (unsigned int)event.peer->address.port); + m_Connected = true; m_IncomingMessages.push(event); } else if (event.type == ENET_EVENT_TYPE_DISCONNECT) { ENSURE(event.peer == m_Server); // Report immediately. LOGMESSAGE("Net client: Disconnected"); + m_Connected = false; m_IncomingMessages.push(event); } else if (event.type == ENET_EVENT_TYPE_RECEIVE) m_IncomingMessages.push(event); } void CNetClientSession::Flush() { ENetPacket* packet; while (m_OutgoingMessages.pop(packet)) if (enet_peer_send(m_Server, CNetHost::DEFAULT_CHANNEL, packet) < 0) - LOGERROR("NetClient: Failed to send packet to server"); + { + // Report the error, but do so silently if we know we are disconnected. + if (m_Connected) + LOGERROR("NetClient: Failed to send packet to server"); + else + LOGMESSAGE("NetClient: Failed to send packet to server"); + } enet_host_flush(m_Host); } void CNetClientSession::ProcessPolledMessages() { ENetEvent event; while(m_IncomingMessages.pop(event)) { if (event.type == ENET_EVENT_TYPE_CONNECT) m_Client.HandleConnect(); else if (event.type == ENET_EVENT_TYPE_DISCONNECT) { // This deletes the session, so we must break; m_Client.HandleDisconnect(event.data); break; } else if (event.type == ENET_EVENT_TYPE_RECEIVE) { CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, m_Client.GetScriptInterface()); if (msg) { LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength()); m_Client.HandleMessage(msg); } // Thread-safe enet_packet_destroy(event.packet); } } } bool CNetClientSession::SendMessage(const CNetMessage* message) { ENSURE(m_Host && m_Server); // Thread-safe. ENetPacket* packet = CNetHost::CreatePacket(message); if (!packet) return false; if (!m_OutgoingMessages.push(packet)) { LOGERROR("NetClient: Failed to push message on the outgoing queue."); return false; } return true; } u32 CNetClientSession::GetLastReceivedTime() const { if (!m_Server) return 0; return m_LastReceivedTime; } u32 CNetClientSession::GetMeanRTT() const { if (!m_Server) return 0; return m_MeanRTT; } CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) : m_Server(server), m_FileTransferer(this), m_Peer(peer), m_IsLocalClient(false), m_HostID(0), m_GUID(), m_UserName() { } u32 CNetServerSession::GetIPAddress() const { return m_Peer->address.host; } u32 CNetServerSession::GetLastReceivedTime() const { if (!m_Peer) return 0; return enet_time_get() - m_Peer->lastReceiveTime; } u32 CNetServerSession::GetMeanRTT() const { if (!m_Peer) return 0; return m_Peer->roundTripTime; } void CNetServerSession::Disconnect(NetDisconnectReason reason) { if (reason == NDR_UNKNOWN) LOGWARNING("Disconnecting client without communicating the disconnect reason!"); Update((uint)NMT_CONNECTION_LOST, NULL); enet_peer_disconnect(m_Peer, static_cast(reason)); } void CNetServerSession::DisconnectNow(NetDisconnectReason reason) { if (reason == NDR_UNKNOWN) LOGWARNING("Disconnecting client without communicating the disconnect reason!"); enet_peer_disconnect_now(m_Peer, static_cast(reason)); } bool CNetServerSession::SendMessage(const CNetMessage* message) { return m_Server.SendMessage(m_Peer, message); } bool CNetServerSession::IsLocalClient() const { return m_IsLocalClient; } void CNetServerSession::SetLocalClient(bool isLocalClient) { m_IsLocalClient = isLocalClient; if (!isLocalClient) return; } Index: ps/trunk/source/network/NetSession.h =================================================================== --- ps/trunk/source/network/NetSession.h (revision 24615) +++ ps/trunk/source/network/NetSession.h (revision 24616) @@ -1,229 +1,232 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef NETSESSION_H #define NETSESSION_H #include "lib/external_libraries/enet.h" #include "network/fsm.h" #include "network/NetFileTransfer.h" #include "network/NetHost.h" #include "ps/CStr.h" #include #include /** * Report the peer if we didn't receive a packet after this time (milliseconds). */ inline constexpr u32 NETWORK_WARNING_TIMEOUT = 2000; class CNetClient; class CNetServerWorker; class CNetStatsTable; typedef struct _ENetHost ENetHost; /** * @file * Network client/server sessions. * * Each session has two classes: CNetClientSession runs on the client, * and CNetServerSession runs on the server. * A client runs one session at once; a server typically runs many. */ /** * Interface for sessions to which messages can be sent. */ class INetSession { public: virtual ~INetSession() {} virtual bool SendMessage(const CNetMessage* message) = 0; }; /** * The client end of a network session. * Provides an abstraction of the network interface, allowing communication with the server. * The NetClientSession is threaded, so all calls to the public interface must be thread-safe. */ class CNetClientSession : public INetSession { NONCOPYABLE(CNetClientSession); public: CNetClientSession(CNetClient& client); ~CNetClientSession(); bool Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient); /** * The client NetSession is threaded to avoid getting timeouts if the main thread hangs. * Call Connect() before starting this loop. */ static void RunNetLoop(CNetClientSession* session); /** * Shut down the net session. */ void Shutdown(); /** * Processes pending messages. */ void ProcessPolledMessages(); /** * Queue up a message to send to the server on the next Loop() call. */ virtual bool SendMessage(const CNetMessage* message) override; /** * Number of milliseconds since the most recent packet of the server was received. */ u32 GetLastReceivedTime() const; /** * Average round trip time to the server. */ u32 GetMeanRTT() const; CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; } private: /** * Process queued incoming messages. */ void Poll(); /** * Flush queued outgoing network messages. */ void Flush(); CNetClient& m_Client; CNetFileTransferer m_FileTransferer; // Net messages received and waiting for fetching. boost::lockfree::queue m_IncomingMessages; // Net messages to send on the next flush() call. boost::lockfree::queue m_OutgoingMessages; + // Last know state. If false, flushing errors are silenced. + bool m_Connected = false; + // Wrapper around enet stats - those are atomic as the code is lock-free. std::atomic m_LastReceivedTime; std::atomic m_MeanRTT; // If this is true, calling Connect() or deleting the session is an error. std::atomic m_LoopRunning; std::atomic m_ShouldShutdown; ENetHost* m_Host; ENetPeer* m_Server; CNetStatsTable* m_Stats; bool m_IsLocalClient; }; /** * The server's end of a network session. * Represents an abstraction of the state of the client, storing all the per-client data * needed by the server. * * Thread-safety: * - This is constructed and used by CNetServerWorker in the network server thread. */ class CNetServerSession : public CFsm, public INetSession { NONCOPYABLE(CNetServerSession); public: CNetServerSession(CNetServerWorker& server, ENetPeer* peer); CNetServerWorker& GetServer() { return m_Server; } const CStr& GetGUID() const { return m_GUID; } void SetGUID(const CStr& guid) { m_GUID = guid; } const CStrW& GetUserName() const { return m_UserName; } void SetUserName(const CStrW& name) { m_UserName = name; } u32 GetHostID() const { return m_HostID; } void SetHostID(u32 id) { m_HostID = id; } u32 GetIPAddress() const; /** * Whether this client is running in the same process as the server. */ bool IsLocalClient() const; /** * Number of milliseconds since the latest packet of that client was received. */ u32 GetLastReceivedTime() const; /** * Average round trip time to the client. */ u32 GetMeanRTT() const; /** * Sends a disconnection notification to the client, * and sends a NMT_CONNECTION_LOST message to the session FSM. * The server will receive a disconnection notification after a while. * The server will not receive any further messages sent via this session. */ void Disconnect(NetDisconnectReason reason); /** * Sends an unreliable disconnection notification to the client. * The server will not receive any disconnection notification. * The server will not receive any further messages sent via this session. */ void DisconnectNow(NetDisconnectReason reason); /** * Prevent timeouts for the client running in the same process as the server. */ void SetLocalClient(bool isLocalClient); /** * Send a message to the client. */ virtual bool SendMessage(const CNetMessage* message); CNetFileTransferer& GetFileTransferer() { return m_FileTransferer; } private: CNetServerWorker& m_Server; CNetFileTransferer m_FileTransferer; ENetPeer* m_Peer; CStr m_GUID; CStrW m_UserName; u32 m_HostID; bool m_IsLocalClient; }; #endif // NETSESSION_H