Search code examples
csocketsforkposix-select

Why to use select() with fork()?


I came across a program (code given below) which implements select() in a scenario where we needed to combine a concurrent TCP echo server and an iterative UDP server into a single server; inorder to multiplex both the TCP and UDP clients.

I am still unable to understand the purpose of using the fork() alongwith select()? And is there any other way to implement the same code only with select().

Edit- From the comments, I understood that fork() DOESN'T have anything to do with IO. And the use of Non-Blocking IO doesn't mean necessarily to use or not to use fork() or select(). But I still can't wrap my head around the use of fork() in the given code.

Code-

// Server program
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define PORT 5000
#define MAXLINE 1024
int max(int x, int y)
{
    if (x > y)
        return x;
    else
        return y;
}
int main()
{
    int listenfd, connfd, udpfd, nready, maxfdp1;
    char buffer[MAXLINE];
    pid_t childpid;
    fd_set rset;
    ssize_t n;
    socklen_t len;
    const int on = 1;
    struct sockaddr_in cliaddr, servaddr;
    char* message = "Hello Client";
    void sig_chld(int);

    /* create listening TCP socket */
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    // binding server addr structure to listenfd
    bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
    listen(listenfd, 10);

    /* create UDP socket */
    udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    // binding server addr structure to udp sockfd
    bind(udpfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

    // clear the descriptor set
    FD_ZERO(&rset);

    // get maxfd
    maxfdp1 = max(listenfd, udpfd) + 1;
    for (;;) {

        // set listenfd and udpfd in readset
        FD_SET(listenfd, &rset);
        FD_SET(udpfd, &rset);

        // select the ready descriptor
        nready = select(maxfdp1, &rset, NULL, NULL, NULL);

        // if tcp socket is readable then handle
        // it by accepting the connection
        if (FD_ISSET(listenfd, &rset)) {
            len = sizeof(cliaddr);
            connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &len);
            if ((childpid = fork()) == 0) {
                close(listenfd);
                bzero(buffer, sizeof(buffer));
                printf("Message From TCP client: ");
                read(connfd, buffer, sizeof(buffer));
                puts(buffer);
                write(connfd, (const char*)message, sizeof(buffer));
                close(connfd);
                exit(0);
            }
            close(connfd);
        }
        // if udp socket is readable receive the message.
        if (FD_ISSET(udpfd, &rset)) {
            len = sizeof(cliaddr);
            bzero(buffer, sizeof(buffer));
            printf("\nMessage from UDP client: ");
            n = recvfrom(udpfd, buffer, sizeof(buffer), 0,
                        (struct sockaddr*)&cliaddr, &len);
            puts(buffer);
            sendto(udpfd, (const char*)message, sizeof(buffer), 0,
                (struct sockaddr*)&cliaddr, sizeof(cliaddr));
        }
    }
}

Solution

  • I am still unable to understand the purpose of using the fork() alongwith select()?

    select is only used here to manage the TCP listening socket and the UDP socket together. It is assumed here that

    • if the TCP listening socket is readable then a new client can be accepted, i.e. the accept will not block. It is also (wrongly) assumed that accept might not fail and will always return a new file descriptor.
    • if the UDP socket is readable then a message can be read from the socket, i.e. the recvfrom will not block
    • that one can write a message to the UDP socket w/o checking that the socket is writable, i.e. the sendto will not block

    The handling of the accepted TCP client is done within a child process in order to keep the code simpler - that's why the fork.

    And is there any other way to implement the same code only with select().

    Yes, but it is more complex. If done properly it has to deal with the case of multiple established client connections at the same time, with TCP possibly blocking on write etc.