Connecting the Dots: A Step-by-Step Guide to Socket Programming

by | Apr 7, 2023 | code fussion

Let’s say Alice and Bob are computer science students at a university. They are working on a project together, but they are in different parts of the campus and need to communicate with each other over the network. They decide to create a simple chat program using socket programming.

Alice writes a program that creates a socket and waits for incoming connections. Bob writes a program that connects to Alice’s socket. Once they are connected, they can send messages back and forth using the socket.

Alice’s program displays a prompt on her screen, asking her to enter a message. When she types a message and hits “enter,” the message is sent through her socket to Bob’s program. Bob’s program receives the message and displays it on his screen. Bob can then type a message and send it back to Alice.

In this way, Alice and Bob can communicate with each other in real time, even though they are in different locations on the network. They could use this chat program to collaborate on their project, share code snippets, or just chat about their day.

This is a simple example of how socket programming can be used to enable communication between programs over a network.

Socket programming is a method of enabling communication between computer programs over a network. It involves using a set of programming functions and structures to create and manage “sockets”, which are software objects that represent the endpoints of a communication channel.

WhatsApp uses socket programming to enable real-time messaging and communication between its users. When a user sends a message in WhatsApp, the message is sent from their device to WhatsApp’s servers using sockets. The servers then forward the message to the recipient’s device using sockets as well.

Now, Let’s get technical

A socket is identified by a 5-tuple, which includes the source IP address, source port number, destination IP address, destination port number, and the transport protocol (e.g. TCP or UDP) being used. This 5-tuple provides a unique identification for the socket, allowing data to be sent and received between the two endpoints.

In socket programming, there are two types of sockets:

  1. Stream Sockets (TCP Sockets): These sockets provide reliable, connection-oriented data transfer between two endpoints. They guarantee that data is delivered in the order it was sent and without errors or duplicates. Stream sockets are often used for applications that require high levels of reliability, such as web browsers, email clients, and file transfer protocols.
  2. Datagram Sockets (UDP Sockets): These sockets provide a connectionless, unreliable way of transmitting data between two endpoints. They do not guarantee that data will be delivered, nor do they ensure that it will be delivered in the order it was sent. Datagram sockets are often used for applications that require fast, lightweight communication, such as online gaming, audio and video streaming, and real-time chat applications.
The architecture of Socket programming

How Sockets work

Create a socket

To create a socket, the socket() system call is used, which returns a socket descriptor (an integer value) that is used to refer to the socket in subsequent socket operations.

int sockid = int socket(int family, int type, int protocol);
  • sockid: socket descriptor, an integer (like a file-handle)
  • Family: integer, communication domain, e.g.,
    • PF_INET for IPV4
    • PF_UNIX, Local communication, File addresses
  • type: communication type
    • SOCK_STREAM – reliable, 2-way, connection-based service
    • SOCK_DGRAM – unreliable, connectionless, messages of maximum length
  • protocol: specifies protocol – usually set to 0 (i.e., use default protocol)
    • IPPROTO_TCP
    • IPPROTO_UDP

Bind Connection

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockid: integer, socket descriptor
  • *addr: struct sockid, the (IP) address and port of the machine.
    • For TCP/IP server, the internet address is usually set to INADDR_ANY, i.e., chooses any incoming interface
  • size: the size (in bytes) of the *addr structure

Specifying Addresses

Socket API defines a generic data type for addresses

struct sockaddr
{
unsigned short sa_family;             /* Address family (e.g. AF_INET) */
char sa_data[14];            /* Family-specific address information */
}

Particular form of the sockaddr used for TCP/IP addresses:

struct sockaddr_in
{
unsigned short sin_family;     /* Internet protocol (AF_INET) */
unsigned short sin_port;        /* Address port (16 bits) */
struct in_addr sin_addr;        /* Internet address (32 bits) */
char sin_zero[8];       /* Not used */
}

bind()- Example with TCP

int sockid;
struct sockaddr_in addrport;
sockid = socket(PF_INET, SOCK_STREAM, 0);
addrport.sin_family = AF_INET;
addrport.sin_port = htons(5100);
addrport.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockid, (struct sockaddr *) &addrport, sizeof(addrport))!= -1) {
…}

Establish Connection: connect()

int status = connect(sockid, &foreignAddr, addrlen);
  • sockid: integer, socket to be used in connection
  • foreignAddr: struct sockaddr: address of the passive participant
  • addrlen: integer, sizeof(name)
  • status: 0 if successful connect, -1 otherwise

Incoming Connection: accept()

int s = accept(sockid, &clientAddr, &addrLen);
  • s: integer, the new socket (used for data-transfer)
  • sockid: integer, the orig. socket (being listened on)
  • clientAddr: struct sockaddr, address of the active participant
    • filled in upon return
  • addrLen: sizeof(clientAddr): value/result parameter
    • adjusted upon return
    • must be set appropriately before call

Exchanging data with a stream socket

int count = send(sockid, msg, msgLen, flags);
  • msg: const void[], message to be transmitted
  • msgLen: integer, length of message (in bytes) to transmit
  • flags: integer, special options, usually just 0
  • count: # bytes transmitted (-1 if error)
int count = recv(sockid, recvBuf, bufLen, flags);
  • recvBuf: void[], stores received bytes
  • bufLen: # bytes received
  • flags: integer, special options, usually just 0
  • count: # bytes received (-1 if error)

Putting everything together

/* server side: create a file called server.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 15000

int main() {
    int create_socket, new_socket, addrlen, bufsize = 1024;
    char buffer[bufsize];
    struct sockaddr_in address;

    // Create socket
    if ((create_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket created\n");

    // Bind socket to port
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(create_socket, (struct sockaddr *)&address, sizeof(address)) == -1) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Binding socket to port %d\n", PORT);

    // Listen for incoming connections
    if (listen(create_socket, 3) == -1) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }
    printf("Listening for connections...\n");

    // Accept incoming connections
    addrlen = sizeof(address);
    if ((new_socket = accept(create_socket, (struct sockaddr *)&address, (socklen_t*)&addrlen)) == -1) {
        perror("Accept failed");
        exit(EXIT_FAILURE);
    }
    printf("Client %s:%d connected\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));

    // Receive and send messages
    do {
        memset(buffer, 0, bufsize);
        if (recv(new_socket, buffer, bufsize, 0) < 0) {
            perror("Receive failed");
            exit(EXIT_FAILURE);
        }
        printf("Message received: %s", buffer);
        if (strcmp(buffer, "/bye\n") == 0) {
            printf("Quitting...\n");
            break;
        }
        printf("Message to send: ");
        fgets(buffer, bufsize, stdin);
        send(new_socket, buffer, bufsize, 0);
    } while (1);

    // Close sockets
    close(new_socket);
    close(create_socket);

    return 0;
}


/* 
	to compile server side : gcc -g server.c -o server
	to run server side:	    ./server
*/

/* client side: create a file called client.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 15000

int main(int argc, char const *argv[]) {
    int sock = 0, valread;
    struct sockaddr_in serv_addr;
    char buffer[1024] = {0};
    char message[1024];

    // Create socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    // Set server address and port
    memset(&serv_addr, '0', sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // Convert IPv4 and IPv6 addresses from text to binary form
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }

    // Connect to server
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }

    // Communicate with server
    do {
        printf("Enter a message to send (type '/bye' to quit): ");
        fgets(message, sizeof(message), stdin);

        if (strcmp(message, "/bye\n") == 0) {
	    send(sock, "Client has left, GoodBye!!!", strlen("Client has left, GoodBye!!!"), 0);
            printf("Quitting...\n");
            break;
        } else {
            send(sock, message, strlen(message), 0);
            valread = recv(sock, buffer, sizeof(buffer), 0);
            printf("Server response: %s", buffer);
            memset(buffer, 0, sizeof(buffer));
        }
    } while (1);

    close(sock);
    return 0;
}


/* 
	to compile: gcc -g client.c -o client 
	to run client side: ./client
*/

Conclusion

Thank you for using our step-by-step guide for socket programming. We hope it has been helpful in understanding the basics of socket programming and how to implement a simple client-server application using sockets in C. Socket programming is an important aspect of network programming and can be used to create a wide range of network applications. Keep practicing and exploring new ideas in socket programming to become proficient in this field.

0 Comments

Submit a Comment

Written by Lee N

Lee N is a Certified System Architect, Certified Cloud Engineer, Certified Oracle Database Programmer, and RedHat Administrator II Expert with 5 years of experience in designing, developing, and monitoring web-based systems.

We are Experts in Data Modeling & Intelligent Systems

We develop fast, secure, & reliable systems to simplify business transactions and model data to discover useful information for business decision making. read more

Related Articles

Sealed Classes and Interfaces

Sealed Classes and Interfaces

Sealed classes and interfaces are most applicable to developers of API libraries in which subclasses and subinterfaces must be strictly controlled.

read more

Stay Up to Date With The Latest Technology Updates

Lenhac Limited

Developing world-class software solutions designed to meet your specialized needs to streamline your business operations.

Join Our Newsletter

Stay updated on Ways to Leverage Your Network for Rapid Business Growth using technology

Follow Us

Let’s get social

×