使用iostream封装TCP Socket
一、如何使用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()获得。
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了。
iostream(&sb);
但是,有良好的面向对象思维的C++程序员可能不满足这样的写法。不如再写个RAII类来封装streambuf,而继承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 "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在这里得到了更正。