/* Copyright (c) 2009-2010 Christopher A. Taylor. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of LibCat nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CAT_SPHYNX_SERVER_HPP #define CAT_SPHYNX_SERVER_HPP #include #include #include #include #include #include #include namespace cat { namespace sphynx { /* Designed for server hardware with many processors. In order to handle many users, the Sphynx server opens up a single UDP port to accept new connections, and several other UDP data ports for data. For retransmissions and detecting link loss due to timeout, the server runs several additional threads that wake up periodically to perform timed tasks. Server uses thread pool to receive packets on connect and worker ports, meaning that packets are processed by any free CPU as soon as they arrive. Sphynx Server UDP Hello Port [1] + In case this thread spins constantly, only use one CPU for new connections since in-game experience is more important than login + Assigns users to a data port after handshake completes UDP Data Ports [4 * (CPU Count)] + Spread users evenly across several ports since only one packet can be processed from a single port at a time + Any free CPU will process incoming packets as fast as possible ServerTimer threads [(CPU Count) / 2] + In case these threads spin constantly, they only consume half of the CPU resources available + Wake up every X milliseconds according to Transport::TICK_RATE + Detect link loss due to silence timeout + Update transport layer + Retransmit lost messages + Re-evaluate bandwidth and transmit queued messages */ //// sphynx::Connexion // Derive from sphynx::Connexion and sphynx::Server to define server behavior class Connexion : public Transport, public ThreadRefObject { friend class Server; friend class Map; friend class ServerWorker; friend class ServerTimer; public: Connexion(); virtual ~Connexion() {} private: volatile u32 _destroyed; u32 _key; // Map hash table index, unique for each active connection Connexion *_next_delete; ServerWorker *_server_worker; u8 _first_challenge[64]; // First challenge seen from this client address u8 _cached_answer[128]; // Cached answer to this first challenge, to avoid eating server CPU time private: // Return false to destroy this object bool Tick(ThreadPoolLocalStorage *tls, u32 now); void OnRawData(ThreadPoolLocalStorage *tls, u8 *data, u32 bytes); virtual bool PostPacket(u8 *buffer, u32 buf_bytes, u32 msg_bytes, u32 skip_bytes); public: CAT_INLINE bool IsValid() { return _destroyed == 0; } CAT_INLINE u32 GetKey() { return _key; } void Destroy(); protected: NetAddr _client_addr; // Last time a packet was received from this user -- for disconnect timeouts u32 _last_recv_tsc; bool _seen_encrypted; AuthenticatedEncryption _auth_enc; protected: virtual void OnConnect(ThreadPoolLocalStorage *tls) = 0; virtual void OnDestroy() = 0; virtual void OnTick(ThreadPoolLocalStorage *tls, u32 now) = 0; }; //// sphynx::Map class Map { protected: CAT_INLINE u32 hash_addr(const NetAddr &addr, u32 salt); public: struct Slot { Connexion *connection; bool collision; Slot *next; }; protected: u32 _hash_salt; CAT_ALIGNED(16) Slot _table[HASH_TABLE_SIZE]; RWLock _table_lock; public: Map(); virtual ~Map(); // Lookup client by address Connexion *Lookup(const NetAddr &addr); // Lookup client by key Connexion *Lookup(u32 key); // May return false if network address in Connexion is already in the map. // This averts a potential race condition but should never happen during // normal operation, so the Connexion allocation by caller won't be wasted. bool Insert(Connexion *conn); // Destroy a list described by the 'next' member of Slot void DestroyList(Map::Slot *kill_list); void Tick(ThreadPoolLocalStorage *tls); }; //// sphynx::ServerWorker class ServerWorker : public UDPEndpoint { friend class Map; protected: Map *_conn_map; ServerTimer *_server_timer; protected: volatile u32 _session_count; public: ServerWorker(Map *conn_map, ServerTimer *server_timer); virtual ~ServerWorker(); void IncrementPopulation(); void DecrementPopulation(); u32 GetPopulation() { return _session_count; } protected: void OnRead(ThreadPoolLocalStorage *tls, const NetAddr &src, u8 *data, u32 bytes); void OnWrite(u32 bytes) {} void OnClose(); }; //// sphynx::ServerTimer class ServerTimer : Thread { protected: Map *_conn_map; protected: ServerWorker **_workers; int _worker_count; protected: Map::Slot *_insert_head; Mutex _insert_lock; protected: Map::Slot *_active_head; public: ServerTimer(Map *conn_map, ServerWorker **workers, int worker_count); virtual ~ServerTimer(); CAT_INLINE bool Valid() { return _worker_count > 0; } public: void InsertSlot(Map::Slot *slot); protected: CAT_INLINE void Tick(ThreadPoolLocalStorage *tls); bool ThreadFunction(void *param); protected: static const int TIMER_THREAD_KILL_TIMEOUT = 10000; WaitableFlag _kill_flag; }; //// sphynx::Server class Server : public UDPEndpoint { public: Server(); virtual ~Server(); bool StartServer(ThreadPoolLocalStorage *tls, Port port, u8 *public_key, int public_bytes, u8 *private_key, int private_bytes, const char *session_key); u32 GetTotalPopulation(); static bool GenerateKeyPair(ThreadPoolLocalStorage *tls, const char *public_key_file, const char *private_key_file, u8 *public_key, int public_bytes, u8 *private_key, int private_bytes); private: static const int SESSION_KEY_BYTES = 32; char _session_key[SESSION_KEY_BYTES]; Port _server_port; Map _conn_map; CookieJar _cookie_jar; KeyAgreementResponder _key_agreement_responder; u8 _public_key[PUBLIC_KEY_BYTES]; static const int WORKER_LIMIT = 32; // Maximum number of workers ServerWorker **_workers; int _worker_count; ServerTimer **_timers; int _timer_count; private: ServerWorker *FindLeastPopulatedPort(); void OnRead(ThreadPoolLocalStorage *tls, const NetAddr &src, u8 *data, u32 bytes); void OnWrite(u32 bytes); void OnClose(); protected: // Must return a new instance of your Connexion derivation virtual Connexion *NewConnexion() = 0; // IP address filter: Return true to allow the connection to be made virtual bool AcceptNewConnexion(const NetAddr &src) = 0; // Lookup client by key Connexion *Lookup(u32 key); }; } // namespace sphynx } // namespace cat #endif // CAT_SPHYNX_SERVER_HPP