目次
クラスには 2 つの重要な機能があります:
オブジェクト内で実行されるように初期化されます。 Server クラスには 3 つのサブクラスがあります:
Session クラス
前述したように、コルーチン ライブラリとして libco を使用します。コルーチンはアプリケーションにとっては透過的ですが、ライブラリ実装にとってはこれが核となります。
コルーチンを作成した後、
コルーチンが CPU 使用権を引き渡す必要がある場合、
コルーチンの復元とコルーチンの作成に使用される関数は
コルーチンのスケジューリング実装
コルーチンの開始
コルーチンの一時停止と再開
コルーチンを破棄する
recv()
reply()
quit
アプリケーション例
SubRoutine
UDPServer
TCPServer
追記
ホームページ バックエンド開発 C#.Net チュートリアル C/C++ コルーチンのアセンブリベースの実装 (サーバー用)

C/C++ コルーチンのアセンブリベースの実装 (サーバー用)

Aug 02, 2018 pm 04:09 PM
c++ コルーチン 非同期 サーバー開発

この記事は C/C コルーチンの実装です。これら 2 つの目標を達成する必要があります:

  1. 機能設計とコードのデバッグを容易にする同期サーバー プログラミングの一連のアイデアを得る - libco のコルーチン部分を使用しました。

  2. 非同期 I/O のパフォーマンスを備えています - libevent Apache php mysql

でイベント I/O を使用しました。構造的には、次の関数です。 libco と libevent を組み合わせたものなので、プロジェクトに libcoevent という名前を付けました。これは、「libevent に基づく同期コルーチン サーバー プログラミング フレームワーク」を意味します。名前にある co という単語は libco ではなく、コルーチンを意味します。

プログラミング言語に関しては、主に libco が x86 または x64 アーキテクチャに基づく Linux のみをサポートしており、そのようなアーキテクチャは基本的に PC であるか、十分なリソースと高いパフォーマンスを備えた優れた組み込みシステムであるため、C を選択しました。 Cの学習には問題ありません。この記事では、コードの実装方法について説明します。

このプロジェクトを使用したい場合は、リンク オプションに 3 つのオプション -lco -levent -lcoevent を追加してください。

#クラスの関係と基本機能

クラスの関係

クラスの継承関係

クラスの基本的な継承関係図は次のとおりです。

C/C++ コルーチンのアセンブリベースの実装 (サーバー用)

実際の呼び出しでは、継承関係ツリーの葉ノード上のクラスのみが実際に使用され、それ以外のクラスは仮想クラスとみなされます。

クラスの所属

プログラムの実行中にさまざまなタイプのインスタンスが所属します。最上位の Base クラスを除き、他のリーフ クラスは他のクラスにアタッチする必要があります。動作環境。依存関係図は次のとおりです。

C/C++ コルーチンのアセンブリベースの実装 (サーバー用)

  • Base クラスは、最も基本的な動作環境を提供し、Server## を管理します。 # オブジェクト;

  • プロシージャ

    オブジェクト管理クライアントオブジェクト。この図では、Server オブジェクトと Session オブジェクトの両方が Client オブジェクトを管理します。

    • Server

      オブジェクトはアプリケーションによって作成され、Base オブジェクト内で実行されるように初期化されます。サーバーが終了するとき、またはサーバーに依存する Base オブジェクトが破棄されるときに、Server オブジェクトが自動的に破棄されるように構成できます。

    • Session

      オブジェクトはセッション モードで Server オブジェクトによって自動的に作成され、アプリケーションによって指定されたプログラム エントリを呼び出すことによって実行されます。 ; セッションが終了するとき (関数呼び出し return)、またはその下位の Server オブジェクト サービスが終了すると、Server オブジェクトは自動的に破棄されます。

  • Client

    オブジェクトは、アプリケーションが Procedure オブジェクトのインターフェイスを呼び出すことによって作成され、サードパーティと対話するために使用されます。サービス。アプリケーションは、事前にインターフェイスを呼び出して Client オブジェクトの破棄を要求することも、Procedure サービスの終了時に自動的にオブジェクトを破棄することもできます。

    #Base クラスと Event クラス

