序文
epoll
以前は、 C++ バックエンド サービス プログラムを実装していました。参照:メッセージ ボードを最初から実装する - README_c++ でメッセージ ボードを作成する_大1234草的博客-CSDN 博客
しかし、他のコードに簡単に組み込めるほど移植性がありません。また、プログラム内で関数を使用しているためepoll
、コードは Windows では実行できません。選択するとselect
、Windows と Linux の両方で実行できます。
したがって、この記事では C++ バックエンド サービス プログラムを書き直します。このプログラムは、さまざまなプロジェクトに簡単に埋め込むことができ、プラットフォーム間で実行できる必要があります。
インターネット上には、このようなプログラムが数多く存在する可能性があります。たとえば、「【C++】HTTP Server オープン ソース ライブラリ (概要レベルの整理)」 | 私のような人。ただし、新人の段階では、より多くのコードを入力する必要があります (先延ばしにするとやりたくないので、現時点ではさまざまな実装の比較はしていません)。
ここでは、どのようなテクニカルルートがあるのかを見てみましょう。1つ目はソケットプログラミングです。socket
Windows と Linux での使用方法は若干異なります。したがって、ここでは直接ソケットプログラミングはオプションではありません。2 つ目は C++ ネットワーク ライブラリです。C++ 標準ライブラリはネットワーク関数を提供していないため、ここではGitHub - boostorg/asio:Boost.org asio モジュールが選択されています。
初めて使用する場合は少し難しいかもしれませんboost::asio
。boost::asio
ただし、詳細すぎるため、この記事では具体的な使用方法については説明しません。ここでは参考文献を推奨します。
-
『Boost プログラム完全開発ガイド』 12.3 asio – 入門レベルの入門、予備知識が得られます
-
GitHub - dongzj1997/Web-Server: C++17 と Boost.Asio を使用して実装されたシンプルで高速な HTTP サーバー– 実践的なチュートリアル、手動で ASIO ネットワーク コードを作成できます
-
C++11 の例 - 1.81.0 - 公式チュートリアルの HTTP サーバー セクション、高品質のコード
この記事のコードは上記のリンクから変更されています。
詳細なコードについては、リポジトリを参照してください。
コード
この記事では、シングルスレッドの非同期ソケット サービス プログラムを実装します。
このプログラムの機能は、クライアントから送信された文字列を受信し、同じ文字列を返して、接続を閉じることです。
コードの基本構造は次のとおりです。
-
サーバー クラスをカプセル化します。サーバー クラスは、関数の開始、停止、信号処理、および接続のリッスンと受け入れの関数を担当します。
-
接続クラスをカプセル化し、接続を確立したソケットに関する読み取りおよび書き込み操作を処理します。(接続ごとに必要なストレージ バッファ スペースが異なるため、各接続には接続オブジェクトがあります)
-
すべての接続オブジェクトを管理するには、connect_manager クラスをカプセル化します。
まだよくわかっていないことがたくさんあります。
主に、ネットワーク プログラミングの基礎知識、C++ の基本構文、boost::asio の基本的な使い方、コード構造の設計、同じ機能を実現するための異なるオペレーティング システムの API の使用の 5 つのパートに分かれています。
-
ソケットを優雅に閉じます。
-
ポートの多重化とアドレスの多重化:ソケット ポートの多重化 SO_REUSEPORT と SO_REUSEADDR - schips - 博客园
-
C++ の左辺値、右辺値、移動セマンティクス、エラー コードなど。
-
ASIO のシングルスレッド非同期、ASIO のマルチスレッド プログラミングの基本原理。
-
コード構造の設計。(この記事のコードは前のリンクから参照されています。コード構造では、connect_manager を使用して接続を管理するのが良い点です。connect が解放されると、connect は connect_managet のメソッドを呼び出します。したがって、サーバーでのこれの実行が回避されます。こういう構造はとても良いですね。)
-
Linux のシグナル、Windows のシグナル、Windows のイベントの違い。
以下が具体的なコードです。
1 つ目はメイン関数コードです。
#include "server.h"
int main(int argc, char *argv[])
{
server s("127.0.0.1","6666");
s.run();
return 0;
}
以下はサーバー クラスのカプセル化です。
#pragma once
#include "connection.h"
#include <boost/asio.hpp>
class server {
public:
server(const std::string& address, const std::string& port);
void run();
void stop();
private:
void do_accept();
private:
boost::asio::io_context m_io_context;
boost::asio::ip::tcp::acceptor m_acceptor;
connection_manager m_connection_manager;
boost::asio::signal_set m_signals;
};
#include "server.h"
#include <iostream>
#include <boost/asio/ip/tcp.hpp>
#include <boost/bind/bind.hpp>
#include <fstream>
server::server(const std::string& address, const std::string& port)
: m_io_context(1),
m_acceptor(m_io_context),
m_connection_manager(),
m_signals(m_io_context)
{
// 在win下,使用taskkill发送信号,会让进程直接退出,并没有执行这里的信号处理。
// 目前不清楚,可参考:https://stackoverflow.com/questions/26404907/gracefully-terminate-a-boost-asio-based-windows-console-application
m_signals.add(SIGINT);
m_signals.add(SIGTERM);
m_signals.async_wait(
[this](boost::system::error_code ec, int signo)
{
if(signo == SIGINT || signo == SIGTERM) {
stop();
}
});
boost::asio::ip::tcp::resolver resolver(m_io_context);
boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(address, port).begin();
m_acceptor.open(endpoint.protocol());
m_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
m_acceptor.bind(endpoint);
m_acceptor.listen();
do_accept();
}
void server::do_accept()
{
// Move accept handler requirements
m_acceptor.async_accept([this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
{
// Check whether the server was stopped by a signal before this
// completion handler had a chance to run.
if (!m_acceptor.is_open()) {
return;
}
if (!ec) {
m_connection_manager.start(std::make_shared<connection>(std::move(socket), m_connection_manager));
}
do_accept();
});
}
void server::run()
{
m_io_context.run();
}
void server::stop()
{
// 服务器停止是通过取消所有未完成的异步操作来实现的。
// 一旦所有操作都完成,io_context::run() 函数将退出。
m_acceptor.close();
m_connection_manager.stop_all();
}
次に接続クラスです。connect_manager クラスも同じファイルに配置します。
#pragma once
#include <memory>
#include <set>
#include <boost/asio/ip/tcp.hpp>
class connection;
typedef std::shared_ptr<connection> connection_ptr;
class connection_manager {
public:
connection_manager() = default;
connection_manager(const connection_manager&) = delete;
connection_manager& operator=(const connection_manager&) = delete;
void start(connection_ptr c);
void stop(connection_ptr c);
void stop_all();
private:
std::set<connection_ptr> m_connections;
};
class connection : public std::enable_shared_from_this<connection> {
public:
connection(const connection&) = delete;
connection& operator=(const connection&) = delete;
connection(boost::asio::ip::tcp::socket socket, connection_manager& manager);
void start();
void stop();
private:
void do_read();
void do_write();
void handle_read(const boost::system::error_code& ec, size_t bytes_transferred);
void handle_write(const boost::system::error_code& ec, size_t bytes_transferred);
private:
boost::asio::ip::tcp::socket m_socket;
int m_write_size = 0;
std::array<char, 4096> m_read_buffer;
std::array<char, 4096> m_write_buffer;
connection_manager& m_connection_manager;
};
#include "connection.h"
#include <boost/bind/bind.hpp>
#include <boost/asio/placeholders.hpp>
void connection_manager::start(connection_ptr c)
{
m_connections.insert(c);
c->start();
}
void connection_manager::stop(connection_ptr c)
{
m_connections.erase(c);
c->stop();
}
void connection_manager::stop_all()
{
for (auto c: m_connections)
c->stop();
m_connections.clear();
}
connection::connection(boost::asio::ip::tcp::socket socket,
connection_manager& manager)
: m_socket(std::move(socket)),
m_connection_manager(manager)
{
}
void connection::start()
{
do_read();
}
void connection::do_read()
{
m_socket.async_read_some(boost::asio::buffer(m_read_buffer),
boost::bind(&connection::handle_read, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
void connection::handle_read(const boost::system::error_code& ec, size_t bytes_transferred)
{
if(!ec) {
// 检查是否接受到完整的信息;(这里假定收到的信息完整)
// 单线程的异步程序,这里会存在问题麻?
m_write_buffer.fill('\0');
m_write_buffer = m_read_buffer;
m_write_size = bytes_transferred;
do_write();
}
else if(ec != boost::asio::error::operation_aborted){
m_connection_manager.stop(shared_from_this());
}
}
void connection::do_write()
{
m_socket.async_write_some(boost::asio::buffer(m_write_buffer.data(), m_write_size),
boost::bind(&connection::handle_write, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
void connection::handle_write(const boost::system::error_code& ec, size_t bytes_transferred)
{
if(!ec) {
// 发送后断开连接
m_connection_manager.stop(shared_from_this());
// 这里的写法比较神奇.调用管理者来释放自己.
// 直接调用connection::stop,会导致connection_manager中该对象的智能指针没有删除(虽然在对象释放后这个智能指针可能指向为空)
}
else if(ec != boost::asio::error::operation_aborted) {
m_connection_manager.stop(shared_from_this());
}
}
void connection::stop()
{
// m_socket.close();
// 优雅的关闭:发送缓冲区中的内容发送完毕后再完全关闭
boost::system::error_code ignored_ec;
m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored_ec);
}