source
and sink
are nonblocking descriptors bound to the data source and recipientbuffer
is a reasonably sized character array that helps shovel bytes lifted from source via read
calls over to the sink
via write
calls.numBytesAvailable
stores the number of meaningful characters in buffer
.numBytesSent
tracks the portion of buffer
that's been pushed to the recipient.isSending
tracks whether all data has been pulled from source
and pushed to sink
.class OutboundFile {
public:
OutboundFile();
void initialize(const std::string& source, int sink);
bool sendMoreData();
private:
int source, sink;
static const size_t kBufferSize = 128;
char buffer[kBufferSize];
size_t numBytesAvailable, numBytesSent;
bool isSending;
// private helper methods discussed later
};
source
is a nonblocking file descriptor bound to some local fileO_RDONLY
), and the descriptor is configured to be nonblocking (O_NONBLOCK
) right from the start.read
calls.sink
is explicitly converted to be nonblocking, since it might be blocking, and sink
will very often be a socket descriptor that really should be nonblocking.OutboundFile::OutboundFile() : isSending(false) {}
void OutboundFile::initialize(const string& source, int sink) {
this->source = open(source.c_str(), O_RDONLY | O_NONBLOCK);
this->sink = sink;
setAsNonBlocking(this->sink);
numBytesAvailable = numBytesSent = 0;
isSending = true;
}
source
and written to sink
, and if so, it returns true
unless it further confirms all of data written to sink
has arrived at final destination, in which case it returns false
to state that syndication is complete.dataReadyToBeSent
checks to see if buffer
houses data yet to be pushed out. If not, then it attempts to readMoreData
. If after reading more data the buffer is still empty—that is, a single call to read
resulted in a -1/EWOULDBLOCK
pair, then we return true
as a statement that there's no data to be written, no need to try, but come back later to see if that changes.writeMoreData
is an opportunity to push data out to sink.bool OutboundFile::sendMoreData() {
if (!isSending) return !allDataFlushed();
if (!dataReadyToBeSent()) {
readMoreData();
if (!dataReadyToBeSent()) return true;
}
writeMoreData();
return true;
}
expensive-server.cc
efficient-server.cc
expensive-server.cc
"Illustrates how nonblocking IO can be used to implement a single-threaded web server. This particular example wastes a huge amount of CPU time (as it while loops forever without blocking), but it does demonstrate how nonblocking IO can be used to very easily serve multiple client requests at a time without threads or multiple processes."expensive-server.cc
can be found here.expensive-server.cc
accept
:static const unsigned short kDefaultPort = 12345;
static const string kFileToServe("expensive-server.cc.html");
int main(int argc, char **argv) {
int serverSocket = createServerSocket(kDefaultPort);
if (serverSocket == kServerSocketFailure) {
cerr << "Could not start server. Port " << kDefaultPort << " is probably in use." << endl;
return 0;
}
setAsNonBlocking(serverSocket);
cout << "Static file server listening on port " << kDefaultPort << "." << endl;
list<OutboundFile> outboundFiles;
size_t numConnections = 0;
size_t numActiveConnections = 0;
size_t numAcceptCalls = 0;
while (true) {
// right here!
int clientSocket = accept(serverSocket, NULL, NULL);
cout << "Num calls to accept: " << ++numAcceptCalls << "." << endl;
if (clientSocket == -1) {
assert(errno == EWOULDBLOCK);
} else {
OutboundFile obf;
obf.initialize(kFileToServe, clientSocket);
outboundFiles.push_back(obf);
cout << "Connection #" << ++numConnections << endl;
cout << "Queue size: " << ++numActiveConnections << endl;
}
expensive-server.cc
auto iter = outboundFiles.begin();
while (iter != outboundFiles.end()) {
if (iter->sendMoreData()) {
++iter;
} else {
iter = outboundFiles.erase(iter);
cout << "Queue size: " << --numActiveConnections << endl;
}
}
}
}
expensive-server.cc
$ cat oneSecond.sh
#!/bin/bash
./expensive-server &
pid=$!
sleep 1
kill $pid
$ ./oneSecond
Static file server listening on port 12345.
Num calls to accept: 1.
Num calls to accept: 2.
Num calls to accept: 3.
Num calls to accept: 4.
Num calls to accept: 5.
Num calls to accept: 6.
... lots of lines removed
Num calls to accept: 196716.
Num calls to accept: 196717.
Num calls to accept: 196718.
Num calls to accept: 196719.
Num calls to accept: 196720.
Num calls to accept: 196721.
$
expensive-server.cc
epoll
Family of System Callsint epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll
Family of System Callsstruct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
static const unsigned short kDefaultPort = 33333;
int main(int argc, char **argv) {
int server = createServerSocket(kDefaultPort);
if (server == kServerSocketFailure) {
cerr << "Failed to start server. Port " << kDefaultPort << " is probably already in use." << endl;
return 1;
}
cout << "Server listening on port " << kDefaultPort << endl;
runServer(server);
return 0;
}
efficient-server.cc
static void runServer(int server) {
setAsNonBlocking(server);
int ws = buildInitialWatchSet(server);
efficient-server.cc
static const int kMaxEvents = 64;
static int buildInitialWatchSet(int server) {
int ws = epoll_create1(0);
struct epoll_event info = {.events = EPOLLIN | EPOLLET, .data = {.fd = server}};
epoll_ctl(ws, EPOLL_CTL_ADD, server, &info);
return ws;
}
efficient-server.cc
struct epoll_event events[kMaxEvents];
while (true) {
int numEvents = epoll_wait(ws, events, kMaxEvents, /* timeout = */ -1);
efficient-server.cc
for (int i = 0; i < numEvents; i++) {
if (events[i].data.fd == server) {
acceptNewConnections(ws, server);
}
} else if (events[i].events & EPOLLIN) { // we're still reading the client's request
consumeAvailableData(ws, events[i].data.fd);
}
} else { // events[i].events & EPOLLOUT
publishResponse(events[i].data.fd);
}
}
}
}
efficient-server.cc
static void acceptNewConnections(int ws, int server) {
while (true) {
int clientSocket = accept4(server, NULL, NULL, SOCK_NONBLOCK);
if (clientSocket == -1) return;
struct epoll_event info = {.events = EPOLLIN | EPOLLET, .data = {.fd = clientSocket}};
epoll_ctl(ws, EPOLL_CTL_ADD, clientSocket, &info);
}
}
efficient-server.cc
static const size_t kBufferSize = 1;
static const string kRequestHeaderEnding("\r\n\r\n");
static void consumeAvailableData(int ws, int client) {
static map<int, string> requests; // tracks what's been read in thus far over each client socket
size_t pos = string::npos;
while (pos == string::npos) {
char buffer[kBufferSize];
ssize_t count = read(client, buffer, kBufferSize);
if (count == -1 && errno == EWOULDBLOCK) return; // not done reading everything yet, so return
if (count <= 0) { close(client); break; } // passes? then bail on connection, as it's borked
requests[client] += string(buffer, buffer + count);
pos = requests[client].find(kRequestHeaderEnding);
if (pos == string::npos) continue;
efficient-server.cc
cout << "Num Active Connections: " << requests.size() << endl;
cout << requests[client].substr(0, pos + kRequestHeaderEnding.size()) << flush;
struct epoll_event info = {.events = EPOLLOUT | EPOLLET, .data = {.fd = client}};
epoll_ctl(ws, EPOLL_CTL_MOD, client, &info); // MOD == modify existing event
}
requests.erase(client);
}
efficient-server.cc
static const string kResponseString("HTTP/1.1 200 OK\r\n\r\n"
"<b>Thank you for your request! We're working on it! No, really!</b><br/>"
"<br/><img src=\"http://vignette3.wikia.nocookie.net/p__/images/e/e0/"
"Agnes_Unicorn.png/revision/latest?cb=20160221214120&path-prefix=protagonist\"/>");
static void publishResponse(int client) {
static map<int, size_t> responses;
responses[client]; // insert a 0 if key isn't present
while (responses[client] < kResponseString.size()) {
ssize_t count = write(client, kResponseString.c_str() + responses[client],
kResponseString.size() - responses[client]);
if (count == -1 && errno == EAGAIN) return;
if (count == -1) break;
assert(count > 0);
responses[client] += count;
}
responses.erase(client);
close(client);
}