译者序:
一个英语从未及格的程序员,学习Boost.Asio而苦啃,留下只言片语,只为他日重品。
地道的中国式英语,看客可不屑。
美丽的分隔线
-------------------------------------------------
在这一章节中,需要确切知道什么时候使用Boost.Asio,并且深入的研究异步编程,这会比同步编程更加有趣。
本节讲解在使用Boost.Asio写一个网络应用程序时需要知道哪些内容。
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地址,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”);
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.protocol()< 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 ip::udp::socket=basic_stream_socket ip::icmp::socket=basic_raw_socket 所有的同步函数都有一个重载,抛出异常或者返回错误代码。如下代码片段: 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()< 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 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 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 boost::array std::array 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组 这个函数连接到一个给定的端点上。 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尝试连接每个地址,直到有个成功。 这些函数从一个从流(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 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< } 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处理缓冲区,读取第一行,并且输出到控制台。 这些函数在满足条件之前一直读: 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 = is buffers_iterator 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 std::pair 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); 随机读写一个流的操作。从指定偏移位置读写。 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:”< } 这个章节深入讨论异步编程时会遇到的问题。阅读一次后,简易回过头来重读一次,巩固这些概念。 正如之前所说,同步编程要比异步编程容易。这是因为他是线性的思路(调用函数A直到结束,调用函数B直到结束,等等。是事件处理思想)。在后一种情况下,你可以说有五个事件,不知道他们的执行顺序,甚至不知道他们都会执行。 异步编程尽管是很难的,但你还是会喜欢他,服务器需要处理大量的并发客户端连接。大量的并发客户端,异步要比同步编程容易。 有一个需要处理1000个并发客户端的服务器程序,消息在客户端和服务器来回传送,结束符为’\n’。 同步代码,1个线程: using namespace boost::asio; struct client{ ip::tcp::socket sock; char buff[1024];//每个消息的最大长度 Int already_read;//已经读了多少 }; std::vector void handle_client(){ while(true){ for(int i=0;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’) 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 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 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 void handle_clients(){ for(int i=0;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()将时刻准备下一个异步读取操作。 为了实现循环监听,io_service提供了4个方法,run(),run_one(),poll(),poll_one()。大部分时候使用service.run()。你将学习到其它函数实现了什么功能。 启动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)。因此会一直循环,知道我们决定关闭应用程序。 你看过的异步操作和处理程序都需要先调用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; 异步工作不仅仅是异步接受客户端连接到服务器,异步读写套接字。它包含任何的操作都可以异步执行。默认情况下你不知道每个异步操作处理程序的调用顺序。此外一个异步的调用(从套接字异步读写,接受连接)。你可以使用service.post()投递异步的调用。例如: #include #include #include #include using namespace boost::asio; io_service service; void func(int i){ std::cout << “func called ,i=”<< i< } 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() < } 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 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=”<
} 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”< } void test(boost::function 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()的参数。 你看下面的操作: 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 typedef boost::system::error_code error_code; typedef boost::shared_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程序。Sockets
同步错误代码
套接字成员函数
其它
读、写、连接函数
The connect functions
The read/write functions
The read_until/async_read_until functions
The *_at functions
异步编程
The need for going asynchronous
异步run(),run_on(),poll(),poll_on()
始终运行(Running forever)
The run_on(),poll(),poll_one()函数
异步工作(Asynchronouse Work)
异步的post() 和 dispatch() 和wrap()
保持活跃(Staying alive)
总结