0

Where to begin? The beginning, I suppose.

I'm new to socket programming. I wanted to give it a shot. After research, I found that it seems that there is no standard C++ oop socket library (there were some third party libraries that might have been oop, but I decided to make my own anyway because, well, it's more fun that way since it's just a personal project anyway), so I decided to make my own.

I decided that my new library would be integrated with iostream. Sure, this probably isn't a good idea usually, but I figured 1) I had a decent reason to and 2) it was just as an academic exercise anyway. It couldn't hurt to at least learn how to do it.

Thus, I did a little digging and came up with this design:

class NetworkStream : public std::iostream {
    public: 
        // class network streambuff
        // should handle the actual writing
        class NetworkBuff : public std::streambuf {
            private:
                SOCKET socket; // I'm working with winsock2, btw
            public:
                NetworkBuff(SOCKET s);

            protected:
                // out

                virtual std::streamsize xsputn(const char* buffer, std::streamsize size);

                virtual std::streamsize overflow(char buffer);

                virtual int underflow();

                virtual std::streamsize xsgetn(char* buffer, std::streamsize size);

        };

        // constructors
        NetworkStream(SOCKET socket); // sets up the socket in NetworkBuff
        virtual ~NetworkStream();

    }; // end network stream

This NetworkStream object is supposed to handle the actual reading and writing on the network. The connection is maintained by higher level code (and by that, I mean an object that inherits from NetworkStream). That code is working (as far as I can tell, anyway) and is irrelevant to my question, so I've omitted it. I'm just mentioning it so you understand my design.

Anyway, the actual implementation for my new stream object looks something like this:

// class network streambuff
// should handle the actual writing

//constructor
NetworkStream::NetworkBuff::NetworkBuff(SOCKET s) {
    socket = s;
}

// out

std::streamsize NetworkStream::NetworkBuff::xsputn(const char* buffer, std::streamsize size) {
    // well, let's send the data
    int result = send(socket,buffer,static_cast<int>(size),0);

    // if that didn't work, throw an error
    if(result == SOCKET_ERROR) throw("Send Failure: " + WSAGetLastError()); 

    // NOTE: I realized after I wrote this that this throw may be useless, 
    // since I think iostream catches any errors thrown at this level, but 
    // just in case

    // and pass through to streambuff, because, well, it can't hurt
    return std::streambuf::xsputn(buffer, size);
}

// basically do the same thing as before...
std::streamsize NetworkStream::NetworkBuff::overflow(char buffer) {
    // well, let's send the data
    int result = send(socket,buffer,sizeof(buffer),0);

    // if that didn't work, throw an error
    if(result == SOCKET_ERROR) throw("Send Failure: " + WSAGetLastError()); 

    // and pass through to streambuff, because, well, it can't hurt
    return std::streambuf::overflow(buffer);
}

As far as I can tell, this works like a charm. I step through my debugger, and my xsputn() gets called. So I can do something like this:

std::string data = "Hello, world!";
networkstream << data;

And it gets called. I think it sent successfully. It doesn't stepping through my debugger, result doesn't have an error. However, I haven't tested it fully yet because it's my receiving function that doesn't work:

std::streamsize NetworkStream::NetworkBuff::xsgetn(char* buffer, std::streamsize size) {
    // well, let's read the data
    int result = recv(socket,buffer,static_cast<int>(size),0);

    // if that didn't work, throw an error
    if(result == SOCKET_ERROR) throw("Receive Failure: " + WSAGetLastError());

    // Now this I think is wrong, specifically comparing against SOCKET_ERROR.
    // That's not my problem, though. My problem is that this function seems to 
    // never get called, so a wrong comparison doesn't matter yet anyway

    // and pass through to streambuff, because, well, it can't hurt
    return std::streambuf::xsgetn(buffer, size);
}

// Now this guy, I'm pretty extra sure is probably wrong, but it's what I got. I 
// couldn't find a good example of using underflow, so I did my best from the 
// research I did find
int NetworkStream::NetworkBuff::underflow() {
    // well, let's read the data
    int result = recv(socket,gptr(),sizeof(*gptr()),0);

    // if that didn't work, throw an error
    if(result == SOCKET_ERROR) throw("Recieve Failure: " + WSAGetLastError());

    // and pass through to streambuff, because, well, it can't hurt
    return std::streambuf::underflow();
}

