SDL 2.0 Tutorial-05: Networking 2

In the last tutorial we finished with a hallow if-statement within our processing loop.

int running = 1;
while(running) {
	int num_rdy = SDLNet_CheckSockets(socket_set, 1000);

	if(num_rdy <= 0) {
		// NOTE: none of the sockets are ready
	} else {
		// NOTE: some number of the sockets are ready
	}
}

If there aren’t any sockets which we need to process (i.e. num_rdy <= 0 ) then we’ll loop over all the currently connected client sockets and perform any tasks which may be pending. This is basically the server’s chance to catch up with game state and perform any security checks. This will work well for a simple TCP game where the server and client need not be in sync the entire time. For something more real-time we would prioritize game state and perhaps use UDP instead.

For now let’s add four wood to the resource count for each connected client.

int ind;
for(ind=0; ind<MAX_SOCKETS; ++ind) {
	if(!clients[ind].in_use) continue;
	clients[ind].amt_wood += 4;
}

Remember this will run every second assuming there aren’t any sockets to be processed (i.e. num_rdy <= 0 ). Why am I saying it’ll run every second? It is because of the following function call:

SDLNet_CheckSockets(socket_set, 1000);

The second argument to this function is 1000, which is the number of milliseconds this function will block while waiting for a socket connection.

Now, if there are sockets ready to be processed we’ll have to check whether it is a client socket or the server socket. If the server socket is ready then we likely have a new client connection. For the client connections we’ll have to check each one until we find the ones which are ready and then process the data that we have received from them.

First let’s handle the case when the server socket is ready:

if(SDLNet_SocketReady(server_socket)) {
	int got_socket = AcceptSocket(next_ind);
	if(!got_socket) {
		num_rdy--;
		continue;
	}

	// NOTE: get a new index
	int chk_count;
	for(chk_count=0; chk_count<MAX_SOCKETS; ++chk_count) {
		if(sockets[(next_ind+chk_count)%MAX_SOCKETS] == NULL) break;
	}

	next_ind = (next_ind+chk_count)%MAX_SOCKETS;
	printf("DB: new connection (next_ind = %d)n", next_ind);

	num_rdy--;
}

Here the function int AcceptSocket(int index); is a shorthand for accepting a new socket connection using sockets[index] and clients[index] . This function returns whether or not the connection was successfully accepted. Here is the function itself:

int AcceptSocket(int index) {
	if(sockets[index]) {
		fprintf(stderr, "ER: Overriding socket at index %d.n", index);
		CloseSocket(index);
	}

	sockets[index] = SDLNet_TCP_Accept(server_socket);
	if(sockets[index] == NULL) return 0;

	clients[index].in_use = 1;
	if(SDLNet_TCP_AddSocket(socket_set, sockets[index]) == -1) {
		fprintf(stderr, "ER: SDLNet_TCP_AddSocket: %sn", SDLNet_GetError());
		exit(-1);
	}

	return 1;
}

The function void CloseSocket(int index); was presented in the previous part of this tutorial. After we accept the new client connection we also update the next_ind variable so that we are ready for the next client connection. As it’s currently written, if a new client connects when the server is full then another client will be kicked to make room for the new connection.

Now let’s process any clients which might have data for us.

int ind;
for(ind=0; (ind<MAX_SOCKETS) && num_rdy; ++ind) {
	if(sockets[ind] == NULL) continue;
	if(!SDLNet_SocketReady(sockets[ind])) continue;

	uint8_t* data;
	uint16_t flag;
	uint16_t length;
				
	data = RecvData(ind, &length, &flag);
	if(data == NULL) {
		num_rdy--;
		continue;
	}

	switch(flag) {
		case FLAG_WOOD_UPDATE: {
			uint16_t offset = 0;
			uint8_t send_data[MAX_PACKET];

			memcpy(send_data+offset, &clients[ind].amt_wood, sizeof(uint8_t));
			offset += sizeof(uint8_t);

			SendData(ind, send_data, offset, FLAG_WOOD_UPDATE);
		} break;

		case FLAG_QUIT: {
			running = 0;
			printf("DB: shutdown by client id: %dn", ind);
		} break;
	}

	free(data);
	num_rdy--;
}

What we are doing here is looping over all the client sockets and checking to see whether a client is connected and whether that connected client has some information ready for us. We also want to make sure that we aren’t working harder than we have to by breaking out of the for-loop when num_rdy == 0 ; because at that point we have processed all the ready client sockets so there is no point in checking any remaining client connections for information.

Once we know that we have a connected socket and that the socket have information ready for us, we fill a buffer with the data they sent us. This is what the uint8_t* RecvData(int index, uint16_t* length, uint16_t* flag); function does for us:

uint8_t* RecvData(int index, uint16_t* length, uint16_t* flag) {
	uint8_t temp_data[MAX_PACKET];
	int num_recv = SDLNet_TCP_Recv(sockets[index], temp_data, MAX_PACKET);

	if(num_recv <= 0) {
		CloseSocket(index);
		const char* err = SDLNet_GetError();
		if(strlen(err) == 0) {
			printf("DB: client disconnectedn");
		} else {
			fprintf(stderr, "ER: SDLNet_TCP_Recv: %sn", err);
		}

		return NULL;
	} else {
		int offset = 0;
		*flag = *(uint16_t*) &temp_data[offset];
		offset += sizeof(uint16_t);

		*length = (num_recv-offset);

		uint8_t* data = (uint8_t*) malloc((num_recv-offset)*sizeof(uint8_t));
		memcpy(data, &temp_data[offset], (num_recv-offset));

		return data;
	}
}

This function gets data from sockets[index] and returns a pointer to a new buffer. The in/out function arguments length and flag are the length of the newly allocated buffer and the packet flag for the information we are receiving from the client.

If the client sends us a request for the amount of wood they current possess (i.e. FLAG_WOOD_UPDATE ) then we reply by sending them a packet with the information they are requesting. To send information to the connected client we use the following shorthand function:

void SendData(int index, uint8_t* data, uint16_t length, uint16_t flag) {
	uint8_t temp_data[MAX_PACKET];

	int offset = 0;
	memcpy(temp_data+offset, &flag, sizeof(uint16_t));
	offset += sizeof(uint16_t);
	memcpy(temp_data+offset, data, length);
	offset += length;

	int num_sent = SDLNet_TCP_Send(sockets[index], temp_data, offset);
	if(num_sent < offset) {
		fprintf(stderr, "ER: SDLNet_TCP_Send: %sn", SDLNet_GetError());
		CloseSocket(index);
	}
}

For debugging purposes the client can also shutdown the server with a FLAG_QUIT . In the next tutorial we will write a simple client which will display the amount of wood the client current has. This amount will continue to increase because the server currently adds four wood each time it enters idle processing. Once we have achieved this we’ll add a “quest” button and timer on the client-side and a few more packet flags allowing the client to request the amount of time left on a quest and to start a quest. The server will check for when a quest completes and will increase the amount of wood for that client as a result.

Till next time!