【问题标题】:HTTP proxy example in C++C++ 中的 HTTP 代理示例
【发布时间】:2020-11-21 08:58:15
【问题描述】:

所以我一直在尝试使用 boost.asio 在 C++ 中编写代理。我的初始项目包括将字符串消息写入套接字的客户端、接收此消息并将字符串消息写入套接字的服务器,以及与上述两个套接字一起工作的代理。

代理代码如下所示(未来的意图是处理多个连接并以某种方式使用传输的数据,回调将执行一些实际工作而不是日志记录):

#include "commondata.h"
#include <boost/bind.hpp>
#include <boost/enable_shared_from_this.hpp>

using namespace boost::asio;
using ip::tcp;
using std::cout;
using std::endl;


class con_handler : public boost::enable_shared_from_this<con_handler> {
private:
    tcp::socket client_socket;
    tcp::socket server_socket;
    enum { max_length = 1024 };
    char client_data[max_length];
    char server_data[max_length];

public:
    typedef boost::shared_ptr<con_handler> pointer;
    con_handler(boost::asio::io_service& io_service):
            server_socket(io_service),
            client_socket(io_service) {
        memset(client_data, 0, max_length);
        memset(server_data, 0, max_length);
        server_socket.connect( tcp::endpoint( boost::asio::ip::address::from_string(SERVERIP), SERVERPORT ));
    }
// creating the pointer
    static pointer create(boost::asio::io_service& io_service) {
        return pointer(new con_handler(io_service));
    }
//socket creation
    tcp::socket& socket() {
        return client_socket;
    }

