Due date: Sunday, September 15, 11:59pm
Due date: Tuesday, September 17, 11:59pm
There are three main goals to this project:
Socket programming: to write C++ code that uses Internet sockets for client-server communication, useful not only for this project but for socket-based programming of any kind.
Network support in game engine: to add general-purpose network support to a game engine as both a learning experience and as a foundation for projects 3 and 4.
Multiperson Internet messaging: to use the above - socket programming and game engine networking - to make a multiperson, Internet messaging application. Doing so demonstrates mastery of socket programming and networking support, and continues progression of game development skills.
Overview | Details | Tips | Submission | Grading
You are to make an Internet messaging application -
To realize these features, you will add use TCP sockets and add networking support to the
Details: Sockets | Networking | Yak
The following system calls will be used in your socket implementation:
// OS support for sockets.
// Socket functions.
socket()
getaddrinfo()
connect()
accept()
listen()
bind()
send()
recv()
// Flags.
MSG_PEEK
// Data types.
struct sockaddr_in
For namespace reasons, the syntax ::
may be needed in front of system calls (e.g., ::send(...)
).
Unlike many system calls, socket code works nearly the same on Windows, Mac and Linux. However, there are a few differences in some of the function names and parameters, as well as using libraries and needed include files. The following code is Linux/Mac specific:
// Linux/Mac specific support for sockets.
// Header files.
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
// System calls.
close()
ioctl()
sigaction()
// Flags.
MSG_DONTWAIT
O_NONBLOCK
// Data types.
socklen_t
The following code is Windows-specific:
// Windows-specific support for sockets.
// Header files.
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <io.h>
// System calls.
closesocket()
ioctlsocket()
WSAStartup()
WSACleanup()
// Data types.
WSADATA
In order to use sockets in Windows, the windows socket library needs to be linked in during compilation and started up (once only) during runtime. Below are some code fragments that show key operations:
// Windows: link in and use socket library.
#pragma comment(lib, "ws2_32.lib")
WSADATA wsa;2, 2), &wsa);
WSAStartup(MAKEWORD( WSACleanup();
Sample code is provided below that shows complete, working process-to-process communication over TCP sockets:
listen-tcp.c
- accept TCP socket connection from client, receive and display incoming text.
talk-tcp.c
- connect via TCP socket to server, get text input from keyboard and send to server.
Note, while the above examples show one-way communication, sockets are two-way - i.e., you can send and receive from the same connected socket.
On all systems, sockets are by default blocking, meaning if a process reads from them and there is no data available, the process blocks until there is data. On Linux and Mac, recv()
can be made non-blocking with the MSG_DONTWAIT
flag, and the socket itself made non-blocking with an fcntl()
call with O_NONBLOCK
.
// Linux/Mac: make (int) socket non-blocking.
int flags = O_NONBLOCK;
fcntl(sock, F_SETFL, flags;
On Windows, a socket can be made non-blocking with an ioctlsocket()
call with FIONBIO
:
// Windows: make (int) socket non-blocking.
1;
u_long mode = ioctlsocket(socket, FIONBIO, &mode);
On Linux and Mac, the ioctl()
call can be used to retrieve the number of bytes available in a socket before reading (e.g., to make sure there is a complete message) using the FIONREAD
parameter as the second argument:
// Linux/Mac: check bytes on (int) socket.
int bytes;
ioctl(socket, FIONREAD, &bytes);
On Windows, the equivalent call is with ioctlsocket()
and FIONBIO
:
// Windows: check bytes on (int) socket.
u_long bytes; ioctlsocket(socket, FIONREAD, &bytes);
Details: Sockets | Networking | Yak
Recommended are three classes to provide networking support in libnetwork.a
) you make and, hence, will behave as if they are part of the engine.
Since the added classes are intended to function as part of the engine, they could be placed in Dragonfly's df::
namespace. This requires code outside of the namespace (e.g., game code) to use df::
to access elements inside the namespace (similar to access to all
The NetworkManager handles the internals of a game's network connection. As such, both a game client and a game server have a NetworkManager. Like other Dragonfly managers, the NetworkManager class inherits from the Manager base class and is a singleton. It is typically started up when the game engine starts and shut down when the game engine stops. Its main private attributes handle sockets -- data types that are int
variables. The suggested class header file is provided below:
//
// NetworkManager.h
//
#ifndef NETWORK_MANAGER_H
#define NETWORK_MANAGER_H
// System includes.
#include <string>
#include <vector>
// Engine includes.
#include "Manager.h"
// Two-letter acronym for easier access to manager.
#define NM df::NetworkManager::getInstance()
namespace df {
const std::string DRAGONFLY_PORT = "9876";
class NetworkManager : public Manager {
private:
// Private since a singleton.
NetworkManager(); const&); // Don't allow copy.
NetworkManager(NetworkManager void operator=(NetworkManager const&); // Don't allow assignment.
std::vector<int> m_sock; // Connected network sockets.
int m_accept_sock; // Socket for accept connections.
public:
// Get the one and only instance of the NetworkManager.
static NetworkManager &getInstance();
// Start up NetworkManager.
int startUp() override;
// Shut down NetworkManager.
void shutDown() override;
// Setup NetworkManager as server (if false, reset to client).
// Return 0 if ok, else -1.
int setServer(bool server = true, std::string port = DRAGONFLY_PORT);
// Return true if successfully setup as server, else false.
bool isServer() const;
// Accept network connection.
// If successful, generate EventNetwork (accept).
// Return sock index if new connection (note, 0 for first).
// Return -1 if no new connection, but no error.
// Return -2 if error.
int accept();
// Make network connection to host at port.
// If successful, generate EventNetwork (connect).
// Return socket index if success, -1 if error.
int connect(std::string host, std::string port = DRAGONFLY_PORT);
// Get number of connected sockets.
int getNumConnections() const;
// Send bytes from buffer to connected network socket index.
// Return 0 if success, -1 if error.
int send(const void *buffer, int bytes, int sock_index=0);
// Receive from connected network socket index (no more than nbytes).
// If leave is true, don't remove data from socket (peek).
// Return number of bytes received, -1 if error.
int receive(void *buffer, int nbytes, bool leave, int sock_index=0);
// Close network connection on indicated socket index.
// If successful, generate EventNetwork (close).
// Return 0 if success, else -1.
int close(int sock_index=0);
// Close all network connections.
// If successful, generate EventNetwork (close), for each.
// Return 0 if success, else negative number.
int closeAll();
// Return true if socket index is connected, else false.
bool isConnected(int sock_index=0) const;
// Check if network data on indicated socket index.
// Return amount of data (0 if no data), -1 if not connected or error.
int isData(int sock_index=0) const;
// Return system socket from socket index, -1 if error.
int getSocket(int sock_index=0) const;
};
// end of namespace df
}
#endif // NETWORK_MANAGER_H
The major NetworkManager methods are described next.
Upon startup()
, the NetworkManager initializes m_accept_sock
to -1 (indicating the NetworkManager is not yet connected and not configured as a server) and the array of sockets (m_sock
) is cleared. Upon shutdown(), if m_accept_sock
is not -1, it is closed via the system call. Similarly, each socket in the m_sock
vector is closed and the m_sock
array cleared.
The setServer(true)
method puts the NetworkManager in server mode, able to accept socket connections from clients. This means creating the socket to use for incoming connections (socket()
), binding to it (bind()
), and listening (listen()
). Typical of most Internet servers, the NetworkManager listens on a "well-known" port (for most network games, this is defined by the game developer) that the client uses when connecting. The default here can be specified via DRAGONFLY_PORT
. The assumption is also that an external naming service provides the IP address and/or hostname where the server (game host) is running. It also makes the socket non-blocking (using ioctlsocket() or fcntl()
) so later checks on the socket for connections via accept()
don't block if there are no new connections. Conversely, setServer(false)
sets the manager up as a client (the default). If it was previously setup as a server, the socket created for accepting connections is closed.
The accept()
method checks for new connectiopns from clients being somewhere else on the Internet (e.g., having a different Internet address). Inside accept()
, the system ::accept()
call is invoked and, if there is a new connection, the socket added to the m_sock
vector.
The complimentary method, connect()
, is invoked by the client to connect to the server. As such, it takes as parameters the server's hostname and port as strings. When invoked, connect()
does the following steps: 1) lookup the host name, translating it into the necessary system structure for connections (getaddrinfo()
), 2) create a socket used by the client to connect to the server (socket()
), and finally 3) connect to the server (connect()
).
The method isConnected()
can be used by either the client or the server to indicate if the NetworkManager has a connected socket. It returns true
if there is a socket in the socket vector (m_sock
), and otherwise returns false
. The method getSocket()
returns the actual socket used for the OS system calls (remember, outside of this, game code that uses the network manager has the socket index only).
The method send()
sends data through the connected socket (using the ::send()
system call).
The method isData()
returns the amount of data (in bytes) that is waiting in the network socket (using ioctlsocket()
or ioctl()
), leaving any data there intact. If there is no data, zero is returned, but this is not considered an error.
The method receive()
retrieves data from the connected socket (using the ::recv()
system call). Receive is non-blocking, so if there is no data pending, the method does not block but returns (the ::recv()
call returns an error, and reports EAGAIN or EWOULDBLOCK as "error" codes in this case). If leave
is true, then the data is left on the socket after the system call (by using the MSG_PEEK flag as a parameter).
Like all game engines, accept()
), when a connection is connected (in connect()
) and when a connection is closed (in close()
). When there is network data, that is generated by the Sentry. The EventNetwork class is described below.
//
// EventNetwork.h - A network event.
//
#ifndef EVENT_NETWORK_H
#define EVENT_NETWORK_H
// System includes.
#include <string>
// Engine includes.
#include "Event.h"
namespace df {
const std::string NETWORK_EVENT = "df-network";
enum class NetworkEventLabel {
1,
UNDEFINED = -
ACCEPT,
CONNECT,
CLOSE,
DATA,
};
class EventNetwork : public Event {
private:
// Must provide label in constructor.
EventNetwork(); m_label; // Label of event.
NetworkEventLabel int m_socket_index; // Socket index of connection.
int m_bytes; // Number of bytes in message (0 if connect).
public:
// Constructor must provide label.
EventNetwork(NetworkEventLabel label);
// Set label.
void setLabel(NetworkEventLabel new_label);
// Get label.
const;
NetworkEventLabel getLabel()
// Set socket index.
void setSocketIndex(int new_socket_index);
// Get socket index.
int getSocketIndex() const;
// Set number of bytes in message.
void setBytes(int new_bytes);
// Get number of bytes in message.
int getBytes() const;
};
// end of namespace df
}
#endif // EVENT_NETWORK_H
The network event labels (ACCEPT
, CONNECT
, CLOSE
, and DATA
) are specified in their own enum class. Implementation of the EventNetwork class is fairly straightforward as the class is mostly a container. The proposed design does not include embedding the network data in the event object (although it could be changed). Instead, the network data is left in the socket. This assumes that any interested game object will pull the data from the socket via NetworkManager (for your
The constructors should set the event type (via Event::setType()
in the base Event class) to df::NETWORK_EVENT
and all other attributes to default values (bytes
to 0 and socket_index
to -1).
The other methods are to get/set the label (df::NetworkEventLabel
), socket index and number of bytes, respectively.
If the NetworkManager was actually implemented in
//
// Sentry.h - Poll NetworkManager for incoming messages.
//
// Engine includes.
#include "Object.h"
namespace df {
class Sentry : public Object {
private:
void handleStep();
public:
Sentry();int eventHandler(const Event *p_e) override;
};
// end of namespace df }
The Sentry eventHandler()
method checks if the event is a step event and, if so, calls handleStep()
which does the real work. The handleStep()
method has a few checks: 1) If the NetworkManager is configured as a server (isServer()
is true
), the Sentry calls NM.accept()
to accept any new network connections. Note that the NetworkManager generates an appropriate EventNetwork when a connection is established. 2) If the NetworkManager is not yet connected, there is nothing else to do - i.e., there are no connected sockets to check for data.
But if there are one or more connected sockets, the Sentry loops through all connected sockets (using NM.getNumConnections()
) and for each connected socket: 1) See if there are enough bytes for an int
. If so, pull out the first 4 bytes (sizeof(int)
) as an int
but leave the data in the buffer (i.e., leave
is true
in the NM.receive()
call). This integer's value holds the number of bytes in the full message (all headers included). If there are enough bytes in the socket for the full message (i.e., the bytes available in the socket is greater than or equal to this int
), generate a network event (with label DATA
) and send to all interested objects by calling WM.onEvent()
.
Details: Sockets | Networking | Yak
With the socket code in place and the foundations for network support in
The client needs to provide a user interface for users to connect to the server, enter their handle/name, and then enter chat messages to other users. In
Once the user is chatting, the interface needs to display the chat messages on the screen. In
Sample code showing use of these classes (TextEntry and TextBox) in an interface suitable for chatting is provided below:
chat: [chat.cpp
] - start up game engine, create and launch name entry.
NameEntry: [NameEntry.h
, NameEntry.cpp
] - get user handle/name for chat session, then create TextBox and input.
Input: [Input.h
, Input.cpp
] - get user chat input and echo it (with name) to TextBox.
The server accepts connections from
Below is a screenshot showing 3 chatting clients connected to a server which is headless, so not shown. (A server can be made headless - i.e., it doesn't need a graphical display - by setting headless:true,
in the df-config.txt
Like all client-server applications,
Message types are preferably specified via an enum class
, such as:
// Types of messages between Yak client and server.
enum class MessageType {
1,
UNDEFINED = -
EXIT,
CHAT, };
For example, a client may send a "hello" message typed by the user to the server. The text itself is 5 bytes. The type of the message is CHAT (indicating this is a chat message the server should send to all clients). The size of the message type is an enum class
is 4 bytes (an int
). The message will also need to contain another int
(another 4 bytes) that has the total message size. So, the total message size is 5 bytes ("hello") + 4 bytes (CHAT) + 4 bytes (the total message size) or 13 bytes. The client would format the message as [13, CHAT, "hello"]
(in binary form, no commas or quotes) and send it to the server.
The suggested design is for the
The server flow would then be:
NM.setServer(true)
NM.accept()
to accept any new connectionsNM.isData()
, then check NM.recv()
for complete messageWM.onEvent()
handleData()
The
The suggested design is for the
The client flow would then be:
setServer(false)
NM.isData()
, then check NM.recv()
for complete messageWM.onEvent()
handleData()
Overview | Details | Tips | Submission | Grading
This section has tips that may be helpful as you design and programming this project.
All system calls should be checked for failure, and this is true for the socket system calls as well. While the return codes indicate success or failure, details how why the call failed can be helpful to the programmer (and, in some cases, the user of the application). For socket code, how the error details are reported depends upon the operating system. Below are examples for error codes reported by Windows versus Linux/Mac.
// Windows - reporting socket error codes.
LPTSTR error_text = NULL;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),/* Output. */
(LPTSTR)& error_text, 0, /* Min. size for output buffer. */
NULL);if (error_text != NULL) {
"Error! number: %d message: %s\n",
fprintf(stderr,
WSAGetLastError(), error_text);
LocalFree(error_text);
error_text = NULL; }
// Linux/Mac - reporting socket error codes.
"Error! number: %d message: %s\n",
fprintf(stderr, errno, strerror(errno));
The dragonfly.log
but this can be changed via environment variables (dynamic values that are used to pass configuration settings to process) and/or in the dragonfly configuration file (default df-config.txt
). The environment variables for game-specific settings:
Set DRAGONFLY_CONFIG
to specify the configuration file name (default df-config.txt
).
Set DRAGONFLY_LOG
to specify the logfile name (default dragonfly.log
).
This can be useful to, say, have specific settings for the server (e.g., the server can be "headless" without a display, or for the client to have the logfile name use the process id). These variables can be set for your shell (e.g., Bash or Powershell), but can also be set in the process itself via a system call. Setting environment variables in code in Windows is done via _putenv_s()
and in Linux/Mac via setenv()
. For example:
// Windows.
"DRAGONFLY_CONFIG", "df-config-server.txt");
_putenv_s(
// Linux/Mac.
"DRAGONFLY_CONFIG", "df-config-server.txt", 1); setenv(
The
// CustomButton.h - illustrate basic Dragonfly button.
// Engine includes.
#include "Button.h"
#include "Color.h"
#include "LogManager.h"
class CustomButton : public df::Button {
public:
// Called when Button clicked.
void callback() override;
};
// Default constructor sets values.
CustomButton::CustomButton() {
setViewString("Button");
setBorder(true);
setLocation(df::CENTER_CENTER);
setColor(df::CYAN);
setHighlightColor(df::YELLOW);
}
// On callback, write message to logfile.
void CustomButton::callback() {
LM.writeLog("CustomButton(): clicked!");
}
To make sending messages easier for your
// Pack and then send the message via the NetworkManager.
int sendMessage(const char *msg, int msg_size, MessageType type, int sock_index);
The function would compute the total message size (including all headers), then write the size, message type and message data to a buffer. Then, the NetworkManager would be used to send the buffer to the indicated socket index. You could make a corresponding recvMessage()
utility function, too.
Compilation errors such as "Redeclaration of class ClassName..." (with the actual class name instead of ClassName) typically occur if the header file, say "ClassName.h", has been included from multiple source files. This error occurs most often for utility-type classes (and functions) that are used by multiple other classes (e.g., the NetworkManager and EventNetwork). The typical fix is to use conditional compilation directives for the compiler pre-processor (e.g., #ifndef
and #define
). See the NetworkManager.h and EventNetwork.h examples above, for reference. Note, if conditional compilation directives are in use, they should be checked that the names used in the #ifndef
and #define
statements are identical.
For more detailed socket-specific programming help, you might try Beej's Guide to Network Programming - Using Internet Sockets.
Overview | Details | Tips | Submission | Grading
Your assignment is to be submitted electronically (via Canvas) on the time and day due. You must hand in the following:
Source code:
.h
files.A README file listing: platform (Windows, Mac or Linux), how to compile (if there are special settings), how to run (ditto) and anything else needed to understand (and grade) your application.
VIDEO showing: A) your code compiling, B) your
Before submitting, "clean" your project:
in Visual Studio:
Build
-> Clean solution
vs-2022/.vs
directoryvs-2022/x64
directoryin Linux/Mac: make clean
This will remove all the temporary (compiled) files. Failure to do may mean you file is too big to submit!
You do not need to submit the Dragonfly library, header files, nor the SFML files.
Use zip
to archive your files.
To submit your assignment (say, lastname-proj2.zip
) via Canvas:
Open: Assignment - Project 2
Click:Upload
Click:Drag your zip file,
Select:lastname-proj2.zip
Click:Submit Assignment
When successfully submitted, you should see a message similar to:
SUBMITTED on September 09, 2024 3:53pm
Important - you must click the Submit Assignment
button at the end or your file will not be submitted!
Overview | Details | Tips | Submission | Grading
Sockets - 30% : Correctly function socket code (demonstrated and testing) is worth about 1/3 of the grade. This includes a single server with 2+ clients that can connect and exchange messages. Understanding and implementing this code is crucial to get networking support in
Networking Support - 30% : Adding networking support to
Documentation - 10% : Not to be overlooked is including the documentation provided, as well as having that documentation be clear, readable and pertinent to the assignment. This primarily includes the README described above but also any needed supporting information. Having well-structured, readable and commented code is part of Documentation, too. Getting in the habit of good documentation is important for large software projects, especially when done in teams.
Note, bugs and/or code that has not been tested can result in points taken off for any of the areas above.
100-90. The submission clearly exceeds requirements. The networking code is in place, handles error conditions robustly and is clear and well-structured. The support for networking in the
89-80. The submission clearly meets requirements. The networking code is in place, handles all error conditions and is well-structured. The support for networking in the
79-70. The submission barely meets requirements. The networking code is in place, but does not handle all error conditions, and may be hard to follow. The support for networking in the
69-60. The project fails to meet requirements in key places. The networking code is missing aspects and does not handle many error conditions, and may be hard to follow. The support for networking in the
59-0. The project does not meet many of the key requirements. The networking code is missing and/or does not work. The support for networking in the
Overview | Details | Tips | Submission | Grading