1
\$\begingroup\$

I'm working with TCP sockets in C, specifically only for client side HTTP(S) requests, and would like to get some feedback on my send and recv code.

You can make some assumptions regarding my code, as it is, by no means a complete example but I'll try to make it reproducible.

  • Assume sfd has been set to O_NONBLOCK
  • Assume SOCKET_ERROR is a macro for -1
  • Assume POLLFD is a typedef for struct pollfd
  • Assume RESPONSE_BUFFER_LEN is 4096
  • Assume errno_is_ok is a macro to check if errno is set to EWOULDBLOCK, EAGAIN or EINTR - these errnos are ignored
  • Assume extend_resbuff (used in the recv code) is a function that extends resbuff by multiplying its current len with 2. It takes care of alloc failures by itself
  • Assume trim_resbuff (used in the recv code) is a function that trims the resbuff to the exact size it needs to be and null terminates it
  • The message sent using my sender function will always contain Connection: close as a header.

My send code, assume a connect call has been made. Also assume connect has returned - or rather, set errno to - EINPROGRESS.

/*
Send given message through given socket
Sends the message in its entirety

Returns true upon success, false upon failure
*/
bool send_all(socket_t sfd, char const* restrict msg, ssize_t msglen)
{
    ssize_t sent = 0;
    ssize_t stat = 0;
    do
    {
        /* Poll for readying the send */
        POLLFD pfds[1] = { { .fd = sfd, .events = POLLOUT } };
        if (poll(pfds, sizeof(pfds) / sizeof(pfds[0]), POLL_TIMEOUT) == 0)
        {
            /* Timeout */
            return false;
        }
        if (pfds[0].revents & POLLOUT)
        {
            /* Ready to write */
            stat = send(sfd, msg + sent, msglen - sent, 0);
            sent += stat;
        }
        else
        {
            /*
            Is it safe to assume an errno is set in this branch?
            The caller is then expected to check the errno

            If this branch is hit, is recovery possible (within the scope
            of this function)?
            */
            return false;
        }
        /*
        This loop exits either when
          * full message is sent
          * stat is SOCKET_ERROR but errno **is not** EAGAIN or EWOULDBLOCK or EINTR
        */
    } while (sent < msglen && (stat != SOCKET_ERROR || errno_is_ok));
    return stat != SOCKET_ERROR;
}

Also of note- the msg is always an HTTP request. Something like GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n. That Connection: close is always present in the headers.

Now, the recv code.

/*
Receive response through given socket
Receives the message in its entirety and stores it into resbuff
resbuff does not need to be allocated - this function manages the allocation

Returns true upon success, false upon failure
*/
bool recv_all(socket_t sfd, char** restrict resbuff, size_t* restrict len)
{
    ssize_t stat = 0;
    size_t idx = 0;                         /* Latest initialized element index of *resbuff */
    *len = RESPONSE_BUFFER_LEN;             /* Length of *resbuff (initially) */
    /* Prepare the resbuff */
    *resbuff = malloc(*len * sizeof(**resbuff));
    if (*resbuff == NULL)
    {
        /* malloc failed */
        return false;
    }
    /* Poll for readying the recv */
    POLLFD pfds[1] = { { .fd = sfd, .events = POLLIN } };
    /* Read responses and append to resbuff until connection is closed */
    do
    {
        if (poll(pfds, sizeof(pfds) / sizeof(pfds[0]), POLL_TIMEOUT) == 0)
        {
            /* Timeout */
            return false;
        }
        /* Extend the buffer if at limit */
        if (idx == *len && !extend_resbuff(resbuff, len))
        {
            /* extend_resbuff failed */
            return false;
        }
        if (pfds[0].revents & POLLIN)
        {
            /* Ready to read */
            stat = recv(sfd, *resbuff + idx, *len - idx, 0);
            idx += (size_t)stat;
        }
        else if (pfds[0].revents & POLLHUP)
        {
            /* Connection closed on remote side - response is most likely all read */
            /*
            I have noticed linux does not reach this even when response is over
            recv, just keeps executing and it keeps returning 0
            which is why the loop exits when recv is 0

            However, on windows (WSAPoll instead of poll) - this branch is triggered
            */
            break;
        }
        else
        {
            /*
            Is it safe to assume an errno is set in this branch?
            The caller is then expected to check the errno

            If this branch is hit, is recovery possible (within the scope
            of this function)?
            */
            return false;
        }
        /*
        This loop exits either when
          * Full response is received and connection is closed (stat is 0)
          * stat is SOCKET_ERROR but errno **is not** EAGAIN or EWOULDBLOCK or EINTR
        */
    } while (stat > 0 && (stat != SOCKET_ERROR || errno_is_ok));
    /*
    Trim resbuff to exactly the size it needs to be (only if stat is not -1)
    the following returns true only if everything succeeds

    (trim_resbuff will not be called if stat is SOCKET_ERROR  in the first place)
    */
    return stat != SOCKET_ERROR && trim_resbuff(resbuff, idx, len);
}

