Skip to content
 

Apache ThriftのJavaサーバにC# (.NET)クライアントから接続する

前回までの記事で、Apache Thriftを使ったJavaサーバとそれに接続するJavaクライアントを作りました。構成としては、

  • Apache Thrift 0.6.1〜0.7.0
  • サーバサイドは Tomcat上で ServletとしてThriftの要求を受付
  • トランスポート層はHTTPで、TBinaryProtocolを使用

という感じです。このJavaのサーバにC#のクライアントから接続できるかどうか、試してみたいと思います。

.NET版のThrift DLLを作る

MacOS X 上でビルドした Thriftコンパイラは、C#のコードを吐く事ができます。ただこれはあくまで自動生成部分です。Javaも ThriftのJarを Mavenで取ってきていましたが、.NET版の実際の動作にはThriftのDLLを準備する必要があります。

どこからかダウンロードできると良いのですが、公式サイトにはソースコードしかありません。Windows用のThrift Compilerは .exeが置いてあるんですがね、、、。

そんなわけで Visual Studio 2010を使ってソースからDLLをビルドします。

ソースを展開する

http://thrift.apache.org/download/ からソースコードをダウンロードします。執筆時点での最新版はthrift-0.7.0.tar.gz です。これを展開すると、いろいろファイルが出てきますが、目的のDLLのソースは、lib/csharp/src の中にあります。

このフォルダに Thrift.sln という VisualStudioのソリューションファイルがあるのですが、なぜかうまく開けません。「選択されたファイルは有効なソリューションファイルではありません。」というエラーが出てしまいます。

thrif-0.6.1でも thrift-0.7.0でもだめでした。海外のサイトにはあまり情報がないようなのですが、日本語のVisual Studio特有の問題なのでしょうか?

Visual Studioでビルドする

仕方がないので空のソリューションを作り、そこにソースを突っ込んでビルドします。手順は以下の通りです。

  • VS2010を立ち上げ、「ファイル」-「新規作成」-「プロジェクト…」を選択します。
  • 新規プロジェクトダイアログで「クラスライブラリ」を選択します。

  • プロジェクト名は Thrift-0.7.0 にしてみました。
  • プロジェクトができたら、ソリューションエクスプローラの上でThrift-0.7.0プロジェクトを右クリックし、プロパティを開きます。
  • プロジェクトのプロパティで、使用する .NETフレームワークのバージョンを選択します。僕は「 .NET Framework 4.0」を選びました。「.NET Framework 4.0 Client Profile」というのもあるのですが、こちらだと、System.Webパッケージが使用できないため、Thriftをビルドできませんでした。
  • プロジェクトを作成したら、thriftのlib/csharp/src の中身をドラッグ&ドロップするなどしてプロジェクトに追加します。
  • 「参照設定」を右クリックして「参照の追加…」メニューを開きます。
  • 参照の追加で、.NETの System.Webを追加します。
  • ここまでの状態で、プロジェクトは以下のようになっているはずです。

  • 「ビルド」-「ソリューションのビルド」を選択してビルドします。設定に間違いがなければビルドに成功するはずです。
  • ビルドに成功したら、プロジェクトディレクトリの中に obj/Debug/Thrift-0.7.0.dll が出来ています。今後はこのファイルをコピーして利用します。

CardServiceのクライアントプロジェクトを作成する

DLLが出来たところで、JavaのCardServiceサーバに接続するクライアントプログラムをC#で作ってみます。VS2010で「ファイル」-「新規作成」-「プロジェクト…」を選択し、今度は「コンソールアプリケーション」を選びます。GUIが得意な人は「Windowsフォームアプリケーション」などでも構いません。適宜読み替えてください。

プロジェクト名は「CardServiceClient」にしてみました。

ThriftのDLLを追加する

プロジェクトが出来上がったら、先ほど作成したDLLをプロジェクトに追加します。プロジェクトのトップに lib フォルダを作り、そこにコピーします。

これだとファイルをコピーしただけなので、実際にThrift-0.7.0.dllが使われるように、参照設定を変更します。「参照設定」を右クリックして「参照の追加…」を選択します。