##BaseC/C++ コルーチンのアセンブリベースの実装 (サーバー用) クラスは、libcoevent のさまざまなサービスを実行するために使用されます。 Base クラスの各インスタンスはスレッドに対応する必要があり、すべてのサービスは

Base

インスタンス内でコルーチン方式で実行されます。上の図からわかるように、Base クラスには、libevent ライブラリの event_base オブジェクトと、このコルーチン ライブラリの一連の Event オブジェクトが含まれています。

EventC/C++ コルーチンのアセンブリベースの実装 (サーバー用) クラスは、実際には libevent から

structevent

の名前を借用します。なぜなら、すべての Event クラスはのインスタンス。libevent の event オブジェクトに対応します。注目する必要があるのは、Procedure クラスと Client クラスです。 Procedure クラス

Procedure

クラスには 2 つの重要な機能があります:

各オブジェクトには libco コルーチンがあります。独自の独立したコンテキスト情報を持ち、独立したサーバー プロセス (プロシージャ) の作成に使用できます。

  1. プロシージャ サブクラスは、

    Client
  2. オブジェクトとサードパーティのサーバー通信を作成できます。交流。
  3. Procedure
  4. クラスには、
Server

Session という 2 つのサブクラスがあります。 サーバー クラスサーバー クラスはアプリケーションによって作成され、

Base

オブジェクト内で実行されるように初期化されます。 Server クラスには 3 つのサブクラスがあります:

  • サブルーチン: 実際にはサーバー プログラムとして機能しませんが、最も基本的な sleep() 関数を提供し、クライアント オブジェクトの作成をサポートします。 Procedure クラスの関数を使用することで、アプリケーションを一時的に作成または常駐する内部プログラムとして使用できます。

  • UDPServer: アプリケーションが UDPServer オブジェクトを作成して初期化した後、プログラムは自動的にデータグラム ソケット インターフェイスにバインドします。アプリケーションは、ネットワーク インターフェイスでデータ パケットを送受信することにより、ネットワーク サービスを実装できます。 UDPServer は、通常モードセッション モードの両方を提供します。

  • TCPServer: アプリケーションが TCPPServer オブジェクトを作成して初期化した後、プログラムは自動的にストリーム ソケットをバインドしてリッスンします。 TCPServer は セッション モード のみをサポートします。

いわゆる「通常モード」は、アプリケーションがServerオブジェクトのエントリ関数を登録し、アプリケーションがServerオブジェクトを操作する動作です。

いわゆる「セッション モード」は、UDPServer または TCPServer オブジェクトを指します。受信データを受信した後、クライアントを自動的に識別し、別の Session オブジェクトを作成します。処理されます。各 Session オブジェクトは 1 つのクライアントのみにサービスを提供します。

Session クラス

Session オブジェクトはアプリケーションによってアクティブに作成できませんが、セッション モードの Server クラスによってオンデマンドで自動的に作成されます。 Session オブジェクトの特徴は、(UDPServer オブジェクトと比較して) 単一のクライアントとのみ通信できることです。したがって、send() 関数はなく、reply() のみです。

ヘッダー ファイル coevent.h で宣言された Session クラスとそのサブクラスは、アプリケーションが明示的に Session# を構築するのを防ぐための純粋な仮想クラスです。 ## オブジェクトをオブジェクトにして、実装の詳細を非表示にします。

Client クラス

Client オブジェクトは Procedure オブジェクトによって作成され、Procedure オブジェクトによってリサイクルされます。 Client オブジェクトの役割は、リモート サーバーとの通信をアクティブに開始することです。このアクションは、クライアント サービス構造の観点から見るとクライアントに属するため、クライアントという名前が付けられます。

DNSClient

Client の特別なサブクラスの 1 つは、

DNSClient クラスです。このクラスは、getaddrinfo( ) ブロッキング問題を解決するために存在します。 DNSClient の実装原理については、コードと以前の記事「DNS メッセージ構造とパーソナル DNS 解析コードの実装」を参照してください。

DNSClient クラスの具体的な実装原則は、UDPClient オブジェクトをカプセル化し、このオブジェクトを使用して DNS メッセージの送受信を完了し、クラス内でメッセージの解析を実装することです。

UDPServer——libevent に基づくコルーチン実装

UDPServer コモン モードの原理は、libevent に基づく非常に典型的な同期コルーチン サーバー フレームワークです。コード実装では、コア関数は次の関数です:

  • _libco_routine()、コルーチンのエントリ関数 この関数を使用すると、次のように変換されます。 liboeven の統一 サービスエントリ関数

  • _libevent_callback()libevent 時間コールバック関数、この関数では、コルーチン コンテキストの回復実現される。

  • UDPServer::recv_in_timeval()、データ受信関数、この関数ではキーデータ待ち関数が実装されており、コルーチンコンテキストの保存も行われます。実現

  • #上記3つの関数のコード量は空行を含めても200行を超えないので、まだわかりやすいと思います。以下に実装原理を詳しく説明します。

libco コルーチン インターフェイス

前述したように、コルーチン ライブラリとして libco を使用します。コルーチンはアプリケーションにとっては透過的ですが、ライブラリ実装にとってはこれが核となります。

以下では、libco のコルーチン関数によって提供されるいくつかのインターフェイスについて説明します (libco のドキュメントの数は単なる「接触」であり、インターネット上でよく苦情が言われています...):

コルーチンの作成と破棄

Libco

構造体 struct stCoRoutine_t * を使用してコルーチンを保存し、co_create() を呼び出してコルーチン オブジェクトを作成できます。 co_release() コルーチンのリソースを破棄します。 コルーチンの入力

コルーチンを作成した後、

co_resume()

を呼び出して、コルーチン関数の先頭からコルーチンの実行を開始します。 コルーチンを一時停止する

コルーチンが CPU 使用権を引き渡す必要がある場合、

co_yield()

を呼び出してコルーチンを解放し、コンテキストを切り替えることができます。呼び出し後、コンテキストは co_resume() を呼び出した最後のコルーチンに復元されます。 co_yield() が呼び出される場所は、「breakpoint」とみなすことができます。 コルーチンの再開

コルーチンの復元とコルーチンの作成に使用される関数は

co_resume()

です。この関数を呼び出して、現在のスタックを指定したコルーチンに切り替えます。コンテキストでは、コルーチンは上記の「breakpoint」から実行を再開します。

コルーチンのスケジューリング実装

前のセクションからわかるように、使用する libco コルーチン関数にはコルーチンの切り替え関数が含まれていますが、いつ切り替えるか、切り替え後に CPU に何が起こるかは次のとおりです。実装してカプセル化する必要があるもの。

コルーチンを作成および破棄するタイミングは、当然、UDPServer クラスが初期化されて破棄されるときです。以下では、コルーチンの開始、一時停止、再開の操作の分析に焦点を当てています。

コルーチンの開始

コルーチンの開始/再開のコードは、_libevent_callback() にあります。

// handle control to user application
co_resume(arg->coroutine);
ログイン後にコピー

現在のコルーチンが実行されていない場合、このコードの実行後、プログラムは libco コルーチンの作成時に指定されたコルーチン関数に切り替えて実行を開始します。 UDPServer の場合、これは _libco_routine() 関数です。この関数は非常に単純で、わずか 3 行で構成されています。

static void *_libco_routine(void *libco_arg)
{
    struct _EventArg *arg = (struct _EventArg *)libco_arg;
    (arg->worker_func)(arg->fd, arg->event, arg->user_arg);
    return NULL;
}
ログイン後にコピー

パラメータを渡すことにより、libco コールバック関数は、実行のためにアプリケーションによって指定されたサーバー関数に変換されます。

しかし、最初の libevent コールバックを実装するにはどうすればよいでしょうか?これはまだ非常に単純で、libevent の event_add() を呼び出すときにタイムアウトを 0 に設定するだけです。これにより、libevent イベントがすぐにタイムアウトになります。このメカニズムにより、Base の実行直後に各 Procedure サービス関数を実行するという目的も達成されます。

コルーチンの一時停止と再開