My primary doubts can be seen in form of comments in my code. Also, not necessarily regarding the code in question but are there any socket options I should change that may make these operations more efficient? Options such as TCP_NODELAY, TCP_QUICKACK, SO_RCVBUF, and SO_SNDBUF. Are the default values for these options good enough?

Note: Performance, even microseconds (not milli), is crucial for this specific implementation. Although that does not mean implementing epoll (for linux) and/or an async event loop. I just want the best performance possible using poll and non blocking sockets :)

\$\endgroup\$
7
  • \$\begingroup\$ Is this example code or taken from a real project? Please take a look at the help center. \$\endgroup\$
    – Mast
    Commented Oct 1, 2020 at 11:43
  • 1
    \$\begingroup\$ You are not using poll() correctly. You use poll to listen to many sockets (if there is no data available on the socket you do other work). I would expect a single thread to be running a poll in a loop. Any sockets that have work generate a work item that is handled by a separate thread (or more generally an active thread pool). \$\endgroup\$ Commented Oct 1, 2020 at 17:55
  • 1
    \$\begingroup\$ The way you are using i totally wastes the non blocking nature of the socket as you ae blocking on the poll call instead. You may as well simply use read() and write() directly on blocking sockets as you will get the same results with less complicated code. \$\endgroup\$ Commented Oct 1, 2020 at 17:56
  • 1
    \$\begingroup\$ You have swapped busy looping on send/recv for busy looping on poll (though you just give instead of actually looping). \$\endgroup\$ Commented Oct 1, 2020 at 18:22
  • 1
    \$\begingroup\$ Should I run the poll on a separate thread: This is a an easy design to do, but you could do it without the thread (ie you set up a lot of reads/writes and then poll until all are done). callback passed in to call once the function finishes: That again is a good simple way to do it (though you may want to be able to partially processes the data as it comes along rather than waiting for everything, ie.. web browser can often start displaying some of the page before the full thing is downloaded). \$\endgroup\$ Commented Oct 1, 2020 at 18:22

1 Answer 1

2
\$\begingroup\$

Overview

I don' think the way you are using poll() is affective. You are basically moving the busy loop from send()/recv() to the poll() function but then giving up when there is a timeout.

If your socket is on loopback that may work great but anything coming across the internet is going to potentially have long waits at some point thus causing your reads to be abandoned and never resumed.

