Socket & Socket Programming in Linux OS environment

What is Socket ?

Combination of IP address and port number is called socket. Socket allows communication between two processes on the same or different machine. In a programmer's perspective, a socket looks like a low-level file descriptor.

Client-Server
                         
Generally socket is used in client-server application framework. A server is a process that performs some functions on request from a client. Most of the application-level protocols like FTP, SMTP, and POP3 make use of sockets to establish connection between client and server and then for exchanging data.

Server and Client are two different processes with different jobs.

Client-Server Communication

int socket(int domain, int type, int protocol);

It creates an endpoint for the communication and returns a file descriptor.

Domain/Address Family/Protocol Family

It specifies communication domain which selects protocol family. Here, I have mentioned only two of the protocol family. These are defined in <sys/socket.h>. Socket address depends on the address family.

AF_UNIX - connects inside same machine.

struct sockaddr_un
  {
    sa_family_t sun_family ;
    char sun_path[];
  };

AF_INET - connects with different machine.

struct sockaddr_in
  {
    short int  sin_family ;
    int        sin_port;
    struct in_addr sin_addr;
  };

Define the domain used.
serv_addr.sin_family = AF_INET;

Permit any incoming IP address using INADDR_ANY, but if the IP address is 127.0.0.1, it will accept connections only from the same computer.
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

Declare the port number to be used.
serv_addr.sin_port = htons(5000);

We can check in which port numbers this client or server is listening using "netstat" in your linux machine.

Type/Types of Socket

The type indicates the communication semantics. Here, only four types were mentioned.

Stream Sockets (SOCK_STREAM)

Stream sockets deliver the packets in the same order as they use TCP (Transmission Control Protocol) for data transmission. Delivery in a networked environment is guaranteed. 

Datagram Sockets (SOCK_DGRAM)

It is connection less as it doesn't require an open connection like in stream sockets. They use UDP for data transmission. Delivery in a networked environment is not guaranteed. 

Raw Sockets 

Raw sockets have been mainly used for developing new communication protocols or gaining access to some of the more cryptic facilities of an existing protocol.

Sequenced Packet Sockets

These sockets allow the user to manipulate the Sequence Packet Protocol (SPP) or Internet Datagram Protocol (IDP) headers on a packet or a group of packets, either by writing a prototype header along with whatever data is to be sent, or by specifying a default header to be used with all outgoing data, and allows the user to receive the headers on incoming packets. 

Protocol 

The protocol number to use is specific to the "communication domain" in which the communication takes place.

If the type is SOCK_STREAM, we use read(), write() or send(), recv() system calls for data transferring. If the type is SOCK_DGRAM, we use sendto(), recvfrom() system calls for data transferring.

On success of socket() system call, the file descriptor for the new socket is returned. On error, -1 is returned and error number is set appropriately. 

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

As of now, we have created a socket using socket(), but no address is assigned to it. bind() assigns the local address to the socket specified by file descriptor. It is necessary to assign a local socket before socket (SOCK_STREAM) receiving connections.  Hence, bind() is must in server program. 

Most kernels don't allow to bind to an already allocated port. Client sockets can call bind(), but almost never do because there is not much point. If a socket doesn't call bind() the OS will just choose an ephemeral(lasting for a very short time) port for it, which is fine for clients because they are doing the calling; no one needs to call them. 

On success, it returns 0. On error, -1 is returned and error number is set appropriately.

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

connect() system call is used to create an outgoing TCP connection to establish a connection to another TCP socket. Generally, connection-based protocol sockets may successfully connect only once. Connectionless protocol sockets may use connect() multiple times to change their association. When the client calls connect(), a connection is pushed onto the server-side queue where it sits until the server accepts the connection.

If the connection or binding succeeds, zero is returned. On error, -1 is returned and error number is set appropriately.

int listen(int sockfd, int backlog);

The sockfd is a passive socket, so it used to accept the incoming connection requests on the server side. The backlog argument defines the maximum length to which the queue of pending connections of sockfd may grow. If a connecction request is received when the queue is full, the client may receive an error with ECONNREFUSED.

On success, zero is returned. On error, -1 is returned and error number is set appropriately. 

int accept(int sockfd, struct sockaddr *addr, socklent_t *addrlen);

It is used with connection-based socket types, and it extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket. Calling accept() the kernel will pass back a new socket. It is now separate from the original listening socket and is what your server will use to communicate with its peer(s).

If no pending connections are present on the queue, and the socket is marked as blocking, accept() blocks the caller until a connection is present.

With the blocking I/O, when the client makes a connection request to the server, the socket processing that connection and the corresponding thread that reads from it is blocked until some read data appears. This data is placed in the network buffer until it is all read and ready for processing. Until the operation is complete, the server can do nothing more than wait.


If the socket is marked non-blocking and no pending connections are present on the queue, accept() fails.

Non-blocking

Non-blocking I/O means that the request is immediately queued and the function is returned. The actual I/O is then processed at some later point.

In order to be notified of incoming connections on a socket, you can use select(), poll(), or epoll(). A  readable event will be delivered when a new connection is attempted and you may then call accept() to get a socket for that connection.



A server actually works on multiple sockets: a local one for listening, and remote one for accepted clients for peer communication. A single client cannot open more than 65535 simultaneous connections to a server, but a server can serve 65535 simultaneous connections per client.

Practically, server is limited only by CPU power, memory etc., It has to serve requests, not by the number of TCP connections to the server.

On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket. On error, -1 is returned, error number is set appropriately, and addrlen is left unchanged.



TCP Client-Server Program

Server Program

