Microsoft DirectX 8.1 (C++) |
This tutorial shows you how to create a simple client/server session. Client/server sessions are often used for creating large-scale multiplayer games. One advantage for using a client/server game rather than a peer-to-peer game is that the majority of the processing can be done on a separate computer—the server—and therefore you do not need to rely on the power of the client's computer. For more information about client/server sessions, see Client/Server Topology and Client/Server Sessions.
Before beginning this tutorial, you should complete Tutorials 1 through 4, which describe how to create a peer-to-peer session; steps for creating a client/server session are very similar. Rather than passing information directly from peer to peer however, a client/server session requires clients to pass information to each other indirectly, through the server. No automatic method exists for the server to pass information from one client to another. If you want this feature available to your users, you need to implement it in the server application.
The Microsoft® DirectX® Software Development Kit (SDK) includes the complete sample code for this tutorial. The sample code can be found at (SDK root)\Samples\Multimedia\DirectPlay\Tutorials\Tut09_ClientServer.
This article contains the following sections.
Note The error handling code for the examples in this article is removed for clarity. For a complete version of the code, see the tutorial sample.
To play this tutorial sample, first run the server application. Enter a session name. A message prints on the screen telling you that you are "currently hosting."
Once the server is hosting, you can run the client application. Enter your computer's IP address and you will be connected to the session. Once connected, choose Send or Exit. If you choose Send, you are prompted to enter a text string. If you choose Exit, the session ends.
The server application is where the main part of the game's processing will most likely be done. The server is responsible for updating clients about the game state, for example when a player joins or leaves the session. The following topics in this section describe the tasks needed to set up a server application. Each task is described in detail below.
Creating a Microsoft® DirectPlay® server object is similar to Creating a DirectPlay Peer Object. The only difference is that when you call CoCreateInstance, you pass the class identifier of a server object (CLSID_DirectPlay8Server), the identifier of the interface (IID_IDirectPlay8Server), and the address of a pointer to an IDirectPlay8Server interface, instead of the equivalent peer parameters. After you've created the DirectPlay server object, you can initialize it by calling the IDirectPlay8Server::Initialize method.
The following excerpt from the tutorial sample illustrates how to create and initialize a DirectPlay server object.
IDirectPlay8Client *g_pDPServer = NULL; . . . // Create the IDirectPlay8Server object. hr = CoCreateInstance(CLSID_DirectPlay8Server, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Server, (LPVOID*) &g_pDPServer); // Initialize DirectPlay. hr = g_pDPServer->Initialize(NULL, DirectPlayMessageHandlerServer, 0);
In the initialization, you pass the pointer to a callback function, DirectPlayMessageHandlerServer, which handles messages received by the server.
To host a session, you must specify the address of the host device. Do this by creating an IDirectPlay8Address object and calling the IDirectPlay8Address::SetSP method. This step is identical to the step Creating an Address Object in Tutorial 2: Hosting a Session.
To begin hosting a DirectPlay server session, call the IDirectPlay8Server::Host method. This method takes the following parameters.
For more information about the parameters, see IDirectPlay8Server::Host.
The following excerpt from the tutorial sample illustrates how to begin hosting a DirectPlay server session.
// Prompt the user for the session name. printf("\nPlease enter a session name.\n"); wscanf(L"%ls", wszSession); // Set up the Application Description. ZeroMemory(&dpAppDesc, sizeof(DPN_APPLICATION_DESC)); dpAppDesc.dwSize = sizeof(DPN_APPLICATION_DESC); dpAppDesc.dwFlags = DPNSESSION_CLIENT_SERVER; // Flag describing the application dpAppDesc.guidApplication = g_guidApp; // GUID for the application dpAppDesc.pwszSessionName = wszSession; // Session name // Host the application. hr = g_pDPServer->Host(&dpAppDesc, // pdnAppDesc &g_pDeviceAddress, 1, // prgpDeviceInfo, cDeviceInfo NULL, NULL, // pdpSecurity, pdpCredentials NULL, // pvPlayerContext 0); // dwFlags
The client application is responsible for handling the user interface (UI) and processing messages from the server. The following topics in this section describe the tasks needed to create a DirectPlay client application. Each task is described in detail below.
Setting up a client object is similar to Creating a DirectPlay Peer Object. The only difference between setting up a client object and a peer object is that when you call CoCreateInstance, instead of passing the equivalent peer parameters, you pass the class identifier of a client object (CLSID_DirectPlay8Client), the identifier of the interface (IID_IDirectPlay8Client), and the address of a pointer to an IDirectPlay8Client interface. After you create the DirectPlay client object, you can initialize it by calling IDirectPlay8Client::Initialize.
The following excerpt from the tutorial sample illustrates how to create and initialize a DirectPlay client object.
// Create the IDirectPlay8Client object. hr = CoCreateInstance(CLSID_DirectPlay8Client, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay8Client, (LPVOID*) &g_pDPClient); // Initialize IDirectPlay8Client object. hr = g_pDPClient->Initialize(NULL, DirectPlayMessageHandlerClient, 0);
In the initialization, you pass the pointer to a callback function, DirectPlayMessageHandlerClient, which handles messages the client receives. In this sample, the pvUserContext parameter is set to NULL. However, if you use the same message handler function for multiple interfaces, you should specify a value for pvUserContext. For more information, see Using Player Context Values.
For the client to connect to the server, you must specify the client's device address. Do this by creating an IDirectPlay8Address object and calling the IDirectPlay8Address::SetSP method. This step is identical to the step Creating an Address Object in Tutorial 2: Hosting a Session.
For the client to connect to the server, you must also create an IDirectPlay8Address object for the address of the server. This step is identical to the step Creating an Address Object in Tutorial 2: Hosting a Session.
If you don't know the address of the server to which you want to connect, you can enumerate all of the available servers. Follow the steps in Tutorial 3: Enumerating Hosted Sessions and replace all instances of the pointer to an IDirectPlay8Peer interface with the pointer to an IDirectPlay8Client interface.
To connect to a DirectPlay client/server session, follow the same process you use to connect to a peer-to-peer session except that the IDirectPlay8Client::Connect method does not take the pvPlayerContext parameter. Therefore, the call to Connect is as follows:
hr = g_pDPClient->Connect(&dpnAppDesc, // pdnAppDesc pHostAddress, // pHostAddr g_pDeviceAddress, // pDeviceInfo NULL, // pdnSecurity NULL, // pdnCredentials NULL, 0, // pvUserConnectData, Size NULL, // pvAsyncContext NULL, // pvAsyncHandle DPNCONNECT_SYNC); // dwFlags
For more information, see Tutorial 4: Connecting to a Session.
After the server is hosting and a client is connected, the client and server can send messages to each other. If more than one client is connected, the server can send messages to a single player, a group of players, or all the players.
The server can send a message to clients using the IDirectPlay8Server::SendTo method. The following excerpt from the client/server tutorial sample illustrates how to call the SendTo method.
hr = g_pDPServer->SendTo(DPNID_ALL_PLAYERS_GROUP, // dpnid &dpnBuffer, // pBufferDesc 1, // cBufferDesc 0, // dwTimeOut NULL, // pvAsyncContext NULL, // pvAsyncHandle DPNSEND_SYNC | // dwFlags DPNSEND_NOLOOPBACK);
Setting the dpnid parameter to DPNID_ALL_PLAYERS_GROUP sends the message to all players connected to the session. To specify a specific player or group, set dpnid to the specific player ID or group ID. The dpnBuffer is a pointer to the DPN_BUFFER_DESC structure that contains the data to send. For a description of the message flags and the other parameters, see IDirectPlay8Server::SendTo.
Messages received from the clients are processed by the DirectPlayMessageHandlerServer function. The message handler function will typically take the following form.
HRESULT WINAPI DirectPlayMessageHandlerServer(PVOID pvUserContext, DWORD dwMessageId, PVOID pMsgBuffer) { HRESULT hr = S_OK; switch (dwMessageId) { case DPN_MSGID_RECEIVE: { PDPNMSG_RECEIVE pMsg; pMsg = (PDPNMSG_RECEIVE) pMsgBuffer; printf("\nReceived Message: %S\n", (WCHAR*)pMsg->pReceiveData); //process data break; } . . . //Other cases } return hr; }
When a message is received, DirectPlay generates a DPN_MSGID_RECEIVE message. The DirectPlayMessageHandlerServer function tells your application what to do with the data it received. In this example, the text in the message data buffer displays on the player's screen.
For other messages that you might want to include, see Handling Client/Server Messages.
A client can send messages to and receive messages from only the server. If a client wants to send a message to another client, the message must first be sent to the server. The server application can implement a method to forward the message to other clients.
A client can send a message to the server using the IDirectPlay8Client::Send method. The following example from the client/server tutorial sample illustrates how to call the Send method.
hr = g_pDPClient->Send(&dpnBuffer, // pBufferDesc 1, // cBufferDesc 0, // dwTimeOut NULL, // pvAsyncContext NULL, // pvAsyncHandle DPNSEND_SYNC); // dwFlags
The pBufferDesc parameter is a pointer to a DPN_BUFFER_DESC structure that tells the application what data to send. The dwTimeOut parameter is set to 0, which means that the message waits in the queue until it is either sent or the connection ends. You can set dwTimeOut to a value so that the message is not sent unless it is sent within the specified number of milliseconds. For a description of the message flags and the other parameters, see IDirectPlay8Client::Send.
Messages received from the server are processed by the DirectPlayMessageHandlerClient function. The client message handler function takes the same form as the DirectPlayMessageHandlerServer function. For more information, see the example of a message handler function in the Receiving Messages from Clients section of Server Messages.
If a DirectPlay client or server object was successfully initialized, you should close the object by calling IDirectPlay8Client::Close or IDirectPlay8Server::Close, and then release all active objects and terminate the application. For further discussion on closing and releasing DirectPlay objects, see Tutorial 1. When a client closes the session, the server receives the DPN_MSGID_DESTROY_PLAYER message but the game will continue if other players are connected. When the server closes the session, the clients receive the DPN_MSGID_TERMINATE_SESSION message and the session ends. For more information about handling these messages, see Handling Client/Server Messages.