Adding socket support to the iostreams hierarchy

From EDM2
Jump to: navigation, search

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.