You do not need multi-threading for this task.
Your program proceeds in lockstep because your I/O calls are of the "blocking" type and you are calling them without knowing that they are "ready". If they are not ready, they'll wait until something is ready to return. This includes fgets, which waits until the Enter key is pressed to return (line-buffered input).
You have 4 I/O statements in the loop, a simplified version of which appears below (error handling removed):
for ( ; ; ) {
gets ( msg ); /* IO_1 */
if ( strcmp ( msg, "q" ) == 0 ) break;
write ( ... ); /* IO_2 */
read ( ... ); /* IO_3 */
printf ( ... ); /* IO_4 */
}
Instantly, the gets() blocks reading from the terminal, waiting for an Enter keypress. If the other client had sent us a message, we won't see it until we send our message. To fix that you must read the keyboard in a non-blocking manner and yet avoid active polling.
You avoid active polling with the select() function, which will block until one of a given set of file descriptors are "ready". Blocking is good because it means you're not looping and testing (active polling), which is worse than wasteful in a multitasking environment! select() allows you to block on a set of file descriptors, waiting until any one of them is ready. (I don't know how standardized select() is, but it should perform the same basic function on different systems.)
For keyboard input, just read a character at a time using getchar() and respond to the newline character by calling write. The STDIN_FILENO file descriptor would be part of the set passed to select(), so you will know that a character is there to read.