Topic 4 Networking (4)

Leedehai
Monday, June 5, 2017

There is no class meeting on Monday, May 29. The class meeting on Friday, June 2 is about system design principles, a topic that is not part of networking. See note page Appendix A - Principles of Systems Design.

This note page touches upon the technique of non-blocking I/O, and uses its application in networking as an example.

4.8 Non-blocking I/O

4.8.1 "Fast" and "slow" system calls

4.8.2 Non-blocking system calls

4.8.2.1 Case study: non-blocking input - read()

#include ... using namespace std; static const string kAlphabet = "abcdefghijklmnopqrstuvwxyz"; static const useconds_t kDelay = 1e5; /* 1e5 us is 0.1 sec */ static void handleRequest(int connSock); static const unsigned short kPort = 41411; int main(int argc, char *argv[]) { /* create a listening socket */ int server = createServerSocket(kPort); ThreadPool pool(128); /* we implemented ThreadPool in Assignment 6 */ while (true) { /* wait and return a connected socket */ int connSock = accept(server, NULL, NULL); pool.schedule([connSock]() { handleRequest(connSock); }); } return 0; } void handleRequest(int connSock) { sockbuf sb(connSock); iosockstream ss(&sb); for (size_t i = 0; i < kAlphabet.size(); i++) { usleep(kDelay); /* wait for 0.1 sec */ ss << kAlphabet[i] << flush; } }
#include ... using namespace std; static const unsigned short kPort = 41411; int main() { int clientSock = createClientSocket("localhost", kPort); size_t numSuccessfulReads = 0; size_t numBytes = 0; while (true) { char ch; /* a single-byte buffer */ ssize_t count = read(clientSock, &ch, 1); /* potentially blocking */ if (count == 0) break; /* break the loop on EOF */ numSuccessfulReads++; numBytes += count; cout << ch << flush; } close(client); cout << endl; cout << "Alphabet Length: " << numBytes << " bytes." << endl; cout << "Num reads: " << numSuccessfulReads << endl; return 0; }

Run the client - we discover that there are 26 read() calls:

$ ./blocking-alphabet-client abcdefghijklmnopqrstuvwxyz Alphabet Length: 26 bytes. Num reads: 26
#include ... using namespace std; static const unsigned short kPort = 41411; int main() { /* send connection request to server */ int clientSock = createClientSocket("localhost", kPort); setAsNonBlocking(clientSock); /* mark the SD as non-blocking */ size_t numReads = 0; size_t numSuccessfulReads = 0; size_t numUnsuccessfulReads = 0; size_t numBytes = 0; while (true) { char ch; ssize_t count = read(client, &ch, 1); numReads++; if (count == 0) break; /* break the loop on EOF */ if (count > 0) { /* read in something */ numSuccessfulReads++; numBytes += count; cout << ch << flush; } else { /* didn't read in anything, return -1 as it's non-blocking */ assert(errno == EAGAIN || errno == EWOULDBLOCK); numUnsuccessfulReads++; } } close(client); cout << endl; cout << "Alphabet Length: " << numBytes << " bytes." << endl; cout << "Num reads: " << numReads << " (" << numSuccessfulReads << " successful, " << numUnsuccessfulReads << " unsuccessful)." << endl; return 0; }

Run the client - we discover that there are 26 successful calls to read() and ~107 unsuccessful calls, because the server just emits one letter every 0.1 second:

$ time ./non-blocking-alphabet-client abcdefghijklmnopqrstuvwxyz Alphabet Length: 26 bytes. Num reads: 11268991 (26 successful, 11268964 unsuccessful).

Here, read() is non-blocking, because the SD is marked as non-blocking.

Data available? Expect ch to be updated and a return value of 1.
No data available, ever (i.e. EOF)? Expect a return value of 0.
No data available right now, but possibly in the future? Expect a return value of -1 and errno to be set to EAGAIN or EWOULDBLOCK.

