Boost Asio Tutorial: Daytime

发布时间 2023-08-25 10:55:48作者: 起风了oc

学一下C++的 Boost.Asio

Daytime.0 如何调试自己编写的TCP/UDP客户端和服务器

教程默认我们会这个。万一有人不会呢,我说下我的方法:在 wsl 使用 nc 命令来调试,感觉挺方便的。

这里安利一下Linux的 nc 命令。nc 命令全称 netcat,很原始的一个工具,但是很方便。用法也很多,借助Linux的管道,能实现各种骚操作。

当然,也能用来调试各种明文协议的服务器、客户端。

  • 调试TCP客户端

    调试客户端需要一个TCP服务器。使用 nc 启动一个TCP服务器:

    # nc -l -p <port>
    # -l 表示监听模式,也就是启动一个服务器
    # -p 指定要监听的端口
    nc -l -p <port>
    # 在9999端口监听TCP连接
    nc -l -p 9999
    
  • 调试TCP服务器

    调试服务器需要一个TCP客户端。使用 nc 作为客户端连接服务器:

    # nc <server_ip> <port>
    # 连接到本地9999端口的TCP服务器
    nc 127.0.0.1 9999
    
  • 调试UDP客户端

    调试UDP客户端需要一个UDP服务器。使用 nc 启动一个UDP服务器:

    # nc -l -u -p <port>
    # -u 表示应用UDP协议
    # -k 表示允许服务端处理多个客户端socket。如果不加这个参数,那这个服务端收到一个UDP报文之后,再也无法接收下一个。
    nc -l -k -u -p 54321
    
  • 调试UDP服务器

    调试服务器需要一个UDP客户端。使用 nc 作为客户端连接服务器:

    # echo "Your message here" | nc -u -w1 <server_ip> <port>
    # -u 表示应用UDP协议
    # -w1 表示等待1秒以确保数据发送完毕
    echo "Hello!" | nc -u -w1 127.0.0.1 54321
    
Daytime.1 同步的TCP日期客户端

本教程展示如何使用 asio 来实现 TCP 客户端。

// 我们首先包含必要的头文件。

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

// 该应用程序的目的是访问日期服务,因此我们需要用户指定服务器。
int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }
        // 所有使用asio的程序都需要至少有一个I/O执行上下文,例如io_context对象。
        boost::asio::io_context io_context;
        // 我们需要将服务器名称转换为 TCP endpoint。为此,我们使用 ip::tcp::resolver 对象。
        tcp::resolver resolver(io_context);
        // resolver获取主机名和服务名称,并将它们转换为endpoint列表。我们使用 argv[1] 中指定的服务器名称和服务名称(在本例中为 "daytime" )执行解析调用。
        // resolver使用 ip::tcp::resolver::results_type 类型的对象返回endpoint列表。该对象是一个范围,有 begin() 和 end() 成员函数,可用于迭代。
        tcp::resolver::results_type endpoints = resolver.resolve(argv[1], "daytime");
        // 现在我们创建并连接套接字。
        // 上面获得的endpoint列表可能同时包含 IPv4 和 IPv6 endpoint,因此需要遍历一遍,直到找到一个有效的endpoint。
        // 这使得客户端程序独立于特定的IP版本。 boost::asio::connect() 函数会自动为我们完成此操作。
        tcp::socket socket(io_context);
        boost::asio::connect(socket, endpoints);
        // 连接已打开。我们现在需要做的就是读取时间服务的响应。
        // 我们使用 boost::array 来保存接收到的数据。
        // boost::asio::buffer() 函数自动确定数组的大小,以帮助防止缓冲区溢出。
        // 我们可以使用 char[] 或 std::vector 代替 boost::array 。
        for (;;) {
            boost::array<char, 128> buf{};
            boost::system::error_code error;

            size_t len = socket.read_some(boost::asio::buffer(buf), error);
            // 当服务器关闭连接时,ip::tcp::socket::read_some() 函数将退出并出现 boost::asio::error::eof 错误,这就是我们知道退出循环的方式。
            if (error == boost::asio::error::eof)
                break;
            else if (error)
                throw boost::system::system_error(error);

            std::cout.write(buf.data(), (long long) len);
        }
    }
    catch (std::exception &e) {
        // 最后,处理可能引发的任何异常。
        std::cerr << e.what() << std::endl;
    }
}
Daytime.2 同步的TCP日期服务器

