数据传输
通常数据传输的过程是将对象中的数据按照某种格式序列化成连续的字节流,然后发送到网络上,当另一端接收到字节流后,按照此格式反序列化成对象。
当连接建立好后,通信双方都有两个可以发送和接收数据的ACE_SOCK_Stream对象。该对象提供了发送和接收的方法。send_n/recv_n用于发送和接收确定数量的字节流,如果没有发送或者接收完,该方法将阻塞。而send/recv就不保证这一点,可能实际发送或者接收的数据比参数指定的少,该方法不会阻塞,而是返回实际发送或者接收的数据大小。send/recv方法实际是从父类ACE_SOCK_IO继承而来的。
网络传输的一种高效的方法是集中写和分散读。不同缓冲区的数据没有必要拷贝到一起,就可以直接按照次序一次型的发送出去。从网络另一端收到后,有可以分散的写到不同的缓冲区中。这就避免了数据复制的开销。ACE_SOCK_Stream的方法recvv_n/sendv_n方法就提供了这个机制。我们后面的示例将严实这个方法的使用。
如果我们使用TCP/IP协议发送数据,TCP/IP协议有一个Nagle算法。该算法将缓存小数据,减少网络发送的次数,从而避免过多通信的开销。在某些情况下,我们需要关闭该算法,让我们的数据能够立刻发送出去。ACE_SOCK_Stream的set_option方法使用参数TCP_NODELAY可以关闭这个算法。另一个方法是当我们使用sendv_n方法时,也会强制数据立刻发送。
下面的示例将一个结构SHMRecord初始化,并序列化到ACE_OutputCDR对象中。然后使用sendv_n方法将数据发出。
#include <iostream>
using namespace std;
#include "ace/INET_Addr.h"
#include "ace/SOCK_Stream.h"
#include "ace/SOCK_Connector.h"
#include "ace/CDR_Stream.h"
class SHMRecord
{
public:
SHMRecord():pData_(NULL){}
ACE_UINT16 type_;
ACE_UINT32 offset_;
void* pData_;
ACE_UINT32 dataLength_;
size_t size() const
{
return 2+4+4+dataLength_;
}
~SHMRecord()
{
if(pData_!=NULL)
delete[] static_cast<char*>(pData_);
}
};
int operator<<(ACE_OutputCDR & cdr,SHMRecord const& record)
{
cdr<<record.type_;
cdr<<record.offset_;
cdr<<record.dataLength_;
cdr.write_char_array(static_cast<char*>(record.pData_),record.dataLength_);
return cdr.good_bit();
}
int operator>>(ACE_InputCDR & cdr,SHMRecord & record)
{
cdr>>record.type_;
cdr>>record.offset_;
cdr>>record.dataLength_;
record.pData_=new char[record.dataLength_]();
cdr.read_char_array(static_cast<char*>(record.pData_),record.dataLength_);
return cdr.good_bit();
}
int main(void)
{
ACE_INET_Addr address("127.0.0.1:7777");
ACE_SOCK_Connector connector;
ACE_SOCK_Stream stream;
if(connector.connect(stream,address)==-1)
{
cout<<strerror(errno)<<endl;
}
SHMRecord record;
record.type_=1;
record.offset_=2;
record.pData_=new char[4]();
record.dataLength_=4;
strcpy(static_cast<char*>(record.pData_),"hih");
const size_t size=record.size()+ACE_CDR::MAX_ALIGNMENT;
ACE_OutputCDR payload(size);
payload<<record;
//create cdr header for this data
ACE_OutputCDR header(ACE_CDR::MAX_ALIGNMENT+8);
header<<ACE_OutputCDR::from_boolean(ACE_CDR_BYTE_ORDER);
header<<ACE_CDR::ULong(size);
iovec iov[2];
iov[0].iov_base=header.begin()->rd_ptr();
iov[0].iov_len=8;
iov[1].iov_base=payload.begin()->rd_ptr();
iov[1].iov_len=size;
stream.sendv_n(iov,2);
cout<<record.type_<<endl;
cout<<record.offset_<<endl;
cout<<static_cast<char*>(record.pData_)<<endl;
cout<<record.dataLength_<<endl;
}
ACE提供了ACE_OutputCDR和ACE_InputCDR类,是针对网络程序经常遇到的将对象数据序列化到字节流和从字节流中反序列化到对象的情况。你可以提供自己的operator<<和operator>>操作,就像上面的例子一样。
这种方式和支持标准C++流的方式是一样的。那么,为什么不直接使用标准C++流呢?因为ACE所支持的平台很多,有些编译器不支持标准C++流。并且据我个人的体验,标准C++流在内存管理上是封装的,你不可能通过公有方法获得内部关里的缓冲区的指针,除非自己定义自己的派生类,这并不容易。还有一个原因是不同编译器和不同的硬件使用了不同的字节对齐方式(大尾数法和小尾数法)。使用ACE的cdr类就可以保证各种环境下都能使用,因为它在内部使用了CORBA公共数据表示的格式。
对于基本的数值类型,各个平台也有可能有长度的差异,比如int究竟是16,32还是64。所以这里使用了ACE提供的基本数值类型,比如ACE_UINT32。
在这个示例程序里,我们实际上创建了两个ACE_OutputCDR对象,一个用来表示数据头,一个存发实际结构中的数据。数据头中前4个字节存放了一个布尔值,表示本机的字节顺序,后面四个字节表示第二个对象的实际长度。
因此,接收数据时首先接收固定长度的头对象,取得字节顺序标志后,调整字节顺序,然后获取实际长度,根据该长度接收第二个ACE_OutputCDR对象存放的实际数据。
下面的例子演示了如何接收发送来的数据。
int main(void)
{
ACE_SOCK_Acceptor acceptor;
ACE_INET_Addr address;
address.set(7777);
if(acceptor.open(address)==-1)
{
cout<<strerror(errno)<<endl;
}
ACE_SOCK_Stream stream;
if(acceptor.accept(stream)==-1)
{
cout<<strerror(errno)<<endl;
}
auto_ptr<ACE_Message_Block>
spBlock(new ACE_Message_Block(ACE_DEFAULT_CDR_BUFSIZE));
ACE_CDR::mb_align(spBlock.get());
if(stream.recv_n(spBlock->wr_ptr(),8)==8)//receive the header of CDR
{
//parse the cdr header
spBlock->wr_ptr(8);
ACE_InputCDR cdr(spBlock.get());
ACE_CDR::Boolean byte_order;
cdr>>ACE_InputCDR::to_boolean(byte_order);
cdr.reset_byte_order(byte_order);
ACE_CDR::ULong length;
cdr>>length;
//receive the data from master
spBlock->size(length+8+ACE_CDR::MAX_ALIGNMENT);
if(stream.recv_n(spBlock->wr_ptr(),length)==length)
{
spBlock->wr_ptr(length);
//必须重新创建一个CDR对象,否则解析不正确
ACE_InputCDR cdr2(spBlock.get());
ACE_CDR::Boolean byte_order;
cdr2>>ACE_InputCDR::to_boolean(byte_order);
cdr2.reset_byte_order(byte_order);
ACE_CDR::ULong length;
cdr2>>length;
auto_ptr<SHMRecord> spRecord(new SHMRecord);
cout<<spRecord->type_<<endl;
cout<<spRecord->offset_<<endl;
cout<<static_cast<char*>(spRecord->pData_)<<endl;
cout<<spRecord->dataLength_<<endl;
}
}
}
ACE_Message_Block类用来管理数据,内部有一个指向ACE_Data_Block对象的指针,ACE_Data_Block类管理实际的缓冲区数据。这种设计允许多个ACE_Message_Block对象共享同一个ACE_Data_Block对象,对于效率的提高很有帮助。多个ACE_Message_Block对象可以组成一个链表(双向或者单向)。
在上面的例子中,我们 创建了一个默认大小的ACE_Message_Block对象,然后将接收的数据写入ACE_Data_Block的缓冲区中,并且移动写指针的位置。ACE_InputCDR通过和ACE_Message_Block对象关联来读取缓冲区的数据。