Lecture 19: More Nonblocking I/O
- The outbound-file-test.cc presented earlier can be used to confirm the
OutboundFile
class implementation works as expected.- The nonblocking aspect of the doesn't really buy us anything.
- Only one copy of the source file is being syndicated, so no harm comes from blocking, since there's nothing else to do.
- To see why nonblocking I/O might be useful, consider the following nonblocking server implementation, presented over several slides.
- Our server is a static file server and responds to every single request—no matter how that request is structures—with the same payload. That payload is drawn from an HTML called "expensive-server.cc.html".
- Presented below is the portion of the server implementation that establishes the executable as a server that listens to port 12345 and sets the server socket to be nonblocking.
// expensive-server.html is expensive because it's always using the CPU, even when there's nothing to do
static const unsigned short kDefaultPort = 12345;
static const string kFileToServe("expensive-server.cc.html");
int main(int argc, char **argv) {
int server = createServerSocket(kDefaultPort);
assert(server != kServerSocketFailure);
setAsNonBlocking(server);
cout << "Static file server listening on port " << kDefaultPort << "." << endl;
}
Lecture 19: More Nonblocking I/O
- As with all servers, our static file server loops interminably and aggressively accepts incoming connections as quickly as possible.
- The first part of the while loop calls and immediately returns from
accept
. The return is immediate, because server has been configured to be nonblocking. - The code immediately following the accept call branches in one of two directions.
- If
accept
returns a -1, we verify the -1 isn't something to be concerned about. - If
accept
surfaces a new connection , we create an newOutboundFile
on its behalf and append it to the runningoutboundFiles
list of clients currently being served.
- If
- The first part of the while loop calls and immediately returns from
list<OutboundFile> outboundFiles;
while (true) {
// part 1: below
int client = accept(server, NULL, NULL);
if (client == -1) {
assert(errno == EWOULDBLOCK); // sanitycheck to confirm -1 doesn't represent a true failure
} else {
OutboundFile obf;
obf.initialize(kFileToServe, client);
outboundFiles.push_back(obf);
}
// part 2: presented on next slide
Lecture 19: More Nonblocking I/O
- As with all servers, our static file server loops interminably and aggressively accepts incoming connections as quickly as possible.
- The second part executes whether or not part 1 produced a new client connection and extended the
outboundFiles
list. - It iterates over every single
OutboundFile
in the list and attempts to send some or all available data out to the client.- If sendMoreData returns true, the loop advances on to the next client via
++iter
. - If sendMoreData returns false, the relevant OutboundFile is removed from
outboundFiles
before advancing. (Fortunately,erase
does precisely what we want, and it returns the iterator addressing the nextOutboundFile
in the list.)
- If sendMoreData returns true, the loop advances on to the next client via
- The second part executes whether or not part 1 produced a new client connection and extended the
list<OutboundFile> outboundFiles;
while (true) {
// part 1: presented and discussed on previous slide
// part 2: below
auto iter = outboundFiles.begin();
while (iter != outboundFiles.end()) {
if (iter->sendMoreData()) ++iter;
else iter = outboundFiles.erase(iter);
}
}
}
Lecture 19: More Nonblocking I/O
- The code for
setAsNonblocking
is fairly low-level.- It relies on a function called
fcntl
to do surgery on the relevant file session in the open file table. - That surgery is little more that toggles some 0 bit to a 1, as can be inferred from the last line of the three line implementation.
- It relies on a function called
- The code for
setAsNonblocking
and a few peer functions are presented below.
void setAsNonBlocking(int descriptor) {
fcntl(descriptor, F_SETFL, fcntl(descriptor, F_GETFL) | O_NONBLOCK); // preserve other set flags
}
void setAsBlocking(int descriptor) {
fcntl(descriptor, F_SETFL, fcntl(descriptor, F_GETFL) & ~O_NONBLOCK); // suppress blocking bit, preserve others
}
bool isNonBlocking(int descriptor) {
return !isBlocking(descriptor);
}
bool isBlocking(int descriptor) {
return (fcntl(descriptor, F_GETFL) & O_NONBLOCK) == 0;
}
Lecture 19: More Nonblocking I/O
- We've been using the
OutboundFile
abstraction without understanding how it works behind the scenes.- We really should see the implementation (or at least part of it) so we have some sense how it works and can be implemented using nonblocking techniques.
- The full implementation includes lots of spaghetti code.
- In particular, true file descriptors and socket descriptors need to be treated differently in a few places—in particular, detecting when all data has been flushed out to the sink descriptor (which may be a local file, a console, or a remote client machine) isn't exactly pretty.
- However, my implementation is decomposed well enough that I think many of the methods—the ones that I'll show in lecture, anyway—are easy to follow and provide a clear narrative.
- At the very least, I'll convince you that the
OutboundFile
implementation is accessible to someone just finishing up CS110.
Lecture 19: More Nonblocking I/O
- Here's is the condensed interface file for the OutboundFile class.
-
source
andsink
are nonblocking descriptors bound to the data source and recipientbuffer
is a reasonably sized character array that helps shovel bytes lifted from source viaread
calls over to thesink
viawrite
calls. -
numBytesAvailable
stores the number of meaningful characters inbuffer
. -
numBytesSent
tracks the portion ofbuffer
that's been pushed to the recipient. -
isSending
tracks whether all data has been pulled fromsource
and pushed tosink
.
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
};
Lecture 19: More Nonblocking I/O
-
source
is a nonblocking file descriptor bound to some local file
-
- Note that the source file is opened for reading (
O_RDONLY
), and the descriptor is configured to be nonblocking (O_NONBLOCK
) right from the start. - For reasons we'll discussed, it's not super important that source be nonblocking, since it's bound to a local file.
- But in the spirit of a nonblocking example, it's fine to make it nonblocking anyway. We just shouldn't expect very many (if any) -1's to come back from our
read
calls.
- Note that the source file is opened for reading (
-
sink
is explicitly converted to be nonblocking, since it might be blocking, andsink
will very often be a socket descriptor that really should be nonblocking.
- The implementations of the constructor and initialize are straightforward:
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;
}
Lecture 19: More Nonblocking I/O
- The first line decides if all data has been read from
source
and written tosink
, and if so, it returnstrue
unless it further confirms all of data written tosink
has arrived at final destination, in which case it returnsfalse
to state that syndication is complete.
- The first call to
dataReadyToBeSent
checks to see ifbuffer
houses data yet to be pushed out. If not, then it attempts toreadMoreData
. If after reading more data the buffer is still empty—that is, a single call toread
resulted in a-1/EWOULDBLOCK
pair, then we returntrue
as a statement that there's no data to be written, no need to try, but come back later to see if that changes. - The call to
writeMoreData
is an opportunity to push data out to sink.
- The implementation of sendMoreData is less straightforward:
bool OutboundFile::sendMoreData() {
if (!isSending) return !allDataFlushed();
if (!dataReadyToBeSent()) {
readMoreData();
if (!dataReadyToBeSent()) return true;
}
writeMoreData();
return true;
}
Lecture 19: More Non-blocking I/O
By Chris Gregg
Lecture 19: More Non-blocking I/O
- 862