This section demonstrates the power of threads by rewriting our client’s echo processing function, str_cli.

In previous chapters (Chapter 6 with select or Chapter 16 with fork), handling two simultaneous inputs (the user typing and the network delivering data) required complex non-blocking logic or multiple processes.

With threads, we can simplify this significantly. We simply create two threads that perform blocking I/O, allowing the OS to schedule them as data becomes available.

1. The Architecture

We split the client into two independent tasks:

  1. The Main Thread: Handles data coming from the Server to Standard Output.
    • It blocks on read (or readline) from the socket.
    • When data arrives, it writes it to stdout.
  2. The copyto Thread: Handles data going from Standard Input to the Server.
    • It blocks on read (or fgets) from stdin.
    • When the user types something, it writes it to the socket.

This design is elegant because we can use standard blocking I/O functions. If one thread is stuck waiting for network data, the other thread is still free to accept user input, and vice versa.

2. Code Analysis
#include "../Unix-Network-Programming/lib/unpthread.h"
 
void *copyto(void *);
 
static int sockfd; /* global for both threads to access */
static FILE *fp;
 
void str_cli(FILE *fp_arg, int sockfd_arg) {
  char recvline[MAXLINE];
  pthread_t tid;
 
  sockfd = sockfd_arg; /* copy arguments to externals */
  fp = fp_arg;
 
  Pthread_create(&tid, NULL, copyto, NULL);
 
  while (Readline(sockfd, recvline, MAXLINE) > 0)
    Fputs(recvline, stdout);
}
 
void *copyto(void *arg) {
  char sendline[MAXLINE];
 
  while (Fgets(sendline, MAXLINE, fp) != NULL)
    Writen(sockfd, sendline, strlen(sendline));
 
  Shutdown(sockfd, SHUT_WR); /* EOF on stdin, send FIN */
 
  return (NULL);
  /* return (i.e., thread terminates) when EOF on stdin */
}

Global Variables You will notice the code uses global variables for the socket descriptor (sockfd) and the file pointer (fp). In C, pthread_create allows us to pass only one argument (void *arg) to the new thread. To avoid creating a special structure to hold multiple arguments (which is the “correct” way but more verbose), the authors used globals for simplicity in this example.

The Main Thread (Socket Stdout)

  1. Creates the second thread: It calls Pthread_create to start the copyto function.
  2. Enters its loop: It calls Readline to read from the socket.
  3. Handles Server Termination: If Readline returns 0 (EOF), it means the server closed the connection.
  4. Exits: It calls exit(0).Calling exit() terminates the entire process, which immediately kills the other thread (copyto). This is desirable here; if the server is gone, we don’t want the user typing into a void.

The copyto Thread (Stdin Socket) This helper thread executes the copyto function:

  1. Enters its loop: It calls Fgets to read from stdin.
  2. Sends Data: It writes the input to the socket using Writen.
  3. Handles User EOF: If the user types Ctrl+D (EOF), the loop breaks.
  4. Sends FIN: It calls Shutdown(sockfd, SHUT_WR). This sends a TCP FIN to the server, telling it “we are done sending data,” but keeps the socket open for reading (in the main thread) just in case the server has final bytes to send back.
  5. Returns: It returns NULL, which terminates only this thread, leaving the main thread running to receive the rest of the server’s response.