ダイアログが開いたら「参照」タブを選択して、今プロジェクトにコピーした lib/Thrift-0.7.0.dll を選択します。

これでプログラムをビルドする準備が整いました。

C#用のソースコードを生成する

Thriftコンパイラ(thriftコマンド)を使って CardServiceのC#用ソースコードを生成します。Javaのサーバ側を作ったときと同じ CardService.thriftファイルを用います。

CardService.thrift

#!/usr/bin/thrift

namespace java jp.ohnaka.thrift
namespace csharp Ohnaka.Thrift

struct Card {
  1: string id;
  2: string name;
}

exception TIllegalArgumentException
{
    1: string message
}

service CardService
{
        void addCard(1: Card card) throws (1:TIllegalArgumentException e);
        list<Card> getCards();
        Card getCardById(1: string id) throws (1:TIllegalArgumentException e);
}

ただし、Javaのコードを生成した時から一行だけ追記しています。C#用のネームスペースを宣言する部分です。

namespace csharp Ohnaka.Thrift

今回は、Ohnaka.Thriftとしてみました。お好みで書き換えてください。準備ができたら C#のコードを生成します。thriftコンパイラは MacOS X 上で作ったものをつかってみましたが、Windows用のThriftコンパイラ(thrift.exe)をダウンロードしてきてもOKです。

# thrift --gen csharp CardService.thrift

Javaのコードと同時に生成するのであれば、

# thrift --gen java --gen csharp CardService.thrift

としてください。

うまくいけば、gen-csharpというフォルダが作られますので、その中身をVisual Studioの CardServiceClientプロジェクトに追加します。

プロジェクトの設定を変更する

CardServiceClientプロジェクトの設定を少し変更する必要があります。というのも、Thrift-0.7.0.dllは、.NET Framework 4.0 を使ってつくられました。しかし、今作ったCardServiceClientはデフォルトで .NET Framework 4.0 Client Profieというものを使うように設定されています。ThriftのDLL は System.WebというClient Profileには入っていない namespaceのライブラリを使っているため、そのままでは動かないのです。

プロジェクトを右クリックして、プロパティを表示し、フレームワークを「.NET Framework 4.0」に変更してください。変更が終わったら、「参照設定」を右クリックし「参照の追加…」から System.Webを追加してください。

次のような画面になればOKです。

サーバを呼び出すコードを記述してみる

ひな形の mainメソッドがかかれた Program.cs というファイルがありますので、これを直接いじって、main()メソッドにコードを書いてみます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Thrift.Transport;
using Thrift.Protocol;
using Ohnaka.Thrift;

namespace CardServiceClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // 接続先のURLを指定
            THttpClient transport = new THttpClient(new Uri("http://localhost:8080/thrift-test-server/card"));
            // バイナリプロトコルを使用(サーバ側と合わせる)
            TProtocol protocol = new TBinaryProtocol(transport);
            // クライアントスタブを作成
            CardService.Client client = new CardService.Client(protocol);

            try
            {
                transport.Open();

                // サーバ側のaddCardメソッドを呼び出す
                Card card1 = new Card();
                card1.Id = "1000";
                card1.Name = "ohnaka";
                card1.Test = new Test();
                client.addCard(card1);

                // もう一個カードを作ってみる
                Card card2 = new Card();
                card2.Id = "1001";
                card2.Name = "kuni";
                client.addCard(card2);

                // サーバ側のgetCardsメソッドを呼び出す
                List<Card> cards = client.getCards();
                System.Console.WriteLine("Num of cards: " + cards.Count);

                // サーバ側のgetCardメソッドを呼び出す
                Card retrievedCard = client.getCardById("1000");
                System.Console.WriteLine("Retrieved card id   = " + retrievedCard.Id);
                System.Console.WriteLine("Retrieved card name = " + retrievedCard.Name);

                // 不正な引数を渡してみる
                try
                {
                    client.addCard(null);
                }
                catch (Exception ex)
                {
                    // どんな例外が飛ぶか調べる
                    System.Console.WriteLine(ex.Message);
                }
                // IDが null なCardを渡してみる
                try
                {
                    client.addCard(new Card());
                }
                catch (Exception ex)
                {
                    // どんな例外が飛ぶか調べる
                    System.Console.WriteLine(ex.Message);
                }
            }
            catch (Exception ex)
            {
                System.Console.WriteLine(ex.Message);
                return;
            }
            finally
            {
                System.Console.WriteLine("End. Please Hit Any Key.");
                System.Console.ReadLine();
            }
        }
    }
}

