Featured resource
pluralsight tech forecast
2025 Tech Forecast

Which technologies will dominate in 2025? And what skills do you need to keep up?

Check it out
Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

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.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 13m
Published
Clock icon Jan 10, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. 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, and accept() 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() and recv() 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:

    1. src/common/Socket.cpp: This file provides the implementation for the Socket 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.

    2. src/server/TCPEchoServer.cpp: This file contains the TCPEchoServer 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 the Socket class in the correct order.

    3. src/client/TCPEchoClient.cpp: Implementing the TCPEchoClient class, this file represents a client that connects to the echo server, sends messages, and receives echoed responses. It calls the Socket class's methods for client-side operations.

    4. 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.

    5. 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 of TCPEchoServer.cpp to support concurrent connections. As you progress, you'll understand how the other files interplay with the Socket class. Each file, along with its associated header file in the include 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.

  2. Challenge

    Step 1: Creating a TCP Socket

    Understanding the socket Function

    A 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);
    
    1. int domain: Specifies the socket's domain. AF_INET is commonly used for IPv4 addresses, prevalent in internet and local network communications.

    2. 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.

    3. int protocol: Sets the socket's protocol. Using 0 allows the operating system to select the default protocol for the type, which is typically TCP for SOCK_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 variable errno is set by the system. This can be translated into a human-readable error message using the function std::strerror(errno).

    In the application, MainServer.cpp invokes the TCPEchoServer start method. This method calls functions from the Socket class to create and prepare the server socket, and then to accept and manage client connections. The first function executed in this sequence is createSocket(), which you will implement in three steps.

  3. 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 to AF_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 struct in_addr contains the host interface address also in network byte order.

    You can assign to in_addr one of the INADDR_* 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 as INADDR_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 the inet_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:

    1. int af: This specifies the address family (AF_INET for IPv4 addresses).
    2. const char * restrict src: This is a pointer to the string containing the IP address to be converted.
    3. 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 a struct in_addr (4 bytes).

    The function returns:

    • 1 if the conversion succeeds.
    • 0 if src does not contain a character string representing a valid network address in the specified address family.
    • -1 on other errors (in which case errno will be set).

    Now, let's implement the next method called in TCPEchoServer::start, Socket::initializeSocketAddress. ---

    Understanding the bind Function

    Once 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:

    1. int sockfd: This is the file descriptor of the socket to which you want to bind an address, the one you created earlier with the socket function.
    2. const struct sockaddr *addr: This parameter is a pointer to a sockaddr structure (sockaddr_in) that contains information about the address to which you're binding the socket, the one you've previously set up. Since sockaddr is designed to be a generic address structure, you'll often cast a pointer to a more specific address structure (like sockaddr_in for IPv4) to a pointer to sockaddr.
    3. socklen_t addrlen: This is the length in bytes of the address structure. For IPv4, this is typically the size of struct sockaddr_in.

    The function returns:

    • 0: On successful completion.
    • -1: On failure, and errno is set to indicate the error. Common error codes include EBADF (invalid descriptor), EINVAL (already bound), EACCES (permission denied), and EADDRINUSE (address already in use).

    Let's implement the method Socket::bindSocket. ---

    Understanding the listen Function

    If 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:

    1. int sockfd: This is the socket file descriptor that refers to the socket bound to an address using the bind function. This is the same descriptor you got when you created the socket.

    2. 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, and errno is set to indicate the error. Common error codes include EBADF (invalid descriptor), EOPNOTSUPP (not supported by the socket type), and EINVAL (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 the accept function, which you'll review in the next step. For now, let's implement the Socket::listenSocket(int backlog).

  4. Challenge

    Step 3: Accepting Client Connections

    Understanding the accept Function

    The 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:

    1. int socket: This is the file descriptor of the listening socket. This is the same descriptor you've been using since the socket and bind steps.
    2. struct sockaddr *restrict address: This parameter is a pointer to a sockaddr 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.
    3. 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, and errno is set to indicate the error. Common error codes include EBADF (invalid descriptor), EINVAL (socket is not listening for connections), and ECONNABORTED (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 the TCPEchoServer::acceptAndHandleClient() method. In turn, this method calls Socket::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 by Socket::acceptSocket() to extract the client IP address and port information from the clientAddr structure.

    For this, there are a few key concepts and functions you need to be familiar with. These include INET_ADDRSTRLEN, inet_ntop, and ntohs (don't confused them with inet_pton and htons, the functions you used in the initializeSocketAddress 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);
    
    1. int af: This specifies the address family. Use AF_INET for IPv4 addresses.
    2. const void * restrict src: This is a pointer to the buffer holding the IP address in binary form. For IPv4, this will be a struct in_addr.
    3. 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.
    4. 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 case errno 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 a sockaddr_in structure into a more readable and usable format.

  5. Challenge

    Step 4: Receiving and Sending Data

    Understanding the recv Function

    Once 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:

    1. int sockfd: This is the socket file descriptor from which the data should be received. This descriptor is typically obtained from the accept function.

    2. void *buf: This parameter is a pointer to a buffer where the received data will be stored. You need to allocate this buffer before calling recv and ensure it is large enough to hold the incoming data.

    3. size_t len: This specifies the size of the buffer pointed to by buf. It indicates the maximum amount of data to be received in a single call to recv.

    4. int flags: These are option flags that can be used to modify the behavior of the recv function. Usually, it's set to zero, meaning a standard receive operation, but other flags include MSG_PEEK (peek at the incoming message without removing it from the queue) and MSG_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, and errno is set to indicate the error. Common error codes include EBADF (invalid descriptor), ECONNRESET (connection reset by peer), and EINVAL (invalid argument).

    In the application, the method TCPEchoServer::handleClient(int clientSocketDescriptor) creates a new instance of the Socket class with the client socket descriptor. This is used to receive data from the client by calling Socket::receiveData(size_t bufferSize). You'll complete the implementation of this method in the following task. ---

    Understanding the send Function

    Network communication is about both sending and receiving messages. While recv is half of this conversation, the send 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:

    1. 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 successful connect call in the case of a client, or accept in the case of a server.

    2. 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 calling send.

    3. 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 by buf.

    4. int flags: These are option flags that modify the behavior of the send function. Common flags include MSG_DONTROUTE (sending data without routing) and MSG_NOSIGNAL (prevent SIGPIPE signal on errors). In most cases, this parameter is set to 0.

    The function returns:

    • The number of bytes that were successfully transmitted from the buffer on successful completion.
    • -1: On failure, with errno set to indicate the error. Common error codes include EBADF (invalid descriptor), ECONNRESET (connection reset by peer), EACCES (write permission denied), and EINVAL (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 calling Socket::sendData(const std::string& message). You'll complete the implementation of this method in the following task.

  6. 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 the std::thread object representing the new thread.
    • printMessage is the function the thread will execute.
    • "Hello from the new thread!" is the argument passed to printMessage.

    When threadObj is created, it starts running printMessage 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 for threadObj to finish its execution before continuing the main program.
    • Detaching (threadObj.detach()): Allows threadObj 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.

  7. Challenge

    Step 6: Connecting from the Client

    Understanding the connect Function

    In the application, when MainClient.cpp invokes the connectToServer method of the TCPEchoClient class, this method subsequently executes the functions of the Socket 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 the connect 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:

    1. int sockfd: This is the socket file descriptor that you want to use to establish the connection, the one that you created using the socket function. Before calling connect, the socket should not be bound to a local address, as connect will automatically assign a local address and port if necessary.

    2. const struct sockaddr *addr: This parameter is a pointer to a sockaddr structure that contains the destination address (IP and port) for the connection.

    3. socklen_t addrlen: This parameter specifies the size of the address structure, ensuring that connect 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, setting errno to indicate the error. Common error codes include ECONNREFUSED (connection refused by the server), ETIMEDOUT (connection attempt timed out), ENETUNREACH (network is unreachable), and EADDRINUSE (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 like send and recv.

  8. Challenge

    Step 7: Closing the Socket

    Understanding the close Function

    When an instance of the Socket class is disposed of, its destructor is automatically invoked. This destructor is responsible for calling the Socket::closeSocket() method. This method should utilize the close 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 the socket function for standard sockets, or by the accept function for a client connection.

    The function returns:

    • 0: On successful completion, indicating that the file descriptor has been closed.
    • -1: On failure, setting errno to indicate the error. Common error codes include EBADF (when fd 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.

  9. 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:

    1. Compilation: Clicking Run will compile the files MainServer.cpp, TCPEchoServer.cpp, and Socket.cpp into an executable named Server. Similarly, MainClient.cpp, TCPEchoClient.cpp, and Socket.cpp will be compiled into another executable named Client.

    2. 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 in MainServer.cpp. You can stop the program by pressing Ctrl+C.

    3. 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 from 1 to 3. 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:

    1. 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.

    2. 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.

    3. Custom Exception Handling: Design a SocketException class to replace the generic std::runtime_error. This tailored exception can provide more specific error handling, improving the robustness and maintainability of your program.

    4. 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++.

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

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.