Learning Socket Programming in C++

Learn Socket Programming in C++
Learn Socket Programming in C++

Introduction

In an era when the transfer of data is happening every second as we speak, computer networking becomes an important subject of study. It is a subject that every good programmer must be thoroughly familiar with. One important topic under this is socket programming. In this article, we will discuss the topic of socket programming and study various ways of its implementation in C++.

Socket programming in C++ is the way of combining or connecting two nodes with each other over a network so that they can communicate easily without losing any data. One socket (node) listens on a particular port at an IP, while the other socket reaches out to the other to form a connection. The server forms the listener socket while the client reaches out to the server.

What is a Socket?

If we take a real-life example then the socket we see in reality is a medium to connect two devices or systems. It can be either a phone charger plugging into the socket or a USB cable into our laptop. In the same way, Sockets let applications attach to the local network at different ports. Every time a socket is created, the program has to specify the socket type as well as the domain address.


Sockets are a mechanism for exchanging data between processes. These processes can either be on the same machine, or on different machines connected via a network. Once a socket connection is established, data can be sent in both directions until one of the endpoints closes the connection.

I needed to use sockets for a project I was working on, so I developed and refined a few C++ classes to encapsulate the raw socket API calls. Generally, the application requesting the data is called the client and the application servicing the request is called the server. I created two primary classes, ClientSocket and ServerSocket, that the client and server could use to exchange data.

The goal of this article is to teach you how to use the ClientSocket and ServerSocket classes in your own applications.

Procedure in Client-Server Communication:

  • Socket: Create a new communication
  • Bind: Attach a local address to a socket
  • Listen: Announce willingness to accept connections
  • Accept: Block caller until a connection request arrives
  • Connect: Actively attempt to establish a connection
  • Send: Send some data over a connection
  • Receive: Receive some data over a connection
  • Close: Release the connection

State diagram for server and client model:

Stages for Server: Socket Creation

Int socketcr = socket( domain , type, protocol )
Socketcr = It is like a descriptor ,an integer (file handle)
Domain = integer type, communication domain, example = AF_INET6 (IPv6 protocol)
Type = communication type
SOCK_DGRAM: UDP(unreliable, connectionless)
Protocol = Protocol value for Internet Protocol(IP), which is 0. This is the same number which appears on the protocol field in the IP header of a packet. (man protocols for more details)

What’s a connection?

relationship between two machines, where two pieces of software know about each other. Those two pieces of software know how to communicate with each other. In other words, they know how to send bits to each other. A socket connection means the two machines have information about each other, including network location (IP address) and TCP port. (If we can use an analogy, IP address is the phone number and the TCP port is the extension).

A socket is an object similar to a file that allows a program to accept incoming connections, make outgoing connections, and send and receive data. Before two machines can communicate, both must create a socket object. A socket is a resource assigned to the server process. The server creates it using the system call socket(), and it can’t be shared with other processes.

blog banner 1

Setsockopt: This helps in manipulating options for the socket referred by the file descriptor socket. This is completely optional, but it helps in the reuse of address and port.  Prevents error such as: “address already in use”.

Blind: After the creation of the socket, bind function binds the socket to the address and port number specified in addr(custom data structure). In the example code, we bind the server to the localhost, hence we use INADDR_ANY to specify the IP address.

Listen: The listen() function marks a connection-mode socket (for example, those of type SOCK_STREAM), specified by the socket argument s, as accepting connections, and limits the number of outstanding connections in the socket listen to queue to the value specified by the backlog argument. The socket s is put into ‘passive’ mode where incoming connection requests are acknowledged and queued pending acceptance by the process.

The backlog parameter of this function is typically used by servers that could have more than one connection request at a time: if a connection request arrives with the queue full, the client receives an error with an indication of ECONNREFUSED.

listen() attempts to continue to function rationally when there are no available descriptors. It accepts connections until the queue is emptied. If descriptors become available, a later call to listen() or accept() re-fills the queue to the current or most recent backlog’, if possible, and resume listening for incoming connections.

Accept: The accept() system call is used with connection-based socket types(SOCK_STREAM, SOCK_SEQPACKET).  It extracts the first connection request on the queue of pending connections for the listening socke sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.  The newly created socket is not in the listening state. The original socket sockfd is unaffected by this call. The argument sockfd is a socket that has been created with socket(2), bound to a local address with bind(2), and is listening for connections after a listen(2).

Stages For Client:

Socket connection: Exactly same as that of server’s socket creation

Connect: The connect() system call initiates a connection on a socket. If the parameter s (a socket) is of type SOCK_DGRAM, then connect() permanently specifies the peer to which datagrams are to be sent. If s is of type SOCK_STREAM, then connect() attempts to make a connection to another socket. The name parameter specifies the other socket. The connect() function is used to create a connection to the specified foreign association. The parameter s specifies an unconnected datagram or stream socket. If the socket is unbound, the system assigns unique values to the local association, and the socket is marked as bound. For stream sockets (type SOCK_STREAM), an active connection is initiated to the foreign host using the name (an address in the namespace of the socket). When the socket call completes successfully, the socket is ready to send/receive data.

