A Simple Introduction to Thrift and Practical Warfare

Keywords: Java socket network Apache

brief introduction

Thift is a software framework for the development of extensible and cross-language services. It combines powerful software stacks and code generation engines to build seamlessly integrated and efficient services in C+, Java, Go, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml.
Official address: thrift.apache.org

install

Thrift is easy to install and can be quickly installed using brew directly under Mac.

brew install thrift

Window s or Linux can be accessed through Official website Download, let's stop talking about it here.

After downloading and installing, we will get a tool named thrift (thrift. exe under Windows), through which we can generate thrift code in various languages.

Basics

data type

Thrift script defines several types of data:

Basic types

  • bool: boolean value, true or false, corresponding to Java boolean

  • byte: 8-bit signed integer corresponding to Java byte

  • i16: 16-bit signed integers, corresponding to Java's short

  • i32: 32-bit signed integer corresponding to Java int

  • i64: 64-bit signed integers, corresponding to Java's long

  • double: 64-bit floating point number, corresponding to Java double

  • String: Unknown encoding text or binary string corresponding to String in Java

struct type

Defining common objects, similar to structure definitions in C, is a JavaBean in Java

union type

Similar to union in C/C++.

Container type:

  • list: Array list for Java

  • set: HashSet for Java

  • map: HashMap for Java

exception type

Exception for Java

service type

Classes corresponding to services.

service types can be inherited, for example:

service PeopleDirectory {
   oneway void log(1: string message),
   void reloadDatabase()
}
service EmployeeDirectory extends PeopleDirectory {
   Employee findEmployee(1: i32employee_id) throws (1: MyError error),
   bool createEmployee(1: Employee new_employee)
}

Notice that in defining the log method for the PeopleDirectory service, we use the oneway keyword, which tells thrift that we don't care about the return value of the function and that we can return directly without waiting for the function to be executed.
One way keyword is usually used to modify functions without return value (void), but it is different from direct functions without return value, such as log function and reload database function. When client calls log function of server remotely through thrift, it can return directly without waiting for the log function of server to finish execution; however, when client calls R When the eloadDatabase method is used, although this method has no return value, the client must block and wait until the server notifies the client that the call is over, the remote call from the client can be returned.

Enumeration type

Like the enum type in Java, for example:

enum Fruit {
    Apple,
    Banana,
}

Example

Here is an example of using various types of IDL files:

enum ResponseStatus {
  OK = 0,
  ERROR = 1,
}

struct ListResponse {
  1: required ResponseStatus status,
  2: optional list<i32> ids,
  3: optional list<double> scores,
  10: optional string strategy,
  11: optional string globalId,
  12: optional map<string, string> extraInfo,
}

service Hello {
    string helloString(1:string para)
    i32 helloInt(1:i32 para)
    bool helloBoolean(1:bool para)
    void helloVoid()
    string helloNull()
}

About IDL Files

The so-called IDL, i.e. interface description language, needs to provide A. thrift suffix file before using thrift. Its content is the service interface information described by IDL.
For example, an interface description is as follows:

namespace java com.xys.thrift

service HelloWorldService {
    string sayHello(string name);
}

Here we define an interface called HelloWorldService, which has a method, sayHello. When thrift --gen java test.thrift is used to generate thrift interface service, a HelloWorldService.java file is generated. In this file, a HelloWorldService.Iface interface is defined. We can implement this interface on the server side.

Basic steps of server-side coding

  • Implementing Service Processing Interface impl

  • Create Processor

  • Create Transport

  • Create Protocol

  • Create Server

  • Start Server

For example:

public class HelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        HelloServer server = new HelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        // Create TProcessor
        TProcessor tprocessor = 
                new HelloWorldService.Processor<HelloWorldService.Iface>(new HelloWorldImpl());

        // Create TServerTransport, TServerSocket inherits from TServerTransport
        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        
        // Create TProtocol
        TProtocolFactory protocolFactory = new TBinaryProtocol.Factory();
        
        TServer.Args tArgs = new TServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(protocolFactory);

        // Create TServer
        TServer server = new TSimpleServer(tArgs);
        // Start Server
        server.serve();
    }
}

