Can you use Thrift as a method of communication be

2019-08-15 13:02发布

问题:

Does Cloud Foundry support Thrift for talking between apps? I can't find anything about this and it seems REST/HTTP or messaging are the preferred mechanisms for inter-app communication.

Has anyone tried to offer a service using Thrift in CF? Is there any good reason why Thrift is not supported; or are there any plans to support it? I prefer Thrift to REST for cross-language communication because it's easy in Thrift to generate all the client-side classes used in the API calls. Plus it supports binary data transfer and so is potentially faster than REST. Plus we already have a Thrift API:).

I'm guessing that - in theory - a client app could talk directly to another CF app instance running a Thrift service; but then you'd be losing the load-balancing advantages that CF currently provides for HTTP or messaging.

回答1:

The Cloud Foundry Router currently supports routing of HTTP/HTTPS requests only. There is an effort underway to create a TCP Router to handle routing of non-HTTP traffic to apps running on CF.

There is a video that previews some of the capabilities the TCP Router will provide, and an incubating release that includes the TCP Router if you are deploying your own CF via BOSH.

The cf-dev mailing list would be a good place to ask additional questions about this.

I'm guessing that - in theory - a client app could talk directly to another CF app instance running a Thrift service

This is correct - apps running on CF can talk to each other using any protocol as long as the configured security groups have the proper ports open.



回答2:

Were there important restrictions over what you could send over HTTP vs Thrift? Also, I'd like to know more about how a client would call different methods on your service. Did you have one generalised HTTP method or did you end up with a REST-like API that mirrored your Thrift API's methods?

Below the code that handles an incoming request and returns the results. The code is part of an article I wrote for a german dev magazine some time ago. The whole package consisted of some JavaScript, some C# and some Delphi code. I might emphasize that we also use that same approach shown here in real code.

Delphi comes with built-in support for ISAPI. The OnHandlerAction is basically an event handler that can be assigned to selected URL endpoints, or to catch all requests. This also implies, that we don't need to care about thread management, because that's what the IIS does for us.

Server side

I decided to use the TJSONProtocol for two reasons. First, one of the clients was the JavaScript part which at that time was only capable to speak JSON. Second goal was to eliminate any risk of garbled data. I never tested TBinaryProtocol nor TCompactProtocol in that context, so I can't say much about that combination.

