Summary
Suddenly I found that my snake was not online yet, so I made up a simple online snake program.
Main Implementation Requirements
The server's user link logic (main loop) and game run logic (in n threads) maintain a customer information vector s and a game information map.
A match can have two users (as many as possible can expand into a watch, have a chance to do it again). When the link comes in and two users are in the same room, the information is removed from the vector s, put into the match, open a thread, and continue to provide services.
There are only two things a client does when it connects to a server, accepting keyboard messages (and sending its own operations to the server) and server messages (display). There is no logic to play.
Achieving results
Repeat login:
Two-man match:
Third person join:
The server
Begin with familiarity select network model.
Customer Information
The main attributes are socket (unique identity) and bureau id (entry to Bureau identity).
Links are automatically broken when the object is destructed.
struct Client { explicit Client(SOCKET sock) : sock(sock) { strcpy(this->userName, ""); code = 0; } Client(SOCKET sock, const char *userName, const sockaddr_in *addr) { this->sock = sock; strcpy(this->userName, userName); this->addr = *addr; code = 0; } ~Client() { if (sock != INVALID_SOCKET) { close(sock); } } SOCKET sock; // Server socket int code; // Play id char userName[USER_NAME_LENGTH]; // User name sockaddr_in addr; // User Address int lastPos; // The data tail position of the message buffer char szMsgBuf[MSG_BUFFER_LENGTH]; // Second Buffer Message Buffer };
Customer Information Management
Use a vector s to manage the client and a smart pointer, shared_ptr, to simply copy the pointer over when transferring to the opposite board.
Perhaps ChangeClientName was a bit problematic at first because the server didn't know the user's name when the user came in with the link, so it couldn't set the name at first initialization.
After calling RemoveOne to move the client out of the vector s, the link will be broken if the client does not join the match (which should not happen) or if the match has not started (and the match is deleted).
class ClientManager { public: // Add a client void Add(SOCKET sock, const char *userName, const sockaddr_in *addr); // Remove a client but the link may not be broken void RemoveOne(SOCKET sock); // Add all clients to the readable collection void SetAllRead(fd_set &fdRead, SOCKET &maxSock); // Get all readable clients void GetAllRead(fd_set &fdRead, std::vector<std::shared_ptr<Client>> &read); // Modify client name std::shared_ptr<Client> ChangeClientName(SOCKET sock, const char *userName); // print data void Dump(); private: std::vector<std::shared_ptr<Client>> clients; // Client socket };
Counterpart information
There are two players in a match (if you expand your watch, you increase it), a thread, and a game data.
struct Table { std::shared_ptr<Client> playerA; // Player A std::shared_ptr<Client> playerB; // Player B bool start; // Open the match pthread_t tid; // Thread id Snake snake; // Snake data };
Administration of Bureau
Use a map to manage the board, and code to uniquely identify the board.
Since map s are accessed by multiple threads, the operation needs to be locked.
class TableManager { public: TableManager() { pthread_mutex_init(&mutex, NULL); } ~TableManager() { pthread_mutex_destroy(&mutex); } // Joining a match is divided into: there is no match, there is one person in the match, and there are two people in the match (expandable) int Add(int code, std::shared_ptr<Client> player); // There may be an exception to get the incoming code from the Bureau Table& GetTable(int code); // Remove the match void RemoveOne(int code); // print data void Dump(); private: std::map<int, Table> tables; // Match pthread_mutex_t mutex; // mutex };
Snake data
There is a map with two players'coordinates and offset direction.
If a frame (set here to 1s, mainly to test the game) does not have any action, the player moves in the direction of the offset.
The death rule is to encounter objects other than null, if two players die together it is a draw.
Explain that snakes are not meant to go back. They can go back here but die directly.
class Snake { private: struct Point { Point(int x = 0, int y = 0) : x(x), y(y) {} bool operator==(const Point &p) { return x == p.x && y == p.y; } int x; int y; }; uint8_t world[WIDTH][HEIGHT]; // Game Zone Point playerA; // Coordinates of Player A Point playerB; // Coordinates of Player B Point offsetA; // Player A's Moving Offset Direction Point offsetB; // Player B's Moving Offset Direction public: Snake() { Init(); } // Initialize the game void Init(); // Processing commands void DealCmd(int cmd); // Handle game logic // Return Value 0 -- Normal Game // 1 -- A Death // 2 -- B Death // 3--All died int DealGame(); // Copy Map void copyWord(uint8_t world[WIDTH][HEIGHT]); };
Game Server
Singleton
Singleton Static local objects of c++11 are used.
class Server : public ServerNet { private: . . . static Server *g_pSingleton; // Unique single instance object pointer . . . public: // Get Single Instance Object static Server &GetInstance() { // Single Instance Implemented by Local Static Properties static Server server; g_pSingleton = &server; return server; } // Get Client static ClientManager& GetClientManager() { return g_pSingleton->clientManager; } // Get the match static TableManager& GetTableManager() { return g_pSingleton->tableManager; } private: // Prohibit external construction Server() {} // Prohibit external destructions virtual ~Server() {} // Prohibit external copy constructs Server(const Server &server); // Prohibit external assignment const Server &operator=(const Server &server); };
Login-related operations
bool Server::OnRun() { if (IsRun()) { fd_set fdRead; // A collection of descriptors (socket s) FD_ZERO(&fdRead); // Clean Collection FD_SET(serverSock, &fdRead); // Add a descriptor (socket) to a collection // Place the socket and get the maximum socket SOCKET maxSock = serverSock; clientManager.SetAllRead(fdRead, maxSock); timeval t = { 1,0 }; if (select(maxSock + 1, &fdRead, NULL, NULL, &t) < 0) { std::cout << "select Task End" << std::endl; Close(); return false; } // Linked if (FD_ISSET(serverSock, &fdRead)) { FD_CLR(serverSock, &fdRead); Accept(); } // readable std::vector<std::shared_ptr<Client>> read; clientManager.GetAllRead(fdRead, read); for_each(read.begin(), read.end(), [&](std::shared_ptr<Client> client) { if (-1 == RecvData(szRecv, *client, std::bind(&Server::OnNetMsg, g_pSingleton, std::placeholders::_1, std::placeholders::_2))) { std::cout << "Client not started<Socket=" << client->sock << ",userName=" << client->userName << ">Exited" << std::endl; clientManager.RemoveOne(client->sock); tableManager.RemoveOne(client->code); } }); return true; } return false; }
New Link
There is a link here to call ServerNet::Accept(), which calls a virtual function, AddClient, which is overridden by Server.
SOCKET ServerNet::Accept() { sockaddr_in clientAddr = {}; int nAddrLen = sizeof(sockaddr_in); SOCKET clientSock = INVALID_SOCKET; clientSock = accept(serverSock, (sockaddr*)&clientAddr, (socklen_t *)&nAddrLen); if (INVALID_SOCKET == clientSock) { std::cout << "socket=<" << serverSock << ">error,Invalid Client Received SOCKET" << std::endl; } else { // There are new links in AddClient(clientSock, "", &clientAddr); std::cout << "socket=<" << serverSock << ">New Client Join: socket = " << clientSock << ",IP = " << inet_ntoa(clientAddr.sin_addr) << std::endl; } return clientSock; } void Server::AddClient(SOCKET sock, const char *userName, const sockaddr_in *addr) { Dump(); clientManager.Add(sock, "", addr); }
readable
Calling RecvData on a readable client will throw the message to OnNetMsg when it is glued out, currently only the login message.
If the login is successful, the client will join the match, if there are just two people, the match thread will be opened and the client will be removed from the vector s.
void Server::OnNetMsg(SOCKET sock, DataHeader *header) { switch (header->cmd) { case CMD_JOIN: { Join *join = (Join*)header; printf("Receive Client<Socket=%d>Request: CMD_JOIN,Data length:%d,userName=%s,code=%d\n", sock, join->dataLength, join->userName, join->code); // Record username to join match int number = tableManager.Add(join->code, clientManager.ChangeClientName(sock, join->userName)); if (1 == number) { // Open the match Table &table = tableManager.GetTable(join->code); if (pthread_create(&table.tid, NULL, thread, reinterpret_cast<void*>(join->code)) != 0) { std::cout << "pthread_create error" << std::endl; } pthread_detach(table.tid); clientManager.RemoveOne(table.playerA->sock); clientManager.RemoveOne(table.playerB->sock); SendJoinResult(table.playerA->sock, number); // Open the match } SendJoinResult(sock, number); // Logon Return Message // End pending expansion if (2 == number) { clientManager.RemoveOne(sock); } break; } } }
Game handling
void* Server::thread(void *arg) { char szRecv[MSG_BUFFER_LENGTH]; // Receive first level buffer int code = reinterpret_cast<long>(arg); TableManager &tableManager = Server::GetTableManager(); Table &table = tableManager.GetTable(code); SOCKET maxSock = std::max(table.playerA->sock, table.playerB->sock); long long oldclock = ustime(); // Record last tick while (true) { fd_set fdRead; // A collection of descriptors (socket s) FD_ZERO(&fdRead); // Clean Collection // Add a descriptor (socket) to a collection FD_SET(table.playerA->sock, &fdRead); FD_SET(table.playerB->sock, &fdRead); timeval t = { 0, 900000 }; int ret = select(maxSock + 1, &fdRead, NULL, NULL, &t); if (ret < 0) { std::cout << "thread select Task End" << std::endl; } // Check if player A is readable if (FD_ISSET(table.playerA->sock, &fdRead)) { if (-1 == RecvData(szRecv, *table.playerA, std::bind(&Server::OnThreadNetMsg, g_pSingleton, std::placeholders::_1, std::placeholders::_2))) { std::cout << "Match <code=" << code << ">End" << std::endl; break; } } // Check if player B is readable if (FD_ISSET(table.playerB->sock, &fdRead)) { if (-1 == RecvData(szRecv, *table.playerB, std::bind(&Server::OnThreadNetMsg, g_pSingleton, std::placeholders::_1, std::placeholders::_2))) { std::cout << "Match <code=" << code << ">End" << std::endl; break; } } // Send messages to two players auto sendServerSync = std::bind(&Server::SendServerSync, g_pSingleton, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); switch (table.snake.DealGame()) { case 0: sendServerSync(table.playerA->sock, table.snake, 0); sendServerSync(table.playerB->sock, table.snake, 0); break; case 1: sendServerSync(table.playerA->sock, table.snake, 1); sendServerSync(table.playerB->sock, table.snake, 2); break; case 2: sendServerSync(table.playerA->sock, table.snake, 2); sendServerSync(table.playerB->sock, table.snake, 1); break; case 3: sendServerSync(table.playerA->sock, table.snake, 3); sendServerSync(table.playerB->sock, table.snake, 3); break; } // Wait until the end of a frame to start accepting messages HpSleep(1000000, oldclock); } tableManager.RemoveOne(code); return NULL; }
This side uses a delayed HpSleep for processing a frame, which is not a simple direct sleep. It is similar to the ideal delay function shown below, and it includes all the results of the program.
long long Server::ustime(void) { struct timeval tv; long long ust; gettimeofday(&tv, NULL); ust = ((long long)tv.tv_sec)*1000000; ust += tv.tv_usec; return ust; } void Server::HpSleep(int us, long long &oldclock) { oldclock += us; // Update tick if (ustime() > oldclock) // If it has already timed out, no delay is required { oldclock = ustime(); } else { while (ustime() < oldclock) // delayed { usleep(2000); // Release CPU control and reduce CPU usage } } }
User handling is also simple
void Server::OnThreadNetMsg(SOCKET sock, DataHeader *header) { switch (header->cmd) { case CMD_CLIENT_OPERATION: { ClientOperation *clientOperation = (ClientOperation*)header; Table &table = tableManager.GetTable(clientOperation->code); table.snake.DealCmd(clientOperation->gameCmd); break; } } } void Server::SendServerSync(SOCKET sock, Snake &snake, int result) { ServerSync ret; ret.result = result; snake.copyWord(ret.world); SendData(sock, &ret); }
Baidu Cloud Link
easyx source program link
A two-player version of Snake Eater
Code Baidu Cloud Link:https://pan.baidu.com/s/1pQNsEnaErfBlSFxutzxT6Q
Extraction Code: tn4x