本教程展示如何使用 asio 来实现 TCP 服务器。

#include <ctime>
#include <iostream>
#include <string>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

// 该函数用来创建要发送回客户端的字符串。
// 该函数将在我们所有的日间服务器应用程序中重用。
std::string make_daytime_string() {
    using namespace std; // For time_t, time and ctime;
    time_t now = time(nullptr);
    return ctime(&now);
}

int main() {
    try {
        boost::asio::io_context io_context;
        // 需要创建 ip::tcp::acceptor 对象来监听新连接。
        // 它被初始化为侦听 TCP 端口 13(IP 版本 4)。
        tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 13));
        // 死循环,一次处理一个连接,每次创建一个代表与客户端的连接的套接字,发送消息,然后等待新连接。
        for (;;) {
            tcp::socket socket(io_context);
            acceptor.accept(socket);
            // 客户端正在访问我们的服务。确定当前时间并将该信息传输给客户端。
            std::string message = make_daytime_string();

            boost::system::error_code ignored_error;
            boost::asio::write(socket, boost::asio::buffer(message), ignored_error);
        }
    }
    catch (std::exception &e) {
        // 最后,处理异常。
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
Daytime.3 异步的TCP日期服务器

asio 实现一个异步的TCP日期服务器。

#include <ctime>
#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/bind.hpp>

using boost::asio::ip::tcp;

// 该函数用来创建要发送回客户端的字符串。
std::string make_daytime_string() {
    using namespace std; // For time_t, time and ctime;
    time_t now = time(nullptr);
    return ctime(&now);
}

class tcp_connection : public boost::enable_shared_from_this<tcp_connection> {
public:
    // 我们将使用 shared_ptr 和 enable_shared_from_this
    // 因为我们希望只要存在引用它的操作,就使 tcp_connection 对象保持活动状态。
    typedef boost::shared_ptr<tcp_connection> pointer;

    static pointer create(boost::asio::io_context &io_context) {
        return pointer(new tcp_connection(io_context));
    }

    tcp::socket &socket() {
        return socket_;
    }

    // 在函数 start() 中,我们调用 boost::asio::async_write() 将数据提供给客户端。
    // 请注意,我们使用 boost::asio::async_write(),而不是 ip::tcp::socket::async_write_some() 来确保发送整个数据块。
    //
    // async_write:会在所有数据发送完毕后调用回调函数。
    // async_write_some:在数据发送过程中就可能调用回调函数,而不需要等待所有数据完全发送。
    //                  当部分数据被写入发送缓冲区时,就有可能触发回调函数。同时,在所有数据发送完毕后,也会触发回调函数。
    //                  async_write_some 在需要更细粒度的数据流控制或实时性要求较高的情况下非常有用。
    void start() {
        // 要发送的数据存储在类成员 message_ 中,因为我们需要在异步操作完成之前保持数据有效。
        // 如果将message定义为局部变量,在函数返回之后这个变量就会被销毁,它所占用的内存会被回收。
        // 当asio执行发送的时候,message很有可能已经被回收了。这时候就会出错。
        message_ = make_daytime_string();
        // 当启动异步操作时,如果使用 boost::bind(),则必须仅指定与处理程序的参数列表匹配的参数。
        // 在此程序中,这两个参数占位符(boost::asio::placeholders::error 和 boost::asio::placeholders::bytes_transferred)我们暂时用不到,
        // handle_write 函数的参数列表也没有接收这两个类型的参数,因此我们传递的这两个参数占位符会被bind方法丢弃
        // 事实上这两个参数完全可以去掉,因为handle_write没有用到。可以改成下面这行注释中的内容。
        // boost::asio::async_write(socket_, boost::asio::buffer(message_), boost::bind(&tcp_connection::handle_write, shared_from_this()))
        boost::asio::async_write(socket_, boost::asio::buffer(message_),
                                 boost::bind(&tcp_connection::handle_write, shared_from_this(),
                                             boost::asio::placeholders::error,
                                             boost::asio::placeholders::bytes_transferred));
        // 对这个socket连接的下一步操作由 handle_write() 负责。
    }

private:
    explicit tcp_connection(boost::asio::io_context &io_context) : socket_(io_context) {}

    void handle_write(const boost::system::error_code & /*error*/, size_t /*bytes_transferred*/) {}

    tcp::socket socket_;
    std::string message_;
};

class tcp_server {
public:
    // 构造函数初始化一个acceptor来监听 TCP 的13端口。
    explicit tcp_server(boost::asio::io_context &io_context)
            : io_context_(io_context),
              acceptor_(io_context, tcp::endpoint(tcp::v4(), 13)) {
        start_accept();
    }

private:
    // 函数 start_accept() 创建一个套接字并发起一个异步接受操作以等待新的连接。
    void start_accept() {
        tcp_connection::pointer new_connection = tcp_connection::create(io_context_);

        acceptor_.async_accept(new_connection->socket(), boost::bind(&tcp_server::handle_accept,
                                                                     this,
                                                                     new_connection,
                                                                     boost::asio::placeholders::error));
    }

    // 当 start_accept() 发起的异步接受操作完成时,调用函数 handle_accept() 。
    // 用于为客户端请求提供服务,然后调用 start_accept() 启动下一个accept操作。
    void handle_accept(tcp_connection::pointer new_connection, const boost::system::error_code &error) {
        if (!error) {
            new_connection->start();
        }

        start_accept();
    }

    tcp::acceptor acceptor_;
    boost::asio::io_context &io_context_;
};

int main() {
    try {
        // 我们需要创建一个tcp_server对象来接受传入的客户端连接。
        // io_context 对象为tcp_server提供 I/O 服务。
        boost::asio::io_context io_context;
        tcp_server server(io_context);
        // 运行 io_context 对象,开始执行异步操作。
        io_context.run();
    }
    catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
Daytime.4 同步的UDP日期客户端

本教程展示如何使用 asio 来实现 UDP 客户端。

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::udp;
using std::cout;
using std::endl;
using std::string;

const string Host = "127.0.0.1";
const string Port = "54321";

// 程序启动与 TCP 的示例基本相同。
int main() {
    try {
        boost::asio::io_context io_context;
        // 我们使用 ip::udp::resolver 对象根据主机和服务名称查找要使用的正确远程endpoint。
        // 服务名称是一个字符串,可以填端口。
        // 该查询被 ip::udp::v4() 参数限制为仅返回 IPv4 endpoint。
        udp::resolver resolver(io_context);
        // 如果 ip::udp::resolver::resolve() 函数没有失败,则保证至少返回列表中的一个端点。所以直接取值是安全的。
        udp::endpoint receiver_endpoint = *resolver.resolve(udp::v4(), Host, Port).begin();

        // 由于 UDP 是面向数据报的,因此我们不会使用流套接字。
        // 创建 ip::udp::socket 并启动与远程端点的联系。
        udp::socket socket(io_context);
        socket.open(udp::v4());

        boost::array<char, 6> send_buf = {{'0', '1', '2', '3', '4', 0}};
        socket.send_to(boost::asio::buffer(send_buf), receiver_endpoint);

        // 现在我们需要准备好接受服务器发回给我们的数据。
        // 我们这边接收服务器响应的endpoint将由 ip::udp::socket::receive_from() 初始化。
        // 注意:使用nc命令启动的UDP服务器,无法对数据报进行回复,所以这段代码用nc启动的服务器没办法测试
        // 需要充分测试的话,得继续教程,自己把UDP服务器写出来
        boost::array<char, 128> recv_buf;
        udp::endpoint sender_endpoint;
        size_t len = socket.receive_from(boost::asio::buffer(recv_buf), sender_endpoint);

        std::cout.write(recv_buf.data(), len);
    }
    catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
Daytime.5 同步的UDP日期服务器

本教程展示如何使用 asio 来实现一个同步的 UDP 服务器程序。

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::udp;
using std::cout;
using std::endl;
using std::string;

// 该函数用来创建要发送回客户端的字符串。
std::string make_daytime_string() {
    using namespace std; // For time_t, time and ctime;
    time_t now = time(nullptr);
    return ctime(&now);
}

int main() {
    try {
        boost::asio::io_context io_context;
        // 创建一个 ip::udp::socket 对象用来接收 UDP 端口 13 上的请求。
        udp::socket socket(io_context, udp::endpoint(udp::v4(), 13));
        // 等待客户端发送数据。Remote_endpoint 对象将由 ip::udp::socket::receive_from() 填充。
        for (;;) {
            boost::array<char, 1> recv_buf;
            udp::endpoint remote_endpoint;
            socket.receive_from(boost::asio::buffer(recv_buf), remote_endpoint);
            // 确定我们要发回给客户端的内容。
            std::string message = make_daytime_string();
            boost::system::error_code ignored_error;
            // 将响应发送到remote_endpoint。
            socket.send_to(boost::asio::buffer(message), remote_endpoint, 0, ignored_error);
        }
    }
    catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
Daytime.6 异步的UDP日期服务器

本教程展示如何使用 asio 来实现一个异步的 UDP 服务器程序。

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/bind.hpp>

using boost::asio::ip::udp;
using std::cout;
using std::endl;
using std::string;

// 该函数用来创建要发送回客户端的字符串。
std::string make_daytime_string() {
    using namespace std; // For time_t, time and ctime;
    time_t now = time(nullptr);
    return ctime(&now);
}

class udp_server {
public:
    // 构造函数初始化一个套接字,并监听 UDP 13端口。
    explicit udp_server(boost::asio::io_context &io_context)
            : socket_(io_context, udp::endpoint(udp::v4(), 13)) {
        start_receive();
    }

private:
    void start_receive() {
        // 函数 ip::udp::socket::async_receive_from() 将导致程序在后台监听新请求。
        // 每当收到请求,io_context对象会调用带有两个参数的 handle_receive() 函数:
        // 一个boost::system::error_code类型的值,指示操作是成功还是失败,以及一个 size_t 指定接收的字节数。
        socket_.async_receive_from(
                boost::asio::buffer(recv_buffer_), remote_endpoint_,
                boost::bind(&udp_server::handle_receive,
                            this,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
    }

    // 函数 handle_receive() 将为客户端请求提供服务。
    void handle_receive(const boost::system::error_code &error, std::size_t /*bytes_transferred*/) {
        // error 参数包含异步操作的结果。
        // 由于我们只提供 1 字节的 recv_buffer_ 来接收客户端的请求,因此如果客户端发送更大的内容,io_context 对象会返回错误。
        // 如果出现这样的错误,我们可以忽略它。
        if (!error) {
            // 确定我们要发送的内容。
            boost::shared_ptr<std::string> message(new std::string(make_daytime_string()));
            // 现在调用 ip::udp::socket::async_send_to() 将数据发送给客户端。
            socket_.async_send_to(boost::asio::buffer(*message),
                                  remote_endpoint_,
                                  boost::bind(&udp_server::handle_send, this, message,
                                              boost::asio::placeholders::error,
                                              boost::asio::placeholders::bytes_transferred));
            // 当启动异步操作时,如果使用 boost::bind(),则必须仅指定与回调函数的参数列表匹配的参数。
            // 在此程序中,两个参数占位符(boost::asio::placeholders::error 和 boost::asio::placeholders::bytes_transferred)会被忽略。
            // 也就是说,这里传递的这两个参数是多余的,bind函数不会把它们绑定给handle_send。(不知道教程为什么老是强调这个。。。

            // 开始监听下一个客户端请求。
            start_receive();
            // 针对这个请求的下一步操作,现在由 handle_send() 负责。
        }
    }

    // 服务请求完成后会调用函数 handle_send() 。
    void handle_send(boost::shared_ptr<std::string> /*message*/, const boost::system::error_code & /*error*/,
                     std::size_t /*bytes_transferred*/) {
    }

    udp::socket socket_;
    udp::endpoint remote_endpoint_;
    boost::array<char, 1> recv_buffer_;
};


int main() {
    try {
        // 创建一个udp_server对象来接受传入的客户端请求,并运行 io_context 对象。
        boost::asio::io_context io_context;
        udp_server server(io_context);
        io_context.run();
    }
    catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
Daytime.7 组合的TCP/UDP日期服务器

本教程展示如何将我们刚刚编写的两个异步服务器组合到一个服务器应用程序中,使用一个端口,同时接收TCP连接和UDP数据报。

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/enable_shared_from_this.hpp>

using boost::asio::ip::tcp;
using boost::asio::ip::udp;
using std::cout;
using std::endl;
using std::string;

// 该函数用来创建要发送回客户端的字符串。
std::string make_daytime_string() {
    using namespace std; // For time_t, time and ctime;
    time_t now = time(nullptr);
    return ctime(&now);
}

// 取自 Daytime.3
class tcp_connection : public boost::enable_shared_from_this<tcp_connection> {
public:
    typedef boost::shared_ptr<tcp_connection> pointer;

    static pointer create(boost::asio::io_context &io_context) {
        return pointer(new tcp_connection(io_context));
    }

    tcp::socket &socket() {
        return socket_;
    }

    void start() {
        message_ = make_daytime_string();

        boost::asio::async_write(socket_, boost::asio::buffer(message_),
                                 boost::bind(&tcp_connection::handle_write, shared_from_this()));
    }

private:
    explicit tcp_connection(boost::asio::io_context &io_context)
            : socket_(io_context) {
    }

    void handle_write() {
    }

    tcp::socket socket_;
    std::string message_;
};

// 取自 Daytime.3
class tcp_server {
public:
    explicit tcp_server(boost::asio::io_context &io_context)
            : io_context_(io_context),
              acceptor_(io_context, tcp::endpoint(tcp::v4(), 13)) {
        start_accept();
    }

private:
    void start_accept() {
        tcp_connection::pointer new_connection =
                tcp_connection::create(io_context_);

        acceptor_.async_accept(new_connection->socket(),
                               boost::bind(&tcp_server::handle_accept, this, new_connection,
                                           boost::asio::placeholders::error));
    }

    void handle_accept(tcp_connection::pointer new_connection,
                       const boost::system::error_code &error) {
        if (!error) {
            new_connection->start();
        }

        start_accept();
    }

    boost::asio::io_context &io_context_;
    tcp::acceptor acceptor_;
};

// 取自 Daytime.6
class udp_server {
public:
    explicit udp_server(boost::asio::io_context &io_context)
            : socket_(io_context, udp::endpoint(udp::v4(), 13)) {
        start_receive();
    }

private:
    void start_receive() {
        socket_.async_receive_from(
                boost::asio::buffer(recv_buffer_), remote_endpoint_,
                boost::bind(&udp_server::handle_receive, this,
                            boost::asio::placeholders::error));
    }

    void handle_receive(const boost::system::error_code &error) {
        if (!error) {
            boost::shared_ptr<std::string> message(
                    new std::string(make_daytime_string()));

            socket_.async_send_to(boost::asio::buffer(*message), remote_endpoint_,
                                  boost::bind(&udp_server::handle_send, this, message));

            start_receive();
        }
    }

    void handle_send(boost::shared_ptr<std::string> /*message*/) {
    }

    udp::socket socket_;
    udp::endpoint remote_endpoint_;
    boost::array<char, 1> recv_buffer_;
};


int main() {
    try {
        boost::asio::io_context io_context;
        // 首先创建一个服务器对象来接受 TCP 客户端连接。
        tcp_server server1(io_context);
        // 还需要一个服务器对象来接受 UDP 客户端请求。
        udp_server server2(io_context);
        io_context.run();
    }
    catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}