procedure TSample.OnHandlerAction( Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
const GENERIC_ERROR_JSON = '[1,"TextToolIsapi",2,0,{"2":{"rec":{"1":{"str":"Internal error: %s"}}}}]';
var b : TBytes;
    s : string;
begin
  try
    Response.Content := TTextToolJsonServer.Process( Request.Content);
  except
    on e:Exception do begin
      b := ( SysUtils.TEncoding.UTF8.GetBytes(e.message));
      SetString( s, PAnsiChar(@b[0]), Length(b));
      Response.Content := Format( GENERIC_ERROR_JSON, [s]);
    end;
  end;
end;

The remaining part is the piece that does the (de)serializing stuff and feeds the processor. In this sample code a processor instance is created for every call. If that does not fit, one can think of a pool of processor instances or the like. In our case the session-specific data are held elsewhere, so the processors are in fact very lightweight.

class function TTextToolJsonServer.Process( const aRequest : string) : string;
var handler   : Samples.TTextTool.Iface;
    processor : IProcessor;
    protIn, protOut : IProtocol;
    stmIn, stmOut : TStringStream;
begin
  stmIn  := nil;
  stmOut := nil;
  try
    stmIn  := TStringStream.Create( aRequest);
    stmOut := TStringStream.Create;

    protIn  := TJSONProtocolImpl.Create(
                 TStreamTransportImpl.Create(
                   TThriftStreamAdapterDelphi.Create( stmIn, FALSE), nil));

    protOut := TJSONProtocolImpl.Create(
                 TStreamTransportImpl.Create(
                   nil, TThriftStreamAdapterDelphi.Create( stmOut, FALSE)));

    handler   := TTextToolHandler.Create;
    processor := Samples.TTextTool.TProcessorImpl.Create( handler);
    processor.Process( protIn, protOut);

    result := stmOut.DataString;

  finally
    stmIn.Free;
    stmOut.Free;
  end;
end;

That's all, the rest is standard processor/handler implementation.

Sample client

The client side counterpart could be as simple as this piece of JavaScript. Unfortunately I don't have a Java example at hand, but it would look very similar.

  function MakeClient()
  {
    var transport = new Thrift.Transport("bin/TextToolSample.dll"); 
    var protocol  = new Thrift.Protocol(transport);
    var client = new Samples.TextToolClient(protocol);
    return client;
  }

  function CountWords() 
  {
    try
    {
      var client = MakeClient();
      var nCount = client.CountWords( document.EDITOR.textinput.value);
      alert("Text contains "+nCount.toString()+" words.");
    }
    catch (error) 
    {
      HandleError( error) 
    }
  }


回答3:

Following on from @JensG's suggested solution I wanted to find a solution that used Thrift/HTTP. This is a fallback in case your CF admins don't want to support Thrift/TCP if, for example, the absence of a .Net buildpack in your CF deployment means the Thrift service has to be hosted outside CF. I'm sharing it here because it wasn't obvious to me how to get Thrift/HTTP working for a C# service implementation. Thrift 0.9.3 was used throughout.

Sample Thrift IDL file

namespace csharp AuthServiceGenCSharp
namespace * AuthServiceGenCSharp.thrift

struct Request
{
    1:string name
    2:i32 age
}

struct Result
{
    1:string reply
    2:bool permissionGranted
}

service AuthorizationService
{
    Result AskPermission(1:Request req)
}

Use the Thrift compiler to generate the C# stubs as normal. Then implement the service:

Sample Service Implementation (C#)

using AuthServiceGenCSharp;

namespace AuthServiceImplementation
{
    /// <summary>
    /// Implementation of the Thrift interface.
    /// </summary>
    public class AuthorizationServiceImpl : AuthorizationService.Iface
    {
        public Result AskPermission(Request req)
        {
            Result result = new Result();
            result.Reply = "Hello " + req.Name;
            result.PermissionGranted = true;
            return result;
        }
    }
}

Create a custom HTTP handler

Fortunately Thrift already provides a class that implements the IHttpHandler interface: Thrift.Transport.THttpHandler. All you have to do is derive your handler from THttpHandler and pass in the processor that the Thrift compiler generated from your IDL. And specify the JSON protocol:

using AuthServiceGenCSharp;
using AuthServiceImplementation;
using Thrift.Protocol;
using Thrift.Transport;

// See also https://codealoc.wordpress.com/2012/04/06/thrift-over-http-with-iis/
namespace AuthServiceHTTPHandler.App_Code
{
    public class AuthServiceHandler : THttpHandler
    {
        public AuthServiceHandler()
            : base(CreateAuthProcessor(), CreateJSONFactory())
        {
            // We call the constructor for THttpHandler, passing in the processor we want to use (i.e., the one that
            // the Thrift compiler generated from our IDL) and the protocol factory for JSON in this case.
            // We can't use THttpHandler directly for 2 reasons. First, it doesn't have a no-arg constructor which
            // all proper handlers must have. (At runtime you'll get this error:
            // HTTP/1.1 500 Internal Server Error [MissingMethodException: Constructor on type &#39;Thrift.Transport.THttpHandler&#39; not found.])
            // Second, we need to tell THttpHandler which processor to use at the very least.
            // Maybe Thrift should make their THttpHandler class abstract to prevent people like me from trying to use it directly!?
        }

        private static AuthorizationService.Processor CreateAuthProcessor()
        {
            return new AuthorizationService.Processor(new AuthorizationServiceImpl());
        }

        private static TJSONProtocol.Factory CreateJSONFactory()
        {
            return new TJSONProtocol.Factory();
        }
    }
}

(A similar example is given on the codealoc blog.) Put this handler in the App_Code folder of your ASP.Net web application. Register the handler in your web.config. I'm using IIS 7.5 in integrated mode so I did this:

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <handlers>
      <remove name="WebDAV" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <add name="AuthServiceHandler"
           path="*."
           verb="*"
           type="AuthServiceHTTPHandler.App_Code.AuthServiceHandler"
           preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  </system.webServer>

Sample Java Client snippet

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TJSONProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.THttpClient;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import AuthServiceGenCSharp.thrift.AuthorizationService.Client;
import AuthServiceGenCSharp.thrift.Request;
import AuthServiceGenCSharp.thrift.Result;

...

TTransport transport = null;
try {
    transport = new THttpClient("http://localhost:9999");
} catch (TTransportException e) {
    e.printStackTrace();
}
TProtocol protocol = new TJSONProtocol(transport);
Client client = new Client(protocol);

try {
    transport.open();
    Request request = new Request("Fred", 32);
    Result result = client.AskPermission(request);
    ObjectMapper mapper = new ObjectMapper();
    String json = mapper.writeValueAsString(result);
    log.info("Response in JSON format: " + json);
    return json;
} catch (TException e) {
    e.printStackTrace();                
} catch (JsonProcessingException e) {                               
    e.printStackTrace();
} finally {
    if (transport != null && transport.isOpen()) {
        transport.close();
    }
}

When the C# ASP.NET app was published from Visual Studio 2010 to IIS 7.5 and the Java client pointed at it, the Thrift response in the body of the HTTP response was:

[1,"AskPermission",2,1,{"0":{"rec":{"1":{"str":"Hello Fred"},"2":{"tf":1}}}}]

and the Java log output:

Response in JSON format: {"reply":"Hello Fred","permissionGranted":true,"setPermissionGranted":true,"setReply":true}

I originally tried implementing a custom HTTP module instead. But, although the correct Thrift response was coming back in the HTTP response, the HTTP header was always 405: Method Not Allowed (the Thrift client uses POST to send its requests). There's another SO post about this. However using an HTTP handler might be better than using a module in the long run because you have the option of creating an asynchronous handler for long-running requests.