いつ co_yield を呼び出すかがこのコルーチン実装の焦点です。co_yield を呼び出す場所は可能性があります。コンテキストの切り替えがどこで発生するかは、非同期プログラミング フレームワークを同期フレームワークに変換する際の重要な技術点でもあります。ここで UDPServerrecv_in_timeval() 関数を参照できます。関数の基本的なロジックは次のとおりです:

C/C++ コルーチンのアセンブリベースの実装 (サーバー用)

最も重要な分岐は libevent イベント フラグの判断であり、最も重要なロジックは event_add() および co_yield() 関数呼び出し。関数のフラグメントは次のとおりです:

struct timeval timeout_copy;
timeout_copy.tv_sec = timeout.tv_sec;
timeout_copy.tv_usec = timeout.tv_usec;
    ...
event_add(_event, &timeout_copy);
co_yield(arg->coroutine);
ログイン後にコピー

ここでは、co_yield() 関数をブレークポイントとして理解します。ここでプログラムが実行されると、CPU の使用権が引き継がれます。そしてプログラムは co_resume() を呼び出す上位レベルの関数に戻ります。この「上位レベルの関数」とは一体どこにあるのでしょうか?実際、これは前述の _libevent_callback() 関数です。

_libevent_callback() の観点から見ると、プログラムは co_resume() 関数から戻り、実行を継続します。この時点で、コルーチンのスケジューリングは実際には libevent から借用していることが理解できます。ここで、上のいくつかの文 co_resume():

// switch into the coroutine
if (arg->libevent_what_ptr) {
    *(arg->libevent_what_ptr) = (uint32_t)what;
}
ログイン後にコピー

ここで、libevent イベント フラグの値がコルーチンに渡されます。前回の出来事の重要な判断材料。時間が来ると、_libevent_callback() は以下の co_resume() を呼び出し、CPU 使用権をコルーチンに返します。

コルーチンを破棄する

ci_yield() に加えて、コルーチン関数呼び出し return の結果も co_resume()# になります。 ## 戻ります。したがって、_libevent_callback() では、コルーチンが終了したかどうかも判断する必要があります。コルーチンが終了した場合は、関連するコルーチン リソースを破棄する必要があります。 if (is_coroutine_end(arg->coroutine)) {...} 条件本文のコードを参照してください。

セッション モード

このプロジェクトの実装では、「セッション モード」と呼ばれるサーバー設計パターンが提供されます。セッション モードは、UDPServer または TCPServer オブジェクトを指します。受信データを受信すると、自動的にクライアントを識別し、処理用に別の

Session オブジェクトを作成します。各 Session オブジェクトは 1 つのクライアントのみにサービスを提供します。

TCPServer の場合、TCP ソケットを監視した後、受信接続があるときに accept() を呼び出すだけなので、上記の関数を実装するのは比較的簡単です。 、新しいファイル記述子を取得し、このファイル記述子の Server の新しいサブクラスを作成できます。これは TCPSession クラスです。

しかし、

UDPServer は、UDP ではこれができないため、さらに面倒です。いわゆるセッションは自分たちで実装するしかありません。

UDPSession は

設計目標を達成します

UDPSession クラスの次の効果を達成する必要があります:

  • クラス

    recv 関数を呼び出すと、対応するリモート クライアントによって送信されたデータのみが受信されます。

  • クラスは

    send## を呼び出します。 # 関数 (実際の実装は reply()) では、UDPServer のポートを使用して返信できます。

recv()

プロジェクトの UDPSession は抽象クラスで、実際の実装は UDPItnlSession です。ただし、正確に言えば、UDPItnlSession の実装は UDPServer に密接に依存しています。この部分については、UDPServer_session_mode_worker() 関数の do-while() ループ本体コードを参照できます。プログラムのアイデアは次のとおりです。

  • UDPServer リモート IP ポート名の組み合わせをキーとして、UDPSession 辞書を維持します。

  • データが到着したら、リモート IP ポートの組み合わせが辞書にあるかどうかを確認します。辞書にある場合は、データを対応するセッションにコピーします。存在しない場合は、セッションを作成します。