#include ... using namespace std; /* mark the SD as non-blocking */ void setAsNonBlocking(int descriptor) { int flags = fcntl(descriptor, F_GETFL); if (flags == -1) flags = 0; /* if fcntl() fails, just go with 0 */ fcntl(descriptor, F_SETFL, flags | O_NONBLOCK); /* retain other flags */ }

4.8.2.2 Case study: non-blocking input/output

class OutboundFile { public: OutboundFile(); void initialize(const string &source, int sink); bool sendMoreData(); private: /* unimportant for this lecture */ int sink, source; static const size_t kBufferSize = 128; char buffer[kBufferSize]; size_t numBytesAvailable, numBytesSent; bool isSending; bool dataReadyToBeSent() const; void readMoreData(); void writeMoreData(); bool allDataFlushed(); };
/** * File: outbound-file-test.cc * --------------------------- * Demonstrates how one should use the OutboundFile class * and can be used to confirm that it works properly. */ #include "outbound-file.h" int main() { OutboundFile obf; obf.initialize("outbound-file-test.cc", STDOUT_FILENO); while (obf.sendMoreData()) {;} return 0; }
#include ... using namespace std; static const unsigned short kDefaultPort = 12345; static const string kFileToServe("expensive-server.cc"); 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; while (true) { int clientSocket = accept(serverSocket, NULL, NULL); if (clientSocket == -1) { assert(errno == EAGAIN || errno == EWOULDBLOCK); } else { /* captured a connection request */ OutboundFile obf; obf.initialize(kFileToServe, clientSocket); outboundFiles.push_back(obf); cout << "Connection #" << ++numConnections << endl; cout << "Queue size: " << ++numActiveConnections << endl; } /* manually multi-plexing: send data to clients, piece by piece */ auto iter = outboundFiles.begin(); while (iter != outboundFiles.end()) { if (iter->sendMoreData()) { ++iter; } else { /* no more data to send */ iter = outboundFiles.erase(iter); cout << "Queue size: " << --numActiveConnections << endl; } } } }

Appendix: implementation of OutboudFile class

The OutboundFile class is designed to read a local file and push its contents out over a supplied descriptor, and to do so without ever blocking.

/** * File: outbound-file.cc * ---------------------- * Presents the implementation of the OutboundFile class. */ #include ... using namespace std; 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; } bool OutboundFile::sendMoreData() { if (!isSending) return !allDataFlushed(); if (!dataReadyToBeSent()) { readMoreData(); if (!dataReadyToBeSent()) return true; } writeMoreData(); return true; } bool OutboundFile::dataReadyToBeSent() const { return numBytesSent < numBytesAvailable; } void OutboundFile::readMoreData() { /* change all that by getting a chunk of readily available data * from (blocking) local file */ ssize_t incomingCount = read(source, buffer, kBufferSize); if (incomingCount == -1) { assert(errno == EWOULDBLOCK); return; } numBytesAvailable = incomingCount; numBytesSent = 0; if (numBytesAvailable > 0) return; close(source); if (isSocketDescriptor(sink)) shutdown(sink, SHUT_WR); else setAsBlocking(sink); isSending = false; } void OutboundFile::writeMoreData() { auto old = signal(SIGPIPE, SIG_IGN); ssize_t outgoingCount = write(sink, buffer + numBytesSent, numBytesAvailable - numBytesSent); signal(SIGPIPE, old); if (outgoingCount == -1) { if (errno == EPIPE) { isSending = false; } else { assert(errno == EWOULDBLOCK); } } else { numBytesSent += outgoingCount; } } bool OutboundFile::allDataFlushed() { bool allBytesFlushed; if (isSocketDescriptor(sink)) { assert(isNonBlocking(sink)); ssize_t count = read(sink, buffer, sizeof(buffer)); allBytesFlushed = count == 0; } else { assert(isBlocking(sink)); int numOutstandingBytes = 0; ioctl(sink, SIOCOUTQ, &numOutstandingBytes); allBytesFlushed = numOutstandingBytes == 0; } if (allBytesFlushed) close(sink); return allBytesFlushed; }
EOF