Boost.Asio初步(二)

Network programming

Even though Boost.Asio can process any kind of data asynchronously, it is mainly used for network programming. This is because Boost.Asio supported network functions long before additional I/O objects were added. Network functions are a perfect use for asynchronous operations because the transmission of data over a network may take a long time, which means acknowledgments and errors may not be available as fast as the functions that send or receive data can execute.

Boost.Asio provides many I/O objects to develop network programs. Example 32.5 uses the class boost::asio::ip::tcp::socket to establish a connection with another computer. This example sends a HTTP request to a web server to download the homepage.

Boost.Asio主要是用来进行网络编程,即便它能异步处理任何类型的数据,在其他的I/O对象被加入进来之前的很长的时间,Boost.Asio就支持网络功能了。异步操作的网络功能是一个非常合适的应用,是因为数据在网络上传输需要花费较长的时间,也就意味着收发成功与失败的确认,不像收发函数调用那么快。

Boost.Asio提供了许多I/O对象来开发网络应用,示例32.5使用类boost::asio::ip::tcp::socket来建立与另一台计算机的连接,这个例子向web server发送了一条下载主页的HTTP请求。

Example 32.5. A web client with boost::asio::ip::tcp::socket

#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace boost::asio;
using namespace boost::asio::ip;

io_service ioservice;
tcp::resolver resolv{ioservice};
tcp::socket tcp_socket{ioservice};
std::array bytes;

void read_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
{
  if (!ec)
  {
    std::cout.write(bytes.data(), bytes_transferred);
    tcp_socket.async_read_some(buffer(bytes), read_handler);
  }
}

void connect_handler(const boost::system::error_code &ec)
{
  if (!ec)
  {
    std::string r =
      "GET / HTTP/1.1\r\nHost: theboostcpplibraries.com\r\n\r\n";
    write(tcp_socket, buffer(r));
    tcp_socket.async_read_some(buffer(bytes), read_handler);
  }
}

void resolve_handler(const boost::system::error_code &ec, tcp::resolver::iterator it)
{
  if (!ec)
    tcp_socket.async_connect(*it, connect_handler);
}

int main()
{
  tcp::resolver::query q{"theboostcpplibraries.com", "80"};
  resolv.async_resolve(q, resolve_handler);
  ioservice.run();
}

Example 32.5 uses three handlers: connect_handler() and read_handler() are called when the connection is established and data is received. resolve_handler() is used for name resolution.

Because data can only be received after a connection has been established, and because a connection can only be established after the name has been resolved, the various asynchronous operations are started in handlers. In resolve_handler(), the iterator it, which points to an endpoint resolved from the name, is used with tcp_socket to establish a connection. In connect_handler(), tcp_socket is accessed to send a HTTP request and start receiving data. Since all operations are asynchronous, handlers are passed to the respective functions. Depending on the operations, additional parameters may need to be passed. For example, the iterator it refers to an endpoint resolved from a name. The array bytes is used to store data received.

In main(), boost::asio::ip::tcp::resolver::query is instantiated to create an object q. q represents a query for the name resolver, an I/O object of type boost::asio::ip::tcp::resolver. By passing q to async_resolve(), an asynchronous operation is started to resolve the name. Example 32.5 resolves the name theboostcpplibraries.com. After the asynchronous operation has been started, run() is called on the I/O service object to pass control to the operating system.

When the name has been resolved, resolve_handler() is called. The handler first checks whether the name resolution has been successful. In this case ec is 0. Only then is the socket accessed to establish a connection. The address of the server to connect to is provided by the second parameter, which is of type boost::asio::ip::tcp::resolver::iterator. This parameter is the result of the name resolution.

The call to async_connect() is followed by a call to the handler connect_handler(). Again ec is checked first to find out whether a connection could be established. If so, async_read_some() is called on the socket. With this call, reading data begins. Data being received is stored in the array bytes, which is passed as a first parameter to async_read_some().

read_handler() is called when one or more bytes have been received and copied to bytes. The parameter bytes_transferred of type std::size_t contains the number of bytes that have been received. As usual, the handler should check first ec whether the asynchronous operation was completed successfully. Only if this is the case is data written to standard output.