how I would structure it:

  void pollLoop()
  {
       bool finished = false;
       do {    
           int count = poll(/* Very short sleep or use signal to force dropout*/);
           if (count < 0) {
               handleError();
           }
           for(int loop = 0;loop < count; ++loop) {
               handleSocket(loop);
           }
           getNewSocketsThatHaveBeenAdded();
       }
       while(!finished);
   }

   void addSocket(int socket, int type /*read or write */, callback, callbackdata)
   {
        lockGlobalMutexForSocket();
        AddInfoToSo_getNewSocketsThatHaveBeenAdded_PicksItUp();
        unlockGlobalMutex();
        // Optionally create a signal so poll() drops out of sleep
   }

   void getNewSocketsThatHaveBeenAdded()
   {
        lockGlobalMutexForSocket();
        // Add data stored by addSocket to data structure used by poll
        // This may be basically a null op.
        // As long as there is no reallocation the above function can
        // simply append socket information this function will result
        // in the size of the structured used by poll() being larger
        // i.e. parameter 2 in poll() `nfds` increases.
        unlockGlobalMutex();
   }

   void handleSocket(loop)
   {
        // Important.
        // Set the appropriate fd to negative in the poll structure
        // so that poll does not report on this socket while you
        // are handling it.
        fd[loop].fd = -fd[loop].fd; // You flip it back when you are done.

        if (fd[loop].dataAvailable) {
             AddToThreadPool(readOrWriteDataAsAppropriate, loop);
        }
        else /* No data available we have reached the end */
             AddToThreadPool(callSocketCallBackWithData, loop);
        }
 }
 

This the basic for most servers (though I would use libevent personally rather than poll() or ppoll()). With this type of structure a handfull of threads can easily handle 10's of thousands of simultaneous connection.

Code Review

Does C support bool? I though that was C++. I though the C version was slightly different?

bool send_all(socket_t sfd, char const* restrict msg, ssize_t msglen)

This must be modern C syntax.
Heard about it not seen it before.

        POLLFD pfds[1] = { { .fd = sfd, .events = POLLOUT } };

You nfds is always 1!

        if (poll(pfds, sizeof(pfds) / sizeof(pfds[0]), POLL_TIMEOUT) == 0)

Basically you will give up if there is any significant delay. But you don't return any information about how far you got. So it is not possible to resume. If you are going to do it this way then this failure should give you the opportunity to resume by including return data about how far you got.

        {
            /* Timeout */
            return false;
        }

You don't check for negative values from poll(). Sometimes there will be an error (or signal) you need to check for these.


You don't check for errors on send(). You need to do that.

            stat = send(sfd, msg + sent, msglen - sent, 0);

Well it better be a OUT signal since you are sending data. But don't you all expect there at some point to be a response on the same socket? With the current implementation you need to complete the send before you start receiving data. What happens if the server on the other end starts to send data before you have finished sending your data? Not all operations require all the data before they can start responding!

        if (pfds[0].revents & POLLOUT)

You should be explicitly checking for an error.

        else
        {
            /*
            Is it safe to assume an errno is set in this branch?
            The caller is then expected to check the errno

            If this branch is hit, is recovery possible (within the scope
            of this function)?
            */
            return false;
        }

This is fine.

    } while (sent < msglen && (stat != SOCKET_ERROR || errno_is_ok));

There are several types of error that are not actually errors and you simply try again().

\$\endgroup\$
3
  • \$\begingroup\$ thanks! One question about "completing the send before receiving the data": Do HTTP servers start sending data before they have actually read the full HTTP request from remote side? \$\endgroup\$
    – Chase
    Commented Oct 2, 2020 at 8:31
  • \$\begingroup\$ @Chase I don't actually know. But I don't see why not. If you know the response you don't need to wait for the full message for a reply. Example form validation. You may fail validation very quickly but the form can contain an arbitrary amount of information. There is no need to wait until you have the whole form before starting a reply with an error response if it is quick. \$\endgroup\$ Commented Oct 2, 2020 at 16:37
  • \$\begingroup\$ Or security validation based on the header information you can quickly send a not authorized after the headers are received without needing to know the body. You still need to read the rest of the body as the connection is going to be reused for the next request. \$\endgroup\$ Commented Oct 2, 2020 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.