• 7.4. 网络编程

    7.4. 网络编程

    虽然 Boost.Asio 是一个可以异步处理任何种类数据的库,但是它主要被用于网络编程。 这是由于,事实上 Boost.Asio 在加入其它 I/O 对象之前很久就已经支持网络功能了。 网络功能是异步处理的一个很好的例子,因为通过网络进行数据传输可能会需要较长时间,从而不能直接获得确认或错误条件。

    Boost.Asio 提供了多个 I/O 对象以开发网络应用。 以下例子使用了 boost::asio::ip::tcp::socket 类来建立与中另一台PC的连接,并下载 'Highscore' 主页;就象一个浏览器在指向 www.highscore.de 时所要做的。

    1. #include <boost/asio.hpp>
    2. #include <boost/array.hpp>
    3. #include <iostream>
    4. #include <string>
    5.  
    6. boost::asio::io_service io_service;
    7. boost::asio::ip::tcp::resolver resolver(io_service);
    8. boost::asio::ip::tcp::socket sock(io_service);
    9. boost::array<char, 4096> buffer;
    10.  
    11. void read_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
    12. {
    13. if (!ec)
    14. {
    15. std::cout << std::string(buffer.data(), bytes_transferred) << std::endl;
    16. sock.async_read_some(boost::asio::buffer(buffer), read_handler);
    17. }
    18. }
    19.  
    20. void connect_handler(const boost::system::error_code &ec)
    21. {
    22. if (!ec)
    23. {
    24. boost::asio::write(sock, boost::asio::buffer("GET / HTTP 1.1\r\nHost: highscore.de\r\n\r\n"));
    25. sock.async_read_some(boost::asio::buffer(buffer), read_handler);
    26. }
    27. }
    28.  
    29. void resolve_handler(const boost::system::error_code &ec, boost::asio::ip::tcp::resolver::iterator it)
    30. {
    31. if (!ec)
    32. {
    33. sock.async_connect(*it, connect_handler);
    34. }
    35. }
    36.  
    37. int main()
    38. {
    39. boost::asio::ip::tcp::resolver::query query("www.highscore.de", "80");
    40. resolver.async_resolve(query, resolve_handler);
    41. io_service.run();
    42. }
    • 下载源代码

    这个程序最明显的部分是三个句柄的使用:connect_handler()read_handler() 函数会分别在连接被建立后以及接收到数据后被调用。 那么为什么需要 resolve_handler() 函数呢?

    互联网使用了所谓的IP地址来标识每台PC。 IP地址实际上只是一长串数字,难以记住。 而记住象 www.highscore.de 这样的名字就容易得多。 为了在互联网上使用类似的名字,需要通过一个叫作域名解析的过程将它们翻译成相应的IP地址。 这个过程由所谓的域名解析器来完成,对应的 I/O 对象是:boost::asio::ip::tcp::resolver

    域名解析也是一个需要连接到互联网的过程。 有些专门的PC,被称为DNS服务器,其作用就象是电话本,它知晓哪个IP地址被赋给了哪台PC。 由于这个过程本身的透明的,只要明白其背后的概念以及为何需要 boost::asio::ip::tcp::resolver I/O 对象就可以了。 由于域名解析不是发生在本地的,所以它也被实现为一个异步操作。 一旦域名解析成功或被某个错误中断,resolve_handler() 函数就会被调用。

    因为接收数据需要一个成功的连接,进而需要一次成功的域名解析,所以这三个不同的异步操作要以三个不同的句柄来启动。 resolvehandler() 访问 I/O 对象 _sock,用由迭代器 it 所提供的解析后地址创建一个连接。 而 sock 也在 connecthandler() 的内部被使用,发送 HTTP 请求并启动数据的接收。 因为所有这些操作都是异步的,各个句柄的名字被作为参数传递。 取决于各个句柄,需要相应的其它参数,如指向解析后地址的迭代器 _it 或用于保存接收到的数据的缓冲区 buffer

    开始执行后,该应用将创建一个类型为 boost::asio::ip::tcp::resolver::query 的对象 query,表示一个查询,其中含有名字 www.highscore.de 以及互联网常用的端口80。 这个查询被传递给 async_resolve() 方法以解析该名字。 最后,main() 只要调用 I/O 服务的 run() 方法,将控制交给操作系统进行异步操作即可。

    当域名解析的过程完成后,resolvehandler() 被调用,检查域名是否能被解析。 如果解析成功,则存有错误条件的对象 _ec 被设为0。 只有在这种情况下,才会相应地访问 socket 以创建连接。 服务器的地址是通过类型为 boost::asio::ip::tcp::resolver::iterator 的第二个参数来提供的。

    调用了 asyncconnect() 方法之后,connect_handler() 会被自动调用。 在该句柄的内部,会访问 _ec 对象以检查连接是否已建立。 如果连接是有效的,则对相应的 socket 调用 async_read_some() 方法,启动读数据操作。 为了保存接收到的数据,要提供一个缓冲区作为第一个参数。 在以上例子中,缓冲区的类型是 boost::array,它来自 Boost C++ 库 Array,定义于 boost/array.hpp.

    每当有一个或多个字节被接收并保存至缓冲区时,readhandler() 函数就会被调用。 准确的字节数通过 std::size_t 类型的参数 _bytes_transferred 给出。 同样的规则,该句柄应该首先看看参数 ec 以检查有没有接收错误。 如果是成功接收,则将数据写出至标准输出流。

    请留意,readhandler() 在将数据写出至 _std::cout 之后,会再次调用 async_read_some() 方法。 这是必需的,因为无法保证仅在一次异步操作中就可以接收到整个网页。 async_read_some()read_handler() 的交替调用只有当连接被破坏时才中止,如当 web 服务器已经传送完整个网页时。 这种情况下,在 read_handler() 内部将报告一个错误,以防止进一步将数据输出至标准输出流,以及进一步对该 socket 调用 async_read() 方法。 这时该例程将停止,因为没有更多的异步操作了。

    上个例子是用来取出 www.highscore.de 的网页的,而下一个例子则示范了一个简单的 web 服务器。 其主要差别在于,这个应用不会连接至其它PC,而是等待连接。

    1. #include <boost/asio.hpp>
    2. #include <string>
    3.  
    4. boost::asio::io_service io_service;
    5. boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), 80);
    6. boost::asio::ip::tcp::acceptor acceptor(io_service, endpoint);
    7. boost::asio::ip::tcp::socket sock(io_service);
    8. std::string data = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
    9.  
    10. void write_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
    11. {
    12. }
    13.  
    14. void accept_handler(const boost::system::error_code &ec)
    15. {
    16. if (!ec)
    17. {
    18. boost::asio::async_write(sock, boost::asio::buffer(data), write_handler);
    19. }
    20. }
    21.  
    22. int main()
    23. {
    24. acceptor.listen();
    25. acceptor.async_accept(sock, accept_handler);
    26. io_service.run();
    27. }
    • 下载源代码

    类型为 boost::asio::ip::tcp::acceptor 的 I/O 对象 acceptor - 被初始化为指定的协议和端口号 - 用于等待从其它PC传入的连接。 初始化工作是通过 endpoint 对象完成的,该对象的类型为 boost::asio::ip::tcp::endpoint,将本例子中的接收器配置为使用端口80来等待 IP v4 的传入连接,这是 WWW 通常所使用的端口和协议。

    接收器初始化完成后,main() 首先调用 listen() 方法将接收器置于接收状态,然后再用 async_accept() 方法等待初始连接。 用于发送和接收数据的 socket 被作为第一个参数传递。

    当一个PC试图建立一个连接时,accepthandler() 被自动调用。 如果该连接请求成功,就执行自由函数 boost::asio::async_write() 来通过 socket 发送保存在 _data 中的信息。 boost::asio::ip::tcp::socket 还有一个名为 async_write_some() 的方法也可以发送数据;不过它会在发送了至少一个字节之后调用相关联的句柄。 该句柄需要计算还剩余多少字节,并反复调用 async_write_some() 直至所有字节发送完毕。 而使用 boost::asio::async_write() 可以避免这些,因为这个异步操作仅在缓冲区的所有字节都被发送后才结束。

    在这个例子中,当所有数据发送完毕,空函数 write_handler() 将被调用。 由于所有异步操作都已完成,所以应用程序终止。 与其它PC的连接也被相应关闭。