Please note that read_handler() calls async_read_some() again after data has been written to std::cout. This is required because you can’t be sure that the entire homepage was downloaded and copied into bytes in a single asynchronous operation. The repeated calls to async_read_some() followed by the repeated calls to read_handler() only end when the connection is closed, which happens when the web server has sent the entire homepage. Then read_handler() reports an error in ec. At this point, no further data is written to std::cout and async_read() is not called on the socket. Because there are no pending asynchronous operations, the program exits.

示例32.5使用了3个句柄函数:连接建立时调用connect_handler(),数据到达时调用read_handler()域名解析时调用resolve_handler()。

数据接收仅发生在连接建立之后,连接建立仅发生在域名解析之后,各种异步操作都是在句柄函数中开始的。在resolve_handler()中,迭代器it指向一个已由name解析而来的endpoint,it和tcp_socket被用来建立连接。在connect_handler()中,tcp_socket被用来发送一个HTTP请求并开始接收数据。由于所有操作都是异步的,句柄函数被传递给相应函数,不同的操作,也许需要传递额外的参数,例如,指向由name解析出的endpoint的迭代器it。字节数组bytes是用来保存接收数据的。

main()中,创建了一个boost::asio::ip::tcp::resolver::query类型的实例q,q代表了域名解析查询器,它是一个boost::asio::ip::tcp::resolver类型的I/O对象。将q传递给async_resolve(),就开始了一个异步域名解析操作。示例32.5需要解析的域名是theboostcpplibraries.com。异步域名操作开始之后,调用I/O服务对象的run(),将控制权交给操作系统。

当域名解析完成后,resolve_handler()被调用,这个函数句柄首先检查域名是否能正确地被解析,即成功时ec是0,仅在此情况下,获得socket,进行建立连接,server的连接地址由第二个参数提供,类型为boost::asio::ip::tcp::resolver::iterator,是域名解析的结果。

对async_connect()的调用会导致connect_handler()被调用,同样,首先检查ec,看看是否连接已建立,如果如此,在这个socket上调用async_read_some(),接收数据操作开始,接收的数据保存在bytes数组中,它是传递给async_read_some()的第一个参数。

当接收到一个或多个字节的数据并拷贝到bytes后,read_handler()被调用,std::size_t类型的参数bytes_transferred含有接收到的字节数。一般情况下,这个句柄函数首先应检查ec,看看异步操作是否成功,仅在成功情况下,接收的数据输出到标准输出流上。

请注意,read_handler()在数据被输出到std::cout之后又一次调用async_read_some(),这样做是有必要的,因为你不能确定要下载的整个主页的内容能在一次异步操作中完成。不断地调用async_read_some()以及read_handler(),直到连接被关闭为止,read_handler()中的ec是一个错误值。此时web server已发送完整个主页,不再有数据输出到std::cout,async_read()也不再被调用。由于没有被挂起的异步操作,程序退出。

Example 32.6. A time server with boost::asio::ip::tcp::acceptor

#include 
#include 
#include 
#include 
#include 
#include 

using namespace boost::asio;
using namespace boost::asio::ip;

io_service ioservice;
tcp::endpoint tcp_endpoint{tcp::v4(), 2014};
tcp::acceptor tcp_acceptor{ioservice, tcp_endpoint};
tcp::socket tcp_socket{ioservice};
std::string data;

void write_handler(const boost::system::error_code &ec,
  std::size_t bytes_transferred)
{
  if (!ec)
    tcp_socket.shutdown(tcp::socket::shutdown_send);
}

void accept_handler(const boost::system::error_code &ec)
{
  if (!ec)
  {
    std::time_t now = std::time(nullptr);
    data = std::ctime(&now);
    async_write(tcp_socket, buffer(data), write_handler);
  }
}

int main()
{
  tcp_acceptor.listen();
  tcp_acceptor.async_accept(tcp_socket, accept_handler);
  ioservice.run();
}

Example 32.6 is a time server. You can connect with a telnet client to get the current time. Afterwards the time server shuts down.

The time server uses the I/O object boost::asio::ip::tcp::acceptor to accept an incoming connection from another program. You must initialize the object so it knows which protocol to use on which port. In the example, the variable tcp_endpoint of type boost::asio::ip::tcp::endpoint is used to tell tcp_acceptor to accept incoming connections of version 4 of the internet protocol on port 2014.

After the acceptor has been initialized, listen() is called to make the acceptor start listening. Then async_accept() is called to accept the first connection attempt. A socket has to be passed as a first parameter to async_accept(), which will be used to send and receive data on a new connection.

