Introduction
The code described in this article implements traditional C++ streams for communication over sockets. It is based on socket++, a library originally developed by Gnanasekaran Swaminathan and carries the following copyright notice:
Copyright (C) 1992,1993,1994 Gnanasekaran Swaminathan
Permission is granted to use at your own risk and distribute this software in source and binary forms provided the above copyright notice and this paragraph are preserved on all copies. This software is provided "as is" with no express or implied warranty.
It has however undergone massive changes and upgrades over the years.
The original socket++ code was dealing only with Unix sockets which are fairly different beasts compared to their Windows counterparts. In this sense, my code is not portable and it can be used only on Windows sockets.
Many functions return an error code object as a result. If you don't know what these hybrids between exceptions and error codes are, see my previous article on this subject.
The example code shown in this article uses the standard TCP services daytime and echo. To make them work, you have to enable these services in the Windows Features dialog box:
Low Level Objects
At the lowest level, there are object encapsulations for sockets and addresses.
Socket Object
The sock
object is an encapsulation of a Winsock SOCKET
handle. It can be used both for stream (TCP) or datagram (UDP) sockets. Its member function encapsulate most Winsock functions. The list is long and it's better to look at the code or the Doxygen documentation. Here, I wanted to point to a few details.
To be a well-behaved C++ object, sock
objects needed a copy constructor and an assignment operator. Meanwhile, socket handles cannot be duplicated using DuplicateHandle
functions. The solution is to use a "lives" counter that keeps track of how many "lives" a SOCKET handle has. The handle is closed (using CloseHandle
function) only when the lives count reaches 0
. The solution is similar to the one used by shared_ptr
objects but back in the day when this code was written, there were no shared_ptr
objects in C++.
If you find the sock
object is missing a wrapper for some function you desperately need in your app, don't worry: there is SOCKET
conversion operator that allows you to use a sock
anywhere a SOCKET
can be used.
Inaddr Objects
The inaddr
object encapsulates a sockaddr_in
structure. Note that our inaddr
object concerns itself only with Internet IPV4 addresses. Just like in the case of sock object, inaddr
objects have a sockaddr
conversion operator that allows you to use it in any place where you would use a sockaddr
.
The member functions of inaddr
take care of host-to-network conversions as well as eventual DNS lookup.
Examples
Here is an example of a TCP connection:
sock a (SOCK_STREAM);
inaddr him ("google.com", 80);
a.connect (him);
Receiving some data from a socket using the daytime
protocol:
char buf[256];
sock a (SOCK_STREAM);
a.connect (inaddr ("localhost", 13));
int len = a.recv (buf, sizeof (buf));
if (len > 0)
{
buf[len] = 0;
printf ("Date is: %s", buf);
}
Sending (and receiving) through a socket using the echo
protocol:
const char *fox = "The quick brown fox jumps over the lazy dog\n";
char buf[256];
sock a (SOCK_STREAM);
a.connect (inaddr ("localhost", 7));
a.send (fox, strlen(fox));
int len = a.recv (buf, sizeof (buf));
if (len > 0)
{
buf[len] = 0;
printf ("Echo: %s", buf);
}
Socket Streams
Now that we've seen the lower level entities, let's move up to socket streams. Here, the main piece is the sockbuf
object. This is derived from both std::streambuf
and sock
classes. As such, it inherits all the sock
member functions (connect
, bind
, recv
, send
, etc.). It also implements the virtual protected
functions required by the std::streambuf
interface (underflow
, overflow
, showmanyc
, etc.).
If you just plan to use the socket streams, you shouldn't concern too much with the implementation details of this class. It suffices to say that it maintains two separate buffers - one for reading and one writing, Buffer size can be changed and even, if need be, user can specify their own buffer.
The actual socket streams are isockstream
, osockstream
and sockstream
. They are specializations of the template class generic_sockstream<strm>
with std:istream
, std::ostream
and respectively std:iostream
as a template argument.
Examples
Let's rewrite the previous examples using our socket streams.
The daytime
example could be written as:
isockstream is (inaddr ("localhost", 13));
string s;
getline (is, s);
cout << "Date is: " << s << endl;
Here is another way of writing the echo
example:
char *fox = "The quick brown fox jumps over the lazy dog";
sockstream ss (inaddr ("localhost", 7));
ss << fox << endl;
char c;
cout << "Received: ";
do
{
ss >> noskipws >> c;
cout << c;
} while (c != '\n');
Programming with Socket Streams
Socket streams behave just like standard file streams and you should have little trouble using them. They have a "pointer to" operator:
sockbuf* operator ->()
that returns the associated sockbuf
object. If you remember, sockbuf
is derived from socket
so this is a handy way to access all the underlining socket functions. For instance, if you want to shutdown one end of the socket associated with a stream, you can just write:
sockstream ws;
....
ws->shutdown (sock::shut_write);
Keep in mind that output data is buffered; use flush()
or endl
to really send the data out.
The End, For Now...
If you liked my socket streams, stay tuned for the next article where I'll show you how to use them to build a multi-threaded TCP server and a small HTTP server that can be easily embedded in your programs.
History
- 1st December, 2019: Initial version