    void start() {
        //read the data into the input buffer
        client_socket.async_read_some(
                boost::asio::buffer(client_data, max_length),
                boost::bind(&con_handler::handle_read,
                            shared_from_this(),
                            client_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        server_socket.async_write_some(
                boost::asio::buffer(client_data, max_length),
                boost::bind(&con_handler::handle_write,
                            shared_from_this(),
                            client_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        server_socket.async_read_some(
                boost::asio::buffer(server_data, max_length),
                boost::bind(&con_handler::handle_read,
                            shared_from_this(),
                            server_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
        client_socket.async_write_some(
                boost::asio::buffer(server_data, max_length),
                boost::bind(&con_handler::handle_write,
                            shared_from_this(),
                            server_data,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
    }

    void handle_read(const char* data, const boost::system::error_code& err, size_t bytes_transferred) {
        if (!err) {
            cout << "proxy handle_read" << endl;
            cout << data << endl;
        } else {
            std::cerr << "error: " << err.message() << std::endl;
            client_socket.close();
        }
    }

    void handle_write(const char* data, const boost::system::error_code& err, size_t bytes_transferred) {
        if (!err) {
            cout << "proxy handle_write" << endl;
            cout << data << endl;
        } else {
            std::cerr << "error: " << err.message() << endl;
            client_socket.close();
        }
    }
};


class Server {
private:
    boost::asio::io_service io_service;
    tcp::acceptor acceptor_;
    void start_accept() {
        // socket
        con_handler::pointer connection = con_handler::create(io_service);

        // asynchronous accept operation and wait for a new connection.
        acceptor_.async_accept(connection->socket(),
                               boost::bind(&Server::handle_accept, this, connection,
                                           boost::asio::placeholders::error));
    }

public:
//constructor for accepting connection from client
    Server()
            : acceptor_(io_service, tcp::endpoint(tcp::v4(), PROXYPORT)) {
        start_accept();
    }

    void handle_accept(const con_handler::pointer& connection, const boost::system::error_code& err) {
        if (!err) {
            connection->start();
        }
        start_accept();
    }
    boost::asio::io_service& get_io_service() {
        return io_service;
    }
};


int main(int argc, char *argv[]) {
    try {
        Server server;
        server.get_io_service().run();
    } catch(std::exception& e) {
        std::cerr << e.what() << endl;
    }
    return 0;
}

如果发送的消息是字符串(我最初用它来测试我的代码是否可以正常工作),那么所有的回调都会按照我希望它们被调用的方式被调用,并且事情似乎正在工作。

这是该案例的代理标准输出:

user@laptop:$ ./proxy 
proxy handle_read
message from the client
proxy handle_write
message from the client
proxy handle_read
message from server
proxy handle_write
message from server

所以客户端发送“来自客户端的消息”字符串,由代理接收并保存,同样的字符串发送到服务器,然后服务器发回“来自服务器的消息”字符串,这也是由代理接收并保存,然后发送给客户端。

当我尝试使用实际的 Web 服务器 (Apache) 和 JMeter 之类的应用程序相互通信时,就会出现问题。这是本案例的标准输出:

user@laptop:$ ./proxy 
proxy handle_write

proxy handle_write

proxy handle_read
GET / HTTP/1.1
Connection: keep-alive
Host: 127.0.0.1:1337
User-Agent: Apache-HttpClient/4.5.5 (Java/11.0.8)


error: End of file

JMeter 测试随后因超时而失败(即代理收到 EOF 错误时),并且似乎没有数据发送到 apache 网络服务器。我现在的问题是,与发送字符串消息的情况相比,为什么以另一种顺序调用回调,以及为什么数据没有被传输到服务器套接字,我猜。提前感谢您的帮助!

【问题讨论】:

标签: c++ sockets http boost


【解决方案1】:

缩写为start()

    client_socket.async_read_some  (buffer(client_data), ...);
    server_socket.async_write_some (buffer(client_data), ...);
    server_socket.async_read_some  (buffer(server_data), ...);
    client_socket.async_write_some (buffer(server_data), ...);
    //read the data into the input 
    client_socket.async_read_some  (buffer(client_data), ...);
    server_socket.async_write_some (buffer(client_data), ...);
    server_socket.async_read_some  (buffer(server_data), ...);
    client_socket.async_write_some (buffer(server_data), ...);

这...不是异步操作的工作方式。它们是异步运行的,这意味着它们都会立即返回。

您正在同时从一些缓冲区读取和写入,而无需等待有效数据。此外,无论收到多少,您总是在写入完整的缓冲区。

所有这些都拼写Undefined Behaviour

从简单开始

从概念上讲,您只是想阅读:

void start() {
    //read the data into the input buffer
    client_socket.async_read_some(
            boost::asio::buffer(client_data, max_length),
            boost::bind(&con_handler::handle_read,
                        shared_from_this(),
                        client_data,
                        boost::asio::placeholders::error,
                        boost::asio::placeholders::bytes_transferred));
}

现在,一旦您收到数据,您可能想要转发该数据:

void handle_read(const char* data, const boost::system::error_code& err, size_t bytes_transferred) {
    if (!err) {
        std::cout << "proxy handle_read" << std::endl;
        server_socket.async_write_some(
                boost::asio::buffer(client_data, bytes_transferred),
                boost::bind(&con_handler::handle_write,
                    shared_from_this(),
                    client_data,
                    boost::asio::placeholders::error,
                    boost::asio::placeholders::bytes_transferred));
    } else {
        std::cerr << "error: " << err.message() << std::endl;
        client_socket.close();
    }
}

请注意,仅在出现错误时关闭连接的一侧似乎有点武断。你可能至少想要cancel() 任何异步操作,可选shutdown() 然后让shared_ptr 破坏你的con_handler

全双工

现在,对于全双工操作,您可以确实同时启动反向继电器。在单独的方法中维护调用链有点笨拙(毕竟您不仅要切换缓冲区,还要切换套接字对)。

意识到你在做同样的事情两次可能是有益的:

client -> [...buffer...] -> server

server -> [...buffer...] -> client

您可以将每一面都封装在一个类中,避免重复所有代码:

struct relay {
    tcp::socket &from, &to;
    std::array<char, max_length> buf{};

    void run_relay(pointer self) {
      from.async_read_some(asio::buffer(buf),
          [this, self](error_code ec, size_t n) {
              if (ec) return handle(from, ec);

              /*
               *std::cout 
               *  << "From " << from.remote_endpoint()
               *  << ": " << std::quoted(std::string_view(buf.data(), n))
               *  << std::endl;
               */
              async_write(to, asio::buffer(buf, n), [this, self](error_code ec, size_t) {
                  if (ec) return handle(to, ec);
                  run_relay(self);
              });
          });
    }

    void handle(tcp::socket& which, error_code ec = {}) {
        if (ec == asio::error::eof) {
            // soft "error" - allow write to complete
            std::cout << "EOF on " << which.remote_endpoint() << std::endl;
            which.shutdown(tcp::socket::shutdown_receive, ec);
        }

        if (ec) {
            from.cancel();
            to.cancel();

            std::string reason = ec.message();
            auto fep = from.remote_endpoint(ec),
                 tep = to.remote_endpoint(ec);
            std::cout << "Stopped relay " << fep << " -> " << tep << " due to " << reason << std::endl;
        }
    }
} c_to_s {client_socket, server_socket, {0}}, 
  s_to_c {server_socket, client_socket, {0}};

注意

  • 我们使用 lambdas 避开了 bind 的混乱
  • 我们在出错时取消中继的两端
  • 我们使用std::array 缓冲区 - 更安全、更易于使用
  • 无论缓冲区大小如何,我们只写入接收到的字节数
  • 在写入完成之前,我们不会安排另一次读取,以避免破坏buf 中的数据

让我们执行con_handler重新开始

使用上面的relay

void start() {
    c_to_s.run_relay(shared_from_this());
    s_to_c.run_relay(shared_from_this());
}

就是这样。我们传递自己,所以con_handler 保持活动状态,直到所有操作完成。

演示Live On Coliru

#define PROXYPORT 8899
#define SERVERIP "173.203.57.63" // coliru IP at the time
#define SERVERPORT 80
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <iomanip>

namespace asio  = boost::asio;
using boost::asio::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;

class con_handler : public boost::enable_shared_from_this<con_handler> {
  public:
    con_handler(asio::io_service& io_service):
        server_socket(io_service),
        client_socket(io_service)
    {
        server_socket.connect({ asio::ip::address::from_string(SERVERIP), SERVERPORT });
    }
    // creating the pointer
    using pointer = boost::shared_ptr<con_handler>;
    static pointer create(asio::io_service& io_service) {
        return pointer(new con_handler(io_service));
    }

    //socket creation
    tcp::socket& socket() {
        return client_socket;
    }

    void start() {
        c_to_s.run_relay(shared_from_this());
        s_to_c.run_relay(shared_from_this());
    }

  private:
    tcp::socket server_socket;
    tcp::socket client_socket;
    enum { max_length = 1024 };

    struct relay {
        tcp::socket &from, &to;
        std::array<char, max_length> buf{};

        void run_relay(pointer self) {
          from.async_read_some(asio::buffer(buf),
              [this, self](error_code ec, size_t n) {
                  if (ec) return handle(from, ec);
    
                  /*
                   *std::cout 
                   *  << "From " << from.remote_endpoint()
                   *  << ": " << std::quoted(std::string_view(buf.data(), n))
                   *  << std::endl;
                   */
                  async_write(to, asio::buffer(buf, n), [this, self](error_code ec, size_t) {
                      if (ec) return handle(to, ec);
                      run_relay(self);
                  });
              });
        }

        void handle(tcp::socket& which, error_code ec = {}) {
            if (ec == asio::error::eof) {
                // soft "error" - allow write to complete
                std::cout << "EOF on " << which.remote_endpoint() << std::endl;
                which.shutdown(tcp::socket::shutdown_receive, ec);
            }

            if (ec) {
                from.cancel();
                to.cancel();

                std::string reason = ec.message();
                auto fep = from.remote_endpoint(ec),
                     tep = to.remote_endpoint(ec);
                std::cout << "Stopped relay " << fep << " -> " << tep << " due to " << reason << std::endl;
            }
        }
    } c_to_s {client_socket, server_socket, {0}}, 
      s_to_c {server_socket, client_socket, {0}};
};

class Server {
    asio::io_service io_service;
    tcp::acceptor acceptor_;

    void start_accept() {
        // socket
        auto connection = con_handler::create(io_service);

        // asynchronous accept operation and wait for a new connection.
        acceptor_.async_accept(
            connection->socket(),
            [connection, this](error_code ec) {
                if (!ec) connection->start();
                start_accept();
            });
    }

  public:
    Server() : acceptor_(io_service, {{}, PROXYPORT}) {
        start_accept();
    }

    void run() {
        io_service.run_for(5s); // .run();
    }
};

int main() {
    Server().run();
}

当运行时

printf "GET / HTTP/1.1\r\nHost: coliru.stacked-crooked.com\r\n\r\n" | nc 127.0.0.1 8899

服务器打印:

EOF on 127.0.0.1:36452

netcat 收到回复:

HTTP/1.1 200 OK 
Content-Type: text/html;charset=utf-8
Content-Length: 8616
Server: WEBrick/1.4.2 (Ruby/2.5.1/2018-03-29) OpenSSL/1.0.2g
Date: Sat, 01 Aug 2020 00:25:10 GMT
Connection: Keep-Alive

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<html>
    ....
</html>

总结

清楚地思考您要达到的目标,避免意外的复杂性。它让我们能够想出一个好的构建块 (relay),从而消除复杂性。

【讨论】:

  • [注意:注意现场演示中的run_for(5s) - 防止服务器运行时间过长。]
  • 太棒了,非常感谢您对所有内容的详细解释,希望您今天过得愉快!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-17
  • 1970-01-01
  • 1970-01-01
  • 2010-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多