#include <stdio.h>
#include <sys/socket.h> // AF_INET and SOCK_STREAM
#include <stdlib.h>     // EXIT_SUCCESS, EXIT_FAILURE
#include <netinet/in.h> // INADDR_ANY
#include <string.h>     // bzero
#define PORT 8080

int main(int argc, char const *argv[])
{
    int server_fd, new_socket, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    char *hello = "Hello from server";

    // Creating socket file descriptor
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Forcefully attaching socket to the port 8080
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
                   &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // Forcefully attaching socket to the port 8080
    if (bind(server_fd, (struct sockaddr *)&address,
             sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
                             (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    valread = read( new_socket , buffer, 1024);
    printf("%s\n",buffer );
    //send(new_socket , hello , strlen(hello) , 0 );
    //printf("Hello message sent\n");
    return 0;
}

Output:
1
Hello from client

Client Program

// Client side C/C++ program to demonstrate Socket programming
#include <stdio.h>
#include <sys/socket.h> //AF_INET and SOCK_STREAM
#include <arpa/inet.h>  //storage size of serv_addr
#include <unistd.h>     //getpid, getppid, fork
#include <string.h>     //bzero
#define PORT 8080

int main(int argc, char const *argv[])
{
    int sock = 0, valread;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[1024] = {0};
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    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;
    }

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    send(sock , hello , strlen(hello) , 0 );
    printf("Hello message sent\n");
    //valread = read( sock , buffer, 1024);
    //printf("%s\n",buffer );
    return 0;
}

Output:
1
Hello message sent

UDP Socket Programming

Server Program

// Server side implementation of UDP client-server model
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT    8080
#define MAXLINE 1024

// Driver code
int main() {
    int sockfd;
    char buffer[MAXLINE];
    char *hello = "Hello from server";
    struct sockaddr_in servaddr, cliaddr;

    // Creating socket file descriptor
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // Filling server information
    servaddr.sin_family    = AF_INET; // IPv4
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // Bind the socket with the server address
    if (bind(sockfd, (const struct sockaddr *)&servaddr,
             sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    int len, n;
    n = recvfrom(sockfd, (char *)buffer, MAXLINE,
                 MSG_WAITALL, (struct sockaddr *) &cliaddr,
                 &len);
    buffer[n] = '\0';
    printf("Client : %s\n", buffer);
    sendto(sockfd, (const char *)hello, strlen(hello),
           MSG_CONFIRM, (const struct sockaddr *) &cliaddr,
           len);
    printf("Hello message sent.\n");

    return 0;
}


1
2
Client : Hello from client
Hello from server

Client Program

// Client side implementation of UDP client-server model
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT     8080
#define MAXLINE 1024

// Driver code
int main() {
    int sockfd;
    char buffer[MAXLINE];
    char *hello = "Hello from client";
    struct sockaddr_in     servaddr;

    // Creating socket file descriptor
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    // Filling server information
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = INADDR_ANY;

    int n, len;

    sendto(sockfd, (const char *)hello, strlen(hello),
           MSG_CONFIRM, (const struct sockaddr *) &servaddr,
           sizeof(servaddr));
    printf("Hello message sent.\n");

    n = recvfrom(sockfd, (char *)buffer, MAXLINE,
                 MSG_WAITALL, (struct sockaddr *) &servaddr,
                 &len);
    buffer[n] = '\0';
    printf("Server : %s\n", buffer);

    close(sockfd);
    return 0;
}

1
2
Hello message sent
Server : Hello from server

Interview Questions


1. Can we connect a socket to a broadcast address?
    Yes if  the broadcast flag is enabled.

2. What is blocking socket?
    a) When accept() blocks the caller until a connection is present. 
    b) If no messages space is available at the socket to hold the message to be transmitted, then send() normally blocks.
    c) If no messages are available at the socket, the recv call waits for a message to arrive. 


When writing a server, we need to be ready to react to many kinds of event which could happen next: a new connection is made, or a client sends us a request, or a client drops its connection. If we make a call to, say, accept, and the call blocks, then we lose our ability to respond to other events.
The traditional answer to this problem is the select system call. We call select indicating various blocking calls we’re interested in. Select then blocks until one or more of those blocking calls is ready, meaning that calling it will not block.
https://www.scottklement.com/rpg/socktut/nonblocking.html

3. How many sockets can listen on one port?
    Only one socket can listen to one port. On server side, a connected socket is assigned to a new (dedicated) port or can use same port. The only actual constraint that the TCP stack must satisfy is that the tuple must be unique for each socket connection. Thus the server can have many TCP sockets using the same local port, as long as each of the sockets on the port is connected to a different remote location.

Multiple Sockets on server side


On a server, a process in listening on a port. Once it gets a connection, it hands it off to another thread. The communication never hogs the listening port.

Connections are uniquely identified by the OS by the following TCP 4-tuple: (local-IP, local-port, remote-IP, remote-port). If any element in the tuple is different, then this is a completely independent connection.

When a client connects to a server, it picks a random, unused high-order source port. This way, a single client can have up to ~64k connections to the server for the same destination port. 

Concurrent servers : It handles multiple clients simultaneously, creates a new socket in a separate process to handle each new connection requested. 

4. How many number of ports available on the system?
    It is around 60k (16bit)

5. When to use UDP socket or DGRAM?

When we don't want reliable data transmission and want to consume less bandwidth, UDP socket will be used. Eg: Video streaming, Internet Radio. 

In case of TCP socket, the data will be transmitted by validating the checksum at both ends, so it consumes more memory. It is also reliable because after the transmission it is acknowledged. Since it carries checksum, it would require more bandwidth. 




Source : Stackoverflow & GeeksforGeeks

Comments

Post a Comment