Boost.Asio C++ Network Programming(Chapter 2)

来源:互联网 发布:数据集中存储的好处 编辑:程序博客网 时间:2024/05/21 19:47

译者序:

        一个英语从未及格的程序员,学习Boost.Asio而苦啃,留下只言片语,只为他日重品。

        地道的中国式英语,看客可不屑。

美丽的分隔线

-------------------------------------------------

第二章Boost.Asio基础

在这一章节中,需要确切知道什么时候使用Boost.Asio,并且深入的研究异步编程,这会比同步编程更加有趣。

The Network API

本节讲解在使用Boost.Asio写一个网络应用程序时需要知道哪些内容。

Boost.Asio namespaces

Boost.Asio下的所有都在boost::asio命名空间下,或者是子命名空间下:

boost:asio:包含核心类和函数。重要的类io_servicestreambuf。我们也有任意的函数

(free functions),如read,read_at,read_until,他们的异步同伴,和writewrite的异步同伴函数。

boost::asio::ip:包含网络部分。包含重要的类如:addressendpointtcpudpicmp还有任意函数(free functionsconnectasync_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::addressip::address_v4ip::address_v6类。

它提供了相当多重要的功能:

ip::address(v4_or_v6_address):转换ip_v4ip_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::tcpip::udpip::icmp,当然也是可以扩展的。你可以创建自己的套接字,是复杂的。如果选择这样做,看看

boost/asio/ip/tcp.hpp,boost/asio/ip/udp.hpp,boost/asio/ip/icmp.hpp,他们都是内部

typedef关键字很小的类。

你会想到ip::tcpip::udpip::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():返回可以不阻塞的同步读取多个字节。

讨论缓冲区。检验flagsflags默认值为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);

一个主机可以解析为多个地址,因此connectasync_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函数替代delimcompletion函数签名:

pair<iterator,bool> completion(iterator begin, iterator end)

这里iterator = is buffers_iterator<streambuf::const_buffers_type>。 你要记住的是,迭代器类型是 random-accessiterator。你可以扫描(beginend)范围,以确定是否需要停止或者继续。返回一个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_oneBoost.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

我们也看到了套接字类。这是核心APIBoost.Asio提供了TCP,UDP,ICMP,你可以扩展自己的协议;

异步编程是必要的。已经见过了为什么需要它,特别是写服务器程序。通常你会使用

service.run()去实现异步循环,但是你可能需要高级的,你可以使用run_one(),poll(),poll_one().

你也可以使用异步执行自己的函数,使用service.post或者servcie.dispath

最后在整个异步操作期间,套接字和缓冲区必须一直存活(alive),我们需要才去特别的措施。你的connection类应该从enabled_shared_from_this集成,所有需要套接字和缓冲区的异步操作保存一个智能指针。

下一章节,演示更多的代码实现echo C/S程序。


0 0
原创粉丝点击