Send/Receive :- The send() and recv() calls specify:

  • The sockets on which to communicate
  • The address in the storage of the buffer that contains, or will contain, the data (addr_of_data, addr_of_buffer)
  • The size of this buffer (len_of_data, len_of_buffer)
  • A flag that tells how the data is to be sent

Steps to establish connection in socket:

The system calls for establishing a connection are somewhat different for the client and the server, but both involve the basic construct of a socket. A socket is one end of an interprocess communication channel. The two processes each establish their own socket.

The steps involved in establishing a socket on the client side are as follows:

  • Create a socket with the socket() system call
  • Connect the socket to the address of the server using the connect() system call
  • Send and receive data. There are a number of ways to do this, but the simplest is to use the read() and write() system calls

The steps involved in establishing a socket on the server side are as follows:

  • Create a socket with the socket() system call
  • Bind the socket to an address using the bind() system call. For a server socket on the Internet, an address consists of a port number on the host machine
  • Listen for connections with the listen() system call
  • Accept a connection with the accept() system call. This call typically blocks until a client connects with the server
  • Send and receive data

Connecting Multiple Clients Without Multithreading

In numerous examples, what we see is how a single client is connected to a socket in a server. However, this is not the case in day to day life. We have multiple clients connected to a server and each has a different socket. 

One way to achieve this feat is by using multithreading. But only someone who has done multithread programming knows it can lead to madness. They are very difficult to code and debug. Even if you end up programming them neatly, the results can be unpredictable. Not to mention the fact that they are not scalable for a large number of clients and there is also a chance of deadlocks occurring.

To tackle these issues, we try to support multiple clients on a network without using multithreading. To help us with this, we have a special function known as select(). 

What is the select() function?

It is a Linux command which uses fd_set data structure and allows us to monitor multiple file descriptors. It gets activated as soon as any file descriptor sends data. Hence it works like an interrupt handler. If some data is there that is to be read on one of the sockets then it is select() that provides that information. It then returns the total number of socket handles that are ready and contained in the fd_set structures.

There are four macros that are associated with the select function, used for manipulating and checking the descriptor sets.

  1. *FD_ZERO(set) – Initializes the set to an empty set. A set should always be cleared before using.
  1. *FD_CLR(s, set) – Removes socket s from set.
  1. *FD_ISSET(s, set) – Check to see if s is a member of set and returns TRUE if so.
  1. *FD_SET(s, set) – Adds a socket s to set.

Using these four macros and the select function, one can handle multiple clients using a single thread.

The Code

Here is an example server side code which echos back the received message. Jump here for the explanation of the code.

#include <stdio.h>
#include <string.h>  
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>   
#include <arpa/inet.h>    
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>

#define TRUE   1
#define PORT 5500

