Adding socket support to the iostreams hierarchy
Written by Gordon Zeglinski
Introduction
As Caesar was warned "Beware the ides of March," so too should programmers be warned "Beware the quirks of compilers." Compared to C-Set++ and Borland, Watcom has some strangeness. Watcom uses "POSIX" file handles in order to open more than the default number of files, _grow_handles() must be called, which is classified as a Watcom specific function. Interestingly enough, there is no reference to this function in any other file I/O related function. Most annoying to say the least.
C++ iostreams provide a simple highly flexible method of performing formatted and unformatted I/O. The basic implementation provides classes to perform I/O on files, memory buffers, and the standard streams (error, input, output). Any good C++ book will tell you how to format output, or write raw data to a stream. Few books will detail how to extend the hierarchy.
In this column, we will examine adding socket support to the iostreams hierarchy. There's no point reinventing the wheel, so we'll start by looking at socket++, a Unix-based socket extension to iostream.
Socket++
The original Unix version has classes to support inet sockets and pipes. In addition, the FTP and SMTP protocols are encapsulated in classes. Remember that BSD sockets can support sockets that aren't Internet based but OS/2 only supports Internet (Inet) sockets. We will only look at this library's generic socket and Inet socket support.
All reading and writing to the medium is performed by the streambuf hierarchy. The generic socket buffer object, sockbuf, is the heart of library. We'll look closer at stream buffers later. That being said, the programmer almost never works directly with the stream buffer. Instead, the stream hierarchy is used to read/write data. The stream hierarchy provides operators and functions to perform formatted and unformatted I/O on a buffer. Three classes are derived to provide input only, output only and input/output streams. These classes are isockstream, osockstream, and iosockstream. These last 3 classes are very simple. As shown below, isockstream defines only a few simple member functions. All of the work functions are inherited from ios and istream.
class isockstream: public istream { protected: isockstream (): ios (0) {} public: isockstream(sockbuf& sb) : ios (new sockbuf(sb)) {} ~isockstream (); sockbuf* rdbuf () { return (sockbuf*)ios::rdbuf(); } sockbuf* operator -> () { return rdbuf(); } void error (const char* errmsg) const; };
Figure 1: Definition of the isockstream class.
Definitions of the other 2 classes are similar (see sockstream.h).
Internet sockets use the buffer class sockinetbuf. This class is derived from sockbuf, and provides tcp/ip specific functions. The classes isockinet, osockinet and iosockinet are derived from their generic socket counter-parts.
The sockbuf class performs all socket operations, binding, connecting, listening, etc. A stream buffer (streambuf) class provides buffers for buffering input and output. Utilizing the unbuffered I/O capabilities doesn't require any additional programming effort, so we will look at the more complex case of buffered I/O.
When a stream needs to write data, it calls member functions in the streambuf class to write the data into the buffer. When the buffer becomes full, the streambuf::overflow is called. The overflow function must move the data from the output buffer (also called the put area) to the output medium (in this case the socket).
When a stream reads from streambuf, it calls streambuf member functions to remove data from the buffer, if the buffer is empty or doesn't contain enough data, streambuf::underflow is called. The underflow function reads data from the input medium and places it in the input buffer (also called the get area).
The get and put areas are sections of the reserve area. The reserve area is the block of memory which is used for buffering I/O. Both underflow and overflow are virtual member functions of streambuf. At the very least, a custom buffer class must declare members for overflow and underflow. Note: some implementations of the iostream classes, have overflow and underflow as pure virtual classes. The following is a modified version of sockbuf.
class sockbuf: public streambuf { public: enum type { sock_stream = SOCK_STREAM, sock_dgram = SOCK_DGRAM, sock_raw = SOCK_RAW, sock_rdm = SOCK_RDM, sock_seqpacket = SOCK_SEQPACKET }; enum option { so_debug = SO_DEBUG, so_acceptconn = SO_ACCEPTCONN, so_reuseaddr = SO_REUSEADDR, so_keepalive = SO_KEEPALIVE, so_dontroute = SO_DONTROUTE, so_broadcast = SO_BROADCAST, so_useloopback = SO_USELOOPBACK, so_linger = SO_LINGER, so_oobinline = SO_OOBINLINE, so_sndbuf = SO_SNDBUF, so_rcvbuf = SO_RCVBUF, so_sndlowat = SO_SNDLOWAT, so_rcvlowat = SO_RCVLOWAT, so_sndtimeo = SO_SNDTIMEO, so_rcvtimeo = SO_RCVTIMEO, so_error = SO_ERROR, so_type = SO_TYPE }; enum level { sol_socket = SOL_SOCKET }; enum msgflag { msg_oob = MSG_OOB, msg_peek = MSG_PEEK, msg_dontroute = MSG_DONTROUTE, msg_maxiovlen = MSG_MAXIOVLEN }; enum shuthow { shut_read, shut_write, shut_readwrite }; enum { somaxconn = SOMAXCONN }; struct socklinger { int l_onoff; // option on/off int l_linger; // linger time socklinger (int a, int b): l_onoff (a), l_linger (b) {} }; protected: struct sockcnt { int sock; int cnt; sockcnt(int s, int c): sock(s), cnt(c) {} }; sockcnt* rep; // counts the # refs to sock int stmo; // -1==block, 0==poll, >0 == waiting time in secs int rtmo; // -1==block, 0==poll, >0 == waiting time in secs virtual int underflow (); virtual int overflow (int c = EOF); virtual int doallocate (); int flush_output (); #ifdef _S_NOLIBGXX int x_flags; // port to USL iostream int xflags () const { return x_flags; } int xsetflags (int f) { return x_flags |= f; } int xflags (int f){ int ret = x_flags; x_flags = f; return ret; } void xput_char (char c) { *pptr() = c; pbump (1); } int linebuffered () const { return x_flags & _S_LINE_BUF; } #endif // _S_NOLIBGXX void Destroy(); //added by gwz public: sockbuf (int soc = -1); sockbuf (int, type, int proto=0); sockbuf (const sockbuf&); virtual ~sockbuf (); sockbuf& operator = (const sockbuf&); operator int () const { return rep->sock; } virtual sockbuf* open(type, int proto=0); virtual sockbuf* close(); virtual int sync(); virtual _G_ssize_t sys_read (char* buf, _G_ssize_t len); virtual _G_ssize_t sys_write (const void* buf, long len); virtual int xsputn (const char* s, int n); int is_open () const { return rep->sock >= 0; } int is_eof () { return xflags() & _S_EOF_SEEN; } virtual int bind(sockAddr&); virtual int connect(sockAddr&); void listen(int num=somaxconn); virtual sockbuf accept(); virtual sockbuf accept(sockAddr& sa); int read(void* buf, int len); int recv(void* buf, int len, int msgf=0); int recvfrom(sockAddr& sa,void* buf, int len, int msgf=0); #ifndef __linux__ int recvmsg (msghdr* msg, int msgf=0); int sendmsg (msghdr* msg, int msgf=0); #endif int write(const void* buf, int len); int send(const void* buf, int len, int msgf=0); int sendto(sockAddr& sa,const void* buf, int len, int msgf=0); int sendtimeout (int wp=-1); int recvtimeout (int wp=-1); int is_readready (int wp_sec, int wp_usec=0) const; int is_writeready (int wp_sec, int wp_usec=0) const; int is_exceptionpending (int wp_sec, int wp_usec=0) const; void shutdown (shuthow sh); int getopt(option op, void* buf,int len, level l=sol_socket) const; void setopt(option op, void* buf,int len, level l=sol_socket) const; type gettype () const; int clearerror () const; int debug (int opt= -1) const; int reuseaddr (int opt= -1) const; int keepalive (int opt= -1) const; int dontroute (int opt= -1) const; int broadcast (int opt= -1) const; int oobinline (int opt= -1) const; int linger (int tim= -1) const; int sendbufsz (int sz=-1) const; int recvbufsz (int sz=-1) const; void error (const char* errmsg) const; };
Figure 2: Definition of the modified sockbuf class.
As mentioned before, the sockbuf class defines the member functions underflow, and overflow. In addition, it defines the member function doallocate. The virtual function doallocate() allows an ancestor of streambuf to customize the allocation of the internal buffers.
int sockbuf::doallocate (){ // return 1 on allocation and 0 if there is no need if (!base ()) { char* buf = new char[2*BUFSIZ]; //allocate a buffer setb (buf, buf+BUFSIZ, 0); //set the reserve area setg (buf, buf, buf); //set the get area buf += BUFSIZ; setp (buf, buf+BUFSIZ); //set the put area return 1; } return 0; }
Figure 3: sockbuf::doallocate() implementation.
The '0' parameter in the call to setb() indicates that streambuf is not to free reserve area.
In the sockbuf definition, I added a destroy function. Below is the function bodies for the destructor and the assignment operator. Note that in the original version of the assignment operator, the destructor was explicitly called. This resulted in hidden destructor code being called that erased the calling instance's virtual function tables. The Destroy() function contains all the programmer written clean up code. It can be called by either the destructor or the assignment operator without strange side effects.
sockbuf& sockbuf::operator = (const sockbuf& sb){ if (this != &sb && rep != sb.rep && rep->sock != sb.rep->sock) { //this is bad.. seems to nuke the virtual tables! // this->sockbuf::~sockbuf(); << original code... Destroy(); rep = sb.rep; stmo = sb.stmo; rtmo = sb.rtmo; rep->cnt++; #ifdef _S_NOLIBGXX xflags (sb.xflags ()); #else // xflags () is a non-const member function in libg++. xflags (((sockbuf&)sb).xflags ()); #endif } return *this; } sockbuf::~sockbuf (){ Destroy(); } void sockbuf::Destroy(){ overflow (EOF); //flush the put area if (rep->cnt == 1 && !(xflags () & _S_DELETE_DONT_CLOSE)) close (); delete [] base (); if (--rep->cnt == 0) delete rep; }
Figure 4: sockbuf::Destroy() implementation.
Porting the Unix socket code to OS/2 wasn't overly difficult. It's simply a matter of removing Unix-like headers and using the appropriate ANSI ones. To save a bit of work, the BSD version of select() was used instead of the OS/2 version.
Wrapping Things Up
Extending the iostream class is simply a matter of creating the appropriate buffer class. In the simplest case, the new buffer class must overload the underflow and overflow streambuf functions. Once the buffer class has been defined, a new stream class is created. The stream class is derived from either istream, ostream or iostream. That's all that needs to be done.