Basic steps of client coding

  • Create Transport

  • Create Protocol

  • Creating Client Based on Potocol

  • Open Transport

  • Call the corresponding method of the service.

public class HelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public static void main(String[] args) throws Exception {
        HelloClient client = new HelloClient();
        client.startClient("XYS");
    }

    public void startClient(String userName) throws Exception {
        // Create TTransport
        TTransport transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
        // Create TProtocol
        TProtocol protocol = new TBinaryProtocol(transport);

        // Create client.
        HelloWorldService.Client client = new HelloWorldService.Client(protocol);
        
        // Open TTransport
        transport.open();
        
        // Calling service methods
        String result = client.sayHello(userName);
        System.out.println("Result: " + result);

        transport.close();
    }
}

Thrift's network stack

As shown in the figure above, thrift's network stack includes transport layer, protocol layer, processor layer and Server/Client layer.

Transport layer

Transport layer provides the abstraction of reading or writing data from the network.
Transport layer and Proocol layer are independent of each other. We can choose different Transport layer according to our own needs without any influence on the logic of the upper layer.

In Thrift's Java implementation, we use the interface TTransport to describe transport layer objects. The common methods provided by this interface are:

open
close
read
write
flush

On the server side, we usually use TServerTransport to listen to client requests and generate corresponding Transport objects. The common methods provided by this interface are:

open
listen
accept
close

For ease of use, Thrift provides the following common Transport s:

  • TSocket: This transport uses blocked socket s to send and receive data.

  • TFramedTransport: Send data in the form of frames with a length in front of each frame. When the server uses non-blocking IO (that is, TNonblocking Server Socket on the server side), TFramedTransport must be used.

  • TMemory Transport: Using memory I/O. Java implementations use ByteArray Output Stream internally

  • TZlib Transport: Data compressed using Zlib. Not implemented in Java.

Protocol Layer (Data Transfer Protocol Layer)

The function of this layer is to convert data structures in memory into data streams or reverse operations that can be transmitted through Transport, which we call serialization and deserialization.

Common protocols are:

  • TBinaryProtocol, Binary Format

  • TCompact Protocol, Compression Format

  • TJSONProtocol, JSON format

  • TSimpleJSONProtocol provides JSON write-only protocol, and the generated files can be easily parsed by scripting language.

  • TDebugProtocoal, using human-readable Text format, helps debug

Note that the client and server protocols are the same.

Processor layer

Processor layer objects are generated by Thrift based on the user's IDL file, and we can't specify them at will.
This layer has two main functions:

  • Read the data from the Protocol layer and transfer it to the corresponding handler

  • Send the handler processing structure to the Prootcol layer.

Server level

The Server layer implementations provided by Thrift are as follows:

  • TNonblocking Server: This is a multi-threaded, non-blocking IO-based Server layer implementation dedicated to handling a large number of concurrent requests

  • THsHaServer: A synchronous/semi-asynchronous server model based on TNonblocking Server.

  • TThreadPool Server: A multi-threaded, blocked IO Server layer implementation that consumes more system resources than TNonblocking Server, but provides higher throughput.

  • TSimpleServer: This implementation is mainly for testing purposes. It has only one thread and blocks IO, so only one connection can be processed at the same time.

Use example

The following example is in my Github Source code, clone directly.

rely on

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.10.0</version>
</dependency>

Thift version: 0.10.0
Note that the version of the jar package needs to be consistent with the thrift version, or there may be some compilation errors

thrift file

test.thrift

namespace java com.xys.thrift

service HelloWorldService {
    string sayHello(string name);
}

Compile

cd src/main/resources/
thrift --gen java test.thrift
mv gen-java/com/xys/thrift/HelloWorldService.java ../java/com/xys/thrift 

