Skip to content

Stage 2: TCP Client

Recap

  • In stage 1, we built a simple TCP server using linux networking APIs

Learning Objectives

  • We will implement a simple TCP client that takes an input from the user, sends it to the TCP server from Stage 1 and prints the response.

Introduction

Recall the previous stage where we relied on a third-party client, netcat, to test the functionality of the TCP server. In this stage we will focus on writing our own TCP client to communicate with the TCP server.

In both the client and server sides of a TCP-based system, socket programming plays a central role in enabling communication. So expect some code to repeat from the previous stage.

NOTE

Code snippets will contain comments in between with a format like this: /* todo */. These are meant to be filled in as you go through the documentation and implement it.

Implementation

implementation.png

Create a file named tcp_client.c in expserver/phase_0. All the code from this stage will be written to it.

The header includes and defines required for this stage are given below.

c
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define SERVER_PORT 8080
#define BUFF_SIZE 10000

Client connects to a server with the help of a socket interface, similar to how the server connects to a client. Thus we use the same socket() function to create a client socket.

c
int main() {
  // Creating listening sock
  int client_sock_fd = /* create a socket of type SOCK_STREAM */

But where (or which server) does this client connect to? To establish a connection, the client must know the IP address and listening port of the server it intends to connect to. We can use an object of struct sockaddr_in for this purpose.

c
  // Creating an object of struct socketaddr_in
  struct sockaddr_in server_addr;

  // Setting up server addr
  server_addr.sin_family = /* fill this */;
  server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
  server_addr.sin_port = /* fill this */;

INFO

server_addr.sin_addr.s_addr is set to the IP address 127.0.0.1. This IP stands for localhost ie. host machine. The inet_addr() function will convert string IP to required numeric format.

Now that we have the client socket and and the server address (IP and port), connect the client to the server with the help of the connect() function. Use error handling to handle cases of unexpected failure.

c
  // Connect to tcp server
  if (connect(client_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
    printf("[ERROR] Failed to connect to tcp server\n");
    exit(1);
  } else {
    printf("[INFO] Connected to tcp server\n");
  }

Milestone #1

This is a good point to test the code you have written. Compile and start the TCP server that we wrote in Stage 1 and this TCP client on separate terminals.

WARNING

Make sure to run the server before you run the client.

Try to start the client before the server and see what happens.

The terminal running the TCP server should display the following message:

bash
[INFO] Client connected to server

The terminal running the TCP client should display the following message:

bash
[INFO] Connected to tcp server

This confirms that the client has established a successful connection to the server!


Let us now implement the functionality for the user to send some messages from the client to the server. The client code has to:

  • Read the input from the user’s terminal
  • Send the message to the server
  • Read the message sent by the server (recall how our server sends the reversed message back to the client)
  • Print the message from the server
c
  while (1) {
    // Get message from client terminal
    char *line;
    size_t line_len = 0, read_n;
    read_n = /* read a line from the user using getline() - https://man7.org/linux/man-pages/man3/getline.3.html */

    /* send message to tcp server using send() */

    /* create a char buffer of BUFF_SIZE and memset to 0 */

    // Read message from client to buffer
    read_n = recv(/* read message sent by server to client into buffer */)

    /* close the connection and exit if read_n <= 0 */

    // Print message from cilent
    printf("[SERVER MESSAGE] %s\n", buff);
  }

  return 0;
}

Milestone #2

Again, start the TCP server and TCP client on separate terminals. You should get successful connection message on both terminals.

The terminal with the TCP server should display the following message:

bash
[INFO] Client connected to server

Type some message in the terminal running the TCP client.

bash
[INFO] Connected to tcp server
hello

The server would receive the message sent by the the client.

bash
[CLIENT MESSAGE] hello

The client terminal should get a response message (reversed string) from the server as a receipt.

bash
[INFO] Connected to tcp server
hello
olleh

Experiments

Iterative and Concurrent Server

An iterative server is a type of server that handles client connections sequentially. This means that the server processes one client request at a time, completing the entire interaction with one client before moving on to the next. This contrasts with a concurrent server, which can handle multiple client requests simultaneously.

Experiment #1

What would happen when multiple clients try and connect to the same server? Let us test it out!

First, let's start the server in one terminal. Then, simultaneously, initiate a client instance in a new terminal. The client establishes a connection with the server, which confirms by printing [INFO] Client connected to server. We can then test the communication by exchanging messages between the client and the server, with the server responding with the reversed string, as done in Milestone #2.

1_client.png

Now, let's open another terminal and start a new client instance while the first one (client #1) is still active. Upon starting, this second client (client #2) attempts to connect to the server, but the server does not confirm the connection, failing to print [INFO] Client connected to server. Consequently, any messages sent from client #2 won’t reach the server.

2_client.png

Meanwhile, client #1 remains connected. To verify the connection, we can send another message from client #1 to the server. The server will receive and process this message, sending the reversed string back to client #1.

3_client.png

Think of why this is happening. We will fix it in the next stage. Close client #1 and try to send a string from client #2 to the server. Observe what happened and move to Experiment #2.

Experiment #2

Did you notice what happened when you closed the connected client instance? The sever also terminated with it. But what if we want the server to keep serving other clients?

Modify the code such that the server does not terminate immedietly after a client disconnects.

HINT

  • A while loop around the accept and recv-send loop could help.
  • Also break from the recv-send loop after each client disconnects.

Now,let us verify our modified code.Follow the same procedure in Experiment 1. We can observe the following:

  1. The server is not closed even after terminating the first client.
  2. Now,client #2 gets connected to the server.

Now,you can try connecting a new client after disconnecting the existing client. This can be repeated any number of times. So now the server is an iterative server. In the next stage we will make a concurrent server.

Experiment #3

Now,we can modify the client code for creating multiple clients in a single execution of client code. We would be using fork system call to implement this.

PRE-REQUISITE READING

Conclusion

Congratulations! You have written a TCP client from the ground up which connected with a TCP server with the ability to send and receive messages.

Recall the problem from Experiment #1. This limitation can be fixed using using two different methods:

  1. With multi-threading
  2. With epoll

In web severs like Apache, multi-threading was used for serving multiple clients simultaneously. Each incoming client request is typically assigned to a separate thread, allowing the server to serve multiple clients concurrently without blocking or slowing down other requests.

Whereas Nginx, a more recent web server compared to Apache, uses an event-driven architecture, which relies on epoll. Instead of creating a new thread for every new connection, a single thread is sufficient to handle multiple clients simultaneously.

eXpServer uses the latter method to achieve concurrency. In the next stage we will modify our TCP server to handle multiple clients using epoll.