Javaで書いたクライアントのコードをほとんどそのままコピペした感じです。ここまで同じに書けるとは思いませんでした。

実行結果はこんな感じ。

うまくJavaのサーバと通信する事ができました!例外もきちんと飛んできています。

C#版のバグ?

リクエストがタイムアウトする

実は最初に試した時にTHttpClientのリクエストがタイムアウトしてエラーになってしまうという問題に悩まされました。色々調べてみたところ、次のバグ報告に行き着きました。

https://issues.apache.org/jira/browse/THRIFT-1260

When calling the server more than 16000 times, an exception is being thrown – “An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full” its because the stream is not being closed.

streamがクローズされていない正で、サーバを16000回以上呼び出した時に「バッファ領域が不足しているか、キューがいっぱいでソケット上の操作が完了できません」という例外がスローされる。

僕が遭遇したのはこの現象ではなかったのですが、streamがクローズされていないというのはかなり怪しい感じがします。試しこのページに添付されていたTHttpClient.csをダウンロードして、thrift-0.7.0.dllを作り直してみました。すると無事にエラーが起こらなくなりました。

この手のバグに簡単に遭遇したというところからして、ThriftはあまりC#上で使われていないのではないかという気がします。Thriftがサポートしているプログラミング言語は多岐に及びますが、枯れている言語と枯れていない言語がありそうで、注意が必要です。

SSL接続がタイムアウトする

試しに接続先を https://〜 にして、SSLでの接続を試みたのですが。見事に失敗しました。サーバに接続しようとしても何も応答が返ってこず、タイムアウトしてしまったのです。

原因を調べていたら以下のサイトが見つかりました。

http://stackoverflow.com/questions/5653868/what-makes-this-https-webrequest-time-out-even-though-it-works-in-the-browser

Using Microsoft Network Monitor, I found that HttpWebRequest would get stuck at a stage where it’s supposed to send back a client key exchange. It simply didn’t. The server duly waited for it, but it never came.

Microsoft Network Monitorを使って調べたところ、HttpWebRequestがクライアント鍵交換を行なう段階でスタックする事を発見しました。単に何もしていないのです。サーバは正しくそれを待っているのですが、返事は来ません。

この問題は、次の方法で回避できるそうです。

What fixed it was forcing HttpWebRequest to use SSL3 instead of TLS (even though TLS is supposed to automatically turn into SSL3 if necessary)

HttpWebRequestがTLSではなくSSL3を使うように強制するとこの問題が直ります(TLSは本来必要があれば自動的にSSL3になるはずなのですが)

この方も良く理由は分からないらしいのですが、TLSを使わずにSSL3を使う事を強制するとうまく接続できるようになるそうです。そのためには、接続前に以下のコードを実行すればOKです。

 ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3; 

これはどうやらグローバルな設定のようなので、どこかで一回やればOKなんだと思いますが、念のため、THttpClientのCreateRequestメソッドの頭に突っ込んでみました。

        private HttpWebRequest CreateRequest()
        {
            // TLSを使わずに強制的にSSLを使うように指示。そうしないとタイムアウトしてしまう
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
            HttpWebRequest connection = (HttpWebRequest)WebRequest.Create(uri);

うーむ。いろいろありますね。

自己署名の証明書を持つサーバにSSL接続する場合

Javaでもよくありますが、第三者に署名されていないいわゆる「オレオレ証明書」のサーバに接続する場合、「証明書が署名されてないぞ!」とエラーになってしまいます。僕はちゃんとした証明書を持っているのでこの問題にはあたりませんでしたが、もしこの問題にあたった方は以下の方法で回避できるそうです。

http://stackoverflow.com/questions/560804/how-do-i-use-webrequest-to-access-an-ssl-encrypted-site-using-https

やはり、ServicePointManagerに手を入れるようです。

ご参考まで。

コメントをどうぞ