Apache Thrift - 实现服务

在 Apache Thrift 中实现服务

Apache Thrift 允许您使用接口定义语言 (IDL) 定义服务和数据类型,并为各种编程语言生成代码。典型的服务实现涉及提供服务的服务器和使用该服务的客户端。

本教程将引导您完成使用生成的代码实现服务的过程,重点关注服务器端和客户端实现。

设置您的环境

在实现服务之前,请确保您具有以下内容:

  • Apache Thrift 编译器:已安装并配置。您可以从 Apache Thrift 网站下载它。
  • 生成的代码:使用 Thrift 编译器为您的目标编程语言生成必要的代码。
  • 编程环境:使用适当的依赖项(例如,Java、Python 等的 Thrift 库)设置您的编程环境。

生成服务代码

在 Thrift IDL 文件中定义您的服务后,下一步是使用您的目标编程语言为服务器和客户端生成相应的代码。

此代码生成过程很重要,因为它提供了在服务器端实现服务逻辑并与客户端服务交互所需的类和接口。

了解 Thrift 编译器的作用

Thrift 编译器("thrift"命令)是一种工具,它读取您的 Thrift IDL 文件并以您指定的编程语言生成代码。生成的代码包括以下内容:

  • 数据结构:与 IDL 文件中定义的结构、枚举、联合和其他数据类型相对应的类或类型。
  • 服务接口: IDL 中定义的每个服务的接口或基类,您必须在服务器应用程序中实现这些接口或基类。
  • 客户端存根:通过调用服务中定义的远程过程提供与服务器交互的方法的客户端类。

示例:Thrift IDL 文件

以下 Thrift IDL 文件定义了一个"User"结构、一个具有两个方法的"UserService"服务以及一个"UserNotFoundException"异常:

namespace java com.example.thrift
namespace py example.thrift

struct User {
  1: i32 id
  2: string name
  3: bool isActive
}

service UserService {
  User getUserById(1: i32 id) throws (1: UserNotFoundException e)
  void updateUser(1: User user)
}

exception UserNotFoundException {
  1: string message
}

使用 Thrift 编译器生成代码:

thrift --gen java example.thrift
thrift --gen py example.thrift

这将生成 Java 和 Python 中实现服务所需的类和接口。

用 Java 实现服务

从 Thrift IDL 文件生成必要的 Java 代码后,下一步就是实现服务。这涉及创建处理客户端请求的服务器端逻辑,以及开发与服务交互的客户端代码。

服务器端实现

在服务器端实现中,您首先需要实现服务接口:Thrift 编译器为每个服务生成一个 Java 接口。实现此接口以定义服务的行为:

public class UserServiceHandler implements UserService.Iface {
   @Override
   public User getUserById(int id) throws UserNotFoundException, TException {
      // 实现通过 ID 检索用户的逻辑
      if (id == 1) {
         return new User(id, "John Doe", true);
      } else {
         throw new UserNotFoundException("User not found");
      }
   }
   @Override
   public void updateUser(User user) throws TException {
      // 实现更新用户的逻辑
      System.out.println("Updating user: " + user.name);
   }
}

然后,我们需要设置服务器:创建一个服务器,用于监听客户端请求并在服务处理程序上调用适当的方法:

public class UserServiceServer {
   public static void main(String[] args) {
      try {
         UserServiceHandler handler = new UserServiceHandler();
         UserService.Processor<UserServiceHandler> processor = new UserService.Processor<>(handler);
         TServerTransport serverTransport = new TServerSocket(9090);
         TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor));

         System.out.println("Starting the server...");
         server.serve();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

其中,