When the thrift --gen java test.thrift command is executed, a gen-java directory is generated in the current directory. The generated server thrift code is stored in the package path format, and we can copy it to the corresponding directory of the project.

Service realization

public class HelloWorldImpl implements HelloWorldService.Iface {
    public HelloWorldImpl() {
    }

    @Override
    public String sayHello(String name) throws TException {
        return "Hello, " + name;
    }
}

Server/Client Implementation

Next, we implement them separately according to different types of servers, and compare the similarities and differences of these models.

TSimpleServer Server Server Model

TSimpleServer is a simple server-side model, which has only one thread and uses blocking IO model, so it is generally used in test environments.

Server-side implementation
public class SimpleHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        SimpleHelloServer server = new SimpleHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        TSimpleServer.Args tArgs = new TSimpleServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new TSimpleServer(tArgs);

        server.serve();
    }
}

In the server-side code, we do not specify the type of Transport explicitly, because TSimpleServer.Args specifies a default TransportFactory when it is constructed, and when there is a new client connection, a Transport instance of TSocket is generated. Because of this, we need to specify the client's Transport as TSocket when we implement it on the client side. Only then.

Client-side Implementation
public class SimpleHelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public void startClient(String userName) throws Exception {
        TTransport transport = null;

        transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
        // The agreement should be consistent with the server.
        TProtocol protocol = new TBinaryProtocol(transport);
        HelloWorldService.Client client = new HelloWorldService.Client(
                protocol);
        transport.open();
        String result = client.sayHello(userName);
        System.out.println("Result: " + result);

        transport.close();
    }

    public static void main(String[] args) throws Exception {
        SimpleHelloClient client = new SimpleHelloClient();
        client.startClient("XYS");
    }
}

TThreadPool Server Server Server Model

TThreadPool Server is an implementation based on thread pool and traditional blocking IO model. Each thread corresponds to a connection.

Server-side implementation
public class ThreadPoolHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        ThreadPoolHelloServer server = new ThreadPoolHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        TThreadPoolServer.Args tArgs = new TThreadPoolServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new TThreadPoolServer(tArgs);
        server.serve();
    }
}

The server-side implementation of TThreadPool Server is not very different from that of TSimpleServer, but it is only necessary to change TSimpleServer to TThreadPool Server in the corresponding place.

Similarly, in TThreadPool Server server-side code, like TSimpleServer, we do not explicitly specify the type of Transport, and the reason here is the same as that of TSimpleServer.

Client-side Implementation

Code implementation is the same as SimpleHelloClient.

TNonblocking Server Server Model

TNonblocking Server is based on thread pool and uses NIO mechanism provided by Java to implement non-blocking IO. This model can handle a large number of client connections concurrently.
Note that when using the TNonblocking Server model, the server-side and client-side Transport layers need to be specified as TFramedTransport or TFastFramedTransport.

Server-side implementation
public class NonblockingHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        NonblockingHelloServer server = new NonblockingHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(SERVER_PORT);
        TNonblockingServer.Args tArgs = new TNonblockingServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());
        // The following statement to set up TransportFactory can be removed
        tArgs.transportFactory(new TFramedTransport.Factory());

        TServer server = new TNonblockingServer(tArgs);
        server.serve();
    }
}

As mentioned earlier, in the server-side code implementations of TThreadPool Server and TSimpleServer, we did not explicitly set up Transport for the server-side because TSimpleServer.Args and TThreadPool Server. Args set up the default TransportFactory, and the resulting Transport is a TSocket instance.

So what happens in TNonblocking Server?
Looking at the code, we can see that when TNonblockingServer.Args is constructed, the constructor of the parent class AbstractNonblockingServerArgs is called. The source code is as follows:

public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) {
    super(transport);
    this.transportFactory(new TFramedTransport.Factory());
}

As you can see, TNonblocking Server. Args will also set up a default TransportFactory, which is typed as TFramedTransport#Factory, so the Transport used by TNonblocking Server is actually TFramedTransport type, which is why clients must also use TFramedTransport (or TFastFramedTransport) type of Transport.

