- Lab
- Core Tech

Guided: Build a Basic TCP Echo Client and Server in C
In this lab, you will build a TCP echo server and client. You'll learn the core concepts of networking and socket programming, as well as how to handle multiple connections by using threads.

Path Info
Table of Contents
-
Challenge
Introduction
Welcome to the Guided: Build a Basic TCP Echo Client and Server in C++ lab.
In this lab, you will develop a basic echo server capable of handling multiple client connections concurrently.
However, due to the substantial differences in low-level network function implementations between Windows and Unix-like systems (including Linux and macOS), both the server and client are designed to work exclusively on Unix-like platforms. ---
Understanding TCP Sockets in Network Communication
The Transmission Control Protocol (TCP) enables data transmission across computer networks. TCP operates on top of the Internet Protocol (IP), which directs data packets to their intended destinations, ensuring that data not only reaches its destination but also does so reliably, in the correct order, and error-free.
In this context, a socket serves as an endpoint for data transmission across a network. It's an abstraction that represents an entry and exit point that software applications utilize for network communication.
Each TCP socket has a unique identifier comprising an IP address and a port number. The IP address specifies the host, while the port number identifies a specific service or application on that host.
In C++, TCP socket programming typically involves:
- Creating a socket: Using the
socket()
function, which specifies the communication domain (IPv4 or IPv6) and the protocol (TCP). - Handling server connections: The server employs
bind()
to associate the socket with an address and port,listen()
to await incoming connections, andaccept()
to establish a connection. - Handling client connections: The client uses
connect()
to form a connection with the server. - Data transmission: Both client and server utilize
send()
andrecv()
functions for data transmission over the established connection. - Multithreading: To manage multiple connections, multithreading (using
std::thread
, for example) can be implemented in the server application. ---
Familiarizing with the Program Structure
There are five main files:
-
src/common/Socket.cpp
: This file provides the implementation for theSocket
class, grouping methods for TCP socket operations, including creation, binding, listening, accepting connections, data transmission, and closure. It's an important component for both server and client and is the only file that interacts with low-level networking functions. -
src/server/TCPEchoServer.cpp
: This file contains theTCPEchoServer
class implementation. It handles the initialization of the TCP echo server, managing client connections, and processing client requests. This involves calling the appropriate methods from theSocket
class in the correct order. -
src/client/TCPEchoClient.cpp
: Implementing theTCPEchoClient
class, this file represents a client that connects to the echo server, sends messages, and receives echoed responses. It calls theSocket
class's methods for client-side operations. -
src/server/MainServer.cpp
: Acting as the server application's starting point, this file initializes the server on a designated port. It also manages system signals for smooth shutdowns and activates the server to attend to and manage client connections. -
src/client/MainClient.cpp
: This is the main entry for the client application, responsible for creating multiple client threads. These threads connect to the echo server concurrently, transmitting different messages and displaying the echoed replies.
You will primarily focus on developing the
Socket.cpp
file, which includes the core network functions for the TCP server and client. Additionally, you'll modify a method ofTCPEchoServer.cpp
to support concurrent connections. As you progress, you'll understand how the other files interplay with theSocket
class. Each file, along with its associated header file in theinclude
directory, is extensively commented to help you understand what they do.You can compile and run the existing program using the Run button located at the bottom-right corner of the Terminal. Initially, the program will not produce a functional output.
Begin by familiarizing yourself with the setup. When you're ready, dive into the coding process. If you have problems, remember that a
solution
directory is available for reference or to verify your methods.Let's start with the server part.
- Creating a socket: Using the
-
Challenge
Step 1: Creating a TCP Socket
Understanding the
socket
FunctionA socket is an endpoint used for sending and receiving data on a network. In C++, a socket can be created using the
socket
function.When dealing with network programming in Unix-based systems, the
man
(manual) command provides detailed documentation about many system functions, command-line tools, configuration files, and more.For example, you can enter the following command in the Terminal:
man socket
This command provides detailed information about the
socket
function, including its parameters and return value:#include <sys/socket.h> int socket(int domain, int type, int protocol);
-
int domain
: Specifies the socket's domain.AF_INET
is commonly used for IPv4 addresses, prevalent in internet and local network communications. -
int type
: Defines the type of socket.SOCK_STREAM
indicates that the socket is stream-oriented, as is the case with TCP. It's used for connection-oriented protocols where data is read in a continuous stream. -
int protocol
: Sets the socket's protocol. Using0
allows the operating system to select the default protocol for the type, which is typically TCP forSOCK_STREAM
.
The function returns an integer known as a socket descriptor, which acts as a reference to the created socket. If the socket creation fails, the function returns
-1
. The reasons for failure could include insufficient memory, network subsystem failure, or invalid parameters. To get a more specific error message, the global variableerrno
is set by the system. This can be translated into a human-readable error message using the functionstd::strerror(errno)
.In the application,
MainServer.cpp
invokes theTCPEchoServer
start
method. This method calls functions from theSocket
class to create and prepare the server socket, and then to accept and manage client connections. The first function executed in this sequence iscreateSocket()
, which you will implement in three steps. -
-
Challenge
Step 2: Binding and Listening on a Socket
Understanding Socket Address Structure
Once the socket is created, the next step is to assign an address to it. For this, you need to populate the
sockaddr_in
structure:struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
Where:
sin_family
specifies the type of addresses the socket can handle. It's always set toAF_INET
, which tells the socket it's going to use IPv4 addresses.sin_port
specifies the port in network byte order.sin_addr
specifies the address of the socket. It can either be a specific IP or a sort of 'anywhere' address.
When sending the port number across the network, it needs to be in network byte order format (which is big endian, meaning that the most significant byte is stored at the lowest memory address) to ensure that the port is interpreted correctly across different network systems. For this, you can use the function
htons
(Host TO Network Short), which converts the port number from host byte order (as a 16-bit number or short integer) to network byte order:#include <arpa/inet.h> uint16_t htons(uint16_t hostshort);
For the internet address, the
s_addr
member of structin_addr
contains the host interface address also in network byte order.You can assign to
in_addr
one of theINADDR_*
values, including:INADDR_LOOPBACK
(127.0.0.1
), which always refers to the local host via the loopback device.INADDR_ANY
(0.0.0.0
), which means any address for binding (all available network interfaces on the host machine).INADDR_BROADCAST
(255.255.255.255
), which means any host and has the same effect on bind asINADDR_ANY
for historical reasons.
However, if a specific IP address is provided, it needs to be converted from its textual representation (like
192.168.1.1
) to binary form, suitable for network operations. This is done using theinet_pton
(Internet Presentation to Network) function:#include <arpa/inet.h> int inet_pton(int af, const char * restrict src, void * restrict dst);
Here's a description of each parameter:
int af
: This specifies the address family (AF_INET
for IPv4 addresses).const char * restrict src
: This is a pointer to the string containing the IP address to be converted.void * restrict dst
: This is a pointer to a buffer where the function will store the binary form of the IP address. For IPv4, this is typically astruct in_addr
(4 bytes).
The function returns:
1
if the conversion succeeds.0
ifsrc
does not contain a character string representing a valid network address in the specified address family.-1
on other errors (in which caseerrno
will be set).
Now, let's implement the next method called in
TCPEchoServer::start
,Socket::initializeSocketAddress
. ---Understanding the
bind
FunctionOnce you have the address in the correct format, you need to assign it to your socket using the
bind
function:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Here's a breakdown of the parameters:
int sockfd
: This is the file descriptor of the socket to which you want to bind an address, the one you created earlier with thesocket
function.const struct sockaddr *addr
: This parameter is a pointer to asockaddr
structure (sockaddr_in
) that contains information about the address to which you're binding the socket, the one you've previously set up. Sincesockaddr
is designed to be a generic address structure, you'll often cast a pointer to a more specific address structure (likesockaddr_in
for IPv4) to a pointer tosockaddr
.socklen_t addrlen
: This is the length in bytes of the address structure. For IPv4, this is typically the size ofstruct sockaddr_in
.
The function returns:
0
: On successful completion.-1
: On failure, anderrno
is set to indicate the error. Common error codes includeEBADF
(invalid descriptor),EINVAL
(already bound),EACCES
(permission denied), andEADDRINUSE
(address already in use).
Let's implement the method
Socket::bindSocket
. ---Understanding the
listen
FunctionIf the binding is successful, the socket is now ready to listen for incoming connections on the address and port you specified. The
listen
function is used to mark a socket as a passive socket, one that will be used to accept incoming connection requests:int listen(int sockfd, int backlog);
Here's the breakdown of its parameters:
-
int sockfd
: This is the socket file descriptor that refers to the socket bound to an address using thebind
function. This is the same descriptor you got when you created the socket. -
int backlog
: This parameter specifies the maximum length for the queue of pending connections. Essentially, it's the number of connections that can be waiting while the process is handling a particular connection. If the queue is full and another client attempts to connect, the connection request may be refused or the client may receive an error, depending on the underlying protocol and network conditions.
The function returns:
0
: On successful completion.-1
: On failure, anderrno
is set to indicate the error. Common error codes includeEBADF
(invalid descriptor),EOPNOTSUPP
(not supported by the socket type), andEINVAL
(socket is not in a state where it can listen for connections).
It's important to note that the
listen
function doesn't start accepting connections, but it prepares the socket so that you can accept connections using theaccept
function, which you'll review in the next step. For now, let's implement theSocket::listenSocket(int backlog)
. -
Challenge
Step 3: Accepting Client Connections
Understanding the
accept
FunctionThe
accept
function is used to accept a connection request from a client on a listening socket. Once a client attempts to connect,accept
establishes a new socket for this specific client-server interaction. This means each client gets a dedicated line of communication, ensuring a clear and uninterrupted exchange of data.The signature of the
accept
function is:int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
Here's a breakdown of its parameters:
int socket
: This is the file descriptor of the listening socket. This is the same descriptor you've been using since thesocket
andbind
steps.struct sockaddr *restrict address
: This parameter is a pointer to asockaddr
structure (sockaddr_in
for IPv4). This structure is filled with the address information of the connecting client. The server can use this information to determine the IP address and port number of the client.socklen_t *restrict address_len
: This is a value-result parameter, it should initially contain the amount of space pointed to by address. On return, it will contain the actual length (in bytes) of the address returned.
The function returns:
- A non-negative integer: A new descriptor referring to the accepted socket, which will be used for subsequent communication with the connected client.
-1
: On failure, anderrno
is set to indicate the error. Common error codes includeEBADF
(invalid descriptor),EINVAL
(socket is not listening for connections), andECONNABORTED
(a connection has been aborted).
The
accept
function is a blocking call by default, meaning it waits for a client to connect before returning. It can be made non-blocking by setting the appropriate flags on the listening socket.Once a connection is accepted, the server will read from or write to the returned socket descriptor to communicate with the client. After the communication is complete, it's important to close the descriptor to free up system resources.
In the application,
TCPEchoServer::start
calls theTCPEchoServer::acceptAndHandleClient()
method. In turn, this method callsSocket::acceptSocket()
, which you'll implement in two steps. ---Extracting Information from the Client
The second step is implementing the
Socket::getSockAddrInfo
method, which is called bySocket::acceptSocket()
to extract the client IP address and port information from theclientAddr
structure.For this, there are a few key concepts and functions you need to be familiar with. These include
INET_ADDRSTRLEN
,inet_ntop
, andntohs
(don't confused them withinet_pton
andhtons
, the functions you used in theinitializeSocketAddress
method).When converting a network address from its binary form to a human-readable string, you'll need a buffer to hold this string.
INET_ADDRSTRLEN
stands for "Internet Address String Length" and it guarantees that your buffer is big enough to hold any IPv4 address without risking buffer overflow.inet_ntop
stands for "Internet Network to Presentation." It's a function used to convert an Internet address in its binary form into a string in IPv4 or IPv6 address format. Here's the function signature and a description of each parameter:#include <arpa/inet.h> const char *inet_ntop(int af, const void * restrict src, char * restrict dst, socklen_t size);
int af
: This specifies the address family. UseAF_INET
for IPv4 addresses.const void * restrict src
: This is a pointer to the buffer holding the IP address in binary form. For IPv4, this will be astruct in_addr
.char * restrict dst
: This is a pointer to a buffer where the function will store the resulting text string. It should be large enough to hold the text string.INET_ADDRSTRLEN
guarantees that.socklen_t size
: This indicates the size of the destination buffer. For IPv4,INET_ADDRSTRLEN
is the recommended size.
The function returns:
- A pointer to the character string containing the formatted IP address (which is the same as the
dst
parameter) if the conversion succeeds. NULL
if there is an error, in which caseerrno
is set to indicate the error.
Finally,
ntohs
is a function used for converting network byte order to host byte order for 16-bit (short) integers. Here's the signature of the function:#include <arpa/inet.h> // or #include <netinet/in.h> uint16_t ntohs(uint16_t netshort);
In the
Socket::getSockAddrInfo
method, you'll use these concepts and functions to extract and convert the IP address and port number from asockaddr_in
structure into a more readable and usable format. -
Challenge
Step 4: Receiving and Sending Data
Understanding the
recv
FunctionOnce the server has accepted a client connection, it should receive the data from the client and echo it back.
The
recv
function is used to receive data from a connected socket. This is its signature:#include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags);
Here's a breakdown of its parameters:
-
int sockfd
: This is the socket file descriptor from which the data should be received. This descriptor is typically obtained from theaccept
function. -
void *buf
: This parameter is a pointer to a buffer where the received data will be stored. You need to allocate this buffer before callingrecv
and ensure it is large enough to hold the incoming data. -
size_t len
: This specifies the size of the buffer pointed to bybuf
. It indicates the maximum amount of data to be received in a single call torecv
. -
int flags
: These are option flags that can be used to modify the behavior of therecv
function. Usually, it's set to zero, meaning a standard receive operation, but other flags includeMSG_PEEK
(peek at the incoming message without removing it from the queue) andMSG_WAITALL
(wait until the full amount of data can be returned).
The function returns:
- The number of bytes actually read into the buffer on successful completion.
0
: This indicates that the other side has closed the connection.-1
: On failure, anderrno
is set to indicate the error. Common error codes includeEBADF
(invalid descriptor),ECONNRESET
(connection reset by peer), andEINVAL
(invalid argument).
In the application, the method
TCPEchoServer::handleClient(int clientSocketDescriptor)
creates a new instance of theSocket
class with the client socket descriptor. This is used to receive data from the client by callingSocket::receiveData(size_t bufferSize)
. You'll complete the implementation of this method in the following task. ---Understanding the
send
FunctionNetwork communication is about both sending and receiving messages. While
recv
is half of this conversation, thesend
function completes the communication loop by transmiting data to a connected socket.The signature for the
send
function is:#include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);
Here's a breakdown of its parameters:
-
int sockfd
: This is the socket file descriptor through which data is to be sent. This descriptor should represent a connection to another socket, typically obtained after a successfulconnect
call in the case of a client, oraccept
in the case of a server. -
const void *buf
: This parameter is a pointer to the buffer containing the data to be sent. The data to be transmitted should be prepared in this buffer before callingsend
. -
size_t len
: This specifies the length of the data in bytes that you want to send, which is the size of the data in the buffer pointed to bybuf
. -
int flags
: These are option flags that modify the behavior of thesend
function. Common flags includeMSG_DONTROUTE
(sending data without routing) andMSG_NOSIGNAL
(preventSIGPIPE
signal on errors). In most cases, this parameter is set to0
.
The function returns:
- The number of bytes that were successfully transmitted from the buffer on successful completion.
-1
: On failure, witherrno
set to indicate the error. Common error codes includeEBADF
(invalid descriptor),ECONNRESET
(connection reset by peer),EACCES
(write permission denied), andEINVAL
(invalid argument).
Handling the return value is important. If the number of bytes sent is less than the number you requested to send, you typically need to handle the remaining data, for example, by sending the rest in a subsequent
send
call.In the application, the method
TCPEchoServer::handleClient(int clientSocketDescriptor)
is also the one that sends the received data to the client by callingSocket::sendData(const std::string& message)
. You'll complete the implementation of this method in the following task. -
-
Challenge
Step 5: Implementing Concurrent Client Handling in the Server
Understanding Multithreading in a TCP Server with
std::thread
While the server is handling one client through the socket created by the
accept
function, the original socket remains open and continues to listen for other incoming connections, ensuring that your server can handle multiple clients, one at a time or concurrently by using threads.What are Threads?
A thread is the smallest sequence of programmed instructions that can be managed independently by an operating system's scheduler. Think of threads as different workers in an office, each handling a separate task. In a server, each thread can manage communication with a different client, allowing multiple clients to be served at the same time.
What is
std::thread
?std::thread
is part of the C++ Standard Library. It provides a way to create new threads in your program.When you create a
std::thread
object, you provide it a function to run and any arguments that function needs. The thread begins executing that function independently of the main program flow. This means your main program can continue running while the thread does its work in the background.Imagine you have a function in your program that prints a message, like this:
void printMessage(const std::string &message) { std::cout << message << std::endl; }
Now, suppose you want to run this function in a separate thread. For this, you would use
std::thread
as follows:std::thread threadObj(printMessage, "Hello from the new thread!");
The first parameter is the function that the thread will execute. The subsequent parameters are the arguments to that function. You can pass as many arguments as your function requires.
This way:
std::thread
is the constructor that creates a new thread.threadObj
is thestd::thread
object representing the new thread.printMessage
is the function the thread will execute."Hello from the new thread!"
is the argument passed toprintMessage
.
When
threadObj
is created, it starts runningprintMessage
in a new thread. The main program continues to execute independently of this new thread.After creating a thread, you can choose to either join or detach it:
- Joining (
threadObj.join()
): Waits forthreadObj
to finish its execution before continuing the main program. - Detaching (
threadObj.detach()
): AllowsthreadObj
to run independently in the background. The thread's resources will be freed once it finishes its job, even if the main program isn't waiting for it.
Using
detach
in your server code (std::thread(...).detach()
) means that your server doesn't wait for each client's handling thread to finish before accepting new connections. This is essential for a responsive, concurrent server that can handle multiple clients simultaneously.If you don't need a reference to the thread object for later use, you can omit naming the
std::thread
instance:std::thread(printMessage, "Hello from the new thread!");
In the file
MainClient.cpp
, you can see how threads are used to send messages to the server concurrently. -
Challenge
Step 6: Connecting from the Client
Understanding the
connect
FunctionIn the application, when
MainClient.cpp
invokes theconnectToServer
method of theTCPEchoClient
class, this method subsequently executes the functions of theSocket
class in the correct sequence to establish a connection with the server:- First, it creates a TCP socket by calling the
Socket::createSocket()
method. - Next, it configures the socket with the server's address and port by calling the
Socket::initializeSocketAddress(const std::string& address, int port)
method. - Finally, it completes the connection process by invoking the
Socket::connectSocket()
method.
When you worked on the server part, you implemented the first two methods. Now it's the turn of
Socket::connectSocket()
. It uses theconnect
function to establish a connection to a server socket.This is the signature of the
connect
function:#include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Here's a breakdown of its parameters:
-
int sockfd
: This is the socket file descriptor that you want to use to establish the connection, the one that you created using thesocket
function. Before callingconnect
, the socket should not be bound to a local address, asconnect
will automatically assign a local address and port if necessary. -
const struct sockaddr *addr
: This parameter is a pointer to asockaddr
structure that contains the destination address (IP and port) for the connection. -
socklen_t addrlen
: This parameter specifies the size of the address structure, ensuring thatconnect
knows the extent of the address data it's working with.
The function returns:
0
: On successful completion, indicating that the connection has been successfully established.-1
: On failure, settingerrno
to indicate the error. Common error codes includeECONNREFUSED
(connection refused by the server),ETIMEDOUT
(connection attempt timed out),ENETUNREACH
(network is unreachable), andEADDRINUSE
(local address is already in use).
The
connect
function is a blocking call by default, meaning it will wait for the connection attempt to either succeed or fail before returning. However, it can be used in non-blocking mode if the socket has been set to non-blocking.After a successful
connect
call, the socket can be used to send and receive data using functions likesend
andrecv
. - First, it creates a TCP socket by calling the
-
Challenge
Step 7: Closing the Socket
Understanding the
close
FunctionWhen an instance of the
Socket
class is disposed of, its destructor is automatically invoked. This destructor is responsible for calling theSocket::closeSocket()
method. This method should utilize theclose
function to terminate the network connection by properly closing the associated socket.The signature of the
close
function is:#include <unistd.h> int close(int fd);
The parameter
int fd
represents the file descriptor that needs to be closed. A file descriptor is a unique identifier for a file or socket opened by a process. In socket programming, this typically refers to the socket descriptor returned by thesocket
function for standard sockets, or by theaccept
function for a client connection.The function returns:
0
: On successful completion, indicating that the file descriptor has been closed.-1
: On failure, settingerrno
to indicate the error. Common error codes includeEBADF
(whenfd
is not a valid open file descriptor).
When the
close
function is invoked on a socket descriptor, it initiates the TCP termination process. This action can potentially interrupt any ongoing or pending read/write operations on the socket. Particularly for TCP sockets, the system will try to transmit any remaining unsent data during this process.It is important to properly utilize
close
in network applications. This practice is essential for freeing up system resources and avoiding resource leaks, which happen when file descriptors are left open after use. Once a socket is closed, its file descriptor becomes invalid for further use in system calls. Any attempt to use a closed file descriptor will result in an error. -
Challenge
Conclusion
Congratulations on successfully completing this Code Lab!
To compile and run your program, you'll need to use the Run button. This is located at the bottom-right corner of the Terminal. Here's how to proceed:
-
Compilation: Clicking Run will compile the files
MainServer.cpp
,TCPEchoServer.cpp
, andSocket.cpp
into an executable namedServer
. Similarly,MainClient.cpp
,TCPEchoClient.cpp
, andSocket.cpp
will be compiled into another executable namedClient
. -
Running the Server: After compilation, the server program will automatically execute using the command:
./Server
The server will then start listening for connections on the port defined by the
SERVER_PORT
constant inMainServer.cpp
. You can stop the program by pressingCtrl+C
. -
Running the Client: To run the client, in the second Terminal execute:
./Client
This client program is designed to send three concurrent messages, each being
"Hello from Client "
followed by a number ranging from1
to3
. The order in which these messages arrive may vary with each execution.
Extending the Program
Consider exploring these ideas to further enhance your skills and expand the capabilities of the program:
-
Cross-Platform Compatibility: Adapt the program to work on Windows. Unlike Unix-based systems that use headers like
<sys/socket.h>
,<netinet/in.h>
, and<arpa/inet.h>
, Windows networking relies on the Winsock API, using the<winsock2.h>
header. -
Advanced Concurrency: Implement more sophisticated concurrency mechanisms, such as
std::async
or thread pools. Even better, consider using a well-established library like Boost. Boost simplifies network application development and concurrency management. Particularly, Boost.Asio for network and low-level I/O programming, and Boost.Thread for thread creation and management offer powerful tools for efficient and effective programming. -
Custom Exception Handling: Design a
SocketException
class to replace the genericstd::runtime_error
. This tailored exception can provide more specific error handling, improving the robustness and maintainability of your program. -
Configurable Parameters: Enhance the program by allowing the configuration of essential parameters such as the port number, maximum number of concurrent connections, and others through program arguments. ### Related Courses on Pluralsight's Library If you're interested in further honing your C++ skills or exploring more topics, Pluralsight offers several excellent courses in the following paths:
These courses cover many aspects of C++ programming. I encourage you to check them out to continue your learning journey in C++.
-
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.