Once another program establishes a connection, accept_handler() is called. If the connection was established successfully, the current time is sent with boost::asio::async_write(). This function writes all data in data to the socket. boost::asio::ip::tcp::socket also provides the member function async_write_some(). This function calls the handler when at least one byte has been sent. Then the handler must check how many bytes were sent and how many still have to be sent. Then, once again, it has to call async_write_some(). Repeatedly calculating the number of bytes left to send and calling async_write_some() can be avoided by using boost::asio::async_write(). The asynchronous operation that started with this function is only complete when all bytes in data have been sent.

After the data has been sent, write_handler() is called. This function calls shutdown() with the parameter boost::asio::ip::tcp::socket::shutdown_send, which says the program is done sending data through the socket. Since there are no pending asynchronous operations, Example 32.6 exits. Please note that although data is only used in accept_handler(), it can’t be a local variable. data is passed by reference through boost::asio::buffer() to boost::asio::async_write(). When boost::asio::async_write() and accept_handler() return, the asynchronous operation has started, but has not completed. data must exist until the asynchronous operation has completed. If data is a global variable, this is guaranteed.

Exercise

Develop a client and a server which can transfer a file from one computer to another. When the server is started, it should display a list of IP addresses of all local interfaces and wait for the client to connect. When the client is started, an IP address from the server and the name of a local file should be passed as command line options. The client should transfer the file to the server which saves it to the current working directory. During transmission the client should display some sort of progress indicator so that the user knows that the transmission is ongoing. Implement the client and server with callbacks.

示例32.6是一个时间服务器。你可以通过telnet客户端连接,得到当前时间。然后,这个时间服务器停掉了。

这个时间服务器使用了I/O对象-boost::asio::ip::tcp::acceptor来接受来自其他程序的连接。你要初始化这个对象使它知道在哪个端口使用哪个协议。示例中,采用类型为boost::asio::ip::tcp::endpoint的变量tcp_endpoint来告诉tcp_acceptor在端口2014使用internet v4协议接受连接。

acceptor初始化后,listent()被调用,使acceptor开始侦听。当尝试接受第一个连接时,async_accept()被调用。Async_accept()的第一个参数是一个sokect句柄,它被用来在这个新连接上发送和接收数据。

一旦另一个程序建立连接,accept_handler()被调用。如果这个连接成功建立,就用boost::asio::async_write()发送当前时间,这个函数在socket上发送所有data中的数据。boost::asio::ip::tcp::socket也提供了另一个成员函数async_write_some(),该函数在至少发送一个字节后调用handler句柄函数,因此,句柄函数必须检查多少字节已被发,还剩多少要发,再一次说明,句柄函数要调用async_write_some()。使用boost::asio::async_write()能避免采用不断地判断待发的字节数和调用async_write_some()这种方式,这个异步操作仅当所有的字节都发送后才算完成。

所有字节都发送后,write_handler()被调用。这个函数以boost::asio::ip::tcp::socket::shutdown_send为参数调用shudown(),表明程序在这个sokect上已发送完数据。由于没有挂起的异步操作,示例32.6的程序就退出了。请注意,尽管变量data仅被用在accept_handler()函数中,它不能是一个局部变量。当boost::asio::async_write()和 accept_handler()返回时,异步操作就开始了,但并没有结束,data必须存在,直到异步操作完成为止,如果data是一个全局变量,就可以做到这点。

练习

开发一个客户端和服务器端程序,将文件从一台机器传输到另一台。当服务器运行时,它显示本地接口的一系列连接的IP地址,并且等待客户端连接。当客户端运行时,从命令行可以输入一个服务器的IP地址和本地的文件名,客户端将文件传输给服务器,服务器将其保存到当前工作目录。在传输过程中,客户端应显示一些传输进度的信息,以便用户了解传输正在进行。采用回调来实现客户端和服务器程序。

Coroutines

Since version 1.54.0, Boost.Asio supports coroutines. While you could use Boost.Coroutine directly, explicit support of coroutines in Boost.Asio makes it easier to use them.

Coroutines let you create a structure that mirrors the actual program logic. Asynchronous operations don’t split functions, because there are no handlers to define what should happen when an asynchronous operation completes. Instead of having handlers call each other, the program can use a sequential structure.

从1.54.0版起,Boost.Asio支持协程。当然,你可以直接用Boost.Coroutine,不过,对协程的显式支持使它在Boost.Asio中使用起来更容易。

