(本文整理自https://legacy.gitbook.com/book/mmoaay/boost-asio-cpp-network-programming-chinese/details)
Boost Asio是一个很强大的实现socket通讯方式的跨平台(windows、linux、solaris、mac os x)解决方案,能同时支持数千个并发的连接。Boost Asio很好地封装了伯克利BSD socket api,支持常用的TCP,UDP,IMCP等协议,并能够使用自定义扩展协议。而且,Boost Asio使用也很简单,只需要引入头文件就可以使用。
其中TCP部分定义了一些用于TCP通信的typedef类型,包括端点类endpoint、套接字类socket、流类iostream,以及接收器acceptor、解析器resolver等。其他通信协议与TCP类似。
endpoint包含IP地址和通信用的端口号。
socket可以在构造时指定使用的协议或者endpoint,或者稍后调用成员函数connect()。连接成功后可以用local_endpoint()和remote_endpoint()获得连接两端的端点信息,用available()获取可读取的字节数,用receive()/read_some()和send()/write_some()读写数据,当操作完成后使用close()函数关闭socket。
acceptor对应Socket API的accept()函数,用于服务器端。acceptor可以像传统socket API一样使用,open()打开端口,bind()绑定再用listen()侦听端口,但更方便的是使用它的构造函数,传入endpoint直接完成这三个动作。
Boost Asio在网路通信、COM串口和文件上成功地抽象了输入输出的概念。你可以基于这些进行同步或异步的输入输出编程,这些函数支持传入包含任意内容的流实例,而不仅仅是一个socket。
read(stream, buffer [, extra options]) async_read(stream, buffer [, extra options], handler) write(stream, buffer [, extra options]) async_write(stream, buffer [, extra options], handler)
同步还是异步?
在构造网络应用时,必要的一步是根据交互场景的需求,决定采取同步还是异步方式进行编程。在同步语境下,socket中的操作是阻塞的,依赖于交互方给出反馈才能继续执行,所以为了不影响主程序的继续运行,一般会创建一个或多个线程来处理在socket上的读写。也就是,同步的服务器/客户端通常是多线程的。
相反的,异步编程是事件驱动的。程序会提供一个回调函数给主程序,当回调函数监听的事件发生时,回调会被调用,并返回操作结果。你需要考虑的是采用阻塞调用和多线程的方式(同步,通常比较简单),或者是更少的线程和事件驱动(异步,通常更复杂)。
下面是一个最基础的同步客户端的例子:
using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.connect(ep);
首先,你的程序至少需要一个io_service实例同底层操作系统的IO服务进行交互。然后指定你想要连接的地址和端口,再建立socket。
下面是一个简单的同步服务器的例子:
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // listen on 2001
ip::tcp::acceptor acc(service, ep);
while ( true) {
socket_ptr sock(new ip::tcp::socket(service));
acc.accept(*sock);
boost::thread( boost::bind(client_session, sock));
}
void client_session(socket_ptr sock) {
while ( true) {
char data[512];
size_t len = sock->read_some(buffer(data));
if ( len > 0)
write(*sock, buffer("ok", 2));
}
}
首先,同样是至少需要一个io_service实例,然后指定你想要监听的端口,再创建一个接收器用来接收客户端的连接。在接下来的循环中,你创建一个虚拟的socket来等待客户端的连接,当一个连接被建立时,你创建一个线程来处理这个连接:如获取请求,进行解析和返回结果。
接下去看一个异步的客户端例子:
using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.async_connect(ep, connect_handler);
service.run();
void connect_handler(const boost::system::error_code & ec) {
// 如果ec返回成功我们就可以知道连接成功了
}
与同步例子不同的是,通过调用函数async_connect注册了回调函数connect_handler。当异步操作完成时,connect_handler被调用,检查错误代码(ec),如果成功,客户端可以向服务端进行异步地写入。
下面的代码是一个基本的异步服务端例子:
using boost::asio;
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // 监听端口2001
ip::tcp::acceptor acc(service, ep);
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
service.run();
void start_accept(socket_ptr sock) {
acc.async_accept(*sock, boost::bind( handle_accept, sock, _1) );
}
void handle_accept(socket_ptr sock, const boost::system::error_code &
err) {
if ( err) return;
// 从这里开始, 你可以从socket读取或者写入
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
}
这里很巧妙的是,在使用这个socket之后,你创建了一个新的socket,然后再次调用start_accept(),用来创建另外一个“等待客户端连接“的异步操作,从而使service.run()循环一直保持忙碌状态。
套接字缓冲区
当从一个套接字读写内容时,你需要一个缓冲区,用来保存读取和写入的数据。缓冲区内存的有效时间必须比I/O操作的时间要长;你需要保证它们在I/O操作结束之前不被释放。
// 非常差劲的代码 ...
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就已经超出有效范围,它的内存当然会被释放。当我们开始从套接字接收一些数据时,我们会把它们拷贝到一片已经不属于我们的内存中;它可能会被释放,或者被其他代码重新开辟来存入其他的数据,结果就是:内存冲突。
一种有效的解决方式是使用共享指针,让缓冲区在操作结束后自动释放。
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对象被释放了,
// 同时也会释放共享指针
void on_read(shared_buffer, const boost::system::error_code & err, std::size_t read_bytes) {}
sock.async_receive(buff.asio_buff(), boost::bind(on_read,buff,_1,_2));
shared_buffer类拥有实质的shared_array<>,shared_array<>存在的目的是用来保存shared_buffer实例的拷贝-当最后一个share_array<>元素超出范围时,shared_array<>就被自动销毁了,而这就是我们想要的结果。
因为Boost.Asio会给完成处理句柄保留一个拷贝,当操作完成时就会调用这个完成处理句柄,所以你的目的达到了。那个拷贝是一个boost::bind的仿函数,它拥有着实际的shared_buffer实例。这是非常优雅的!