It compiles real nicely. However, when I try to use it:

std::string data = "";
networkstream >> data; // should wait for data on the network

It seems to pretend nothing happened. No, actually, according to my debugger, it seems to set a fail bit, ignore my virtually overloaded functions, and continue execution like nothing happened.

So, my question, in one paragraph, is: what exactly is wrong with my underflow/xsgetn function that is causing it to fail and essentially ignore my code? I'm sure I'm doing something wrong, but what exactly?

  • `"Send Failure: " + WSAGetLastError()` and `"Receive Failure: " + WSAGetLastError()` don't do what you think they do. You need to use `"Send Failure: " + std::to_string(WSAGetLastError())` and `"Receive Failure: " + std::to_string(WSAGetLastError())` (or equivalent) instead. Also, when calling `send()` in `NeworkBuffer::overflow()`, you need to use `send(socket,&buffer,sizeof(buffer),0);` instead, but don't call `send()` at all if `buffer` is `Traits::eof()`. – Remy Lebeau May 16 '19 at 01:58
  • 1
    But yes, your reading code looks a bit off. I suggest you have a look at the implementation of `std::basic_filebuf` and `std::basic_stringbuf` for examples, or 3rd party `streambuf` implementations to see how to read and write data to external sources/targets. And BTW, I know there are 3rd party socket implementations of `std::streambuf` floating around, you should go find one instead of writing your own. – Remy Lebeau May 16 '19 at 02:05
  • Ah, yes. Good catch. I knew about the `"Send Failure: " + WSAGetLastError()` thing. I just threw it together and figured if I ever encountered an error, I'd fix it so I could read. Also, thanks for the tip about the `Traits::eof()` –  May 16 '19 at 02:06
  • Have you read the [`std::streambuf`](https://en.cppreference.com/w/cpp/io/basic_streambuf) documentation on cppreference.com yet? – Remy Lebeau May 16 '19 at 02:07
  • Yeah, I knew about 3rd parties too. Like I said, it was kind of just for fun. That's why I'm even bothering to mess with iostream anyway. I'm just trying to learn. Thanks for the tip about filebuf and stringbuf, btw. –  May 16 '19 at 02:08
  • Yes, I've read it. That was part of my research. It wasn't clear to me though, exactly what underflow is supposed to do, other than return `gptr()`, I think. –  May 16 '19 at 02:10

1 Answers1

0

As it turns out, I can't just pass through. That's not how streambuf is designed.

Best as I can tell (and someone please correct me if I'm wrong), streambuf actually lives up to its name. It is supposed to be a buffer for the stream.

What wasn't clear, to me anyway, is that streambuf, while it is supposed to handle buffering, it has no actual internal way of holding or handling data. That's the whole point of overloading it. YOU are supposed to handle buffering. You can't pass through (again, someone correct if I'm wrong) through to the parent. That's just not how it's designed.

What you need (or I needed, I suppose) was a good implementation of underflow and implement it proper. Fortunately, after more digging, I was able to find one. Long story short, I went with something like this:

// Shoutout to http://www.voidcn.com/article/p-vjnlygmc-gy.html where I found out
// how to do this proper
std::streambuf::int_type NetworkStream::NetworkBuff::underflow() {
    const int readsize = 30;

    // first, check to make sure the buffer is not exausted:
    if(gptr() < egptr()) {
        return traits_type::to_int_type(*gptr());
    }

    // Now, let's read...

    // btw, inputbuffer is a data member that is a std::string
    // clear the buffer
    inputbuffer.clear();
    inputbuffer.resize(readsize);

    // let's read
    int bytesread = recv(socket,&inputbuffer[0],static_cast<int>(readsize)); 

    // return the end of file if we read no bytes
    if(bytesread == 0) {
        return traits_type::eof();
    }

    // set the pointers for the buffer...
    setg(&inputbuffer[0],&inputbuffer[0],&inputbuffer[readsize-1]);

    // finally, let's return the data type
    return traits_type::to_int_type(*gptr());

}