该程序将演示如何将一个简单结构序列化后发送到网络上,如何从网络上接收到数据后反序列化回结构。
ACE的 C++ WRAPPER FACADE层将网络通信分成三种角色:连接者 (ACE_SOCK_Connector)、等待者 (ACE_SOCK_Acceptor)和传输者 (ACE_SOCK_Stream)。
建立连接
首先使用 ACE_SOCK_Connector::connect连接某个服务器(使用 ip地址和端口号),该服务器上使用 ACE_SOCK_Acceptor::accept等待外部的连接请求。 ACE_INET_Addr类进行管理 SOCKET通信使用的 IP地址和端口号。 当连接建立的时候,连接者和等待者都初始化一个传输者用于通信。
下面就是连接者如何连接本机的 7777端口的服务程序代码:
#include <iostream>
using namespace std;
#include "ace/INET_Addr.h"
#include "ace/SOCK_Stream.h"
#include "ace/SOCK_Connector.h"
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;
}
}
如果连接成功, connect方法返回 0,如果连接失败,返回- 1,线程专有的 errno变量将被设置对应的错误码,你可以通过 strerror函数获取错误信息描述字符串,也可以使用线程安全的版本 strerror_r。 ACE不使用异常报错,原因之一是早些时候异常并不被所有的 C++编译器支持,原因之二是异常对性能仍然有影响,作为高性能底层库 ACE仍然采用了 C风格进行错误处理。但是你仍然可以在自己的应用逻辑中使用异常,并不会和 ACE发生冲突。
下面是等待者的示例:
#include <iostream>
using namespace std;
#include "ace/INET_Addr.h"
#include "ace/SOCK_Stream.h"
#include "ace/SOCK_Acceptor.h"
int main(void)
{
ACE_SOCK_Acceptor acceptor;
//本地端口 7777的 ACE_INET_Addr对象
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;
}
}
注意, ACE_SOCK_Acceptor::accept和 ACE_SOCK_Connector::connect方法都可以接收一个 ACE_TIME_Value*参数。该参数缺省直为 NULL,就像上面的两个示例,表示除非建立连接,否则不会返回 ;如果我们创建 ACE_TIME_Value time(0,0)对象作为参数,则表示方法不会阻塞,如果不能立刻建立连接,就返回- 1,并且 errno为 EWOULDBLOCK;如果我们创建 ACE_TIME_Value time(5,0)对象作为参数,就表示方法会最多等待 5秒钟,如果 5秒钟内还没有建立连接,就返回- 1,并且 errno为 ETIME.
ACE_SOCK_Acceptor对象没有状态,因此多线程可以在不锁定的情况下共享该对象。
数据传输
通常数据传输的过程是将对象中的数据按照某种格式序列化成连续的字节流,然后发送到网络上,当另一端接收到字节流后,按照此格式反序列化成对象。 当连接建立好后,通信双方都有两个可以发送和接收数据的 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;//如果使用ACE_LACKS_CDR_ALIGNMENT宏,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;
}
这种方式和支持标准 C++流的方式是一样的。那么,为什么不直接使用标准 C++流呢?因为 ACE所支持的平台很多,有些编译器不支持标准 C++流。并且据我个人的体验,标准 C++流在内存管理上是封装的,你不可能通过公有方法获得内部关里的缓冲区的指针,除非自己定义自己的派生类,这并不容易。还有一个原因是不同编译器和不同的硬件使用了不同的字节顺序(大尾数法和小尾数法)。正确的使用 ACE的 cdr类就可以保证各种环境下都能使用,因为它在内部使用了 CORBA公共数据表示的格式。 在这个示例程序里,我们实际上创建了两个 ACE_OutputCDR对象,一个用来表示数据头,一个存放实际结构中的数据。数据头中前 4个字节存放了一个布尔值,表示本机的字节顺序,后面四个字节表示第二个对象的实际长度。这里我没有使用宏 #define ACE_LACKS_CDR_ALIGNMENT,否则数据会被紧缩。布尔值将只有一个字节大小。
还有一种方法,本例没有提供,这里说明一下。首先在config.h文件中加上宏 #define ACE_ENABLE_SWAP_ON_WRITE ,然后重新编译ACE。然后创建CDR对象时明确就是通过网络字节顺序( 大尾数法):
ACE_OutputCDR ocdr(&mb, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
ACE_InputCDR icdr(&mb, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
对于基本的数值类型,各个平台也有可能有长度的差异,比如 int究竟是 16, 32还是 64。所以这里使用了 ACE提供的基本数值类型,比如 ACE_UINT32。
本例中,接收数据时首先接收固定长度的头对象,取得字节顺序标志后,调整字节顺序,然后获取实际长度,根据该长度接收第二个 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对象关联来读取缓冲区的数据。