译者序:
一个英语从未及格的程序员,学习Boost.Asio而苦啃,留下只言片语,只为他日重品。
地道的中国式英语,看客可不屑。
美丽的分隔线
-------------------------------------------------
第二章Boost.Asio基础
在这一章节中,需要确切知道什么时候使用Boost.Asio,并且深入的研究异步编程,这会比同步编程更加有趣。
The Network API
本节讲解在使用Boost.Asio写一个网络应用程序时需要知道哪些内容。
Boost.Asio namespaces
Boost.Asio下的所有都在boost::asio命名空间下,或者是子命名空间下:
boost:asio:包含核心类和函数。重要的类io_service和streambuf。我们也有任意的函数
(free functions),如read,read_at,read_until,他们的异步同伴,和write和write的异步同伴函数。
boost::asio::ip:包含网络部分。包含重要的类如:address,endpoint,tcp,udp,icmp还有任意函数(free functions)connect和async_connect。注意在boost::asio::ip::tcp::socket名称,socket只是typedefboost::asio::ip::tcp下的一个关键字。
boost::asio::error:此命名空间包含在调用I/O时的一些错误代码。
boost::asio::ssl:此命名空间包含SSL处理相关类。
boost::asio::local:此命名空间包含了POSIX具体类。
boost::asio::windows:此命名空间包含了windows具体类。
IP Addresses
处理IP地址,Boost.Asio提供了ip::address,ip::address_v4,ip::address_v6类。
它提供了相当多重要的功能:
ip::address(v4_or_v6_address):转换ip_v4或ip_v6地址到ip::address。
ip::address::from_string(str):从IPv4地址(点分隔)或者IPv6地址(十六进制计数法)创建一个地址。
ip::address::to_string():返回一个友好表示IP地址。
ip::address_v4::broadcast([addr,mask]):这个函数创建一个广播地址。
ip::address_v4::any():返回一个任意地址。
ip::address_v4::loopback(),ip_address_v6::lookback():返回环回地址。
ip::host_name():返回当前主机名称字符串。
大部分时候这么使用ip::address_from_string:
ip::address addr=ip::address::from_string(“127.0.0.1”);
如果需要连接一个主机,下面的代码片段是无效的:
// 抛出异常
ip::address addr=ip::address::from_string(“www.yahoo.com”);
Endpoints
Endpoint是一个地址和端口。每一个不同的socket类都有自己的端点类:如
ip::tcp::endpoint,ip::udp::endpoint,ip::ecmp::endpoint。
如果你想连接本机80端口:
ip::tcp::endpoint ep(ip::address::from_string(“127.0.0.1”),80);
可以通过三种方式构造endpoint:
endpoint():默认构造函数有时可用于UDP/ICMP sockets。
endpoint(protocol,port):用于接受连接的服务器端socket。
endpoint(addr,port):通过地址和端口构造一个endpoint。
例如:
ip::tcp::endpoint ep1;
ip::tcp::endpoint ep2(ip::tcp::v4(),80);
ip::tcp::endpoint ep3(ip::address::from_string(“127.0.0.1”),80);
如果你想连接一个主机(没有IP地址):
//输出87.248.122.122
io_service service;
ip::tcp::resolver resolver(service);
ip::tcp::resolver::query query(“www.yahoo.com”,80);
ip::tcp::resolver::iterator iter = resolver.resolve(query);
ip::tcp::endpoint ep=*iter;
std::cout << ep.address().to_string() << std::endl;
你需要更换tcp套接字类型。首先,创建一个需要查询的名称,然后使用
resolver()函数。如果成功,至少返回一条记录。给定迭代器,可以只使用第一个或者遍历列表。
给定一个endpoint,你可以获取地址,端口,和ip协议(v4或者v6):
std::cout << ep.address().tostring() << “:”<<ep.port()
<< “/”<< ep.protocol()<<std::endl;
Sockets
Boost.Asio有三种类型的套接字类:ip::tcp,ip::udp,ip::icmp,当然也是可以扩展的。你可以创建自己的套接字,是复杂的。如果选择这样做,看看
boost/asio/ip/tcp.hpp,boost/asio/ip/udp.hpp,boost/asio/ip/icmp.hpp,他们都是内部
typedef关键字很小的类。
你会想到ip::tcp,ip::udp,ip::icmp类占位符,他们提供了便捷访问类和方法:
ip::tcp::socket,ip::tcp::acceptor,ip::tcp::endpoint,ip::tcp::resolver,ip::tcp::iostream
ip::udp::socket,ip::udp::endpoint,ip::udp::resolver
ip::icmp::socket,ip::icmp::endpoint,ip::icmp::resolver
套接字类创建一个响应的套接字,你给定io_service给构造函数:
io_service service;
ip::udp::socket sock(service);
sock.set_option(ip::udp::socket::reuse_address(true));
每个套接字名称都是一个typedef关键字:
ip::tcp::socket = basic_stream_socket<tcp>
ip::udp::socket=basic_stream_socket<udp>
ip::icmp::socket=basic_raw_socket<icmp>
同步错误代码
所有的同步函数都有一个重载,抛出异常或者返回错误代码。如下代码片段:
sync_func(arg1,arg2,...,argN);// throws
boost::asio::error_code ec;
sync_func(arg1,arg2,...,argN,ec);//返回错误代码
在本章的剩余部分,你会看到更多的同步功能。为了保持简单,省略了返回错误代码,但是他们是存在的。
套接字成员函数
函数被分成几组。不是每个套接字都可以使用所有的函数。本节的最后会显示函数属于哪些类。
注意所有的异步函数理解返回,同步函数只有在完成操作后才返回。
连接相关函数
这些函数是连接或者绑定套接字,断开它,或者查询连接是否有效或者无效:
assign(protocl,socket):指定一个原始(本地)套接字到一个套接字实例。用它处理遗留代码(本地套接字已经创建)。
open(protocol):打开一个给定协议(v4或者v6)的套接字。主要用于UDP/ICMP套接字,或者服务端套接字。
bind(endpoint):绑定一个地址。
connect(endpoint):同步连接到给定的地址。
async_connect(endpoint):异步连接到给定的地址。
io_open():如果套接字已经打开返回true。
close():关闭套接字。所有的异步操作被取消,并且返回
error::operation_aborted错误代码。
shutdown(type_of_shutdown):从现在开始禁止发送操作,接收操作,或者两个同时禁止。
cancel():在当前套接字上取消所有的异步操作。这个套接字的所有异步操作都完成并且返回error::operation_aborted错误代码。
实例如下:
ip::tcp::endpoint ep(ip::address::from_string(“127.0.0.1”),80);
ip::tcp::socket sock(service);
sock.open(ip::tcp::v4());
socket.connect(ep);
sock.write_some(buffer(“GET /index.html\r\n”));
char buff[1024];
sock.read_some(buffer(buff,1024));
sock.shutdown(ip::tcp::socket::shutdown_receive);
sock.close();
读写函数
在套接字上执行输入输出功能。
异步操作函数的,处理者的签名是一样的:
void handler(const boost::system::error_code &e,size_t bytes)。
async_receive(buffer,[flags,]handler):在一个套接字上启动异步接受操作。
async_read_some(buffer,handler):和async_receive是等价的。
async_receive_from(buffer,endpoint,[,flags],handler):在一个指定的端点上启动一个异步的接受操作。
async_send(buffer[,flags],handler):启动一个异步发送数据操作。
async_write_come(buffer,handler):和async_send是等价的。
async_send_to(buffer,endpoint,handler):在一个指定的端点上启动发送数据的异步操作。
receive(buffer[,flags]):同步接受数据到给定的缓冲区。阻塞直到接收到数据,或者发生错误。
read_some(buffer):等价于receive。
receive_from(buffer,endpoint[,flags]):在指定的端点,接受数据到给定的缓冲区,函数阻塞直到接收到数据,或者出现错误。
send(buffer[,flags]):同步发送缓冲区数据。函数阻塞直到发送成功,或者出现错误。
write_some(buffer):等价于send。
send_to(buffer,endpoint[,flags]):同步发送缓冲区数据到给定的端点。函数阻塞直到发送成功或者出现错误。
available():返回可以不阻塞的同步读取多个字节。
讨论缓冲区。检验flags。flags默认值为0,也可以是一个组合:
ip::socket_type::socket::message_peek:这个标记仅仅查看消息。它返回一个消息,但是下次读取是将重读这个消息。
ip::socket_type::socket::message_out_of_band:这个标记处理out-of-band(OOB)数据。OOB数据是比普通数据更重要的数据。关于OOB的讨论超出了本书的范围。
ip::socket_type::socket::message_do_not_route:这个标记指定消息不使用路由表发送。
ip::socket_type:socket::message_end_of_record:这个标记指定数据记录的结束标记。这个不能在windows下指定。
使用message_peek,如下面代码片段所示:
char buff[1024];
sock.receive(buffer(buff),ip::tcp::socket::message_peek);
memset(buff,1024,0);
// 重读上一次读取的
sock.receive(buffer(buff));
以下例子,引导读者使用同步和异步不同的套接字:
1.例子1同步读写一个tcp套接字。
ip::tcp::endpoint ep(ip::address::from_string(“127.0.0.1”),80);
ip::tcp::socket sock(service);
sock.connect(ep);
sock.write_some(buffer(“GET /index/html\r\n”));
std::cout<<”bytes available” << sock.available()<<std::endl;
char buff[512];
size_t read=sock.read_some(buffer(buff));
2.同步读写一个udp套接字。
ip::udp::socket sock(service);
sock.oopen(ip::udp::v4);
ip::udp::endpoint receiver_ep(“87.248.112.181”,80);
sock.send_to(buffer(“testing\n”),receive_ep);
char buff[512];
ip::udp::endpoint sender_ep;
sock.receive_from(buffer(buff),sender_ep);
注意:在上面的代码片段中,从UDP套接字接受数据使用的是receive_from,需要一个默认构造函数的endpoint。
3.异步读写一个udp套接字
using namespace boost::asio;
io_service service;
ip::udp::socket sock(service);
boost::asio::ip::udp::endpoint sender_ep;
char buff[512];
void on_read(const boost::sytem::error_code &err,std::size_t read_bytes){
std::cout << “read” << read_bytes << std::endl;
Sock.async_recieve_from(buffer(buff),sender_ep,on_read);
}
int main(int argc,char* argv[]){
ip::udp::endpoint ep(ip::address::from_string(“127.0.0.1”,8001);
sock.open(ep.protocol());
sock.set_option(boost::asio::ip::udp::socket::reuse_address(true));
sock.bind(ep);
sock.async_receive_from(buffer(buff,512),sender_ep,on_read);
serivce.run();
}
套接字控制
这些函数处理套接字高级选项
get_io_service():返回构造函数给定的io_servcie实例。
get_option(option):返回套接字选项。
set_option(option):设置套接字选项。
io_control(cmd):在套接字上执行I/O命令。
下面是可以设置或者获取的套接字选项:
名称 | 描述 | 类别 |
broadcast | If true, it allows broadcasting messages | bool |
debug | If true, it enables socket-level debugging | bool |
do_not_route | If true, it prevents routing and use local interfaces only | bool |
enable_ connection_ aborted | If true, it reports connections that were aborted on accept() | bool |
keep_alive | If true, it sends keep-alives | bool |
linger | If true, socket lingers on close() if there's unsent data | bool |
receive_buffer_ size | This is a received buffer size for a socket | int |
receive_low_ watemark | This provides a minimum number of bytes to process for socket input | int |
reuse_address | If true, socket can be bound to an address already in use | bool |
send_buffer_ size | This sends buffer size for a socket | int |
send_low_ watermark | This provides a minimum number of bytes to send for socket output | int |
ip::v6_only | If true, it allows only IPv6 communication | bool |
每个名称代表一个套接字typedef类型 或者一个类,使用如下代码所示:
ip:tcp::endpoint ep(ip::address::from_string(“127.0.0.1”),80);
ip::tcp::socket sock(service);
sock.connet(ep);
// TCP套接字可复用地址
ip::tcp::socket::reuse_address ra(true);
sock.set_option(ra);
// 获取套接字接受缓冲区大小
ip::tcp::socket::receive_buffer_size rbs;
sock.get_option(rbs);
std::cout << rbs.value() << std::endl;
// 设置接受缓冲区大小为8192
ip::tcp::socket::send_buffer_size sbs(8192);
sock.set_option(sbs);
注意:使用以上功能时,需要打开套接字,否则会抛出异常。
TCP 和 UDP 和ICMP
在前面说过,不是所有的套接字可以使用所有的函数。做了一个列表展示不同函数。一些函数没在这里出现,意味着可以应用于所有的套接字。
Name | TCP | UDP | ICMP |
async_read_some | Yes | - | - |
async_receive_from | - | Yes | Yes |
async_write_some | Yes | - | - |
async_send_to | - | Yes | Yes |
read_some | Yes | - | - |
receive_from | - | Yes | Yes |
write_some | Yes | - | - |
send_to | - | Yes | Yes |
其它杂项功能
其它无关输入输出的功能函数如下:
local_endpoint():返回套接字的本地连接地址。
remote_endpoint():返回需要连接的远端地址。
native_handle():返回原始套接字句柄。你只想在不使用boost.asio的情况下使用原始套接字。
no_blocking():如果套接字是非阻塞的返回true,否则返回false。
native_no_blocking():如果套接字是非阻塞返回true,否则返回false。它是在本地调用原始套接字。通常不需要这个(no_blocking已经缓存了结果);如果使用native_hander使用他。
at_mark():如果套接字是读OOB数据返回true。很少需要这个。
其它
最后一点,套接字实例是无法复制的,复制构造函数和赋值重载(operator=)是无法调用的。
ip::tcp::socket s1(service),s2(service);
s1=s2;//编译期错误
ip::tcp::socket s3(s1);// 编译器错误
这是有道理的,每个实例拥有和管理资源(原始套接字本身)。如果允许复制构造,我们会结束两个拥有相同原始套接字的实例,他们需要某种管理机制(一个实例拥有所有权,或者引用计数,或者其它方法)。Boost.Aso选择不允许复制(如果你想使用副本,只能使用智能指针)。
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
socket_ptr sock1(new ip::tcp::socket(servcie));
socket_ptr sock2(sock1);
socket_ptr sock3;
sock3 = sock1;
套接字缓冲区
当从一个套接字读取或者向一个套接字写入数据时,需要一个缓冲区,存放输入输出数据。这个缓冲区存储器必须脱离I/O操作;你要确保在I/O操作期间或者超出作用域不会被释放。
在同步操作时很方便,当然发送和接受结束后,buff还是存在。
char buff[512];
...
sock.receive(buffer(buff));
strcpy(buff,”ok\n”);
sock.send(buffer(buff));
如下代码片段所示,异步操作就没这么简单了:
// 错误的代码
void on_read(const boost::system::error_code &err,std::size_t read_bytes){
...
}
void func(){
char buff[512];
sock.async_receive(buffer(buff),on_read);
}
调用async_receive()函数之后,buff将离开他的作用域,因此他的内存空间会被释放。当我们正在这个套接字上接受数据,我们将复制到一个不属于我们的内容中;它可以被释放,或者重新分配给其它代码需要的数据,因此,是错误的内存。
上述问题的几种解决方案:
使用全局缓冲区。
创佳一个缓冲区,在操作完成后释放它。
一个连接对象管理着套接字,和其它额外的数据,如buffer(s)。
第一个解决方案是不好的,因为我们直到全局变量是非常不好的。此外,如果两个处理器使用相同的缓冲区会发生什么?
这里是如何实现第二种解决方案:
void on_read(char *ptr,const boost::system::error_code &err, std::size_t read_bytes){
delete [] ptr;
}
...
char *buff=new char[512];
sock.async_receive(buffer(buf,512),bost::bind(on_read,buff,_1,_2));
或者你想让缓冲区离开作用域时自动完成操作(销毁?),使用智能指针:
struct shared_buffer{
boost::shared_array<char> buff;
int size;
shared_buffer(size_t size):buff(new char[size]),size(size){
}
mutable_buffers_1 asio_buff() const{
return buffer(buff.get(),size);
}
};
// 当前on_read离开作用域时,boost::bind的对象将被释放,
// 也将释放shared_buffer
void on_read(shared_buffer,const boost::system::error_code &err,std::size_t read_bytes){}
...
shared_buffer buff(512);
sock.async_receive(buff.asio_buff(),boost::bind(on_read,buff,_1,_2));
类shared_buffer用好内部类shared_array<>,一个shared_buffer的实例副本随着
shared_array<>一直存在,当离开作用域时,shared_array<>将自动销毁,这正式我们所需要的。
这正是你期待的,当调用完成处理程序时,Boost.Asio拷贝一个副本给完成处理程序。这个拷贝是boost::bind的一个函数参数(functor),他内部包含了一个我们给定的shared_buffer实例的拷贝。
第三个选项是使用连接对象管理套接字和相关数据。如缓冲区(buffers),通常是正确的,但是却很复杂。将在本章最后讨论。
缓冲区包装类(函数)
在你看过的代码片段中,每当执行读/写操作时都需要一个缓冲区(buffer),调用buffer()函数包装一个缓冲区对象。
char buff[512];
sock.async_receive(buffer(buff),on_read);
这主要包装了一个任意缓冲区给一个类以便Boost.Asio遍历缓冲区。可以使用如下的代码片段:
sock.async_receive(some_buffer,on_read);
some_buffer需要满足一定的条件,即(ConstBufferSequence 或者
MutableBufferSequence (可以查看Boost.Asio的文档)。自己创建满足这样条件的类是很复杂的,但是Boost.Asio已经提供了一些满足这些条件的类。不要直接访问他们,使用buffer()函数即可。
你可以使用buffer()的任意功能:
A char[] const array
A void* pointer and size in characters
An std::string string
An POD[] const array (POD stands for plain old data, meaning, constructor and destructor do nothing)
An std::vector array of any POD
A boost::array array of any POD
An std::array array of any POD
如下代码所示:
struct pod_sample{int i;long l;char c;}
...
char b1[512];
void *b2=new char[512];
std::string b3;b3.resize(128);
pod_sample b4[16];
std::vector<pod_sample> b5;b5.resize(16);
boost::array<pod_sample,16> b6;
std::array<pod_sample,16) b7;
sock.async_send(buffer(b1),on_read);
sock.async_send(buffer(b2,512),on_read);
sock.async_send(buffer(b3),on_read);
sock.async_send(buffer(b4),on_read);
sock.async_send(buffer(b5),on_read);
sock.async_send(buffer(b6),on_read);
sock.async_send(buffer(b7),on_read);
所有的一切,都不不要自己创建类来满足ConstBufferSequence 或者
MutableBufferSequence,你可以根据需要创建一个类,放入缓冲区,返回mutable_buffers_1实例,这就是前面我们讲的shared_buffer类。
读、写、连接函数
Boost.Asio提供处理I/O的函数,我们分为4组
The connect functions
这个函数连接到一个给定的端点上。
connect(socket,begin[,end][,condition]):函数尝试同步连接到列表中的每个端点上。begin迭代器是socket_type::resolver::query调用的结果(你可能需要再次看看EndPoint章节)。指定的end迭代器是可选项,你可以忽略它。在每个尝试连接前可以提供一个条件函数。函数签名Iterator connect_condition(const boost::system::error_code & err, Iterator next);你可以选择返回不同的迭代器,允许跳过某些端点。
async_connect(socket,begin[,end][,condition],handler):这个函数执行异步连接,在最后调用完成处理程序,处理程序签名:
void handler(const boost::system::error_code & err, Iterator iterator)。第二个参数传递给完成处理程序成功的端点,否则就是end迭代器。
例子如下代码所示:
using namespace boost::asio::ip;
tcp::resolver resolver(service);
tcp::resolver::iterator iter=resolver.resolve(tcp::resolver::query(“www.yahoo.com”,”80”));
tcp::socket sock(service);
connect(sock,iter);
一个主机可以解析为多个地址,因此connect和async_connect尝试连接每个地址,直到有个成功。
The read/write functions
这些函数从一个从流(stream)中读取,或者向流(stream)中写入(可以是一个套接字,或者任何有流行为的类)。
async_read(stream,buffer[,completion],handler):异步从流中读取。完成后,完成处理程序(handler)被调用。完成处理程序函数签名:
void handler(const boost::system::error_ code & err, size_t bytes)。你可以选择指定completion函数,completion函数在读取成功后调用,并且告诉Boost.Asio async_read完成了(如果不成功,继续读)。他的签名:
size_t completion(const boost::system::error_code& err, size_t bytes_transfered). 当completion函数返回0,我们认为读操作完成;如果返回非0值,表示下次调用async_read_some可以读取的最大字节数。
async_write(stream,buffer[,completion],handler):异步向流中写入数据。参数和aysnc_read类似。
read(stream,buffer[,completion]):同步从流中读取数据。参数和async_read类似。
write(stream,buffer[,completion]):同步向流中写入数据。参数和async_read类似。
async_read(stream, stream_buffer [, completion], handler)
async_write(strean, stream_buffer [, completion], handler)
write(stream, stream_buffer [, completion])
read(stream, stream_buffer [, completion])
首先,注意套接字实例,第一个参数是流(stream)。包括但是不限于套接字。例如可以不是一个套接字,可以是windows文件句柄。
每个读/写操作结束会触发这些条件的一个或者多个:
提供的缓冲区满了(读)或者缓冲区的数据被写入(写)。
completion函数返回0(如果你提供这样的函数)。
发生了错误。
下面的代码异步从套接字中读取,直到满足’\n’:
io_service servcie;
ip::tcp::socket socket(service);
char buff[512];
int offset = 0;
size_t up_to_enter(const boost::system::error_code &,size_t bytes){
for(size_t i=0;i<bytes;i++){
if(buff[i+offset]==’\n’)
return 0;
return 1;
}
}
void on_read(const boost::system::error_code &,size_t){}
...
async_read(sock,buffer(buff),up_to_enter,on_read);
Boost.Asio提供了几个completion的帮助函数。
transfer_at_least(n)
transfer_exactly(n)
transfer_all()
如下代码所示:
char buff[512];
void on_read(const boost::system::error_code&,size_t){}
// 读取整整32个字节。
async_read(sock,buffer(buff),transfer_exactly(32),on_read);
最后四个函数不使用常规的buffer,使用stream_buffer函数,Boost.Asio提供的
std::streambuf的继承类。STL 流(streams)和流缓冲(stream buffers)是非常灵活的,如下面代码所示:
io_service servcie;
void on_read(stream &buf,const boost::system::error_code &,size_t){
std::istream in(&buf);
std::string line;
std::getline(in,line);
std::cout << “first line:”<< line<<std::endl;
}
int main(int argc,char *argv[]){
HANDLE file=::CreateFile(“readme.txt”,GENERIC_READ,0,0,
OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPEND,0);
windows::stream_hander h(service,file);
Streambuf buf;
async_read(h,buf,transfer_exactly(256),
boost::bind(on_read,boost::ref(buf),_1,_2));
service.run();
}
这里,演示了async_read可以使用windows文件句柄。首先读取256个字节数据,然后存储在缓冲区。当读操作完成,on_read被调用,创建一个std::istream处理缓冲区,读取第一行,并且输出到控制台。
The read_until/async_read_until functions
这些函数在满足条件之前一直读:
async_read_until(stream,stream_buffer,delim,handler):开始一个异步读取操作。遇到delimeter读操作将停止。delimeter可以是任意字符,std::string或者boost::regex。完成处理程序标签:void handler(const boost::system::error_code & err, size_t bytes)。
async_read_until(stream,stream_buffer,completion,handler):这个函数和前面一个相同,但是使用completion函数替代delim。completion函数签名:
pair<iterator,bool> completion(iterator begin, iterator end);
这里iterator = is buffers_iterator<streambuf::const_buffers_type>。 你要记住的是,迭代器类型是 random-accessiterator。你可以扫描(begin,end)范围,以确定是否需要停止或者继续。返回一个pair,第一个参数是函数结束字符一个迭代器(
the first member will be an iterator passed at the end of the last character consumed by the function );第二个参数如果是true读操作将停止,否则继续。
read_until(stream,stream_buffer,delim):同步读取操作。参数和async_read_until类似。
read_until(stream,stream_buffer,comletion):同步读取操作。参数和
async_read_until类似。
下面的列子将读取ygie标识符号:
typedef buffers_iterator<streambuf::const_buffers_type> iterator;
std::pair<iterator,bool> match_punct(iterator begin,iterator end){
while(begin!=end){
if(std::ispunct(*begin))
return std::make_pair(begin,true);
}
return std::make_pair(end,false);
}
void on_read(const boost::system::error_code &,size_t ){}
....
streambuf buf;
async_read_until(sock,buf,match_punct,on_read);
如果向读取一个空格,修改最后一行:
async_read_until(sock,buff,’ ’,on_read);
The *_at functions
随机读写一个流的操作。从指定偏移位置读写。
async_read_at(stream,offset,buffer[,completion],handler):在一个流的指定偏移位置开始一个异步的读操作。当操作完成时,会调用完成处理程序(handler)。完成处理程序(handler)函数标签:void handler(const boost::system::error_code& err, size_t bytes) 。通常使用buffer()包装函数或者streambuf 函数。如果提供了completion函数,每次读取成功都会调用,并且告诉Boost.Asio async_read_at操作完成(如果不是,继续读)。completion函数签名:
size_t completion(const boost::system::error_code& err, size_t bytes)。如果
completion函数返回0,我们认为读操作完成;如果返回非0值,表示下一次调用
async_read_some_at的最大可以读取的字节数。
async_write_at(stream,offset,buffer[,completion],handler):启动一个异步写入操作。参数和async_read_at类似。
read_at(stream,offset,buffer[,completion]):一个给定的流上,从偏移位置开始读取。参数和async_read_at类似。
write_at(stream,offset,buffer[,completion]):在一个给定的流上,从偏移位置开始写入。参数和async_read_at类似。
这些函数不适用与套接字。他们处理随机读取流;换句话说,流可以随机访问。套接字显然不是这样的(套接字是只能向前的(forward-only))。
从文件中的256偏移位置读取128个字节:
io_service service;
int main(int argc,char*argv){
HANDLE file = ::CreateFile(“readme.txt”,GENERIC_READ,0,0,
OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,0);
windows::random_access_handle h(service,file);
streambuf buf;
read_at(h,256,buf,transfer_exactly(128));
std::istream in(&buf);
std::string line;
std::getline(in,line);
std::cout << “first line:”<<line<< std::endl;
}
异步编程
这个章节深入讨论异步编程时会遇到的问题。阅读一次后,简易回过头来重读一次,巩固这些概念。
The need for going asynchronous
正如之前所说,同步编程要比异步编程容易。这是因为他是线性的思路(调用函数A直到结束,调用函数B直到结束,等等。是事件处理思想)。在后一种情况下,你可以说有五个事件,不知道他们的执行顺序,甚至不知道他们都会执行。
异步编程尽管是很难的,但你还是会喜欢他,服务器需要处理大量的并发客户端连接。大量的并发客户端,异步要比同步编程容易。
有一个需要处理1000个并发客户端的服务器程序,消息在客户端和服务器来回传送,结束符为’\n’。
同步代码,1个线程:
using namespace boost::asio;
struct client{
ip::tcp::socket sock;
char buff[1024];//每个消息的最大长度
Int already_read;//已经读了多少
};
std::vector<client> clients;
void handle_client(){
while(true){
for(int i=0;i<clients.size();++i)
if(clients[i].sock.available())
on_read(clients[i]));
}
}
void on_read(client *c){
int to_read = std::min(1024-c.already_read,c.sock.available());
c.sock.read_some(buffer(c.buff+c.already_read,to_read));
c.already_read += to_read;
if(std::find(c.buff,c.buff+c.already_read,’\n’)<c.buff+c.alread_read){
int pos=std::find(c.buff,c.buff+c.alread_read,’\n’)-c.buff;
std::string msg(c.buff,c.buff+pos);
std::copy(c.buff+pos,c.buff+1024,c.buff);
c.alread_read-=pos;
on_read_msg(c,msg);
}
}
void on_read_msg(client &c,const std::string &msg){
// 分析消息,会写
if(msg ==”request_login”)
c.sock.write(“request_ok\n”);
else if
...
}
任何服务器都要避免的事情就是服务器反应缓慢(网络基本应用程序)。在例子中
handler_clients()功能尽可能少。如果函数阻塞在任意一点,后续客户端发送到来的消息需要等待阻塞结束才能继续处理他们。
为了保持响应,当有数据时,我们只读取一个套接字。
if(client[i].sock.available()) on_read(clients[i])。在on_read中,我们只读取可用的;调用read_until(c.sock,buffer(...),’\n’)是一个糟糕的注意。因为这会阻塞知道我们读取到完整的消息(我们不知道他什么时候会发生)。
这里的瓶颈在on_read_msg()函数;只要执行,任何到来的消息都放入。一个好的on_read_msg方法确保不会发生,但仍有可能发生(有时候向一个缓冲区写入数据会阻塞,直到缓冲区满了)。
同步代码:10个线程:
using namespace boost::asio;
struct client{
// ... 和前面一样
bool set_reading(){
boost::mutex::scoped_lock lk(cs_);
if(is_reading_) return false;// 已经读
else{is_reading_=true;return true;}
}
void unset_reading(){
boost::mutex::scoped_lock lk(cs_);
is_reading_ =false;
}
private:
boost::mutex cs_;
bool is_reading_;
};
std::vector<client> clients;
void handle_clients(){
for(int i=0;i<10;++i){
boost::thread(handle_clients_thread);
}
}
void hande_clients_thread(){
while(true)
for(int i=0;i<clients.size();++i)
if(clients[i].sock.available())
if(clients[i].set_reading()){
on_read(clients[i]);
clients[i].unset_reading();
}
}
void on_read(client &c){
// 和前面相同
}
void on_read_msg(client &c,const std::string &msg){
// 和前面相同
}
为了使用更多线程,我们需要同步,在set_reading()和set_unreading()函数中。
set_reading()函数是非常重要的。在一步内”测试并且标记读取“,如果有两个步骤,”测试读取“和”标记读取“,可能有两个线程对同一个客户端测试成功,然后两个线程调用同一个客户端的on_read函数,破坏了数据并且应用程序可能会崩溃。
你会注意到,代码变得越来越复杂。
同步的代码第三种写法,一个客户端一个线程,随着客户端增加,也会变得没有了(线程)。
让我们开始异步。我们不断的异步读取。当客户端发送一个请求时,on_msg被调用,然后回复,饭后等待下一次请求(做另一次异步操作)。
异步代码,10个线程:
using namespace boost::asio;
io_service service;
struct client{
ip::tcp::socket sock;
streambuf buff;
};
std::vector<client> clients;
void handle_clients(){
for(int i=0;i<clients.size();++i)
async_read_until(clients[i].sock,clienets[i].buff,’\n’,
boost::bind(on_read,clients[i],_1,_2));
for(int i=0;i<10;++i)
boost::thread(handle_clients_thread);
}
void handle_clients_thread(){
service.run();
}
void on_read(client &c,const error_code &err,size_t read_bytes){
std::istream in(&c.buff);
std::string msg;
std::getline(in,msg);
if(msg == “request_login”)
c.sock.async_write(“request_ok\n”,on_write);
else if...
...
// 现在,等待下一次客户端读取
async_read_until(c.sock,c.buff,’\n’,
boost::bind(on_read,c,_1,_2));
}
注意代码变得简单了。客户端结构体只有两个成员,handle_clients()函数只是调用async_read_until,然后创建10个线程,每个线程调用service.run()。这些线程 处理和调度异步读写操作。需要注意的是,on_read()将时刻准备下一个异步读取操作。
异步run(),run_on(),poll(),poll_on()
为了实现循环监听,io_service提供了4个方法,run(),run_one(),poll(),poll_one()。大部分时候使用service.run()。你将学习到其它函数实现了什么功能。
始终运行(Running forever)
启动run(),只要有挂起的操作它就会一直运行下去或者手动调用io_service::stop()。为了保持io_servcie实例一直运行,通常需要添加一个或者多个异步操作,并保持她们运行,如下代码所示:
using namespace boost::asio;
io_service service;
ip::tcp::socket sock(service);
char buff_read[1024],buff_write[1024]=”ok”;
void on_read(const boost::system::error_code &err,std::size_t bytes);
void on_write(const boost::system::error_code &err,std::size_t bytes){
sock.async_read_some(buffer(buff_read),on_read);
}
void on_read(const boost::sytem::error_code &err,std::size_t bytes){
//.....process the read
sock.async_write_some(buffer(buff_write,3),on_write);
}
void on_connect(const boost::system::error_code &err){
sock.async_read_some(buffer(buff_read),on_read);
}
int main(int argc,char *argv[]){
ip::tcp::endpoint ep(ip::address::from_string(“127.0.0.1”),2001);
sock.async_connect(ep,on_connect);
service.run();
}
当函数service.run()被调用,有一个异步操作被挂起,当套接字连接到一个服务器上,
on_connect被调用,这将增加一个异步操作。on_connect完成后,我们只剩下一个挂起操作(read)。当on_read被调用,我们回复,然后添加一个异步操作。当on_read完成,我们剩下一个挂起的操作(write)。当on_write被调用,向服务器发送一条消息,并且添加一个异步操作。当on_write完成,我们还有一个异步操作(read)。因此会一直循环,知道我们决定关闭应用程序。
The run_on(),poll(),poll_one()函数
你看过的异步操作和处理程序都需要先调用io_service::run()。这是比较简单的情况,90%到95%的情况你需要这样写。同样适用的调用run_one(),poll(),poll_one()。
run_one()函数执行调度最多一个异步操作:
如果没有挂起的操作,函数立即返回,并且返回0。
如果有挂起的操作,阻塞知道第一个操作执行,并且返回1。
可以使用下面的等价代码片段:
io_service service;
service.run();
while(!service.stoped())
service.run_one();
也可以使用run_one(),启动一个异步操作,并且等到他完成:
io_service servcie;
bool write_complete = false;
void on_write(const boost::system::error_code &err,size_t byte){
write_compleate=true;
}
...
std::string data = “login ok”;
write_complete = false;
async_write(sock,buffer(data),on_write);
do service.run_once()while(!write_complete);
也有一些例子使用run_one,Boost.Asio中的blocking_tcp_client.cpp和
blocking_udp_client.cpp。
poll_one函数执行最多一个挂起的操作,不阻塞。
如果至少有一个挂起的操作,准备无阻塞运行,poll_one运行返回1。
否则函数立即返回,并且返回0。
挂起的操作,准备无阻塞欲行,通常意味着:
定时器过期,aysnc_wait需要调用。
一个I/O操作完成(如async_read),它的处理程序需要调用。
一个自定义的操作添加到io_service实例的队列中(下一章节详细解释)。
你可以使用poll_one确保所有的I/O操作处理函数完成,然后需要做其它的工作:
io_service service;
while(true){
// run all handlers of completed IO operations
while(service.poll_one());
//... Do other work here
}
poll()执行所有的挂起操作,无阻塞,如下等价代码片段:
io_servcie service;
service.poll();//OR
while(service.poll_one());
在崩溃的时候,所有的操作都会抛出boost::system::system_error 异常。这不应该发生,在这里抛出一个异常通常是致命的,可能是一个资源错误,也可以是一个你自己的完成处理程序(handler)抛出一个异常。因此每个函数都不使用异常,而使用重载boost::system::error_
Code参数的函数,然后检查返回值。
io_service service;
boost::system::error_code err=0;
service.run(err);
if(err)
std::cout << “Error” << err << std::endl;
异步工作(Asynchronouse Work)
异步工作不仅仅是异步接受客户端连接到服务器,异步读写套接字。它包含任何的操作都可以异步执行。默认情况下你不知道每个异步操作处理程序的调用顺序。此外一个异步的调用(从套接字异步读写,接受连接)。你可以使用service.post()投递异步的调用。例如:
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <iostream>
using namespace boost::asio;
io_service service;
void func(int i){
std::cout << “func called ,i=”<< i<<std::endl;
}
void worker_thread(){
service.run();
}
int main(int argc ,char *argv[]){
for(int i=0;i<10;++i)
service.post(boost::bind(func,i));
boost::thread_group threads;
for(int i=0;i<3;++i)
threads.create_thread(worker_thread);
// wait for all thread to be created
boost::this_thread::sleep(boost::posix_time::millisec(500));
threads.join_all();
}
前面的例子中,service.post(some_function)添加了一些异步调用。函数立即返回,请求io_service实例调用给定的some_function函数,再一个线程中调用service.run()。在我们的例子中,我们首先创建了3个线程。不能顺序的调用异步函数。不能期望是post时的顺序。上面的例子的运行结果可能如下:
func called,i=0
func called,i=2
func called,i=1
func called,i=4
func called,i=3
func called,i=6
func called,i=7
func called,i=8
func called,i=5
func called,i=9
有时候想顺序执行一些异步操作。好比去餐厅,先下订单,然后吃。对于这点,使用io_servcie::strand,顺序执行你的异步操作。考虑下面的例子:
using namespace boost::asio;
io_service service;
void func(int i){
std::cout << “func called,i=”<< i<<”/”<< boost::this_thread::get_id()
<<std::endl;
}
void worker_thread(){
service.run();
}
int main(int argc,char *argv[]){
io_servcie::strand strand_one(servcie),strand_trw(service);
for(int i=0;i<5;i++)
service.post(strand_one.wrap(boost::binid(func,i)));
for(int i=0;i<5;i++)
service.post(strand_two.wrap(boost::bind(func,i)));
boost::thread_group threads;
for(int i=0;i<3;++i)
threads.create_thread(worker_thread);
// wait for all threads to created
boost::this_thread::sleep(boost::posix_time::millisec(500));
threads.join_all();
}
前面的代码中,我们确前5个和后5个是序列化的(were serialized),即调用1之前调用0,调用2之前调用1。同样调用6之前调用5,调用7之前调用6,以此类推。要注意的是,虽然是函数调用是序列化的,但是可以不在不同的线程中。输出可能如下:
func called, i= 0/002A60C8
func called, i= 5/002A6138
func called, i= 6/002A6530
func called, i= 1/002A6138
func called, i= 7/002A6530
func called, i= 2/002A6138
func called, i= 8/002A6530
func called, i= 3/002A6138
func called, i= 9/002A6530
func called, i= 4/002A6138
异步的post() 和 dispatch() 和wrap()
Boost.Asio提供三种添加异步完成函数的方法:
service.post(handler):函数确保请求io_service实例invoke函数后立即返回。函数随后在一个调用service.run()的线程中将被调用。
service.dispatch(handler):函数请求io_service实例调用给定的函数,但除此之外,在调用了service.run()线程中它可以执行处理程序(handler)。
service.wrap(handler):当调用servcie.dispatch(hander)函数创建一个包装行数(wrapper fu
nction)。这有点儿混乱,稍后解释。
你看见了上面的post的例子,连同可能的运行结果。我们修改它,看看service.dispatch的影响结果:
using namespace boost::asio;
io_service servcie;
void func(int i){
std::cout << “func called,i=”<<i<<std::endl;
}
void run_dispatch_and_post(){
for(int i=0;i<10;i+=2){
service.dispatch(boost::bind(func,i));
service.post(boost::bind(func,i+1));
}
}
int main(int argc,char *argv){
service.post(run_dispatch_and_post);
service.run();
}
在解释之前我们先看看结果:
func called, i= 0
func called, i= 2
func called, i= 4
func called, i= 6
func called, i= 8
func called, i= 1
func called, i= 3
func called, i= 5
func called, i= 7
func called, i= 9
偶数先输出,然后是奇数。这是因为我用dispatch()写偶数,用post写奇数。dispatch处理函数调用之前他已经返回了,因为当前线程调用了service.run(),而post总是立即返回。
现在,我们讨论service.wrap(handler)。wrap()函数返回一个functor,可作为一个参数传递给另外一个函数。
using namespace boost::asio;
io_service service;
void dispatched_func_1(){
std::cout << “dispatched 1” << std::endl;
}
void dispatched_func_2(){
std::cout<<”dispatched 2”<<std::endl;
}
void test(boost::function<void()> func){
std::cout << “test” << std::endl;
service.dispatch(dispatched_func_1);
func();
}
void service_run(){
service.run();
}
int main(int argc,char *argv[]){
test(service.wrap(dispatched_func_2));
boost::thread th(service_run);
boost::this_thread::sleep(boost::posix_time::millisec(500));
th.join();
}
行test(service.wrap(dispatched_func_2))将warp dispatched_func_2并且创建一个functor给函数test的参数。当test()被调用,它dispatch函数1,并且调用func(),这里,你会看到func()等效与service.dispatch(dispatched_func_2),因为是顺序调用的。输出结果肯定是:
test
dispatched 1
dispatched 2
io_service::strand类(用户序列化异步动作)也包含函数poll(),dispatch()和wrap()。他的意思和io_service的一样。大多数情况下使用io_service::strand::wrap作为io_service::poll()或者io_servcie::dispatch()的参数。
保持活跃(Staying alive)
你看下面的操作:
io_servcie service;
ip::tcp::socket sock(servcie);
char buff[512];
...
read(sock,buffer(buff));
在这种情况下,套接字(sock)和缓冲区(buff)在调用read是必须一直存在。换句话说调用read后他们必须是有效的。这正是讨论的焦点,所有传递给函数的参数在内部应该都是有效的。当我们使用异步时,事情越来越复杂:
io_servcie service;
ip::tcp::socket sock(service);
char buff[512];
void on_read(const boost::system::error_code &,size_t){}
...
async_read(sock,buffer(buff),on_read);
这种情况下,套接字(sock)和缓冲区(buff)在读取(read)操作自己内部必须有效,但我们不知道会发生什么,因为是异步的。
当使用套接字缓冲区,你可以使用一个缓冲区实例(buffer)给一个异步调用(使用
boost::shared_array<>)。我们可以使用同样的原则创建套接字读写缓冲区。然后所有的异步操作我们会传递一个boost::bind functor with a shared pointer:
using namespace boost::asio;
io_service service;
struct connection:boost::enable_shared_from_this<connection>{
typedef boost::system::error_code error_code;
typedef boost::shared_ptr<connection> ptr;
connect():sock_(service),started_(true){}
void start(ip::tcp::endpoint ep){
sock_.async_connect(ep,boost::bind(&connection::on_connect,
shared_from_this(),_1));
}
void stop(){
if(!started_) return;
started_ = false;
sock_.close();
}
bool started(){return started_;}
private:
void on_connect(const error_code &err){
// here you decide what to do with the connection:read or write
if(!err)
do_read();
else
stop();
}
void on_read(const error_code &err,size_t bytes){
if(!started()) return;
std::string msg(read_buffer_,bytes);
if(msg == “can_login”) do_write(“access_data”);
else if(msg.find(“data ”)==0) process_data(msg)
else if(msg == “login_fail”) stop();
}
void on_write(const error_code &err,size_t bytes){
do_read();
}
void do_read(){
sock.async_read_some(buffer(read_buffer_),
boost::bind(&connection::on_read,shared_from_this(),_1,_2));
}
void do_write(const std::string &msg){
if(!started()) return;
//注意:做另外一个async_read之前你想发送更多的消息,你需要另一个缓冲区
std::copy(msg.begin(),msg.end(),write_buffer_);
sock_.async_write_some_(buffer(write_buffer_,msg.size()),
boost::bind(&connection::on_write,shared_from_this(),_1,_2));
}
void process_data(const std::string &msg){
// 执行服务器的到来的消息,然后执行另外写
}
private:
ip::cp::socket sock_;
Enum {max_msg=1024};
char read_buffer_[max_msg];
char write_buffer_[max_msg];
bool started_;
};
int main(int argc,char* argv[]){
ip::tcp::endpoint ep(ip::address::from_string(“127.0.0.1”),8001);
Connection::ptr(new connection)->start(ep);
}
在所有的异步调用中,我们把boost::bind functor当作一个参数。在内部,functor保持一个connection实例的一个智能指针。只要有一个异步操作挂起,Boost.Asio保存一个boost::functor的一个副本,保持一个connection实例的指针指针,保持connection是活着的。
当然,connection只是一个框架(skeleton)类;你要改写它以适应不同的情况(在服务器中看起来不同)。
注意,很容易的创建一个connection实例,connection::ptr(new connection
)->start(ep)。这里启动一个异步连接服务器。当需要关闭连接时,调用stop()。
一旦实例启动(start()),它将等待连接。当连接发生,on_connect被调用。如果没有错误,继续read操作(do_read())。当read操作完成,解释消息;你的应用程序的on_read()会不同。当你写一个消息,你必须把他复制到缓冲区,并且像在on_write()中那样发送,因为,异步操需要存活的缓冲区。最后注意-当写时,必须要指定多少个字节要写,否则,将把缓冲区全部发送。
总结
网络API是非常庞大的。这里只是作为一个实现参考,在实现你的网络程序时,应该回过头来再看看。
Boost.Asio实现了端点的概念,可以想象为一个地址和端口。如果你不知道一个主机的确切IP地址,你可以使用一个解析器(resolver)对象解析一个主机名称,如www.yahoo.com一个活多个IP。
我们也看到了套接字类。这是核心API。Boost.Asio提供了TCP,UDP,ICMP,你可以扩展自己的协议;
异步编程是必要的。已经见过了为什么需要它,特别是写服务器程序。通常你会使用
service.run()去实现异步循环,但是你可能需要高级的,你可以使用run_one(),poll(),poll_one().
你也可以使用异步执行自己的函数,使用service.post或者servcie.dispath。
最后在整个异步操作期间,套接字和缓冲区必须一直存活(alive),我们需要才去特别的措施。你的connection类应该从enabled_shared_from_this集成,所有需要套接字和缓冲区的异步操作保存一个智能指针。
下一章节,演示更多的代码实现echo C/S程序。