0. この記事について
この記事はHohetoさんの 仮想通貨botter Advent Calendar 2022 の24日目(裏)の記事です。ぜひ最後まで読んでいってください!
こんにちは。ねぎ草(@negi_grass)と申します。
crypto-botters(GitHub)というRustのライブラリを開発しているのでその紹介をしようと思います。貢献してくれる方は、是非issueを立てたりプルリクを送ったりしてください!
大変申し訳ございません。何も考えずにハイフンをクレート名に使ってしまいました。使うときは use crypto-botters;
ではなく use crypto_botters;
と書いてください。
最新バージョンは、この記事を書いた当時のコードと大きく異なっています。GitHubのREADMEが最新の内容です。(generic-api-clientの部分は変わっていません。)
0.1. 目的
ライブラリをOSSとして公開しているのは、Rustのbottersのコミュニティに貢献するためです。Rustを使っているbotterは一定数いるはずなのに、あまりRustに関する情報交換が活発でないような気がしたので、その活性化のきっかけになれればと思います。
0.2. 何のライブラリか
タイトルにもある通り、完全に同じものを目指しているわけではありませんが、まちゅけんさんのpybottersのRust版的なものです。(私が勝手に思ってるだけです。)できることは似ていますが、DataStoreの機能が(まだ?)なく、対応しているのはまだBinaneとbitFlyerだけです。また、WebSocketの自動再接続に少しだけ力を入れています。
また、非同期、特にtokioのランタイムの中で使われることを想定しています。そうでない場合はエラーが出ると思います。
1. Rustのメリット
まずはじめにRustの宣伝をさせてください。(Rustを使っている方は飛ばしてください。)ここでは、Rustメリットを3つ紹介します。
1.1. 静的型付けコンパイル言語である
コンパイル時に型チェックが実行されるため、実行時に型を間違えているためにプログラムが死ぬ、なんてことは起きません。またライブラリを使うとき、関数の引数と返り値の型はすべてソースコードに書いてあるため、いちいち使い方をググる必要がありません。(私が動的型付けがあまり好きではない第一の理由です。)コード補完もしっかり利きます。
1.2. 高速である
静的型付けコンパイル言語である上、ガベージコレクションが無いため非常に高速です。(これは所有権という仕組みのお陰です。もちろん手動でmalloc
やfree
などを呼ぶ必要はありません。)
bot を作る上では
- バックテストをするとき
- アビトラやマーケットメイクなどの戦略を実行するとき
などで役立つと思います。
1.3. 公式ツールが優秀
RustにはCargoという公式ツールがあり、Cargo.toml
というファイルに依存関係を書いておくことで、コマンド一つで
- 依存関係を調べ、同じパッケージは互換性のあるバージョンならまとめる
- ダウンロードしていないパッケージのダウンロード
- 変更されたパッケージをコンパイル
- 実行
をやってくれます。他にも機能はいろいろあります。
2. パッケージの構造
crypto-bottersは1つのワークスペースの中の、現在は4つのクレートでできています。ディレクトリ構造は以下の通りです。
crypto-botters
├─crypto-botters-binance
│ ├─src
│ └─Cargo.toml
├─crypto-botters-bitflyer
│ ├─src
│ └─Cargo.toml
├─generic-api-client
│ ├─src
│ └─Cargo.toml
├─src
└─Cargo.toml
crypto-botters
がワークスペースのルートパッケージです。これらの依存関係は以下の通りです。
crypto-botters-binance
とcrypto-botters-bitflyer
がgeneric-api-client
に依存していて、それらすべてにcrypto-botters
が依存しています。
それぞれのクレートの役割について説明します。
2.1. generic-api-client
generic-api-clientには実際にHTTPリクエストを送ったり、WebSocketを接続したりするコードが含まれます。 generic という名前の通り、どんなAPIにも対応できるような設計のため、認証やデータのパース等は行いません。RequestHandler
とWebSocketHandler
というトレイトを提供しており、これらを実装した構造体が前述した処理を行うことを想定しています。RequestConfig
やWebSocketConfig
を通じて、なるべくカスタマイズ性が高くなるようにしています。
2.2. crypto-botters-*
上に書いた、RequestHandler
とWebSocketHandler
を各取引所向けに実装した構造体を提供しているだけです。認証とデータのパースを行います。ファイルはlib.rs
しかありません。
2.3. crypto-botters
何もしていません。 ユーザーがこのパッケージ構造を気にせず使えるよう、必要なものを1つのクレートにまとめているだけです。binance
とbitflyer
という名前の feature をCargo.toml
で定義し、有効化されていたらcrypto-botters-binance
やcrypto-botters-bitflyer
を依存関係に加えてre-exportしているだけです。generic-api-client
は feature の有効無効に関わらずre-exportしています。
pub use generic_api_client::{http as http, websocket as websocket};
#[cfg(feature = "binance")]
pub use crypto_botters_binance as binance;
#[cfg(feature = "bitflyer")]
pub use crypto_botters_bitflyer as bitflyer;
// EOF
2.4. つまり
つまり言いたいのは、generic-api-client
が重要で、他は大したことはないということです。使いたい取引所がまだ実装されていなかったり、今ある実装が気に入らなかったりする場合は、自分でRequestHandler
やWebSockekHandler
を実装するだけで簡単に望む機能を実現できます。
取引所の追加については、それ用のissue にコメントしてください。
3. 使い方
crypto-bottersを実際に使う例を載せ、解説を加えます。
この記事に載せていない例がexamplesディレクトリにあります。すべて単体で動作確認済です。是非参考にしてください。
let binance = Binance::new(None, None);
let client = Client::new();
let orderbook: serde_json::Value = client.get(
"https://api.binance.com/api/v3/ticker/bookTicker",
Some(&json!({ "symbol": "BTCUSDT" })),
&binance.request_no_url(BinanceSecurity::None),
).await.expect("failed get orderbook");
println!("BTC bidPrice:\n{:?}", orderbook["bidPrice"]);
HTTPリクエストを送る際は、Client::get()
などを呼びます。最後の引数にはRequestHandler
を渡します。Binance.request_no_url()
がRequestHandler
を実装したBinanceRequestHandler
を返すので、それの参照を渡します。BinanceSecurity::None
は認証をしないという意味です。
no_urlとはurlを指定しない、という意味です。Binance.request()
から返されるBinanceRequestHandler
はurlの先頭にhttps://api.binance.com
などを付与しますが、Binanceの場合これが何種類もあるため、Binance.request()
の引数に指定しなければなりません。request_no_url()
を使って回避できます。
二番目の引数の json!({ "symbol": "BTCUSDT" })
はリクエストパラメータです。json!
とは serde_json のValue
型という、JSONを表す型を返すマクロです。この他にも&[("symbol","BTCUSDT")]
と指定したり、symbol
というフィールドを持った構造体を渡す事もできます。
#[derive(Serialize)]
struct TradesLookupParams<'a> {
symbol: &'a str,
limit: u16,
}
#[derive(Deserialize)]
struct OldTrade {
id: i64,
price: Decimal,
qty: Decimal,
quote_qty: Decimal,
time: u64,
is_buyer_maker: bool,
is_best_match: bool,
}
let key = env::var("BINANCE_API_KEY").expect("no API key found");
let secret = env::var("BINANCE_API_SECRET").expect("no API secret found");
let binance = Binance::new(Some(key), Some(secret));
let client = Client::new();
let trades: Vec<OldTrade> = client.get(
"/api/v3/historicalTrades",
Some(&TradesLookupParams { symbol: "BTCUSDT", limit: 3 }),
&binance.request(BinanceSecurity::Key, BinanceHttpUrl::Spot),
).await.expect("failed to get trades");
println!("Trade price:\n{:?}", trades[0].price);
これは認証機能の例です。このエンドポイントは署名ではなく、APIキーを送ることが必要です(BinanceSecurity::Key
の部分)。
また、trades
の型に注目してください。自分で定義したOldTrade
型のVec
と指定すると、自動でレスポンスをその型に変換してくれます。面倒くさい場合は前の例のように、serde_json::Value
と指定してください。
let key = env::var("BITFLYER_API_KEY").expect("no API key found");
let secret = env::var("BITFLYER_API_SECRET").expect("no API secret found");
let bitflyer = BitFlyer::new(Some(key), Some(secret));
let connection = WebSocketConnection::new(
"/json-rpc",
bitflyer.websocket(|message| {
println!("{:?}", message);
}, vec!["child_order_events"], true),
).await.expect("failed to connect websocket");
// receive messages
tokio::time::sleep(Duration::from_secs(10)).await;
// reconnect
connection.reconnect_state().request_reconnect();
// we should still see private channel messages
tokio::time::sleep(Duration::from_secs(10)).await;
これはbitFlyerのプライベートWebSocketチャンネルに購読する例です。bitFlyerのWebSocketは認証メッセージを送ってレスポンスを確認してからプライベートチャンネルの購読メッセージを送る仕様なのですが、そういうこともやってくれます。
BitFlyer.websocket()
に渡しているクロージャですが、これはメッセージを受け取ったときに実行されるものです。この例では標準出力に出力しているだけです。
connection.reconnect_state().request_reconnect();
では再接続をライブラリに要求しています。再接続をしたあとでも、認証は自動で行われます。再接続については後で詳しく説明します。
4. generic-api-client の構造と使い方
generic-api-client
が提供しているパブリックな構造体やトレイトなどについて説明します。http
モジュールとwebsocket
モジュールに分かれています。
4.1. http モジュール
HTTPリクエストを送るためのモジュールです。
4.1.1. Client
Client
はHTTPリクエストを送るための構造体です。Client::new()
で構築します。メソッドは
request()
get()
get_no_query()
post()
post_no_body()
put()
put_no_body()
delete()
delete_no_query()
の9つです。request()
以外は引数を補ってrequest()
を呼んでいるだけです。GET
とDELTE
はボディは指定できず、POST
とPUT
はパラメータは指定できません。指定したい場合はrequest()
を直接呼んでください。
request()
のシグネチャを見てみましょう。
pub async fn request<Q, B, H>(
&self, method: Method, url: &str, query: Option<&Q>, body: Option<B>, handler: &H,
) -> Result<H::Successful, RequestError<H::BuildError, H::Unsuccessful>>
where
Q: Serialize + ?Sized,
H: RequestHandler<B>,
型パラメータが3つあります。Q
はクエリの型、B
はボディの型、H
はハンドラの型です。ハンドラにはRequestHandler
を実装したものを渡します。RequestHandler
については後で詳しく説明します。
返り値はResult<H::Successful, RequestError<H::BuildError, H::Unsuccessful>>
と、ハンドラの関連型(associated type)のResult
になっています。これはハンドラの実装によって返り値の型を変えられるようにするためです。例えば、ハンドラにBinnaceRequestHandler
を指定した場合、返り値がErr
バリアントの場合はBinanceHandlerError
型になります。(実際は次に説明するRequestError
の中に入っています。)
get()
やpost()
などに no_query や no_body がついたバージョンがあるのは、クエリやボディにNone
を指定すると型推論が働かず、何か適当な型を指定しなければならないという問題を回避するためです。Rustでもオーバーロードができるようになって欲しいですね。
4.1.2. RequestError
request()
などの返り値のErr
の方に入っている、エラーを表す列挙型です。
バリアントは
SendRequest(reqwest::Error)
ReceiveResponse(reqwest::Error)
BuildRequestError(E)
ResponseHandleError(R)
の4つです。SendRequest
とReceiveResponse
の意味はそのままです。reqwest
からエラーが返されたらそれをそのまま渡しています。
BuildRequestError
は、request()
がハンドラにリクエストを作らせたときに、ハンドラがエラーを返したときに使われます。中身がジェネリックのE
になっていますが、これはリクエストハンドラの実装によってエラーの型を変えられるようにするためです。エラーになるのは、認証を有効にしているのにAPIキーが指定されていなかった場合などを想定しています。
ResponseHandleError
は、request()
がハンドラにレスポンスを処理させたときに、ハンドラがエラーを返したときに使われます。これも中身はジェネリックです。エラーになるのは、サーバが、例えば注文を出すのに必要なお金がないなどの理由でエラーを返してきたときなどを想定しています。
4.1.3. RequestConfig
RequestConfig
はリクエストの設定をするための構造体です。ハンドラによってrequest()
に渡されます。
フィールドは
max_try: u8
retry_cooldown: Duration
timeout: Duration
url_prefix: String
の4つです。timeout
はリクエストのタイムアウトでデフォルトで3秒、url_prefix
はURLの先頭に付与されるもので"https://api.binance.com"
などを想定しています。
残りの2つについてですが、これはrequest()
の自動再試行機能の設定です。私はEC2でbotを動かしているのですが、なぜかリクエストがタイムアウトが失敗することがたまにあります。その場合には少し間を開けて再試行すると問題なく動くので、この機能を入れました。request()
はリクエストの送信に失敗すると、retry_cooldown
の間待ってからリクエストを作り直して再送信します。max_try
はリクエストの最大送信回数を指定します。1回目も含めての回数なので、デフォルトは1
で自動再試行機能は無効化されています。
4.1.4. RequestHandler
ここまででご理解いただけたと思いますが、RequestHandler
はリクエストの様々な処理をやってくれる構造体に実装する重要なトレイトです。
関連型は
Successful
Unsuccessful
BuildError
の3つ、
メソッドは
build_request()
handle_response()
request_config()
の3つです。
また、B
という型パラメータが1つあります。
4.1.4.1. request_config()
request_config()
はリクエストの設定を表すRequestConfig
をrequest()
に渡すためのメソッドです。&self
以外に引数はなく、返り値はRequestConfig
です。
4.1.4.2. build_request()
build_request()
はrequest()
から作りかけのreqwest::RequestBuilder
を受け取って完成させるメソッドです。この中で認証に必要なヘッダ付与などを行います。シグネチャを見てみましょう。
fn build_request(&self, builder: RequestBuilder, request_body: &Option<B>, attempt_count: u8)
-> Result<Request, Self::BuildError>;
attempt_count
はこれが何回目のリクエストかが入っています。何に使うのかはわかりませんが一応渡しています。
返り値の型はResult<Request, Self::BuildError>
です。正常に認証などを行えた場合はOk(reqwest::Request)
を、APIキーがないなどのエラーの場合はErr(BuildError)
を指定します。BuildError
は関連型なので自由に指定できます。request()
はBuildError
をRequestError::BuildRequestError(BuildError)
という風にRequestError
に入れて呼び出し元に返します。
ボディの型が&Option<B>
とジェネリックになっていますが、これはrequest()
に渡されるボディの型もジェネリックだからです。(request()
のB
という型パラメータを思い出してください。)一つのRequestHandler
で複数種類のボディに対応できるよう、B
は関連型ではなくジェネリックの型パラメータにしています。
4.1.4.3. handle_response()
handle_response()
はrequest()
からレスポンスを受け取ってrequest()
の呼び出し元に返す形に加工するメソッドです。
シグネチャを見てみましょう。
fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes)
-> Result<Self::Successful, Self::Unsuccessful>;
引数の意味はそのままです。返り値はResult<Successful,Unsuccessful>
です。Successful
もUnsuccessful
もどちらも関連型なので自由に指定できます。前にも書きましたが、APIからエラーメッセージが返された場合にErr(Unsuccessful)
を返すことを想定しています。
request()
の呼び出し元には、返り値がOk(Successful)
の場合はそのままOk(Successul)
が、Err(Unsuccessful)
の場合はRequestError::RequestHandleError(Unsuccessful)
という風にRequestError
に入れられたものが返されます。
4.1.4.4. 実装例
実際にRequestHandler
を実装しているBinanceRequestHandler
の実装を見てみましょう。(関数の中身は省略しています。)
pub struct BinanceRequestHandler<'a, R: DeserializeOwned> {
api_key: Option<&'a str>,
api_secret: Option<&'a str>,
security: BinanceSecurity,
base_url: BinanceHttpUrl,
max_try: u8,
_phantom: PhantomData<*const R>,
}
impl<'a, B, R> RequestHandler<B> for BinanceRequestHandler<'a, R>
where
B: Serialize,
R: DeserializeOwned,
{
type Successful = R;
type Unsuccessful = BinanceHandlerError;
type BuildError = &'static str;
fn request_config(&self) -> RequestConfig
fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8)
-> Result<Request, Self::BuildError>
fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes)
-> Result<Self::Successful, Self::Unsuccessful>
}
フィールドにAPIキーやAPIシークレット、認証の有無、URLのFQDN、RequestConfig
にセットするmax_try
などがあります。
わかりにくいのはR
という型パラメータです。そのまま関連型のSuccessful
に指定されています。これはリクエストが成功したときにrequest()
の呼び出し元に返される型でした。なぜこれが型パラメータになっているかというと、ユーザーがrequest()
を呼び出すときに型を指定できるようにするためです。
この例を思い出してください。
let trades: Vec<OldTrade> = client.get(
"/api/v3/historicalTrades",
Some(&TradesLookupParams { symbol: "BTCUSDT", limit: 3 }),
&binance.request(BinanceSecurity::Key, BinanceHttpUrl::Spot),
).await.expect("failed to get trades");
呼び出し元がtrades
の型をVec<OldTrade>
と指定することで、自動的にその型に変換されています。これはBinanceRequestHandler
のhandle_response()
のなかで、serde_json::from_slice()
を使っているのですが、それではどうやって変換先の型を知るのでしょうか。request()
からは型の情報は渡されず、Successful
という関連型に自分で指定しなければなりません。
その答えが、変換先の型をR
というジェネリックの型パラメータに入れることです。つまり、この例でrequest()
に渡しているハンドラの型はBinanceRequestHandler<Vec<OldTrade>>
です。このハンドラによって処理されたリクエストはすべてVec<OldTrade>
に変換されます。別の型に変換したいときは、別のBinanceRequestHandler
を使わなければなりません。Binance::request()
でBinanceRequestHandler
を取得していますが、そのシグネチャを見てみましょう。
pub fn request<R: DeserializeOwned>(&self, security: BinanceSecurity, base_url: BinanceHttpUrl)
-> BinanceRequestHandler<R>
この関数にもR
という型パラメータがあることがわかります。このようにして、ユーザーが指定した型にデシリアライズしています。
api_key
やapi_secret
がString
ではなく&'a str
なのは、BinanceRequestHandler
が再利用されることが想定されておらず、Client::request()
に渡したらその場で破棄されることを想定しているからです。リクエストごとにBinance::request()
を呼び出してBinanceRequestHandler
を取得する設計となっているため、無駄なコピーが発生しないように参照になっています。
ちなみに、_phantom
はコンパイラに怒られないようにするために必要です。これがないとR
の型パラメータがフィールドの型にも関数のシグネチャにも使われていない状態となるため、
error[E0392]: parameter `R` is never used
と言われてしまいます。
4.2. websocket モジュール
WebSocket接続を行うためのモジュールです。
4.2.1. WebSocketConnection
WebSocketの接続を表す構造体です。この構造体が存在している間はWebSocketが切断されても自動で再接続されます。この構造体がdropされると接続も閉じられます。
WebSocketConnection
を構築するためには、WebSocketConnection::new()
を使ってください、Client::request()
と同様に、WebSocketHandler
トレイトを実装したハンドラを渡す必要があります。受信したメッセージはこのハンドラに渡されます。
4.2.1.1. 再接続
前にも少し述べましたが、WebSocketConnection
には再接続機能があります。再接続が行われるのは、
- メッセージ受信時に何かしらのエラーが発生したとき
- 接続を開いてから
WebSocketConfig
で指定された時間が経過したとき - ユーザーが
ReconnectState::request_reconnect()
を呼んだとき
の3通りです。時間経過で自動で再接続する機能があるのは、例えばBinanceでは一つの接続は24時間で自動で閉じられてしまうためです。crypto-botters-binance
を使っている場合は、デフォルトで12時間で再接続されます。
再接続の方法ですが、メッセージがダブったり抜けたりしないようにするために、工夫をしています。generic-api-client
のデフォルトのignore_duplicate_during_reconnection
というオプションがfalse
の場合は以下の様になっています。
generic-api-client
のデフォルトではfalse
ですが、crypto-botters-binance
やcrypto-botters-bitflyer
はデフォルトでtrue
に変更します。
4.2.1.2 ignore_duplicate_during_reconnection: false
新しい接続を開き終わってから古い接続を切断します。
ここで重要なのが新しい接続を開く前から古い接続を閉じ終わったあとまでを囲っている、赤い枠です。この枠の中では、接続が二重に開かれているため受け取ったメッセージをそのまま使うとダブりが発生する可能性があります。そこで、以下のようなアルゴリズムでダブりをなるべく排除します
- メッセージを受け取ったら、同じメッセージを新旧の接続のどちらが多く既に受信しているか調べる。
- もしどちらも受信したことがなければメッセージは有効
- ダブりである恐れがないので有効
- すでに多く受信している方から受信した場合は有効
- 例えば新しい接続がそのメッセージを3回、古い接続が2回受信している場合に、新しい接続が同じメッセージを更にもう1回受け取った場合
- 受信回数が少ない方から受信した場合は無効
- 受信回数が少ないということは、その接続がもう片方の接続よりも遅れているということを示唆しています。その状況で、もう片方の接続が既に多く受信しているメッセージを受信したのなら、そのメッセージはもう片方の接続が既に受信しているメッセージが遅れて届いただけだと思われるので、無効として無視します。
もちろん、このやり方は確実ではありません。というか、全く同じメッセージが短時間のうちに何回も送られてくる場合に2つの接続の間で遅延が発生すると、実際に何回のメッセージが送られてきたのかを判断する方法はありません。
4.2.1.3 ignore_duplicate_during_reconnection: true
しかし、全く同じメッセージが複数回受信されない ということがわかっていれば、もっと精度を上げる事ができます。それがignore_duplicate_during_reconnection: true
です。このモードでは、以下のようにして再接続を行います。
赤い枠の始まりから緑の線の長さだけ待ち、新しい接続を開くとまた緑の線の長さだけ待ち、古い接続を切断してからも緑の線の長さだけ待ったあとに赤い枠を閉じます。
ignore_duplicate_during_reconnection: false
ではダブりが発生しないように新しい接続を開いたらすぐに古い接続を閉じていましたが、ignore_duplicate_during_reconnection: true
の場合は同じメッセージは複数回受信されないとわかっているので、ダブりについて心配する必要はありません。そのため、今度は逃すメッセージをなくすことに集中できます。3回待機を挟んで赤い枠の範囲を広げ、なるべく多くのメッセージを受信することで逃すメッセージを減らしています。
この場合、赤い枠の中での処理も単純です。
- 既に受信したメッセージはすべて無視する。
このモードでBinanceのBTCUSDTの取引をリアルタイムで受信している状況で、再接続を行ってみました。BinanceのBTCUSDTはかなりの頻度で取引があるのですが、再接続中も1件の抜けもダブりもありませんでした。
問題なく動いていると思われるため、crypto-botters-binance
やcrypto-botters-bitflyer
ではデフォルトでこちらのモードを使用しています。
4.2.1.4. その他の機能
WebSocketConnection::send_message()
というメソッドを呼ぶことで、メッセージを送信することができます。
また、WebSocketConnection::reconnect_state()
というメソッドを呼ぶことで、このWebSocketConnection
に対応するReconnectState
という構造体を得られます。これについては次に詳しく説明します。
4.2.2. ReconnectState
ReconnectState
は、上で説明した再接続の状況を管理するための構造体です。内部でArc
を使用しているため、クローンすることができます。メソッドは2つだけです。
-
is_reconnecting()
は、現在WebSocketConnection
が再接続を行なっている場合にtrue
を返します。 -
request_reconnect()
を呼ぶと、WebSocketConnection
に再接続をさせることができます。
4.2.3. WebSocketMessage
WebSocketMessage
はWebSocketのメッセージを表す列挙型です。tungstenite::Message
を少し変えただけです。列挙型なのはテキストのメッセージ以外にもpingやpongなどがあるからです。
4.2.4. WebSocketConfig
WebSocketConfig
はWebSocketConnection
の設定を表す構造体で、RequestConfig
のwebsocketモジュール版です。
フィールドは
connect_cooldown: Duration
refresh_after: Duration
url_prefix: String
ignore_duplicate_during_reconnection: bool
reconnection_wait: Duration
の5つです。
connect_cooldown
は接続を行ってから、次に接続を行うまでに待つ必要のある時間です。
refresh_after
は自動再接続する時間です。generic-api-client
のデフォルトはDuration::Zero
で、時間経過による自動再接続は無効化されていますが、crypto-botters-binance
ではデフォルトで12時間に設定されています。
url_prefix
はRequestConfig
のときと同じです。"wss://stream.binance.com:9443"
などを指定することを想定しています。
ignore_duplicate_during_reconnection: bool
はWebSocketConnection
の説明のなかで説明しました。crypto-botters-binance
やcrypto-botters-bitflyer
では短くno_duplicate
と呼ばれています。
reconnection_wait
は、WebSocketConnection
の再接続の説明のなかの緑の線の長さです。デフォルトでは300msです。
4.2.5. WebSocketHandler
最後に、WebSocketHandler
です。RequestHandler
と異なり、型パラメータや関連型はありません。
スレッド間を転送されるため、WebSockethandler
を実装する構造体は、Send
を実装していて、'static
である必要があります。
メソッドは
websocket_config()
handle_start()
handle_message()
handle_close()
の4つあります。
4.2.5.1. websocket_config()
websocket_config()
やRequestConfig::request_config()
に対応します。引数は&self
のみ、返り値はWebSocketConfig
です。
4.2.5.2. handle_start()
handle_start()
は接続が開かれたときに呼ばれます。引数は&mut self
のみです。再接続したときにも呼ばれることに注意してください。
返り値はVec<WebSocketMessage>
です。これは、WebSocketの接続に送られるメッセージです。bitFlyerのWebSocketは接続を開いたときに購読メッセージをおくる仕様になっているため、handle_start()
が購読メッセージを返すようになっています。
4.2.5.3. handle_message()
handle_message()
はメッセージを受信したときに呼ばれます。引数として&mut self
とWebSocketMessage
を取ります。返り値はhandle_starT()
と同じくVec<WebSocketMessage>
で、同じようにこのメソッドから返されたメッセージはWebSocketを通して送られます。
これはhandle_start()
やhandle_close()
に関しても同じですが、handle_message()
内で 重い処理を行わないで ください。handle_message()
が同時に実行されることはありません。handle_message()
が返らないと、次のメッセージを渡せません。
4.2.5.4. handle_close()
これは接続が閉じられたときに呼ばれます。&mut self
の他にreconnect
というbool
型の引数を取ります。再接続のために接続が閉じられたときはreconnect
がtrue
、そうでない場合、つまりWebSocketConnection
がdropされてもう再接続されることがない場合はreconnect
がfalse
になります。
5. さいごに
ここまで読んでくれてありがとうございました。
要望・質問・提案・苦情等ある方は、GithubかTwitterまでお願いします。
次に挙げるのは参考になった記事等です。
感謝します!