使用iostream封装TCP Socket

2013年6月11日 由 Creater 留言 »

一、如何使用iostream
TCP连接是面向流的连接,这一点与iostream 要表达的概念非常吻合。在使用阻塞Socket处理数据时,如果能借用iostream已经具备的强大的字符串流处理功能,是不是可以简化我们某些地方的程序设计呢?比如说需要在服务端和客户端之间某种类的对象,我们可以重载ostream与之的<<操作符和istream与之的>>操作符,这样使用操作符直观、方便地序列化和反序列化对象了。从某种意义上讲,iostream提供了一种简单的对象序列化的解决方案。
众所周知,cin是istream类的一个全局变量,cout是ostream类的一个全局变量。istream、ostream默认情况下对应的是标准输入、输出设备。而iostream类的构造函数却明确需要一个streambuf(即basic_streambuf >)类的指针,这意味着iostream需要通过streambuf来定义特定的输入输出行为。
iostream是ios的子类,ios还有两个与此相关的方法,一个是rdbuf(),返回streambuf指针,一个是rdbuf(streambuf*),重新设置streambuf。通过两个函数,我们往往可以实现一些非常巧妙的行为。
二、其实iostream是通过streambuf来读写数据
我们来看看streambuf类。这个类其实更像是一个接口,虽然它并没有定义成抽象类,但它本身确实是什么也做不了,这就意味它是在等着我们去继承它。与此同时,它本身也实现了部分接口提供给子类来使用。
streambuf维持了两个buffer,即input buffer和output buffer,streambuf就是使用这两个buffer进行数据的收发。streambuf本身并不负责内存的申请和回收,所以我们要做的是在构造时申请两块一定大小的内存交给streambuf,然后在析构时回收之。

/**
 * 用于tcp_stream的streambuf
 */
class tcp_streambuf : public std::streambuf {
public:
    tcp_streambuf(SOCKET socket, int buf_size) : _socket(socket), _buf_size(buf_size) {
        char* gbuf = new char[_buf_size];
        char* pbuf = new char[_buf_size];
        setg(gbuf, gbuf, gbuf);
        setp(pbuf, pbuf + _buf_size);
    }
    ~tcp_streambuf() {
        delete[] eback();
        delete[] pbase();
    }
}

每个buffer都有三个点,即首地址,当前指针,尾部地址。input buffer的三个点分别由eback(),gptr(),egptr()获得,output buffer的三个点分别由pbase(),pptr(),epptr()获得。

  • eback, a pointer to the beginning of the buffer.
  • gptr, a pointer to the next element to read.
  • egptr, a pointer just past the end of the buffer.

Similarly, an output buffer is characterized by:

  • pbase, a pointer to the beginning of the buffer.
  • pptr, a pointer to the next element to write.
  • epptr, a pointer just past the end of the buffer.

setg、setp方法分别用来设置两个buffer的三个点。
streambuf的关键功能由通过underflow,overflow,sync三个virtual方法来实现,在streambuf类中这三个方法什么也没做,这正是我们需要重写的三个方法。当input buffer中数据读完了,iostream会调用streambuf的underflow来输入数据,当output buffer被填满时,iostream会调用streambuf的overflow来输出数据,当要输出endl、ends或者调用flush()方法时,iostream会调用streambuf的sync方法。
underflow方法从输入流中提取一定量的数据到input buffer中,并返回当前位置的字符值,但并不移动当前指针的位置。如果对方关闭连接或者其他错误,返回EOF。

/**
     * 输入缓冲区为空时被调用
     */
    virtual int_type underflow() {
        int ret = recv(_socket, eback(), _buf_size, 0);
        if (ret > 0) {
            setg(eback(), eback(), eback() + ret);
            return traits_type::to_int_type(*gptr());
        } else {
            // ret == 0 || ret == SOCKET_ERROR
            return traits_type::eof();
        }
    }

sync方法将output buffer中的数据全部输出出去。

/**
     * 同步输出缓冲区中的数据
     */
    virtual int sync() {
        int all = pptr() - pbase();
        int sent = 0;
        while (sent < all) {             int ret = send(_socket, pbase() + sent, all - sent, 0);             if (ret > 0) {
                sent += ret;
            } else if (ret == SOCKET_ERROR) {
                return -1;
            }
        }
        return 0;
    }

overflow方法先将output buffer输出出去,再把新的字符写入output buffer。按MSDN的规定,如果这个新的字符_Meta是EOF,则返回traits_type::not_eof(_Meta)。

/**
     * 当输出缓冲区溢出时被调用
     */
    virtual int_type over_flow(int_type _Meta = traits_type::eof()) {
        if (sync() == -1) {
            return traits_type::eof();
        } else {
            if (!traits_type::eq_int_type(_Meta, traits_type::eof())) {
                sputc(traits_type::to_char_type(_Meta));
            }
            return traits_type::not_eof(_Meta);
        }
    }

三、最后一步
有了这样的tcp_streambuf,我们就可以通过它的对象指针来创建iostream了。
tcp_streambuf sb(socket)
iostream(&sb);
但是,有良好的面向对象思维的C++程序员可能不满足这样的写法。不如再写个RAII类来封装streambuf,而继承iostream则是一个不错的想法。

class tcp_stream : public std::iostream {
public:
    tcp_stream(int socket, int buf_size = 1024);
    ~tcp_stream();
};
tcp_stream::tcp_stream(int socket, int buf_size /*= 1024*/) : std::iostream(new tcp_streambuf(socket, buf_size)) {
}
tcp_stream::~tcp_stream() {
    delete rdbuf();
}

好了,下面我们使用它来写一个echo client。

#include "stdafx.h"
#include "tcp_stream.h"
void main() {
    using namespace std;
    using namespace boost::asio::ip;
    boost::asio::io_service io_service;
    tcp::socket socket(io_service);
    try {
        socket.connect(tcp::endpoint(address_v4::loopback(), 1127));
        tcp_stream ts(socket.native());
        while (!ts.eof()) {
            string request, response;
            cin >> request;
            ts << request << endl;             ts >> response;
            if (!response.empty()) {
                cout << response << endl;
            }
        }
    } catch (boost::system::system_error& err) {
        cout << err.what() << endl;
    }
}

四、值得注意的几点
1.iostream屏蔽了streambuf可能抛出的所有异常,所以不要指望在streambuf里运用异常机制,我这里通过EOF来表示出错。
2.因为TCP Socket的出错或者对方连接关闭是无法提前预知的,这不像文件结尾可以直接检测。所以当前读入数据时出错后eof()才会返回true。
3.输出数据时如果不输出endl或ends,或者调用flush()方法,数据会暂存在streambuf的output buffer里,所以如果需要实时发送数据需要注意这一点。
4.网上还有一篇文章《用streambuf简单封装socket》,感谢这篇文章的作者给予我的启发,不过他的代码里有个BUG就是出错后往往会导致死循环,因为他的underflow返回的EOF值不正确,这个BUG在这里得到了更正。

广告位

发表评论

你必须 登陆 方可发表评论.