  • 服务器传输:指定通信传输(例如套接字)。
  • 处理器:通过将传入请求委托给服务处理程序来处理传入请求。
  • 服务器:服务器侦听请求并将其传递给处理器。

客户端实现

在客户端实现中,您首先需要创建客户端:Thrift 编译器为每个服务生成一个客户端类。使用此类调用服务器上的方法:

public class UserServiceClient {
   public static void main(String[] args) {
      try {
         TTransport transport = new TSocket("localhost", 9090);
         transport.open();

         TProtocol protocol = new TBinaryProtocol(transport);
         UserService.Client client = new UserService.Client(protocol);

         User user = client.getUserById(1);
         System.out.println("User retrieved: " + user.name);

         user.isActive = false;
         client.updateUser(user);

         transport.close();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

其中,

  • 传输:管理与服务器的连接。
  • 协议:指定如何序列化数据(例如二进制协议)。
  • 客户端:提供调用远程服务的方法。

用 Python 实现服务

在 Python 中实现 Thrift 服务时,该过程涉及几个与其他语言(如 Java)类似的步骤。

您需要实现服务逻辑,设置服务器以处理客户端请求,并确保服务顺利运行。

服务器端实现

在服务器端实现中,您首先需要实现服务接口:在 Python 中,Thrift 编译器为每个服务生成一个基类。将此基类子类化以实现您的服务逻辑:

from example.thrift.UserService import Iface
from example.thrift.ttypes import User, UserNotFoundException

class UserServiceHandler(Iface):
   def getUserById(self, id):
      if id == 1:
         return User(id=1, name="John Doe", isActive=True)
      else:
         raise UserNotFoundException(message="User not found")

   def updateUser(self, user):
      print(f"Updating user: {user.name}")

然后,我们需要设置服务器:创建一个 Thrift 服务器来监听传入的请求并将其传递给服务处理程序:

from thrift.Thrift import TProcessor
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TSimpleServer
from example.thrift.UserService import Processor

if __name__ == "__main__":
   handler = UserServiceHandler()
   processor = Processor(handler)
   transport = TSocket.TServerSocket(port=9090)
   tfactory = TTransport.TBufferedTransportFactory()
   pfactory = TBinaryProtocol.TBinaryProtocolFactory()

   server = TSimpleServer(processor, transport, tfactory, pfactory)

   print("Starting the server...")
   server.serve()

其中,

  • 处理器:管理对处理程序的请求委托。
  • 传输和协议工厂:设置服务器的通信和数据序列化方法。
  • 服务器:启动服务器以处理客户端请求。

客户端实现

在客户端实现中,首先需要创建客户端:使用生成的客户端类连接到服务器并调用其方法:

from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from example.thrift.UserService import Client

if __name__ == "__main__":
   transport = TSocket.TSocket('localhost', 9090)
   transport = TTransport.TBufferedTransport(transport)
   protocol = TBinaryProtocol.TBinaryProtocol(transport)
   client = Client(protocol)

   transport.open()

   try:
      user = client.getUserById(1)
      print(f"User retrieved: {user.name}")

      user.isActive = False
      client.updateUser(user)
   except Exception as e:
      print(f"Error: {e}")

   transport.close()

其中,

  • 传输和协议:管理通信和数据格式。
  • 客户端:提供远程服务的接口,允许您调用服务器上的方法。

处理异常

正确处理异常可确保您的服务能够顺利管理错误并向客户端提供有意义的反馈。

在 Apache Thrift 中,可以在 IDL 文件中定义异常,并在服务实现和客户端代码中处理异常。处理异常涉及:

  • 在 Thrift IDL 中定义异常:在 Thrift IDL 文件中指定异常,以便服务器和客户端都了解可能发生的错误类型。
  • 在服务实现中抛出异常:在服务方法中实现逻辑以在必要时抛出异常。
  • 在服务器端处理异常:在服务器实现中管理异常,以确保服务可以从错误中恢复并提供有意义的响应。
  • 在客户端处理异常:在客户端代码中实现错误处理以管理服务器抛出的异常并做出适当的响应。

在 Thrift IDL 中定义异常

使用 exception 在 Thrift IDL 文件中定义异常关键字。您可以指定服务方法可以抛出的自定义异常类型:

示例:带有异常的 Thrift IDL 文件

exception InvalidOperationException {
   1: string message
}

service CalculatorService {
   i32 add(1: i32 num1, 2: i32 num2) throws (1: InvalidOperationException e)
   i32 divide(1: i32 num1, 2: i32 num2) throws (1: InvalidOperationException e)
}

其中,

  • 异常定义:"InvalidOperationException"是具有单个字段"message"的自定义异常。
  • 方法签名:"add"和"divide"方法指定抛出"InvalidOperationException"。使用"throws"关键字将异常包含在方法签名中。

在服务实现中抛出异常

在服务实现中,您需要根据方法的逻辑抛出异常。这涉及使用 IDL 中定义的异常:

from thrift.Thrift import TException

class InvalidOperationException(TException):
   def __init__(self, message):
      self.message = message

class CalculatorServiceHandler:
   def add(self, num1, num2):
      return num1 + num2

   def divide(self, num1, num2):
      if num2 == 0:
         raise InvalidOperationException("Cannot divide by zero")
      return num1 / num2

其中,

  • 自定义异常类:"InvalidOperationException"继承自"TException",并包含"message"属性。
  • 抛出异常:在"divide"方法中,如果除数为零,则会引发"InvalidOperationException"。

在服务器端处理异常

在服务器端,您应该处理异常以确保服务可以管理错误并提供适当的响应。

Python 服务器代码中的异常处理

from thrift.server import TSimpleServer
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from calculator_service import CalculatorService, CalculatorServiceHandler

if __name__ == "__main__":
   handler = CalculatorServiceHandler()
   processor = CalculatorService.Processor(handler)
   transport = TSocket.TServerSocket(port=9090)
   tfactory = TTransport.TBufferedTransportFactory()
   pfactory = TBinaryProtocol.TBinaryProtocolFactory()

   server = TSimpleServer.TSimpleServer(processor, transport, tfactory, pfactory)

   print("Starting the Calculator service on port 9090...")
   try:
      server.serve()
   except InvalidOperationException as e:
      print(f"Handled exception: {e.message}")
   except Exception as e:
      print(f"Unexpected error: {str(e)}")

其中,

  • 异常处理块:"try"块启动服务器,"except"块处理异常。"InvalidOperationException"被捕获并明确处理,而其他异常则由通用"Exception"块捕获。

在客户端处理异常

在客户端,您需要处理服务器抛出的异常。这可确保客户端可以管理错误并做出适当的反应。

带有异常处理的 Python 客户端代码示例

from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from calculator_service import CalculatorService, InvalidOperationException

try:
   transport = TSocket.TSocket('localhost', 9090)
   transport = TTransport.TBufferedTransport(transport)
   protocol = TBinaryProtocol.TBinaryProtocol(transport)
   client = CalculatorService.Client(protocol)

   transport.open()
    
   try:
      result = client.divide(10, 0)  # This will raise an exception
   except InvalidOperationException as e:
      print(f"Exception caught from server: {e.message}")
   finally:
      transport.close()

except Exception as e:
   print(f"Client-side error: {str(e)}")

其中,

  • 异常处理块:"try"块包围与服务器交互的代码。"except"块捕获服务器抛出的"InvalidOperationException",而一般的"Exception"块处理任何客户端错误。

同步与异步处理

在服务架构中,处理和处理任务的方式会显著影响性能、响应能力和用户体验。

同步和异步处理是两种基本方法,它们在处理操作的方式上有所不同,尤其是在网络或分布式系统中。

同步处理

同步处理是一种按顺序执行任务的方法。在此模型中,每个任务必须在下一个任务开始之前完成。这意味着系统等待一个操作完成后再继续执行下一个操作。

同步处理的特点如下:

  • 阻塞调用:每个操作都会阻塞后续操作的执行,直到完成为止。例如,如果调用服务方法,则调用者会等到该方法返回结果后再继续。
  • 简单流程:由于操作是接连执行的,因此执行流程简单且易于理解。由于代码按线性顺序执行,因此更容易实现和调试。
  • 可预测的性能:由于操作按请求的顺序完成,因此性能是可预测的。
  • 资源利用率:如果操作正在等待外部资源(例如网络响应),则可能导致资源利用率低下,因为在此期间系统保持空闲状态。

示例

考虑一个同步 Thrift 服务实现,其中客户端调用方法,服务器处理请求并返回结果,然后客户端才能继续:

# 客户端同步调用
# 客户端等待,直到服务器响应结果
result = client.add(5, 10)
print(f"Result: {result}")

在此示例中,客户端对"client.add"的调用被阻塞,直到服务器响应结果。客户端在等待期间无法执行其他任务。

异步处理

异步处理允许同时执行任务,而不会阻塞其他任务的执行。在此模型中,可以启动操作,然后独立于主执行流程运行。

以下是异步处理的特征:

  • 非阻塞调用:操作被启动并可以在后台运行,从而允许主线程或进程继续执行其他任务。例如,服务方法调用可以在后台完成操作时立即返回。
  • 复杂流程:由于任务是同时处理的,因此执行流程可能更复杂。这通常需要回调、承诺或未来对象来管理完成情况。
  • 提高性能:异步处理可以通过更有效地使用系统资源来提高性能,尤其是在 I/O 绑定操作中,这些操作中的任务通常等待外部响应。
  • 并发性:允许同时执行多个任务,这在高延迟环境中或处理许多同时发生的请求时非常有用。

示例

考虑一个异步 Thrift 服务实现,其中客户端在等待服务器响应时不会阻塞:

import asyncio
from thrift.transport import TSocket, TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TAsyncServer

async def call_add(client):
   result = await client.add(5, 10)  # Non-blocking call
   print(f"Result: {result}")

async def main():
   transport = TSocket.TSocket('localhost', 9090)
   transport = TTransport.TBufferedTransport(transport)
   protocol = TBinaryProtocol.TBinaryProtocol(transport)
   client = CalculatorService.Client(protocol)

   await transport.open()
   await call_add(client)
   await transport.close()

asyncio.run(main())

本例中,"call_add"是一个异步函数,不会阻塞其他任务的执行。"await"关键字用于执行对"client.add"的非阻塞调用,允许程序继续执行其他代码。