Example 32.7. Coroutines with Boost.Asio

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace boost::asio;
using namespace boost::asio::ip;

io_service ioservice;
tcp::endpoint tcp_endpoint{tcp::v4(), 2014};
tcp::acceptor tcp_acceptor{ioservice, tcp_endpoint};
std::list tcp_sockets;

void do_write(tcp::socket &tcp_socket, yield_context yield)
{
  std::time_t now = std::time(nullptr);
  std::string data = std::ctime(&now);
  async_write(tcp_socket, buffer(data), yield);
  tcp_socket.shutdown(tcp::socket::shutdown_send);
}

void do_accept(yield_context yield)
{
  for (int i = 0; i < 2; ++i)
  {
    tcp_sockets.emplace_back(ioservice);
    tcp_acceptor.async_accept(tcp_sockets.back(), yield);
    spawn(ioservice, [](yield_context yield)
      { do_write(tcp_sockets.back(), yield); });
  }
}

int main()
{
  tcp_acceptor.listen();
  spawn(ioservice, do_accept);
  ioservice.run();
}

The function to call to use coroutines with Boost.Asio is boost::asio::spawn(). The first parameter passed must be an I/O service object. The second parameter is the function that will be the coroutine. This function must accept as its only parameter an object of type boost::asio::yield_context. It must have no return value. Example 32.7 uses do_accept() and do_write() as coroutines. If the function signature is different, as is the case for do_write(), you must use an adapter like std::bind or a lambda function.

Instead of a handler, you can pass an object of type boost::asio::yield_context to asynchronous functions. do_accept() passes the parameter yield to async_accept(). In do_write(), yield is passed to async_write(). These function calls still start asynchronous operations, but no handlers will be called when the operations complete. Instead, the context in which the asynchronous operations were started is restored. When these asynchronous operations complete, the program continues where it left off.

do_accept() contains a for loop. A new socket is passed to async_accept() every time the function is called. Once a client establishes a connection, do_write() is called as a coroutine with boost::asio::spawn() to send the current time to the client.

The for loop makes it easy to see that the program can serve two clients before it exits. Because the example is based on coroutines, the repeated execution of an asynchronous operation can be implemented in a for loop. This improves the readability of the program since you don’t have to trace potential calls to handlers to find out when the last asynchronous operation will be completed. If the time server needs to support more than two clients, only the for loop has to be adapted.

Exercise

Develop a client and a server which can transfer a file from one computer to another. When the server is started, it should display a list of IP addresses of all local interfaces and wait for the client to connect. When the client is started, an IP address from the server and the name of a local file should be passed as command line options. The client should transfer the file to the server which saves it to the current working directory. During transmission the client should display some sort of progress indicator so that the user knows that the transmission is ongoing. Implement the client and server with coroutines.

用Boost.Asio使用协程的函数调用是boost::asio::spawn()。第一个传递的参数必须是I/O服务对象,第二个参数是协程函数,这个函数必须接受一个boost::asio::yield_context类型的参数作为其唯一参数,且不能有返回值。示例32.7中,do_accept()和do_write()作为协程,如果函数类型不同,就像do_write()一样,你必须用std::bind或lambda函数来做一个适配器。

与句柄不同,你可以传递一个boost::asio::yield_context类型的对象给异步函数。do_accept()传递参数yield给async_accept(),在do_write()中,yield传递给async_write()。这些函数调用仍然开启异步操作,但操作完成后没有句柄函数被调用,而是将异步操作开始的上下文保存起来,当异步操作完成时,程序从它离开的地方继续运行。

do_accept()包含一个循环,当这个函数被每次被调用时,一个新的socket传递给async_accept()。一旦一个客户端建立了一个连接,用boost::asio::spawn()将do_write()作为协程,do_write()被调用,将当前时间发送给客户端。

很容易看出,for循环使程序在接受两个客户端后就退出了。由于这个示例基于协程,不断执行异步操作可以通过for循环来实现。这样提高了程序的可读性,因为你不需要关注当上一个异步操作完成后可能产生的句柄函数调用。如果这个时间服务器需要支持超过两个客户端,仅修改for循环就可以了。

练习

开发一个客户端和服务器端程序。【要求与上面练习相同】,采用协程来实现客户端和服务器端。

你可能感兴趣的:(C/C++)