一、如何使用iostream

TCP连接是面向流的连接,这一点与iostream 要表达的概念非常吻合。在使用阻塞Socket处理数据时,如果能借用iostream已经具备的强大的字符串流处理功能,是不是可以简化我们某些地方的程序设计呢?比如说需要在服务端和客户端之间某种类的对象,我们可以重载ostream与之的<<操作符和istream与之的>>操作符,这样使用操作符直观、方便地序列化和反序列化对象了。从某种意义上讲,iostream提供了一种简单的对象序列化的解决方案。

众所周知,cin是istream类的一个全局变量,cout是ostream类的一个全局变量。istream、ostream默认情况下对应的是标准输入、输出设备。而iostream类的构造函数却明确需要一个streambuf(即basic_streambuf<char, char_traits<char> >)类的指针,这意味着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()获得。

setgsetp方法分别用来设置两个buffer的三个点。

streambuf的关键功能由通过underflowoverflowsync三个virtual方法来实现,在streambuf类中这三个方法什么也没做,这正是我们需要重写的三个方法。当input buffer中数据读完了,iostream会调用streambufunderflow来输入数据,当output buffer被填满时,iostream会调用streambufoverflow来输出数据,当要输出endlends或者调用flush()方法时,iostream会调用streambufsync方法。

 

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在这里得到了更正。

 

 

作者: 书豪 发表于 2011-01-26 22:08 原文链接

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架