Online Snake Snake

Keywords: C++ socket Game Development select pthread

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

Posted by carefree on Thu, 14 Oct 2021 09:14:24 -0700