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 toO_NONBLOCK
- Assume
SOCKET_ERROR
is a macro for -1 - Assume
POLLFD
is a typedef forstruct pollfd
- Assume
RESPONSE_BUFFER_LEN
is 4096 - Assume
errno_is_ok
is a macro to check if errno is set toEWOULDBLOCK
,EAGAIN
orEINTR
- 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 :)
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\$read()
andwrite()
directly on blocking sockets as you will get the same results with less complicated code. \$\endgroup\$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\$