ref: 244333454585d1740ae6ffa148f9112128778499
parent: f1f8ecdeeadab2242c80cfa442c44c9d18a6fd6e
author: Simon Howard <fraggle@soulsphere.org>
date: Wed Sep 27 19:06:57 EDT 2017
net: Add protocol negotiation on client connection. The idea here is that we define a protocol versioning scheme, where we give a symbolic (string) name to each protocol version. This allows us to define new protocol versions in the future while retaining compatibility between different Chocolate Doom versions. It also allows forks of Chocolate Doom to define their own protocols while retaining the ability to connect to Chocolate Doom servers (and vice versa, for Chocolate Doom clients to connect to forked servers). When a client connects to a server, we send a list of protocols that are supported. The server compares it to its own list of protocols and selects a common protocol that will be used. The protocol is then sent back to the client in a reply packet. In order to do this, we must change the way that clients connect to servers. Previously the client sent a SYN packet, and the server would respond with an ACK packet. Instead, get rid of ACK packets and send a SYN packet back instead. This also simplifies the state machine when performing a connect. Since this is a fundamental change in the protocol, change the magic number sent when connecting, but continue to recognize the old magic number and send a reject message to old clients.
--- a/src/net_client.c
+++ b/src/net_client.c
@@ -426,6 +426,41 @@
NET_CL_SendTics(starttic, endtic);
}
+// Parse a SYN packet received back from the server indicating a successful
+// connection attempt.
+static void NET_CL_ParseSYN(net_packet_t *packet)
+{
+ net_protocol_t protocol;
+ char *server_version;
+
+ server_version = NET_ReadString(packet);
+ if (server_version == NULL)
+ {
+ return;
+ }
+
+ protocol = NET_ReadProtocol(packet);
+ if (protocol == NET_PROTOCOL_UNKNOWN)
+ {
+ return;
+ }
+
+ // We are now successfully connected.
+ client_connection.state = NET_CONN_STATE_CONNECTED;
+ client_connection.protocol = protocol;
+
+ // Even though we have negotiated a compatible protocol, the game may still
+ // desync. Chocolate Doom's philosophy makes this unlikely, but if we're
+ // playing with a forked version, or even against a different version that
+ // fixes a compatibility issue, we may still have problems.
+ if (strcmp(server_version, PACKAGE_STRING) != 0)
+ {
+ fprintf(stderr, "NET_CL_ParseSYN: This is '%s', but the server is "
+ "'%s'. It is possible that this mismatch may cause the game "
+ "to desync.\n", PACKAGE_STRING, server_version);
+ }
+}
+
// data received while we are waiting for the game to start
static void NET_CL_ParseWaitingData(net_packet_t *packet)
@@ -826,6 +861,10 @@
{
switch (packet_type)
{
+ case NET_PACKET_TYPE_SYN:
+ NET_CL_ParseSYN(packet);
+ break;
+
case NET_PACKET_TYPE_WAITING_DATA:
NET_CL_ParseWaitingData(packet);
break;
@@ -921,6 +960,7 @@
NET_WriteInt16(packet, NET_PACKET_TYPE_SYN);
NET_WriteInt32(packet, NET_MAGIC_NUMBER);
NET_WriteString(packet, PACKAGE_STRING);
+ NET_WriteProtocolList(packet);
NET_WriteConnectData(packet, data);
NET_WriteString(packet, net_player_name);
NET_Conn_SendPacket(&client_connection, packet);
@@ -927,8 +967,7 @@
NET_FreePacket(packet);
}
-// connect to a server
-
+// Connect to a server
boolean NET_CL_Connect(net_addr_t *addr, net_connect_data_t *data)
{
int start_time;
@@ -959,7 +998,7 @@
// Initialize connection
- NET_Conn_InitClient(&client_connection, addr);
+ NET_Conn_InitClient(&client_connection, addr, NET_PROTOCOL_UNKNOWN);
// try to connect
--- a/src/net_common.c
+++ b/src/net_common.c
@@ -19,6 +19,7 @@
#include "doomtype.h"
#include "d_mode.h"
+#include "i_system.h"
#include "i_timer.h"
#include "net_common.h"
@@ -34,9 +35,17 @@
#define KEEPALIVE_PERIOD 1
+static struct
+{
+ net_protocol_t protocol;
+ char *name;
+} protocol_names[] = {
+ {NET_PROTOCOL_CHOCOLATE_DOOM_0, "CHOCOLATE_DOOM_0"},
+};
+
// reliable packet that is guaranteed to reach its destination
-struct net_reliable_packet_s
+struct net_reliable_packet_s
{
net_packet_t *packet;
int last_send_time;
@@ -44,11 +53,13 @@
net_reliable_packet_t *next;
};
-static void NET_Conn_Init(net_connection_t *conn, net_addr_t *addr)
+static void NET_Conn_Init(net_connection_t *conn, net_addr_t *addr,
+ net_protocol_t protocol)
{
conn->last_send_time = -1;
conn->num_retries = 0;
conn->addr = addr;
+ conn->protocol = protocol;
conn->reliable_packets = NULL;
conn->reliable_send_seq = 0;
conn->reliable_recv_seq = 0;
@@ -56,18 +67,20 @@
// Initialize as a client connection
-void NET_Conn_InitClient(net_connection_t *conn, net_addr_t *addr)
+void NET_Conn_InitClient(net_connection_t *conn, net_addr_t *addr,
+ net_protocol_t protocol)
{
- NET_Conn_Init(conn, addr);
+ NET_Conn_Init(conn, addr, protocol);
conn->state = NET_CONN_STATE_CONNECTING;
}
// Initialize as a server connection
-void NET_Conn_InitServer(net_connection_t *conn, net_addr_t *addr)
+void NET_Conn_InitServer(net_connection_t *conn, net_addr_t *addr,
+ net_protocol_t protocol)
{
- NET_Conn_Init(conn, addr);
- conn->state = NET_CONN_STATE_WAITING_ACK;
+ NET_Conn_Init(conn, addr, protocol);
+ conn->state = NET_CONN_STATE_CONNECTED;
}
// Send a packet to a connection
@@ -80,38 +93,6 @@
NET_SendPacket(conn->addr, packet);
}
-// parse an ACK packet from a client
-
-static void NET_Conn_ParseACK(net_connection_t *conn, net_packet_t *packet)
-{
- net_packet_t *reply;
-
- if (conn->state == NET_CONN_STATE_CONNECTING)
- {
- // We are a client
-
- // received a response from the server to our SYN
-
- conn->state = NET_CONN_STATE_CONNECTED;
-
- // We must send an ACK reply to the server's ACK
-
- reply = NET_NewPacket(10);
- NET_WriteInt16(reply, NET_PACKET_TYPE_ACK);
- NET_Conn_SendPacket(conn, reply);
- NET_FreePacket(reply);
- }
-
- if (conn->state == NET_CONN_STATE_WAITING_ACK)
- {
- // We are a server
-
- // Client is connected
-
- conn->state = NET_CONN_STATE_CONNECTED;
- }
-}
-
static void NET_Conn_ParseDisconnect(net_connection_t *conn, net_packet_t *packet)
{
net_packet_t *reply;
@@ -282,9 +263,6 @@
switch (*packet_type)
{
- case NET_PACKET_TYPE_ACK:
- NET_Conn_ParseACK(conn, packet);
- break;
case NET_PACKET_TYPE_DISCONNECT:
NET_Conn_ParseDisconnect(conn, packet);
break;
@@ -370,35 +348,6 @@
conn->reliable_packets->last_send_time = nowtime;
}
}
- else if (conn->state == NET_CONN_STATE_WAITING_ACK)
- {
- if (conn->last_send_time < 0
- || nowtime - conn->last_send_time > 1000)
- {
- // it has been a second since the last ACK was sent, and
- // still no reply.
-
- if (conn->num_retries < MAX_RETRIES)
- {
- // send another ACK
-
- packet = NET_NewPacket(10);
- NET_WriteInt16(packet, NET_PACKET_TYPE_ACK);
- NET_Conn_SendPacket(conn, packet);
- NET_FreePacket(packet);
- conn->last_send_time = nowtime;
-
- ++conn->num_retries;
- }
- else
- {
- // no more retries allowed.
-
- conn->state = NET_CONN_STATE_DISCONNECTED;
- conn->disconnect_reason = NET_DISCONNECT_TIMEOUT;
- }
- }
- }
else if (conn->state == NET_CONN_STATE_DISCONNECTING)
{
// Waiting for a reply to our DISCONNECT request.
@@ -530,5 +479,106 @@
return false;
return true;
+}
+
+static net_protocol_t ParseProtocolName(char *name)
+{
+ int i;
+
+ for (i = 0; arrlen(protocol_names); ++i)
+ {
+ if (!strcmp(protocol_names[i].name, name))
+ {
+ return protocol_names[i].protocol;
+ }
+ }
+
+ return NET_PROTOCOL_UNKNOWN;
+}
+
+// NET_ReadProtocol reads a single string-format protocol name from the given
+// packet, returning NET_PROTOCOL_UNKNOWN if the string describes an unknown
+// protocol.
+net_protocol_t NET_ReadProtocol(net_packet_t *packet)
+{
+ char *name;
+
+ name = NET_ReadString(packet);
+ if (name == NULL)
+ {
+ return NET_PROTOCOL_UNKNOWN;
+ }
+
+ return ParseProtocolName(name);
+}
+
+// NET_WriteProtocol writes a single string-format protocol name to a packet.
+void NET_WriteProtocol(net_packet_t *packet, net_protocol_t protocol)
+{
+ int i;
+
+ for (i = 0; i < arrlen(protocol_names); ++i)
+ {
+ if (protocol_names[i].protocol == protocol)
+ {
+ NET_WriteString(packet, protocol_names[i].name);
+ return;
+ }
+ }
+
+ // If you add an entry to the net_protocol_t enum, a corresponding entry
+ // must be added to the protocol_names list.
+ I_Error("NET_WriteProtocol: protocol %d missing from protocol_names "
+ "list; please add it.", protocol);
+}
+
+// NET_ReadProtocolList reads a list of string-format protocol names from
+// the given packet, returning a single protocol number. The protocol that is
+// returned is the last protocol in the list that is a supported protocol. If
+// no recognized protocols are read, NET_PROTOCOL_UNKNOWN is returned.
+net_protocol_t NET_ReadProtocolList(net_packet_t *packet)
+{
+ net_protocol_t result, p;
+ unsigned int num_protocols;
+ char *name;
+ int i;
+
+ if (!NET_ReadInt8(packet, &num_protocols))
+ {
+ return NET_PROTOCOL_UNKNOWN;
+ }
+
+ result = NET_PROTOCOL_UNKNOWN;
+
+ for (i = 0; i < num_protocols; ++i)
+ {
+ name = NET_ReadString(packet);
+ if (name == NULL)
+ {
+ return NET_PROTOCOL_UNKNOWN;
+ }
+
+ p = ParseProtocolName(name);
+ if (p != NET_PROTOCOL_UNKNOWN)
+ {
+ result = p;
+ }
+ }
+
+ return result;
+}
+
+// NET_WriteProtocolList writes a list of string-format protocol names into
+// the given packet, all the supported protocols in the net_protocol_t enum.
+void NET_WriteProtocolList(net_packet_t *packet)
+{
+ int i;
+
+ NET_WriteInt8(packet, NET_NUM_PROTOCOLS);
+
+ for (i = 0; i < NET_NUM_PROTOCOLS; ++i)
+ {
+ NET_WriteProtocol(packet, i);
+ }
}
--- a/src/net_common.h
+++ b/src/net_common.h
@@ -21,28 +21,18 @@
#include "net_defs.h"
#include "net_packet.h"
-typedef enum
+typedef enum
{
- // sending syn packets, waiting for an ACK reply
- // (client side)
-
+ // Client has sent a SYN, is waiting for a SYN in response.
NET_CONN_STATE_CONNECTING,
- // received a syn, sent an ack, waiting for an ack reply
- // (server side)
-
- NET_CONN_STATE_WAITING_ACK,
-
- // successfully connected
-
+ // Successfully connected.
NET_CONN_STATE_CONNECTED,
- // sent a DISCONNECT packet, waiting for a DISCONNECT_ACK reply
-
+ // Sent a DISCONNECT packet, waiting for a DISCONNECT_ACK reply
NET_CONN_STATE_DISCONNECTING,
- // client successfully disconnected
-
+ // Client successfully disconnected
NET_CONN_STATE_DISCONNECTED,
// We are disconnected, but in a sleep state, waiting for several
@@ -50,7 +40,6 @@
// to arrive, and we need to send another one. We keep this as
// a valid connection for a few seconds until we are sure that
// the other end has successfully disconnected as well.
-
NET_CONN_STATE_DISCONNECTED_SLEEP,
} net_connstate_t;
@@ -82,6 +71,7 @@
net_connstate_t state;
net_disconnect_reason_t disconnect_reason;
net_addr_t *addr;
+ net_protocol_t protocol;
int last_send_time;
int num_retries;
int keepalive_send_time;
@@ -93,8 +83,10 @@
void NET_Conn_SendPacket(net_connection_t *conn, net_packet_t *packet);
-void NET_Conn_InitClient(net_connection_t *conn, net_addr_t *addr);
-void NET_Conn_InitServer(net_connection_t *conn, net_addr_t *addr);
+void NET_Conn_InitClient(net_connection_t *conn, net_addr_t *addr,
+ net_protocol_t protocol);
+void NET_Conn_InitServer(net_connection_t *conn, net_addr_t *addr,
+ net_protocol_t protocol);
boolean NET_Conn_Packet(net_connection_t *conn, net_packet_t *packet,
unsigned int *packet_type);
void NET_Conn_Disconnect(net_connection_t *conn);
@@ -101,10 +93,15 @@
void NET_Conn_Run(net_connection_t *conn);
net_packet_t *NET_Conn_NewReliable(net_connection_t *conn, int packet_type);
-// Other miscellaneous common functions
+// Protocol list exchange.
+net_protocol_t NET_ReadProtocol(net_packet_t *packet);
+void NET_WriteProtocol(net_packet_t *packet, net_protocol_t protocol);
+net_protocol_t NET_ReadProtocolList(net_packet_t *packet);
+void NET_WriteProtocolList(net_packet_t *packet);
+// Other miscellaneous common functions
unsigned int NET_ExpandTicNum(unsigned int relative, unsigned int b);
-boolean NET_ValidGameSettings(GameMode_t mode, GameMission_t mission,
+boolean NET_ValidGameSettings(GameMode_t mode, GameMission_t mission,
net_gamesettings_t *settings);
#endif /* #ifndef NET_COMMON_H */
--- a/src/net_defs.h
+++ b/src/net_defs.h
@@ -98,20 +98,39 @@
void *handle;
};
-// magic number sent when connecting to check this is a valid client
+// Magic number sent when connecting to check this is a valid client
+#define NET_MAGIC_NUMBER 1454104972U
-#define NET_MAGIC_NUMBER 3436803284U
+// Old magic number used by Chocolate Doom versions before v3.0:
+#define NET_OLD_MAGIC_NUMBER 3436803284U
// header field value indicating that the packet is a reliable packet
#define NET_RELIABLE_PACKET (1 << 15)
+// Supported protocols. If you're developing a fork of Chocolate
+// Doom, you can add your own entry to this list while maintaining
+// compatibility with Chocolate Doom servers.
+typedef enum
+{
+ // Protocol introduced with Chocolate Doom v3.0. Each compatibility-
+ // breaking change to the network protocol will produce a new protocol
+ // number in this enum.
+ NET_PROTOCOL_CHOCOLATE_DOOM_0,
+
+ // Add your own protocol here; be sure to add a name for it to the list
+ // in net_common.c too.
+
+ NET_NUM_PROTOCOLS,
+ NET_PROTOCOL_UNKNOWN,
+} net_protocol_t;
+
// packet types
typedef enum
{
NET_PACKET_TYPE_SYN,
- NET_PACKET_TYPE_ACK,
+ NET_PACKET_TYPE_ACK, // deprecated
NET_PACKET_TYPE_REJECTED,
NET_PACKET_TYPE_KEEPALIVE,
NET_PACKET_TYPE_WAITING_DATA,
--- a/src/net_server.c
+++ b/src/net_server.c
@@ -555,16 +555,14 @@
NET_FreePacket(packet);
}
-static void NET_SV_InitNewClient(net_client_t *client,
- net_addr_t *addr,
- char *player_name)
+static void NET_SV_InitNewClient(net_client_t *client, net_addr_t *addr,
+ net_protocol_t protocol)
{
client->active = true;
client->connect_time = I_GetTimeMS();
- NET_Conn_InitServer(&client->connection, addr);
+ NET_Conn_InitServer(&client->connection, addr, protocol);
client->addr = addr;
client->last_send_time = -1;
- client->name = M_StringDuplicate(player_name);
// init the ticcmd send queue
@@ -580,99 +578,121 @@
// parse a SYN from a client(initiating a connection)
-static void NET_SV_ParseSYN(net_packet_t *packet,
- net_client_t *client,
+static void NET_SV_ParseSYN(net_packet_t *packet, net_client_t *client,
net_addr_t *addr)
{
unsigned int magic;
net_connect_data_t data;
+ net_packet_t *reply;
+ net_protocol_t protocol;
char *player_name;
char *client_version;
+ int num_players;
int i;
- // read the magic number
-
+ // Read the magic number and check it is the expected one.
if (!NET_ReadInt32(packet, &magic))
{
return;
}
- if (magic != NET_MAGIC_NUMBER)
+ switch (magic)
{
- // invalid magic number
+ case NET_MAGIC_NUMBER:
+ break;
- return;
+ case NET_OLD_MAGIC_NUMBER:
+ NET_SV_SendReject(addr,
+ "You are using an old client version that is not supported by "
+ "this server. This server is running " PACKAGE_STRING ".");
+ return;
+
+ default:
+ return;
}
- // Check the client version is the same as the server
-
+ // Read the client version string. We actually now only use this when
+ // sending a reject message, as we only reject if we can't negotiate a
+ // common protocol (below).
client_version = NET_ReadString(packet);
-
if (client_version == NULL)
{
return;
}
- if (strcmp(client_version, PACKAGE_STRING) != 0)
+ // Read the client's list of accepted protocols. Net play between forks
+ // of Chocolate Doom is accepted provided that they can negotiate a
+ // common accepted protocol.
+ protocol = NET_ReadProtocolList(packet);
+ if (protocol == NET_PROTOCOL_UNKNOWN)
{
- //!
- // @category net
- //
- // When running a netgame server, ignore version mismatches between
- // the server and the client. Using this option may cause game
- // desyncs to occur, or differences in protocol may mean the netgame
- // will simply not function at all.
- //
+ char reject_msg[256];
- if (M_CheckParm("-ignoreversion") == 0)
- {
- NET_SV_SendReject(addr,
- "Different " PACKAGE_NAME " versions cannot play a net game!\n"
- "Version mismatch: server version is: " PACKAGE_STRING);
- return;
- }
+ M_snprintf(reject_msg, sizeof(reject_msg),
+ "Version mismatch: server version is: " PACKAGE_STRING "; "
+ "client is: %s. No common compatible protocol could be "
+ "negotiated.", client_version);
+ NET_SV_SendReject(addr, reject_msg);
+ return;
}
- // read the game mode and mission
-
- if (!NET_ReadConnectData(packet, &data))
+ // Read connect data, and check that the game mode/mission are valid
+ // and the max_players value is in a sensible range.
+ if (!NET_ReadConnectData(packet, &data)
+ || !D_ValidGameMode(data.gamemission, data.gamemode)
+ || data.max_players > NET_MAXPLAYERS)
{
return;
}
- if (!D_ValidGameMode(data.gamemission, data.gamemode))
+ // Read the player's name
+ player_name = NET_ReadString(packet);
+ if (player_name == NULL)
{
return;
}
- // Check max_players value. This must be in a sensible range.
+ // At this point we have received a valid SYN.
- if (data.max_players > NET_MAXPLAYERS)
+ // Not accepting new connections?
+ if (server_state != SERVER_WAITING_LAUNCH)
{
+ NET_SV_SendReject(addr,
+ "Server is not currently accepting connections");
return;
}
- // read the player's name
+ // Before accepting a new client, check that there is a slot free.
+ NET_SV_AssignPlayers();
+ num_players = NET_SV_NumPlayers();
- player_name = NET_ReadString(packet);
-
- if (player_name == NULL)
+ if ((!data.drone && num_players >= NET_SV_MaxPlayers())
+ || NET_SV_NumClients() >= MAXNETNODES)
{
+ NET_SV_SendReject(addr, "Server is full!");
return;
}
- // received a valid SYN
+ // TODO: Add server option to allow rejecting clients which set
+ // lowres_turn. This is potentially desirable as the presence of such
+ // clients affects turning resolution.
- // not accepting new connections?
+ // Adopt the game mode and mission of the first connecting client:
+ if (num_players == 0 && !data.drone)
+ {
+ sv_gamemode = data.gamemode;
+ sv_gamemission = data.gamemission;
+ }
- if (server_state != SERVER_WAITING_LAUNCH)
+ // Check the connecting client is playing the same game as all
+ // the other clients
+ if (data.gamemode != sv_gamemode || data.gamemission != sv_gamemission)
{
- NET_SV_SendReject(addr, "Server is not currently accepting connections");
+ NET_SV_SendReject(addr, "You are playing the wrong game!");
return;
}
- // allocate a client slot if there isn't one already
-
+ // Allocate a client slot if there isn't one already
if (client == NULL)
{
// find a slot, or return if none found
@@ -702,67 +722,30 @@
}
}
- // New client?
-
- if (!client->active)
+ // Client already connected?
+ if (client->active)
{
- int num_players;
+ return;
+ }
- // Before accepting a new client, check that there is a slot
- // free
+ // Activate, initialize connection
+ NET_SV_InitNewClient(client, addr, protocol);
- NET_SV_AssignPlayers();
- num_players = NET_SV_NumPlayers();
+ // Save the SHA1 checksums and other details.
+ memcpy(client->wad_sha1sum, data.wad_sha1sum, sizeof(sha1_digest_t));
+ memcpy(client->deh_sha1sum, data.deh_sha1sum, sizeof(sha1_digest_t));
+ client->is_freedoom = data.is_freedoom;
+ client->max_players = data.max_players;
+ client->name = M_StringDuplicate(player_name);
+ client->recording_lowres = data.lowres_turn;
+ client->drone = data.drone;
+ client->player_class = data.player_class;
- if ((!data.drone && num_players >= NET_SV_MaxPlayers())
- || NET_SV_NumClients() >= MAXNETNODES)
- {
- NET_SV_SendReject(addr, "Server is full!");
- return;
- }
-
- // TODO: Add server option to allow rejecting clients which
- // set lowres_turn. This is potentially desirable as the
- // presence of such clients affects turning resolution.
-
- // Adopt the game mode and mission of the first connecting client
-
- if (num_players == 0 && !data.drone)
- {
- sv_gamemode = data.gamemode;
- sv_gamemission = data.gamemission;
- }
-
- // Save the SHA1 checksums
-
- memcpy(client->wad_sha1sum, data.wad_sha1sum, sizeof(sha1_digest_t));
- memcpy(client->deh_sha1sum, data.deh_sha1sum, sizeof(sha1_digest_t));
- client->is_freedoom = data.is_freedoom;
- client->max_players = data.max_players;
-
- // Check the connecting client is playing the same game as all
- // the other clients
-
- if (data.gamemode != sv_gamemode || data.gamemission != sv_gamemission)
- {
- NET_SV_SendReject(addr, "You are playing the wrong game!");
- return;
- }
-
- // Activate, initialize connection
-
- NET_SV_InitNewClient(client, addr, player_name);
-
- client->recording_lowres = data.lowres_turn;
- client->drone = data.drone;
- client->player_class = data.player_class;
- }
-
- if (client->connection.state == NET_CONN_STATE_WAITING_ACK)
- {
- // force an acknowledgement
- client->connection.last_send_time = -1;
- }
+ // Send a reply back to the client, indicating a successful connection
+ // and specifying the protocol that will be used for communications.
+ reply = NET_Conn_NewReliable(&client->connection, NET_PACKET_TYPE_SYN);
+ NET_WriteString(reply, PACKAGE_STRING);
+ NET_WriteProtocol(reply, protocol);
}
// Parse a launch packet. This is sent by the key player when the "start"