Looking back at the code implementation, we can see that the sentence tArgs. transportFactory (new TFramed Transport. Factory ()) is redundant in the code, but I have retained it for the sake of emphasizing it.

Client-side Implementation
public class NonblockingHelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public void startClient(String userName) throws Exception {
        TTransport transport = null;

        // It is also possible for clients to use TFastFramedTransport.
        transport = new TFramedTransport(new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT));
        // The agreement should be consistent with the server.
        TProtocol protocol = new TBinaryProtocol(transport);
        HelloWorldService.Client client = new HelloWorldService.Client(
                protocol);
        transport.open();
        String result = client.sayHello(userName);
        System.out.println("Result: " + result);

        transport.close();
    }

    public static void main(String[] args) throws Exception {
        NonblockingHelloClient client = new NonblockingHelloClient();
        client.startClient("XYS");
    }
}
Asynchronous Client Implementation

In the TNonblocking Server server model, besides the painful client invocation mode, we can also use the asynchronous invocation mode in the client. The specific code is as follows:

public class NonblockingAsyncHelloClient {
    public static final String SERVER_IP = "localhost";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public void startClient(String userName) throws Exception {
        TAsyncClientManager clientManager = new TAsyncClientManager();
        TNonblockingTransport transport = new TNonblockingSocket(SERVER_IP,
                SERVER_PORT, TIMEOUT);

        // The agreement should be consistent with the server.
        TProtocolFactory protocolFactory = new TBinaryProtocol.Factory();
        HelloWorldService.AsyncClient client = new HelloWorldService.AsyncClient(
                protocolFactory, clientManager, transport);

        client.sayHello(userName, new AsyncHandler());

        Thread.sleep(500);
    }

    class AsyncHandler implements AsyncMethodCallback<String> {
        @Override
        public void onComplete(String response) {
            System.out.println("Got result: " + response);
        }

        @Override
        public void onError(Exception exception) {
            System.out.println("Got error: " + exception.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        NonblockingAsyncHelloClient client = new NonblockingAsyncHelloClient();
        client.startClient("XYS");
    }
}

As you can see, the implementation of asynchronous client invocation is more complex. Compared with Nonblocking HelloClient, we can see a few differences:

  • A TAsyncClient Manager instance needs to be defined in the asynchronous client, but not in the synchronous client mode.

  • The Transport layer of the asynchronous client uses TNonblocking Socket, while the synchronous client uses TFramedTransport.

  • The Procotol layer object of the asynchronous client needs to be generated using TProtocolFactory, while the synchronous client needs to be manually generated by the user.

  • The Client of the asynchronous client is HelloWorldService.AsyncClient, while the Client of the synchronous client is HelloWorldService.Client.

  • Last but not least, the asynchronous client needs to provide an asynchronous Handler to handle the server's replies.

Let's look at the AsyncHandler class again. This class is used for asynchronous callback. When we normally receive a response from the server, Thrift automatically calls back our onComplete method, so we can set up our subsequent processing logic here.
When an exception occurs on the server side of the Thrift remote call (e.g., the server is not started), it will be called back to the onError method, in which we can do the corresponding error handling.

THsHaServer Server Server Model

That is Half-Sync/Half-Async, semi-synchronous/semi-asynchronous server model, the underlying implementation actually depends on TNonblocking Server, so the required Transport is also TFramed Transport.

Server-side implementation
public class HsHaHelloServer {
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws Exception {
        HsHaHelloServer server = new HsHaHelloServer();
        server.startServer();
    }

    public void startServer() throws Exception {
        TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(
                new HelloWorldImpl());

        TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(SERVER_PORT);
        THsHaServer.Args tArgs = new THsHaServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.protocolFactory(new TBinaryProtocol.Factory());

        TServer server = new THsHaServer(tArgs);
        server.serve();
    }
}
Client-side Implementation

Consistent with Nonblocking HelloClient.

Reference resources

Posted by misheck on Fri, 12 Apr 2019 13:30:31 -0700