Your simulation will be pain and cause bugs if you don't do that.
Here's the simulation loop on the server:
stepnumber = 0;
forever() {
input = empty_set();
while (!clock_is_time_for_step(stepnumber+1)) {
keep_collecting_input(input, stepnumber+1);
}
step_state_from_to(stepnumber, stepnumber+1, input);
send_updates_to_players(stepnumber+1);
stepnumber += 1;
}
Here is the simulation loop for the client:
#define HALF_RTT 5 // or whatever
stepnumber = 0;
forever() {
input = empty_set();
while (!has_received_server_packet_for_step(stepnumber+1)) {
keep_receiving_input(input, stepnumber+1);
}
step_state_from_to(stepnumber, stepnumber+1, input);
render_state_to_screen();
send_update_to_server(stepnumber+1+HALF_RTT, read_keyboard_and_mouse());
stepnumber += 1;
}
Note that they are very, very, similar! The main difference is that the server receives commands and sends out collected state/commands, whereas the client receives collected state/commands and sends user input for some future step. Also, the client is driven by the server's packet rate, whereas the server is driven by an actual clock.