質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.64%

APIのラッパーを作成したい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,838

aglkjggg

score 749

 Ⅰ. 現在の環境

# 機能、役割
1 WebSocketのみで通信可能なAPIサーバ
2 1と3の仲介を行うサーバ。1にある機能をHTTPアクセス可能なAPIとして実装します。いわゆる1のラッパーとなるサーバ。
3 HTTPのみで通信可能なクライアント

2の開発・動作環境
Windows 10 64bit
Visual Studio 2017
C# 7.0
ASP.NET としてプロジェクトを作成しました。

 Ⅱ. 作りたい物(やりたい事)

1と3を通信させたいです。

しかし、以下の2つの理由の為2のサーバを作成しました。

  • 通信プロトコルが異なる
    1と3はそれぞれ WebSocket と HTTP でしか通信できません。
    1でHTTP通信, 3でWebSocket通信といった事が許されていません。

  • WebSocket は一定時間ごとにPingしないと切断されてしまう
    2のサーバは常駐するタイプのアプリケーションとして作成しました。
    よって、一定時間ごとにPingする事に対応できました。

 Ⅲ. 発生している問題

 同時に複数リクエストがあった場合失敗してしまう

  • 「3のクライアント」から「2のサーバ」に対して同時にリクエストされた場合、
    同時にリクエストされた物が全て失敗し、クライアントに正しくレスポンスを返す事ができません。
  • エラー内容は無く、WebSocket.csの無限ループを無限に実行している形となってしまう為、クライアントに正しくレスポンスを返すことができていない状態です。
  • 「1の上位サーバ」と「2のサーバ」間は複数同時リクエストに問題が無いことは確認がとれています。

以下のプログラムが「2のサーバ」のプログラムです。

UserProfileController.cs

[Route("api/[controller]")]
public class UserProfileController : Controller
{
  // 以下のようなリクエストでこの関数が呼ばれる
  // http://example.com/api/userprofile/1000
  [HttpGet]
  public async Task<JsonResult> Get(int userId)
  {
      return Json(WebSocket.Instance.GetUserProfile(userId));
  }
}

WebSocket.cs
WebSocket クラスはシングルトンとして作っています。
「2のサーバ」の起動と同時に「WebSocket クラス」のインスタンスが作成(初期化)されます。

public class WebSocket
{
  private WebSocket websocket;
  private int messageId = 0;
  private static Api instance;
  public static Api Instance
  {
    get
    {
      if (instance == null)
      {
        instance = new Api();
      }

      return instance;
    }
  }

  private WebSocket()
  {
    websocket = new WebSocket($"ws://example.com");
    websocket.AutoSendPingInterval = 60; // 60 秒ごとに自動的にPingを送信する
    websocket.EnableAutoSendPing = true;
  }

  public async Task<dynamic> GetUserProfile(int userId)
  {
    // WebSocket の接続確立後は messageId は常にユニークな数値
    var tmpMessageId = this.messageId++;

    // ユーザーID 1000のプロファイルを取得する
    // { "messageId": 1, "action" : "GetUserProfile", "userId" : 1000 }
    var message = new
    {
      messageId = tmpMessageId,
      action = "GetUserProfile",
      userId = userId
    };

    dynamic response = null;
    EventHandler<MessageReceivedEventArgs> handler = null;
    handler = (sender, e) =>
    {
      // e.Message にはレスポンスのJSONが含まれている
      // { "messageId" : 1, "result" : { "user_name" : "hoge", "age" : 10 }}
      var json = (dynamic)JsonConvert.DeserializeObject(e.Message);
      if (json.messageId == tmpMessageId)
      {
        // レスポンスを変数に保存する
        // この匿名関数ではreturnできない。
        response = json;
      }

      // 受信が成功したのでイベントハンドラを消す
      websocket.MessageReceived -= handler;
    };

    // リクエストする前にイベントハンドラを設定する
    websocket.MessageReceived += handler;

    // リクエストする
    websocket.send(JsonConvert.SerializeObject(message));

    // 受信できるまで待つ
    while(true)
    {
      if(response != null)
      {
        break;
      }
      await Task.Delay(100);
    }

    return JsonConvert.SerializeObject(response);
  }
}

 Ⅳ. 聞きたいこと

どの点を改善すれば同時複数リクエストでも失敗しないサーバになりますでしょうか?
また、現在の構成以外でも1と3をうまく通信させる方法はありますでしょうか?

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

最初のリクエストで messageId が 1 になります。
次のリクエストで 2 になります。

最初のレスポンスでハンドラが呼ばれます。tmpMessageId は 1 です。messageId は 2 です。
レスポンスは捨てられます。

来ないレスポンスをずっと待ち続けます。

あと、シングルトンならコンストラクタは private にしてください。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/13 14:22 編集

    確認した所、messageIdが異なる別のイベントハンドラでレスポンスを受け取っていた為、本来受け取りたいイベントハンドラがずっと待ち続ける動きをしていました。

    何度も検証した所タイミング次第という部分があります。
    タイミングが良ければそれぞれのイベントハンドラでレスポンスを受信できますが、
    タイミングが悪ければ期待するイベントハンドラでレスポンスを受信できません。

    この点を解決する為に調べた所イベントの処理が得意というRx(Reactive Extensions)を知ったのですが、
    HTTPのリクエストがイベントではなく単なるメソッドなので、
    HTTP側の処理とWebSocket側の処理をRxでうまく記述する方法が分かりませんでした。

    キャンセル

  • 2017/05/13 14:45

    他のイベントハンドラで受け取っていても登録されたハンドラ全てが呼ばれるのではないかと思うのですが、そうはならなかったということですか?

    キャンセル

  • 2017/05/13 15:22 編集

    ありがとうございます!
    頂いたコメントで気づき、解決しました!
    messageId の一致する、しないに関わらずイベントハンドラが削除されていたのが問題でした。

    以下のように変更するとうまくいきました。

    websocket.MessageReceived -= handler;



    if(response != null)
    {
    websocket.MessageReceived -= handler;
    }

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.64%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる