使用C ++和Windows Winsock的简单客户端-服务器网络(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/simple-client-server-network-using-cplusplus-and-w-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 18 分钟阅读 - 8620 个词 阅读量 0使用C ++和Windows Winsock的简单客户端-服务器网络(译文)
原文地址:https://www.codeproject.com/Articles/412511/Simple-client-server-network-using-Cplusplus-and-W
原文作者:bshokati
译文由本站 robot-v1.0 翻译
前言
How to create a client-server network for multiplayer game programming using C++ and Windows Winsock
如何使用C ++和Windows Winsock为多人游戏编程创建客户端-服务器网络
介绍(Introduction)
我最近在圣地亚哥加州大学完成了视频游戏的设计和实施课程,并与另一位队友一起负责视频游戏的网络方面.我想编写一个指南来说明如何使用C ++,Windows Winsock 2库和TCP建立简单的客户端-服务器网络.在本指南的最后,您将在服务器和客户端之间发送和接收简单的数据包.(I recently finished a video game design and implementation course at UC San Diego and was in charge of the networking side of the video game along with another teammate. I want to write a guide to illustrate how to set up a simple client-server network using C++, the Windows Winsock 2 library, and TCP. By the end of this guide, you will have simple packets sent and received between the server and the client.)
背景(Background)
微软(Microsoft) 提供了有关如何使用其Winsock库创建客户端和服务器的非常有用的指南,但我只是想补充说明,并指导那些使用它进行游戏编程的人,并说明我如何使用它.我将点连接起来,这样您就不必(provides very useful guides on how to go about creating a client and a server using their Winsock library but I just wanted to add clarification and guide those who are using it for game programming and explain how I used it. I connected the dots so that you don’t have to)
我强烈建议您使用Microsoft Visual Studio遵循本教程.我正在使用版本2010 Ultimate,并且我的项目是Visual C ++ Win32控制台应用程序.(I highly recommend that you follow this tutorial using Microsoft Visual Studio. I am using version 2010 Ultimate and my project is a Visual C++ Win32 Console Application.)
客户端连接到服务器(Client connecting to Server)
服务器和客户端将各自具有自己的套接字,它们将用于通过TCP连接发送和接收数据.首先,我们将创建一个类,该类会将Winsock 2库的发送和接收函数包装为更简单的形式,以提高可读性.我们将在服务器和客户端网络类中使用该类来发送和接收数据.此类不是完全必需的,但将来会使我们的代码更易于理解.(The server and the client will each have their own sockets, which they will use to send and receive data through a TCP connection. First we will create a class which will wrap the send and receive functions of the Winsock 2 library into a more simpler form for better readability. We will use this class inside our server and client network classes to send and receive data. This class is not completely necessary but will make our code more simpler to understand in the future.)
让我们创建一个名为" NetworkServices"的类.在其头文件(" NetworkService.h")中,包含以下库:(Lets create a class called “NetworkServices”. In its header file (“NetworkService.h”) include the following libraries:)
#pragma once
#include <winsock2.h>
#include <Windows.h>
上述库包含Winsock库的接收和发送功能所需的功能.(The above libraries contain the functions required for the receive and send functions of the Winsock library.)
此类不需要构造函数/解构函数.另外,声明以下静态包装函数,它们将由服务器和客户端使用:(This class will not need a constructor/deconstructor. Also, declare the following static wrapper functions which will be used by both the server and the client:)
class NetworkServices
{
public:
static int sendMessage(SOCKET curSocket, char * message, int messageSize);
static int receiveMessage(SOCKET curSocket, char * buffer, int bufSize);
};
现在,将定义写入包装函数,并使用Winsock库函数在类(NetworkServices.cpp)中发送和接收数据:(Now lets write the definitions to our wrapper functions and use the Winsock library functions to send and receive data inside the class (NetworkServices.cpp):) **注意:我的cpp文件中已#include" stdafx.h",这是Visual Studio自动生成的文件.您可能不需要此,在这种情况下,您可以删除该行.但是,在VS中工作时,如果不包括此文件,则用户会报告错误.(**Note: I have #included “stdafx.h”, a file that is automatically generated by Visual Studio, in my cpp files. You may not need this, in which case you may remove the line. However, users have reported errors when not including this file when working in VS.)
#include "stdafx.h"
#include "NetworkServices.h"
int NetworkServices::sendMessage(SOCKET curSocket, char * message, int messageSize)
{
return send(curSocket, message, messageSize, 0);
}
sendMessage()使用套接字类型的对象来发送消息,指向要发送的消息所在的缓冲区的指针以及消息的大小.然后,我们调用Winsock库的send()并向其提供必要的信息.如果您想要设置send()的标志以不同于默认值的方式工作,通常使用send()提供的" 0",但是在本例中,我们不需要设置任何标志. send函数将返回一个int值,表示成功发送的字节数;如果通过套接字发送有问题,则返回一个错误值.确保在您的实际应用程序中错误检查此值.如果您想了解发送功能的工作原理,请继续(sendMessage() takes a socket type object to send the message through, a pointer to the buffer where the message we want to send is located, and the size of the message. We then call send() of the Winsock library and provide it with the necessary information. The “0” provided to send() is usually used if you’d want to set flags for send() to work in a different way than its default but in our case, we do not need to set any flags. The send function will return an int value representing the number of bytes it successfully sent or an error value if there was a problem sending through the socket. Make sure to error check this value in your real application. If you’d like to see how the send function works then go) 这里(here) .(.)
int NetworkServices::receiveMessage(SOCKET curSocket, char * buffer, int bufSize)
{
return recv(curSocket, buffer, bufSize, 0);
}
receiveMessage()使用套接字类型对象来检查可用于在网络上的该套接字读取的任何数据.它将读取的所有数据放入"缓冲区",并且需要缓冲区大小来指示每次调用receiveMessage()时可以读取的最大值.然后,我们将此信息提供给recv(),后者是Winsock库函数,用于从套接字接收数据.它还将返回一个int值,表示它读入缓冲区的字节数;如果从套接字接收到一个问题,则返回一个错误.确保在您的实际应用程序中错误检查此值.提供给recv()的" 0"与发送中的" 0"相同,(receiveMessage() takes a socket type object to check for any data that is available for reading at that socket on the network. It will place any data read into our “buffer”, and will require a buffer size to indicate how much is the maximum it can read each time receiveMessage() is called. We then provide this information to recv() which is the Winsock library function for receiving data from a socket. It will also return an int value representing the number of bytes it read into our buffer or an error if there was a problem receiving from the socket. Make sure to error check this value in your real application. The “0” provided to recv() works the same way as the one in send,) 这里(here) 有关recv()的更多信息.(is more information on recv().)
现在,我们创建网络的客户端.声明一个名为" ClientNetwork.h"的ClientNetwork头文件,并包含以下库:(We now create the client side of the network. Declare a ClientNetwork header file called “ClientNetwork.h” and include the following libraries:)
// Networking libraries
#include <winsock2.h>
#include <Windows.h>
#include "NetworkServices.h"
#include <ws2tcpip.h>
#include <stdio.h>
并定义以下常量和库链接.(and define the following constants and library links.)
// size of our buffer
#define DEFAULT_BUFLEN 512
// port to connect sockets through
#define DEFAULT_PORT "6881"
// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")
在头文件中声明以下变量/函数:(Declare the following variables/functions in the header file:)
class ClientNetwork
{
public:
// for error checking function calls in Winsock library
int iResult;
// socket for client to connect to server
SOCKET ConnectSocket;
// ctor/dtor
ClientNetwork(void);
~ClientNetwork(void);
};
我们准备实现与服务器的客户端连接.在ClientNetwork.cpp中ClientNetwork的构造函数中,初始化Winsock.我将不进一步解释其工作原理.如果您想了解更多信息,可以检查Microsoft库.但是,您极有可能不需要更改这些值.(We are ready to implement client connection to server. In the constructor for ClientNetwork inside ClientNetwork.cpp, initialize the Winsock. I will not go in depth with the explanation for how this works. You may check the Microsoft library if you wish to learn more. However, you will not most likely need to change these values.)
#include "stdafx.h"
#include "ClientNetwork.h"
ClientNetwork::ClientNetwork(void) {
// create WSADATA object
WSADATA wsaData;
// socket
ConnectSocket = INVALID_SOCKET;
// holds address info for socket to connect to
struct addrinfo *result = NULL,
*ptr = NULL,
hints;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
exit(1);
}
// set address info
ZeroMemory( &hints, sizeof(hints) );
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP; //TCP connection!!!
目前,我已将服务器的地址设置为localhost,如果需要,可以更改此地址.(Currently, I have set the address for the server to be localhost, you may change this if you’d like.)
//resolve server address and port
iResult = getaddrinfo("127.0.0.1", DEFAULT_PORT, &hints, &result);
if( iResult != 0 )
{
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
exit(1);
}
// Attempt to connect to an address until one succeeds
for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {
// Create a SOCKET for connecting to server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
exit(1);
}
// Connect to server.
iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR)
{
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
printf ("The server is down... did not connect");
}
}
// no longer need address info for server
freeaddrinfo(result);
// if connection failed
if (ConnectSocket == INVALID_SOCKET)
{
printf("Unable to connect to server!\n");
WSACleanup();
exit(1);
}
这是重要的信息.我们将设置套接字为非阻塞套接字,以便在没有数据要发送/接收时,它不会等待send()和receive()函数.这对于我们的多人游戏来说是必需的,因为我们希望游戏在没有任何东西可以发送或接收给客户的情况下继续前进.(Here is an important piece of information. We are going to set our socket to be non-blocking so that it will not wait on send() and receive() functions when there is no data to send/receive. This is necessary for our multiplayer game since we’d like the game to keep going if there isn’t anything to send or receive to or from a client.)
// Set the mode of the socket to be nonblocking
u_long iMode = 1;
iResult = ioctlsocket(ConnectSocket, FIONBIO, &iMode);
if (iResult == SOCKET_ERROR)
{
printf("ioctlsocket failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
exit(1);
}
我们还将禁用(we will also disable) gle(nagle) . (您不必)(. (you don’t have to))
//disable nagle
char value = 1;
setsockopt( ConnectSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof( value ) );
}
这是我们ClientNetwork构造函数的结尾.(This is the end of our constructor for our ClientNetwork.)
现在,让我们切换到设置服务器网络类.(Now lets switch and go to setting up our ServerNetwork class.) 在头文件中,包括以下库:(In the header file, include the following libraries:)
#include <winsock2.h>
#include <Windows.h>
#include "NetworkServices.h"
#include <ws2tcpip.h>
#include <map>
using namespace std;
#pragma comment (lib, "Ws2_32.lib")
并定义以下常量:(and define the following constants:)
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "6881"
这也是我们需要在头文件中声明的内容:(Here is also what we need to declare in the header file:)
class ServerNetwork
{
public:
ServerNetwork(void);
~ServerNetwork(void);
// Socket to listen for new connections
SOCKET ListenSocket;
// Socket to give to the clients
SOCKET ClientSocket;
// for error checking return values
int iResult;
// table to keep track of each client's socket
std::map<unsigned int, SOCKET> sessions;
};
让我们在cpp文件中定义ServerNetwork构造函数:(Let’s define the ServerNetwork constructor inside the cpp file:)
#include "stdafx.h"
#include "ServerNetwork.h"
ServerNetwork::ServerNetwork(void)
{
// create WSADATA object
WSADATA wsaData;
// our sockets for the server
ListenSocket = INVALID_SOCKET;
ClientSocket = INVALID_SOCKET;
// address info for the server to listen to
struct addrinfo *result = NULL;
struct addrinfo hints;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
exit(1);
}
// set address information
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP; // TCP connection!!!
hints.ai_flags = AI_PASSIVE;
该服务器将不需要地址,因为它位于本地计算机上.(The server will not need an address since it will be on the local machine.)
// Resolve the server address and port
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if ( iResult != 0 ) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
exit(1);
}
// Create a SOCKET for connecting to server
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
exit(1);
}
// Set the mode of the socket to be nonblocking
u_long iMode = 1;
iResult = ioctlsocket(ListenSocket, FIONBIO, &iMode);
if (iResult == SOCKET_ERROR) {
printf("ioctlsocket failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
exit(1);
}
// Setup the TCP listening socket
iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
exit(1);
}
// no longer need address information
freeaddrinfo(result);
// start listening for new clients attempting to connect
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
exit(1);
}
}
这样就完成了我们的服务器初始化.现在,我们将需要接受尝试连接的客户端.(This completes our server initialization. Now we will need to accept clients that attempt to connect.)
将以下声明添加到ServerNetwork头文件中:(Add the following declaration to the ServerNetwork header file:)
// accept new connections
bool acceptNewClient(unsigned int & id);
这是定义.此函数将带有一个ID(一种用于我们跟踪此特定客户端的方法),并且一旦客户端连接,便会将其分配的套接字和ID添加到我们的表中.如果添加了新客户端,它将返回true.(And here is the definition. This function will take an ID (a way for us to track this specific client) and once a client is connected, will add their assigned socket and ID to our table. It will then return true if there was a new client added.)
// accept new connections
bool ServerNetwork::acceptNewClient(unsigned int & id)
{
// if client waiting, accept the connection and save the socket
ClientSocket = accept(ListenSocket,NULL,NULL);
if (ClientSocket != INVALID_SOCKET)
{
//disable nagle on the client's socket
char value = 1;
setsockopt( ClientSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof( value ) );
// insert new client into session id table
sessions.insert( pair<unsigned int, SOCKET>(id, ClientSocket) );
return true;
}
return false;
}
现在,我们将创建一个ServerGame类.在这里,我们将声明并初始化一个ServerNetwork对象,该对象将用作我们的网络.在更大的游戏范围内,ServerGame应该在服务器上保存有关游戏的所有信息,包括有关网络上客户端的信息.您会看到我们的前进.创建一个ServerGame.h文件:(Now we will create a ServerGame class. In there we will declare and initialize a ServerNetwork object which will be used as our network. In the larger scope of the game, ServerGame should hold every information about the game on the server, including the information about the clients on the network. You will see as we go along. Create a ServerGame.h file:)
#include "ServerNetwork.h"
class ServerGame
{
public:
ServerGame(void);
~ServerGame(void);
void update();
private:
// IDs for the clients connecting for table in ServerNetwork
static unsigned int client_id;
// The ServerNetwork object
ServerNetwork* network;
};
在其cpp文件中:(In its cpp file:)
#include "stdafx.h"
#include "ServerGame.h"
unsigned int ServerGame::client_id;
ServerGame::ServerGame(void)
{
// id's to assign clients for our table
client_id = 0;
// set up the server network to listen
network = new ServerNetwork();
}
void ServerGame::update()
{
// get new clients
if(network->acceptNewClient(client_id))
{
printf("client %d has been connected to the server\n",client_id);
client_id++;
}
}
ServerGame构造函数中的代码将设置我们之前在ServerNetwork中进行的所有网络连接,并将使其套接字侦听传入的连接.在ServerGame :: update()中,我们将接受尝试连接的新客户端,并为它们分配当前的client_id,如果为该客户端分配了ID为下一个客户端做准备,则将其递增.(The code inside the constructor of ServerGame will set up all of the networking we did earlier inside of ServerNetwork and will have its socket listen for incoming connections. In ServerGame::update() we will accept new clients that are trying to connect and assign them the current client_id and increment if the ID is assigned to them to prepare for the next client.)
让我们创建一个ClientGame,其中应该包含所有客户端信息,包括网络.(Lets create a ClientGame which should hold all of the client side information, including the network.) 创建一个名为" ClientGame.h"的头文件:(Create a header file named “ClientGame.h”:)
#include <winsock2.h>
#include <Windows.h>
#include "ClientNetwork.h"
并声明以下变量/函数:(and declare these variables/functions:)
class ClientGame
{
public:
ClientGame();
~ClientGame(void);
ClientNetwork* network;
};
在cpp文件中:(Inside the cpp file:)
#include "stdafx.h"
#include "ClientGame.h"
ClientGame::ClientGame(void)
{
network = new ClientNetwork();
}
我们终于可以检查客户端是否真正连接了.(We are finally ready to check if the client will actually connect.)
创建一个main.cpp并在顶部包含以下库:(Create a main.cpp and include these libraries at the top:)
// may need #include "stdafx.h" in visual studio
#include "stdafx.h"
#include "ServerGame.h"
#include "ClientGame.h"
// used for multi-threading
#include <process.h>
并在main.cpp中创建以下全局变量/函数:(and make these global variables/functions in main.cpp:)
void serverLoop(void *);
void clientLoop(void);
ServerGame * server;
ClientGame * client;
这是主要功能:(Here is the main function:)
int main()
{
// initialize the server
server = new ServerGame();
我们将在单独的线程上运行服务器,以便它将始终在检查新客户端.将来它还将发送和接收数据.(We will run the server on a separate thread, so that it will always be checking for new clients. It will also be sending and receiving data in the future.)
// create thread with arbitrary argument for the run function
_beginthread( serverLoop, 0, (void*)12);
// initialize the client
client = new ClientGame();
这是我们的游戏循环.它将保留控制台,以便我们可以看到ServerNetwork的输出,并且可以对其进行扩展以执行其他操作.(This is our game loop. It will hold the console so that we can see the output from ServerNetwork and it can be extended to do other things.)
clientLoop();
}
在main.cpp中添加这些功能,serverLoop将由服务器线程运行,clientLoop将由主线程运行:(Add these functions inside main.cpp, serverLoop will be ran by the server thread and clientLoop by the main thread :)
void serverLoop(void * arg)
{
while(true)
{
server->update();
}
}
void clientLoop()
{
while(true)
{
//do game stuff
//will later run client->update();
}
}
恭喜你!您现在有一个客户端连接到服务器.如果运行该程序,则应该在控制台中看到一条消息,指出客户端0已连接到服务器.(Congratulations! you now have a client connected to the server. If you run the program, you should see a message in console stating that client 0 has been connected to the server.)
发送和接收数据(Sending and Receiving Data)
现在我们需要设置一些数据来发送.让我们创建一个名为" NetworkData.h"的头文件.我们不需要cpp文件.该文件将为我们定义一些数据类型.(Now we need to set up some data to send. Lets create a header file called “NetworkData.h”. We do not need a cpp file. This file will just define some data types for us.)
在此头文件中:(In this header file:)
#include <string.h>
#define MAX_PACKET_SIZE 1000000
enum PacketTypes {
INIT_CONNECTION = 0,
ACTION_EVENT = 1,
};
现在,我们将定义一个数据包结构,以充当要发送的数据的容器:(and now we will define a packet struct to serve as a container to our data to send:)
struct Packet {
unsigned int packet_type;
void serialize(char * data) {
memcpy(data, this, sizeof(Packet));
}
void deserialize(char * data) {
memcpy(this, data, sizeof(Packet));
}
};
packet_type字段将使用我们刚创建的枚举器中的值填充.(The packet_type field will be filled using the values in the enumerator we just created.) 序列化功能用于将packet_type数据转换为我们可以通过网络发送的字节.(The serialize function is used to convert the packet_type data into bytes that we can send over the network.) 反序列化功能用于将通过网络接收的字节转换回我们可以解释的packet_type数据.(The deserialize function is used to convert bytes received over the network back into packet_type data that we can interpret.)
您将需要在ServerGame,ServerNetwork,ClientGame和ClientNetwork类中包含NetworkData.h!(You will need to include NetworkData.h inside your ServerGame, ServerNetwork, ClientGame, and ClientNetwork classes!)
让客户端在它的构造函数中首次连接到服务器时发送一个INIT_CONNECTION数据包:(Lets have the client send an INIT_CONNECTION packet when it is first connected to the server inside its constructor:)
ClientGame::ClientGame(void)
{
network = new ClientNetwork();
// send init packet
const unsigned int packet_size = sizeof(Packet);
char packet_data[packet_size];
Packet packet;
packet.packet_type = INIT_CONNECTION;
packet.serialize(packet_data);
NetworkServices::sendMessage(network->ConnectSocket, packet_data, packet_size);
}
我们刚刚创建了一个数据包,并为其指定了INIT_CONNECTION类型,然后使用我们先前编写的sendMessage()函数通过网络将其发送.我们使用了客户端用来连接服务器的套接字.(We just created a packet and gave it an INIT_CONNECTION type and sent it over the network using the sendMessage() function which we wrote earlier. We used the socket that the client had used to connect to the server.)
现在,让我们在服务器端阅读该消息.(Now lets read that message on the server side.) 将以下函数添加到ServerNetwork的头文件中:(Add the following function to ServerNetwork’s header file:) 在公开场合:(under public:)
// receive incoming data
int receiveData(unsigned int client_id, char * recvbuf);
然后这对于cpp定义:(Then this for the cpp definition:)
// receive incoming data
int ServerNetwork::receiveData(unsigned int client_id, char * recvbuf)
{
if( sessions.find(client_id) != sessions.end() )
{
SOCKET currentSocket = sessions[client_id];
iResult = NetworkServices::receiveMessage(currentSocket, recvbuf, MAX_PACKET_SIZE);
if (iResult == 0)
{
printf("Connection closed\n");
closesocket(currentSocket);
}
return iResult;
}
return 0;
}
上面的函数将接收在套接字上等待给定客户端ID的数据,并用从网络读取的数据填充传递的缓冲区(recvbuf).(The above function will receive data waiting on the socket for a given client ID and fill the passed buffer (recvbuf) with the data read from the network.)
让我们从ServerGame调用它以读取从客户端发送的数据.(Lets call it from the ServerGame to read data sent from the client.)
将以下内容添加到ServerGame的头文件中:(Add the following to the header file of ServerGame:)
在公开场合:(under public:)
void receiveFromClients();
私下:(under private:)
// data buffer
char network_data[MAX_PACKET_SIZE];
在cpp文件中定义receiveFromClients().在此功能中,我们将遍历连接时在会话表中保存的所有客户端.然后我们对它们全部调用receiveData(),并在network_data缓冲区中获取数据.然后,我们对数据包进行反序列化,并在它是INIT还是ACTION数据包之间进行切换.同样,您可以将此程序扩展为使用不同的数据包,并构造不同大小的容器以执行不同的操作.(Define receiveFromClients() in the cpp file. In this function we go through all the clients that we saved in the sessions table earlier as each were connected. Then we call receiveData() on all of them and get the data insideour network_data buffer. We then deserialize the packet and switch between whether it is an INIT or ACTION packet. In this same way, you may extend this program for different packets and struct containers of different sizes to do different things.)
void ServerGame::receiveFromClients()
{
Packet packet;
// go through all clients
std::map<unsigned int, SOCKET>::iterator iter;
for(iter = network->sessions.begin(); iter != network->sessions.end(); iter++)
{
// get data for that client
int data_length = network->receiveData(iter->first, network_data);
if (data_length <= 0)
{
//no data recieved
continue;
}
int i = 0;
while (i < (unsigned int)data_length)
{
packet.deserialize(&(network_data[i]));
i += sizeof(Packet);
switch (packet.packet_type) {
case INIT_CONNECTION:
printf("server received init packet from client\n");
break;
case ACTION_EVENT:
printf("server received action event packet from client\n");
break;
default:
printf("error in packet types\n");
break;
}
}
}
}
现在,从Server Game :: update()中的Clients()收到呼叫:(Now call receiveFromClients() inside of ServerGame::update():)
void ServerGame::update()
{
// get new clients
if(network->acceptNewClient(client_id))
{
printf("client %d has been connected to the server\n",client_id);
client_id++;
}
receiveFromClients();
}
如果现在运行该程序,则应该看到一条消息,说明客户端已连接到服务器,然后再显示另一条消息,表明服务器已收到INIT数据包.(If you run the program now, you should see a message for client connecting to the server and then another message stating that the server has received an INIT packet.)
让我们扩展程序,让客户端将ACTION数据包发送到服务器,并让服务器将ACTION数据包发送到客户端.(Lets extend the program for the clients to send ACTION packets to the server and the server to send ACTION packets to the clients.)
客户端将发送一个INIT数据包,然后服务器将发送一个ACTION数据包,并在收到后,客户端将发送一个ACTION数据包,然后服务器将接收ACTION数据包,然后发送另一个,依此类推.(The client will send an INIT packet, then the server will send an ACTION packet and upon receipt, the client will send an ACTION packet, and then the server will receive the ACTION packet, and send another, and etc.)
在ServerNetwork中,添加一个将消息发送到所有连接的客户端的功能.(In ServerNetwork, add a function which will send messages to all connected clients.)
在头文件中:(In the header file:)
// send data to all clients
void sendToAll(char * packets, int totalSize);
在cpp文件中:(In the cpp file:)
// send data to all clients
void ServerNetwork::sendToAll(char * packets, int totalSize)
{
SOCKET currentSocket;
std::map<unsigned int, SOCKET>::iterator iter;
int iSendResult;
for (iter = sessions.begin(); iter != sessions.end(); iter++)
{
currentSocket = iter->second;
iSendResult = NetworkServices::sendMessage(currentSocket, packets, totalSize);
if (iSendResult == SOCKET_ERROR)
{
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(currentSocket);
}
}
}
在ServerGame.h中,在公共字段下添加以下声明:(Inside ServerGame.h, add the following declaration under public fields:)
void sendActionPackets();
在ServerGame.cpp中,添加以下函数,该函数会将动作数据包发送到所有连接的客户端:(Inside the ServerGame.cpp, add the following function which will send action packets to all connected clients:)
void ServerGame::sendActionPackets()
{
// send action packet
const unsigned int packet_size = sizeof(Packet);
char packet_data[packet_size];
Packet packet;
packet.packet_type = ACTION_EVENT;
packet.serialize(packet_data);
network->sendToAll(packet_data,packet_size);
}
将ServerGame :: receiveFromClients()修改为以下内容:(Modify the ServerGame::receiveFromClients() to the following:)
void ServerGame::receiveFromClients()
{
Packet packet;
// go through all clients
std::map<unsigned int, SOCKET>::iterator iter;
for(iter = network->sessions.begin(); iter != network->sessions.end(); iter++)
{
int data_length = network->receiveData(iter->first, network_data);
if (data_length <= 0)
{
//no data recieved
continue;
}
int i = 0;
while (i < (unsigned int)data_length)
{
packet.deserialize(&(network_data[i]));
i += sizeof(Packet);
switch (packet.packet_type) {
case INIT_CONNECTION:
printf("server received init packet from client\n");
sendActionPackets();
break;
case ACTION_EVENT:
printf("server received action event packet from client\n");
sendActionPackets();
break;
default:
printf("error in packet types\n");
break;
}
}
}
}
让我们为客户端添加功能以从服务器接收数据包.(Lets add functionality for clients to receive packets from the server.)
在ClientNetwork.h中的公共字段下添加以下内容:(In ClientNetwork.h add the following under public fields:)
int receivePackets(char *);
并在其cpp文件中:(and in its cpp file:)
int ClientNetwork::receivePackets(char * recvbuf)
{
iResult = NetworkServices::receiveMessage(ConnectSocket, recvbuf, MAX_PACKET_SIZE);
if ( iResult == 0 )
{
printf("Connection closed\n");
closesocket(ConnectSocket);
WSACleanup();
exit(1);
}
return iResult;
}
在ClientGame的头文件中,在公共字段下添加以下内容:(In ClientGame’s header file, add the following under public fields:)
void sendActionPackets();
char network_data[MAX_PACKET_SIZE];
void update();
在cpp文件中,添加类似于ServerGame中的sendActionPackets()函数:(In the cpp file, add the sendActionPackets() function similar to the one in ServerGame:)
void ClientGame::sendActionPackets()
{
// send action packet
const unsigned int packet_size = sizeof(Packet);
char packet_data[packet_size];
Packet packet;
packet.packet_type = ACTION_EVENT;
packet.serialize(packet_data);
NetworkServices::sendMessage(network->ConnectSocket, packet_data, packet_size);
}
然后编写更新功能,该功能将连续从服务器接收动作数据包并以响应方式发送动作数据包:(Then write the update function which will continuously receive action packets from the server and send action packets in repsonse:)
void ClientGame::update()
{
Packet packet;
int data_length = network->receivePackets(network_data);
if (data_length <= 0)
{
//no data recieved
return;
}
int i = 0;
while (i < (unsigned int)data_length)
{
packet.deserialize(&(network_data[i]));
i += sizeof(Packet);
switch (packet.packet_type) {
case ACTION_EVENT:
printf("client received action event packet from server\n");
sendActionPackets();
break;
default:
printf("error in packet types\n");
break;
}
}
}
最后,在main.cpp的clientLoop内部调用客户端的更新函数,以便网络发送和接收:(And finally, call client’s update function inside the clientLoop in main.cpp so that the network will send and receive:)
void clientLoop()
{
while(true)
{
//do game stuff
client->update();
}
}
恭喜!!我们终于完成了.如果运行该程序,则应该能够看到从客户端和服务器发送和接收的消息!(Congratulations!! We are finally done. If you run the program, you should be able to see messages being sent and received from the client and the server!!)
最后一分钟的笔记.我没有介绍关闭连接,但是应该很简单.您需要从会话表中删除客户端,然后使用(Just a few last minute notes. I have not covered closing the connection but it should be pretty straight forward. You will need to remove clients from our sessions table and then close the socket using the) closesocket()(closesocket()) .我将作为练习留给您.(. This I will leave to you as an exercise.)
您可以通过添加除我们已经定义的Packet类型以外的更多数据类型来扩展程序.只需将其添加到NetworkData.h文件并编写相应的序列化/反序列化函数即可.通过网络接收它们时,请确保考虑它们的大小.(You may extend the program by adding more data types other than the Packet type that we already defined. Just add it to the NetworkData.h file and write the corresponding serialize/deserialize functions. Make sure to account for their sizes when receiving them over the network.)
另外,使用Visual Studio构建程序时出现错误,您的头文件中可能需要" #pragma一次".(Also, I had an error when building the program with Visual Studio, you may need “#pragma once” in your header files.)
祝您好运!!(Good luck in your endeavors!!)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
VC10.0 C++ Windows Dev server TCP/IP game 新闻 翻译