データをコピーするコードについては、UDPItnlSession クラスの forward_incoming_data() 関数実装を参照してください。

reply()

データの送信は実際には非常に簡単で、UDPServer の fd 上で sendto() を直接実行するだけです。

quit

セッション モードの Server オブジェクトの場合、コードはセッションによって呼び出すことができる関数を提供し、サーバーが終了してリソースを破棄する必要があります。 quit_session_mode_server()。実装の原則は、サーバーに対して EV_SIGNAL イベントをトリガーすることです。通常の I/O イベントでは、これは発生すべきではないため、ここでは終了信号として使用します。サーバーがこの信号を検出すると、終了ロジックがトリガーされます。

アプリケーション例

このプロジェクトのサンプル コードはサーバーとクライアントの 2 つの部分に分かれており、サーバーでは libcoevent が使用され、クライアントでは Python のみが使用されます。 簡単なプログラムを書きました。この記事では、コードのクライアント部分については説明しません。

Server のコードは、Server クラスの 3 つのサブクラスのアプリケーション例を提供します。空行、デバッグ文、エラー判定などを含むロジックを用いて、1つのプロセスと2つのサービスを300行以内に実装しました。ロジックは依然として非常に明確であり、多くのコードが節約されていると言うべきです。

SubRoutine

は、関数 _simple_test_routine() を使用した 1 回限りの線形ネットワーク ロジックを示します。プログラムでは、ルーチンは最初に DNSClient オブジェクトを作成し、デフォルトのドメイン ネーム サーバーからドメイン名を要求し、次にサーバーの 80 ポートの connect() を要求します。成功したら、直接戻ります。

この関数は、SubRoutine の使用シナリオと Client オブジェクトの使用法、特に DNSClient の簡単な使用法を示します。

UDPServer

UDPServer のエントリ関数は _udp_session_routine() で、クライアントにドメイン名クエリ サービスを提供する機能があります。クライアントはクエリ対象のドメイン名として文字列を送信し、サーバーは DNSClient オブジェクトを通じてクエリ結果を要求した後、クライアントにクエリ結果を返します。

この関数は、UDPSession オブジェクトと DNSClient の (より複雑で完全な) 使用法を示します。

TCPServer

エントリ関数は _tcp_session_routine() で、ロジックは比較的単純で、主に TCPSession の使用法を示すためのものです。

追記

libcoeventは原則として開発され、必要な機能を実現しており、サーバープログラムを作成するために使用できます。もちろん、これは最初のバージョンなので、コードの多くはまだ少し乱雑に見えます。このライブラリの意義は、C/Cコルーチンのより独創的な実装原理を教育的観点から丁寧に解説できることと、コルーチンサーバーライブラリとしても利用できることです。

読者がこのライブラリを批判することを歓迎します。また、読者が新しい要件を提案することも歓迎します。たとえば、TODO とみなせるいくつかの要件を追加することにしました:

  1. implementationHTTPServer は、TCPServer のサブクラスとして HTTP fcgi サービスを提供し、

  2. # は SSLClient# を実装します。 ## 外部 SSL 要求を処理するクラス。

関連記事:

C# ネットワーク プログラミング シリーズ記事 (8) UdpClient は同期 UDP サーバーを実装します

C php サーバーを実装する言語

関連ビデオ:


C# チュートリアル

以上がC/C++ コルーチンのアセンブリベースの実装 (サーバー用)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

C文字列におけるcharの役割は何ですか C文字列におけるcharの役割は何ですか Apr 03, 2025 pm 03:15 PM

Cでは、文字列でCharタイプが使用されます。1。単一の文字を保存します。 2。配列を使用して文字列を表し、ヌルターミネーターで終了します。 3。文字列操作関数を介して動作します。 4.キーボードから文字列を読み取りまたは出力します。

Docker環境にPECLを使用して拡張機能をインストールするときにエラーが発生するのはなぜですか?それを解決する方法は? Docker環境にPECLを使用して拡張機能をインストールするときにエラーが発生するのはなぜですか?それを解決する方法は? Apr 01, 2025 pm 03:06 PM

