Boost.Asio的所有内容都包含在boost::asio命名空间或者其子命名空间内。
boost::asio:这是核心类和函数所在的地方。
boost::asio::ip:这是网络通信部分所在的地方。
boost::asio::error:这个命名空间包含了调用I/O例程时返回的错误码
boost::asio::ssl:包含了SSL处理类的命名空间
boost::asio::local:这个命名空间包含了POSIX特性的类
boost::asio::windows:这个命名空间包含了Windows特性的类
Boost.Asio提供了ip::address , ip::address_v4和ip::address_v6类。 它们提供的重要函数如下:
经常会使用的函数是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"); // 错误,异常
端点是使用某个端口连接到的一个地址。
不同类型的socket有它自己的endpoint类,比如ip::tcp::endpoint、ip::udp::endpoint和ip::icmp::endpoint
:
例如创建一个本机80端口的端点:ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 80);
。
有三种方式建立端点:
endpoint():默认构造函数,可以用来创建UDP/ICMP socket。
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;
首先,为要查询的名字创建一个查询器,然后用resolve()函数解析它。
如果成功,它至少会返回一个入口。可以利用返回的迭代器,使用第一个入口或者遍历整个列表来拿到全部的入口。
给定一个端点,可以获得他的地址,端口和IP协议(v4或者v6):std::cout << ep.address().to_string() << ":" << ep.port() << "/" << ep.protocol() << std::endl;
Boost.Asio有三种类型的套接字类:ip::tcp, ip::udp和ip::icmp。使用它们可以便捷地访问其类或者函数,如下:
socket类创建一个相应的socket。而且总是在构造的时候传入io_service实例:
io_service service;
ip::udp::socket sock(service)
sock.set_option(ip::udp::socket::reuse_address(true));
这些方法是用来连接或绑定socket、断开socket字连接以及查询连接是活动还是非活动的:
示例:
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 80);
ip::tcp::socket sock(service);
sock.open(ip::tcp::v4());
sock.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();
在套接字上执行I/O操作的函数。分为同步函数和异步函数。
对于异步函数来说,处理程序的格式void handler(const boost::system::error_code& e, size_t bytes)
都是一样的,如下:
async_receive(buffer, [flags,] handler)
async_read_some(buffer, handler)
async_receive_from(buffer, endpoint[, flags], handler)
async_send(buffer [, flags], handler)
async_write_some(buffer, handler)
async_send_to(buffer, endpoint, handler)
receive(buffer [, flags])
read_some(buffer)
receive_from(buffer, endpoint [, flags])
send(buffer [, flags])
write_some(buffer)
send_to(buffer, endpoint [, flags])
available()
其中,buffer是数据缓冲区(很重要,稍后在一切中介绍),flags是标记,默认为0,可以是以下值:
常用的可能是message_peek,使用方法请参照下面的代码片段:
char buff[1024];
sock.receive(buffer(buff), ip::tcp::socket::message_peek );
memset(buff,1024, 0);
// 重新读取之前已经读取过的内容
sock.receive(buffer(buff) );
用来处理套接字的高级选项:
可以获取/设置的套接字选项:
名字 | 描述 | 类型 |
---|---|---|
reuse_address | 如果为true,套接字能绑定到一个已用的地址 | bool |
send_buffer_size | 套接字发送缓冲区大小 | int |
receive_buffer_size | 套接字接收缓冲区大小 | int |
send_low_watermark | 规定套接字数据发送的最小字节数 | int |
receive_low_watemark | 规定套接字输入处理的最小字节数 | int |
ip::v6_only | 如果为true,则只允许IPv6的连接 | bool |
linger | 如果为true,套接字会在有未发送数据的情况下挂起close() | bool |
keep_alive | 如果为true,会发送心跳 | bool |
do_not_route | 如果为true,则阻止路由选择只使用本地接口 | bool |
enable_connection_aborted | 如果为true,记录在accept()时中断的连接 | bool |
broadcast | 如果为true,允许广播消息 | bool |
debug | 如果为true,启用套接字级别的调试 | bool |
使用示例:
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 80);
ip::tcp::socket sock(service);
sock.connect(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);
套接字实例不能被拷贝,因为拷贝构造方法和=操作符是不可访问的:
ip::tcp::socket s1(service), s2(service);
s1 = s2; // 编译时报错
ip::tcp::socket s3(s1); // 编译时报错
这是非常有意义的,因为每一个实例都拥有并管理着一个资源(原生套接字本身)。
如果允许拷贝构造,结果是会有两个实例拥有同样的原生套接字;这样就需要去处理所有者的问题(让一个实例拥有所有权?或者使用引用计数?还是其他的方法)。
Boost.Asio选择不允许拷贝(如果你想要创建一个备份,请使用共享指针)。
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
socket_ptr sock1(new ip::tcp::socket(service));
socket_ptr sock2(sock1); // ok
socket_ptr sock3;
sock3 = sock1; // ok
当从一个套接字读写内容时,需要一个缓冲区,用来保存读取和写入的数据。
缓冲区内存的有效时间必须比I/O操作的时间要长,需要保证它们在I/O操作结束之前不被释放。否则操作未知内存,就要段错误啦。
对于同步操作来说,这很容易,这个缓冲区在receive和send时都存在。如下:
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)
{
// 数据到来后开始读取到buff,但此时buff已经被释放,导致操作未知内存
}
void func() {
char buff[512];
sock.async_receive(buffer(buff), on_read); // 启动异步接收数据
// 因为是异步操作,真正的数据接收操作发生在何时是未知的
} //此时buff的作用域结束,buff被释放
有几种方法可以解决这个问题:
从之前的代码中可以看到,当需要对一个buffer进行读写操作时,代码会把实际的缓冲区对象封装在一个buffer()方法中,然后再把它传递给方法调用:
char buff[512];
sock.async_receive(buffer(buff), on_read);
因为Boost.Asio的接收缓冲区需要满足一定的需求,叫做ConstBufferSequence或者MutableBufferSequence。使用buffer()方法能达到目的。
以下类型都可以包装到buffer()方法中:
示例如下:
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);
主要介绍connect/read/write函数。
async_read(stream, buffer [, completion] ,handler)
异步地从一个流读取。结束时其处理方法被调用。
处理方法的格式是:void handler(const boost::system::error_ code & err, size_t bytes);。
可以选择指定一个完成处理方法。完成处理方法会在每个read操作调用成功之后调用,然后告诉Boost.Asio async_read操作是否完成(如果没有完成,它会继续读取)。
格式是:size_t completion(const boost::system::error_code& err, size_t bytes_transfered) 。当这个完成处理方法返回0时,我们认为read操作完成;如果它返回一个非0值,它表示了下一个async_read_some操作需要从流中读取的字节数。
read(stream, buffer [, completion])
async_write(stream, buffer [, completion], handler)
write(stream, buffer [, completion])
注意第一个参数变成了流,而不单是socket。这个参数包含了socket但不仅仅是socket。比如,你可以用一个Windows的文件句柄来替代socket。
当下面情况出现时,所有read和write操作都会结束:
下面的代码会异步地从一个socket中间读取数据直到读取到’\n’:
io_service service;
ip::tcp::socket sock(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也提供了一些简单的完成处理仿函数:
示例:
char buff[512];
void on_read(const boost::system::error_code &, size_t) {
}
// 读取32个字节
async_read(sock, buffer(buff), transfer_exactly(32), on_read);
同步编程比异步编程简单很多。这是因为,线性的思考是很简单的。
尽管异步编程更难,但是你会更倾向于选择使用它,比如,写一个需要处理很多并发访问的服务端时,并发访问越多,异步编程就比同步编程越简单。
在任何服务端(和任何基于网络的应用)都需要避免的,就是代码无响应的情况。同步编程意味着多线程,多线程意味着同步和锁,锁意味着性能消耗。在异步编程中就不用考虑这些问题了。
如果有等待执行的操作,run()会一直执行,直到你手动调用io_service::stop()。
为了保证io_service一直执行,通常添加一个或者多个异步操作,然后在它们被执行时,继续一直不停地添加异步操作,比如下面代码:
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::system::error_code &err, std::size_t bytes)
{
// ... 处理读取操作 ...
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();
}
异步工作不仅仅指用异步地方式接受客户端到服务端的连接、异步地从一个socket读取或者写入到socket。它包含了所有可以异步执行的操作。
通常是不知道每个异步handler的调用顺序的。除了通常的异步调用(来自异步socket的读取/写入/接收)。
可以使用io_service::strand会让异步方法被顺序调用(但这并不意味着它们都在同一个线程执行)。
三种添加异步操作的方法:
在使用某个变量时,它应该在有效作用域内,这一点不难理解。这在异步编程中尤其重要,因为稍不留神,就有可能犯下操作已经释放的内存的错误。
如下示例:
// sock和buff的存在时间都必须比read()操作本身时间要长,但是read操作持续的时间我们是不知道的,因为它是异步的。
io_service 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);
当使用socket缓冲区的时候,你会有一个buffer实例在异步调用时一直存在(使用boost::shared_array<>)。
在这里,我们可以使用同样的方式,通过创建一个类并在其内部管理socket和它的读写缓冲区。
然后,对于所有的异步操作,传递一个包含智能指针的boost::bind仿函数给它:
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;
connection() : 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) {
// 这里你决定用这个连接做什么: 读取或者写入
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操作之前你想要发送多个消息,
// 所以你需要多个写入buffer
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::tcp::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.Asio实现了端点的概念,可以认为是IP和端口。如果不知道准确的IP,可以使用resolver对象将主机名。
API的核心——socket类。Boost.Asio提供了TCP、UDP和 ICMP的实现。
异步编程是刚需。你应该已经明白为什么有时候需要用到它,尤其在写服务端的时候。
调用service.run()来实现异步循环就已经可以让你很满足,但是有时候你需要更进一步,尝试使用run_one()、poll()或者poll_one()。
实现异步时,你可以异步执行你自己的方法;使用service.post()或者service.dispatch()。
最后,为了使socket和缓冲区(read或者write)在整个异步操作的生命周期中一直活动,连接类需要继承自enabled_shared_from_this,然后在内部保存它需要的缓冲区,而且每次异步调用都要传递一个智能指针给this操作。
编程新手容易在最后一点上栽跟头。其实,即使了有经验的编程人员,在面对程序崩溃时gdb打印出的boost栈信息时,也不会兴奋。
本文理论偏多,读一遍有个印象,可以在编程尝试中遇到问题而不知如何解决时,再来回顾一下。
Boost.Asio基本原理