Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 264 additions & 23 deletions netcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,38 @@ void netcode_socket_send_packet( struct netcode_socket_t * socket, struct netcod
(void) result;
}

static int netcode_socket_error_would_block( int error )
{
#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
return error == WSAEWOULDBLOCK;
#else // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
return error == EAGAIN || error == EWOULDBLOCK;
#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
}

/*
Classifies the result of one recvfrom. A positive result is the number of bytes received.
Zero means would block: the socket is drained and the receive loop stops. -1 means skip this
datagram and keep draining. A zero-length datagram is legal and is not the end of the queue,
so it is skipped, and so is any other recvfrom error: errors are per packet, as UDP is.
*/

static int netcode_classify_receive_result( int result, int error, int would_block )
{
if ( result > 0 )
return result;

if ( result == 0 )
return -1;

if ( would_block )
return 0;

netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: recvfrom failed with error %d\n", error );

return -1;
}

int netcode_socket_receive_packet( struct netcode_socket_t * socket, struct netcode_address_t * from, void * packet_data, int max_packet_size )
{
netcode_assert( socket );
Expand All @@ -899,28 +931,15 @@ int netcode_socket_receive_packet( struct netcode_socket_t * socket, struct netc
int result = recvfrom( socket->handle, (char*) packet_data, max_packet_size, 0, (struct sockaddr*) &sockaddr_from, &from_length );

#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
if ( result == SOCKET_ERROR )
{
int error = WSAGetLastError();

if ( error == WSAEWOULDBLOCK )
return 0;

netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: recvfrom failed with error %d\n", error );

return 0;
}
int error = ( result == SOCKET_ERROR ) ? WSAGetLastError() : 0;
#else // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
if ( result <= 0 )
{
if ( errno == EAGAIN )
return 0;
int error = ( result < 0 ) ? errno : 0;
#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS

netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: recvfrom failed with error %d\n", errno );
result = netcode_classify_receive_result( result, error, netcode_socket_error_would_block( error ) );

return 0;
}
#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
if ( result <= 0 )
return result;

if ( netcode_sockaddr_to_address( &sockaddr_from, from ) != NETCODE_OK )
{
Expand Down Expand Up @@ -3269,10 +3288,15 @@ void netcode_client_receive_packets( struct netcode_client_t * client )

if ( !client->config.network_simulator )
{
// process packets received from socket
// process packets received from socket. at most NETCODE_CLIENT_MAX_RECEIVE_PACKETS are read
// per update, so a flood of datagrams cannot hold one update in the receive loop

while ( 1 )
int num_packets_received = 0;

while ( num_packets_received < NETCODE_CLIENT_MAX_RECEIVE_PACKETS )
{
num_packets_received++;

struct netcode_address_t from;
uint8_t packet_data[NETCODE_MAX_PACKET_BYTES];
int packet_bytes = 0;
Expand All @@ -3290,9 +3314,14 @@ void netcode_client_receive_packets( struct netcode_client_t * client )
packet_bytes = netcode_socket_receive_packet( &client->socket_holder.ipv6, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
}

// zero is drained. a negative result is a datagram to skip, and the drain goes on

if ( packet_bytes == 0 )
break;

if ( packet_bytes < 0 )
continue;

netcode_client_process_packet( client, &from, packet_data, packet_bytes );
}
}
Expand Down Expand Up @@ -5064,10 +5093,15 @@ void netcode_server_receive_packets( struct netcode_server_t * server )

if ( !server->config.network_simulator )
{
// process packets received from socket
// process packets received from socket. at most NETCODE_SERVER_MAX_RECEIVE_PACKETS are read
// per update, so a flood of datagrams cannot hold one update in the receive loop

while ( 1 )
int num_packets_received = 0;

while ( num_packets_received < NETCODE_SERVER_MAX_RECEIVE_PACKETS )
{
num_packets_received++;

struct netcode_address_t from;
memset( &from, 0, sizeof(from) );

Expand All @@ -5088,9 +5122,14 @@ void netcode_server_receive_packets( struct netcode_server_t * server )
packet_bytes = netcode_socket_receive_packet( &server->socket_holder.ipv6, &from, packet_data, NETCODE_MAX_PACKET_BYTES );
}

// zero is drained. a negative result is a datagram to skip, and the drain goes on

if ( packet_bytes == 0 )
break;

if ( packet_bytes < 0 )
continue;

netcode_server_read_and_process_packet( server, &from, packet_data, packet_bytes, current_timestamp, allowed_packets );
}
}
Expand Down Expand Up @@ -10522,6 +10561,205 @@ void test_loopback_callback_required()
netcode_set_assert_function( netcode_default_assert_handler );
}

static void test_receive_result_classification()
{
#if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
int would_block_error = WSAEWOULDBLOCK;
int other_error = WSAECONNRESET;
#else // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS
int would_block_error = EAGAIN;
int other_error = ECONNREFUSED;
#endif // #if NETCODE_PLATFORM == NETCODE_PLATFORM_WINDOWS

// a zero-length datagram is legal. it is skipped, and the socket is not drained

check( netcode_classify_receive_result( 0, 0, netcode_socket_error_would_block( 0 ) ) == -1 );

// would block is the only result that means the socket is drained

check( netcode_socket_error_would_block( would_block_error ) );
check( netcode_classify_receive_result( -1, would_block_error, netcode_socket_error_would_block( would_block_error ) ) == 0 );

// any other error skips this datagram and the drain goes on

check( !netcode_socket_error_would_block( other_error ) );
check( netcode_classify_receive_result( -1, other_error, netcode_socket_error_would_block( other_error ) ) == -1 );

// a datagram with bytes in it is its size

check( netcode_classify_receive_result( 5, 0, netcode_socket_error_would_block( 0 ) ) == 5 );
}

/*
The receive drain, fed through the receive override with no socket. Each entry is one call:
a positive entry is a junk datagram of that many bytes, -1 is a datagram the receive path
skips, and once the entries run out the override returns 0, the socket is drained.
*/

#define TEST_RECEIVE_DRAIN_MAX_ENTRIES ( NETCODE_SERVER_MAX_RECEIVE_PACKETS + 16 )

struct test_receive_drain_t
{
int num_entries;
int num_calls;
int entries[TEST_RECEIVE_DRAIN_MAX_ENTRIES];
};

static struct test_receive_drain_t test_receive_drain;

static void test_receive_drain_reset()
{
memset( &test_receive_drain, 0, sizeof( test_receive_drain ) );
}

static void test_receive_drain_add( int packet_bytes, int count )
{
int i;
for ( i = 0; i < count; i++ )
{
check( test_receive_drain.num_entries < TEST_RECEIVE_DRAIN_MAX_ENTRIES );
test_receive_drain.entries[test_receive_drain.num_entries++] = packet_bytes;
}
}

static int test_receive_drain_receive_packet( void * context, struct netcode_address_t * from, uint8_t * packet_data, int max_packet_bytes )
{
(void) context;

int index = test_receive_drain.num_calls++;

if ( index >= test_receive_drain.num_entries )
return 0;

int packet_bytes = test_receive_drain.entries[index];

if ( packet_bytes < 0 )
return -1;

check( packet_bytes <= max_packet_bytes );

// junk: no packet type has prefix 0xFF, so these bytes never read as a packet

memset( packet_data, 0xFF, packet_bytes );

check( netcode_parse_address( "[::1]:50000", from ) == NETCODE_OK );

return packet_bytes;
}

static struct netcode_server_t * test_receive_drain_create_server()
{
struct netcode_server_config_t server_config;
netcode_default_server_config( &server_config );
server_config.protocol_id = TEST_PROTOCOL_ID;
server_config.override_send_and_receive = 1;
server_config.send_packet_override = test_override_send_packet;
server_config.receive_packet_override = test_receive_drain_receive_packet;
memcpy( &server_config.private_key, private_key, NETCODE_KEY_BYTES );

struct netcode_server_t * server = netcode_server_create( "[::1]:40000", &server_config, 0.0 );

check( server );

netcode_server_start( server, 1 );

return server;
}

static struct netcode_client_t * test_receive_drain_create_client()
{
struct netcode_client_config_t client_config;
netcode_default_client_config( &client_config );
client_config.override_send_and_receive = 1;
client_config.send_packet_override = test_override_send_packet;
client_config.receive_packet_override = test_receive_drain_receive_packet;

struct netcode_client_t * client = netcode_client_create( "[::1]:50000", &client_config, 0.0 );

check( client );

return client;
}

static void test_receive_drain_stops_at_cap()
{
// one server update reads at most NETCODE_SERVER_MAX_RECEIVE_PACKETS datagrams, however many are queued

{
test_receive_drain_reset();
test_receive_drain_add( 100, NETCODE_SERVER_MAX_RECEIVE_PACKETS + 10 );

struct netcode_server_t * server = test_receive_drain_create_server();

netcode_server_update( server, 0.0 );

check( test_receive_drain.num_calls == NETCODE_SERVER_MAX_RECEIVE_PACKETS );

// the next update reads the rest, then finds the socket drained

netcode_server_update( server, 0.1 );

check( test_receive_drain.num_calls == NETCODE_SERVER_MAX_RECEIVE_PACKETS + 10 + 1 );

netcode_server_destroy( server );
}

// and one client update reads at most NETCODE_CLIENT_MAX_RECEIVE_PACKETS

{
test_receive_drain_reset();
test_receive_drain_add( 100, NETCODE_CLIENT_MAX_RECEIVE_PACKETS + 10 );

struct netcode_client_t * client = test_receive_drain_create_client();

netcode_client_update( client, 0.0 );

check( test_receive_drain.num_calls == NETCODE_CLIENT_MAX_RECEIVE_PACKETS );

netcode_client_update( client, 0.1 );

check( test_receive_drain.num_calls == NETCODE_CLIENT_MAX_RECEIVE_PACKETS + 10 + 1 );

netcode_client_destroy( client );
}
}

static void test_receive_drain_skips_empty_datagram()
{
// a skipped datagram between two others does not end the drain: one update reads all three,
// then finds the socket drained

{
test_receive_drain_reset();
test_receive_drain_add( 100, 1 );
test_receive_drain_add( -1, 1 );
test_receive_drain_add( 100, 1 );

struct netcode_server_t * server = test_receive_drain_create_server();

netcode_server_update( server, 0.0 );

check( test_receive_drain.num_calls == 3 + 1 );

netcode_server_destroy( server );
}

{
test_receive_drain_reset();
test_receive_drain_add( 100, 1 );
test_receive_drain_add( -1, 1 );
test_receive_drain_add( 100, 1 );

struct netcode_client_t * client = test_receive_drain_create_client();

netcode_client_update( client, 0.0 );

check( test_receive_drain.num_calls == 3 + 1 );

netcode_client_destroy( client );
}
}

#define RUN_TEST( test_function ) \
do \
{ \
Expand Down Expand Up @@ -10586,6 +10824,9 @@ void netcode_test()
RUN_TEST( test_disable_timeout );
RUN_TEST( test_loopback );
RUN_TEST( test_loopback_callback_required );
RUN_TEST( test_receive_drain_skips_empty_datagram );
RUN_TEST( test_receive_result_classification );
RUN_TEST( test_receive_drain_stops_at_cap );
#if NETCODE_PACKET_TAGGING
RUN_TEST( test_packet_tagging );
#endif // #if NETCODE_PACKET_TAGGING
Expand Down
Loading