int main(int argc , char *argv[])
{
    int opt = TRUE;
    int master_sock , addrlen , new_sock , client_sock[30] ,
            maximum_clients = 30 , act, i , value_read , sock_descriptor;
    int maximum_socket_descriptor;
    struct sockaddr_in adr{};

    char buff[1025];  //data buffer of 1K
    fd_set readfds; //set of socket file descriptors
    char *message = "ECHO Daemon v1.0 \\r\\n"; //message
    
    for (i = 0; i < maximum_clients; i++) //initialise all client_sock to 0 
    {
        client_sock[i] = 0;
    }
    if( (master_sock = socket(AF_INET , SOCK_STREAM , 0)) == 0) //creating a master socket 
    {
        perror("Failed_Socket");
        exit(EXIT_FAILURE);
    }

    //These are the types of sockets that we have created
    adr.sin_family = AF_INET;
    adr.sin_addr.s_addr = INADDR_ANY;
    adr.sin_port = htons( PORT );
    
    if (bind(master_sock, (struct sockaddr *)&adr, sizeof(adr))<0) //bind the socket to localhost port 5500
    {
        perror("Failed_Bind");
        exit(EXIT_FAILURE);
    }
    printf("Port having listener:  %d \\n", PORT);
    
    if (listen(master_sock, 3) < 0) //Specify 3 as maximum pending connections for master socket
    {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    
    addrlen = sizeof(adr); //Accepting the Incoming Connection 
    puts("Looking For Connections");
    
    //*******************************//
    // Here we start using select functions and the macros for multiple client handling
    
    while(TRUE)
    {
        FD_ZERO(&readfds); //Clearing the socket set
        FD_SET(master_sock, &readfds); //Adding the master socket to the set 
        maximum_socket_descriptor = master_sock;
        
        for ( i = 0 ; i < maximum_clients ; i++) //Adding child sockets to set 
        {
            sock_descriptor = client_sock[i]; //Descriptor for Socket 
            
            if(sock_descriptor > 0) //if the socket descriptor is valid then adding it to the read list 
                FD_SET( sock_descriptor , &readfds);

            if(sock_descriptor > maximum_socket_descriptor) //Highest File Descriptor Number which is needed for the select function 
                maximum_socket_descriptor = sock_descriptor;
        }

        //Waiting for something to happen on the master socket. As the wait time is NULL the wait is indefinite
        act = select( maximum_socket_descriptor + 1 , &readfds , nullptr , nullptr , nullptr);

        if ((act < 0) && (errno!=EINTR))
        {
            printf("Failed_Select");
        }
        if (FD_ISSET(master_sock, &readfds)) //Any activity on the master socket is treated as an incoming connection
        {
            if ((new_sock = accept(master_sock,
                                     (struct sockaddr *)&adr, (socklen_t*)&addrlen))<0)
            {
                perror("Accept!");
                exit(EXIT_FAILURE);
            }

            //Informing the user of the socket number which will be sued to send and receive messages
            printf("This is a New Connection,The socket file descriptor is %d and the IP is : %s on Port : %d\\n"
                   , new_sock , inet_ntoa(adr.sin_addr) , ntohs
                    (adr.sin_port));

            if( send(new_sock, message, strlen(message), 0) != strlen(message)) // Sending Greeting Message on New Connection
            {
                perror("Send!!");
            }
            puts("Welcome Text Sent Affirmative.");

            for (i = 0; i < maximum_clients; i++) // Adding new socket to the array of sockets
            {
                if( client_sock[i] == 0 ) // Checking if the position is empty
                {
                    client_sock[i] = new_sock;
                    printf("Adding new socket to the list of sockets as %d\\n" , i);

                    break;
                }
            }
        }
        for (i = 0; i < maximum_clients; i++) //If not the master socket then it is some i/o activity on some other socket
        {
            sock_descriptor = client_sock[i];
            if (FD_ISSET( sock_descriptor , &readfds))
            {
                //Checking if the activity was for closing and reading the incoming message
                if ((value_read = read( sock_descriptor , buff, 1024)) == 0)
                {
                    //If someone disconnected, getting their details and printing a message
                    getpeername(sock_descriptor , (struct sockaddr*)&adr , \\
                        (socklen_t*)&addrlen);
                    printf("Disconnected Host. Their , IP %s and PORT %d \\n" ,
                           inet_ntoa(adr.sin_addr) , ntohs(adr.sin_port));
                    close( sock_descriptor ); //Closing the socket and marking it as 0 in the list to be reused
                    client_sock[i] = 0;
                }
                else //Echoing back the message that came in the socket
                {
                    buff[value_read] = '\\0'; //Setting the string terminating NULL byte on the end of the data that is read
                    send(sock_descriptor , buff , strlen(buff) , 0 );
                }
            }
        }
    }
    return 0;
}

Explanation of the code:

In the above code we first create an fd_set variable readfds, which monitors all the clients’ active file descriptors as well as the active file descriptors on the main servers listening socket. For an old client sending data, readfds would already be activated and thus we’ll check in the existing list to see which client has sent the data. When a new client connects to the server, master-sock will be activated and a new file descriptor will be open for that particular client. We store this file descriptor in a client_list and add it to the readfds variable in the next iteration to monitor the activity from the client.

Compiling and running the above code would create a server. telnet command can then be used on port 5500 to connect to the server. using multiple devices we can see that we can connect multiple devices to the server without using multithreading at all.

Use of socket programming:

Socket programs are used to communicate between various processes usually running on different systems. It is mostly used to create a client-server environment. This post provides the various functions used to create the server and client program and an example program. In the example, the client program sends a file name to the server and the server sends the contents of the file back to the client. Socket programming usually pertains to the basic communication protocols like TCP/UDP and raw sockets like ICMP. These protocols have a small communication overhead when compared to underlying protocols such as HTTP/DHCP/SMTP etc.

Some of the basic data communications between client and server are:

  • File Transfer: Sends name and gets a file.
  • Web Page: Sends URL and gets a page.
  • Echo: Sends a message and gets it back.

Disadvantages:

  • The C++ can establish communication only with the machine requested and not with any other machine on the network.
  • Sockets allow only raw data to be sent. This means that both client and server need to have mechanisms to interpret the data.

Frequently Asked Questions

What is TCP socket programming?

Socket Programming is used to establish communication between two network nodes.

Is socket programming still used?

Yes, socket programming is still widely used.

What is the best language for socket programming?

Java and C#/C++ are the best languages for socket Programming.

What is a socket HTTP? Why is socket programming used?

Socket programming is used to create endpoints to establish communication between network nodes.

Can socket programming be done in Windows?

Yes, socket programming can be done in Windows using Winsock.

To read more about C++, click here.

Key Takeaways

Computer Networking is an important subject of study for every programmer. Mastery in understanding transfer of data between two or more machines requires thorough knowledge in a number of topics. One such important topic is socket programming. Socket programming in C++ is the way of combining or connecting two nodes with each other over a network so that they can communicate easily without losing any data. This article discusses the topic with its implementation in C++ in detail.

To read more about Networking, check out this article.

By Akhil Sharma