エラーの原因とソリューションPECLを使用してDocker環境に拡張機能をインストールする場合、Docker環境を使用するときに、いくつかの頭痛に遭遇します...

c-subscript 3 subscript 5 c-subscript 3 subscript 5アルゴリズムチュートリアルを計算する方法 c-subscript 3 subscript 5 c-subscript 3 subscript 5アルゴリズムチュートリアルを計算する方法 Apr 03, 2025 pm 10:33 PM

C35の計算は、本質的に組み合わせ数学であり、5つの要素のうち3つから選択された組み合わせの数を表します。計算式はC53 = 5です! /(3! * 2!)。これは、ループで直接計算して効率を向上させ、オーバーフローを避けることができます。さらに、組み合わせの性質を理解し、効率的な計算方法をマスターすることは、確率統計、暗号化、アルゴリズム設計などの分野で多くの問題を解決するために重要です。

マルチスレッドをC言語で実装する4つの方法 マルチスレッドをC言語で実装する4つの方法 Apr 03, 2025 pm 03:00 PM

言語のマルチスレッドは、プログラムの効率を大幅に改善できます。 C言語でマルチスレッドを実装する4つの主な方法があります。独立したプロセスを作成します。独立して実行される複数のプロセスを作成します。各プロセスには独自のメモリスペースがあります。擬似マルチスレッド:同じメモリ空間を共有して交互に実行するプロセスで複数の実行ストリームを作成します。マルチスレッドライブラリ:pthreadsなどのマルチスレッドライブラリを使用して、スレッドを作成および管理し、リッチスレッド操作機能を提供します。 Coroutine:タスクを小さなサブタスクに分割し、順番に実行する軽量のマルチスレッド実装。

個別の関数使用距離関数C使用チュートリアル 個別の関数使用距離関数C使用チュートリアル Apr 03, 2025 pm 10:27 PM

std :: uniqueは、コンテナ内の隣接する複製要素を削除し、最後まで動かし、最初の複製要素を指すイテレーターを返します。 STD ::距離は、2つの反復器間の距離、つまり、指す要素の数を計算します。これらの2つの機能は、コードを最適化して効率を改善するのに役立ちますが、隣接する複製要素をstd ::のみ取引するというような、注意すべき落とし穴もあります。 STD ::非ランダムアクセスイテレーターを扱う場合、距離は効率が低くなります。これらの機能とベストプラクティスを習得することにより、これら2つの機能の力を完全に活用できます。

C言語でヘビの命名法を適用する方法は? C言語でヘビの命名法を適用する方法は? Apr 03, 2025 pm 01:03 PM

C言語では、Snake命名法はコーディングスタイルの慣習であり、アンダースコアを使用して複数の単語を接続して可変名または関数名を形成して読みやすくします。編集と操作、長い命名、IDEサポートの問題、および歴史的な荷物を考慮する必要がありますが、それは影響しませんが。

c c Apr 04, 2025 am 07:54 AM

CのRelease_Semaphore関数は、取得したセマフォをリリースするために使用され、他のスレッドまたはプロセスが共有リソースにアクセスできるようにします。セマフォのカウントを1増加し、ブロッキングスレッドが実行を継続できるようにします。

Cプログラマー&#の未定義の行動ガイド Cプログラマー&#の未定義の行動ガイド Apr 03, 2025 pm 07:57 PM

Cプログラミングで未定義の動作を調査する:詳細なガイドこの記事では、Cプログラミングの未定義の動作に関する電子書籍を紹介します。これは、Cプログラミングの最も困難であまり知られていない側面のいくつかをカバーする合計12の章です。この本は、C言語の入門的な教科書ではありませんが、C言語プログラミングに精通している読者を対象としており、未定義の行動のさまざまな状況と潜在的な結果を探ります。著者Dmitrysviridkin、編集者アンドレイ・カーポフ。 6か月間の慎重な準備の後、この電子書籍はついに読者と会いました。印刷バージョンも将来発売されます。この本はもともと11の章を含めることが計画されていましたが、作成プロセス中にコンテンツは継続的に豊かになり、最終的に12の章に拡張されました。

See all articles