Thrift - 在 Scala 中使用 Thrift RPC



在 Scala 中使用 Thrift RPC 的示例。

什么是 Thrift

Apache Thrift 是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它最初是由 Facebook 为“大规模跨语言服务开发”而开发的,现在是 Apache 的开源项目。

Thrift 的特点:

  • 使用二进制格式,跨语言序列化的代价较低。
  • 数据结构与传输表现分离,支持多种消息格式。
  • 支持丰富的数据类型,性能优异。
  • 很多开源项目都支持 thrift 。

使用 Thrift

Apache Thrift 可以作为 RPC 框架,也可作为序列化/反序列化工具来使用。

Apache Thrift 虽然支持多种语言,但是并不原生支持 Scala 。我在本文中将使用 mill 作为构建工具来展示一个简单的使用 Thrift 作为 RPC 的示例。本文的例子使用与gRPC例子相似的结构。

项目结构:

ThriftExample
  |
  +-- ServerExample
  |     |-- resources
  |     |     +-- hello.thrift
  |     +-- src
  |           |-- RPCServer.scala
  |           +-- HelloServer.scala
  |     
  +-- ClientExample
  |     |-- resources
  |     |     +-- hello.thrift
  |     +-- src
  |           +-- HelloClient.scala
  |
  +-- build.sc
  |
  +-- lib
       +-- thrift-0.14.1.exe

定义服务

hello.thrift 内容:


namespace java learn.thrift.services

struct Person {
  1:  string  name
}

struct ToBeGreeted {
  1:  Person  person,
  2:  string  msg
}

struct Greeting {
  1:  string  message
}

service HelloWorld {
  Greeting sayHello(1:ToBeGreeted request)
}

服务端

RPCServer.scala 内容:


package learn.thrift.server

import java.net.InetSocketAddress

import org.apache.thrift.TProcessor
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.server.TServer
import org.apache.thrift.server.TServer.Args
import org.apache.thrift.server.TSimpleServer
import org.apache.thrift.transport.TServerSocket
import org.apache.thrift.transport.TServerTransport


trait RPCServer {
  def runServer(
    processor: TProcessor,
    address: InetSocketAddress
  ): Unit = {
    val serverTransport: TServerTransport = new TServerSocket(address)
    val serverArgs: Args = new Args(serverTransport)
    serverArgs.processor(processor)
    serverArgs.protocolFactory(new TBinaryProtocol.Factory())
    // TSimpleServer 简单地阻塞IO,一次只能接收和处理一个 Socket 连接。
    val server: TServer = new TSimpleServer(serverArgs)
    println("Start Server.") 

    Runtime.getRuntime.addShutdownHook(new Thread() {
      override def run(): Unit = {
        println("Shutdown server.")
        server.stop()
      }
    })

    server.serve()
  }
}

HelloServer.scala 内容:


package learn.thrift.hello.server

import java.net.InetSocketAddress

import learn.thrift.server.RPCServer
import learn.thrift.services._


object HelloServer extends RPCServer {

  class HelloService extends HelloWorld.Iface {
    override def sayHello(request: ToBeGreeted): Greeting = {
      val greeter = request.getPerson() match {
        case aperson: Person => aperson.getName()
        case _ => "friend"
      }

      val messageText = request.getMsg().toString match {
        case msg if msg.length > 0 => msg
        case _ => "~No message~"
      }

      val greeting = new Greeting(s"Hello ${greeter}, ${messageText}")
      greeting
    }
  }

  def main(args: Array[String]): Unit = {
    val serviceHandler = new HelloService()
    val serviceProcessor = new HelloWorld.Processor(serviceHandler)

    runServer(serviceProcessor, new InetSocketAddress("127.0.0.1", 40032))
  }
}

客户端

HelloClient.scala 内容:


package learn.thrift.hello.client

import scala.util.{Try, Success, Failure}

import org.apache.thrift.transport.TTransport
import org.apache.thrift.transport.TSocket
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.protocol.TProtocol

import learn.thrift.services._


object HelloClient {
  def main(args: Array[String]): Unit = {

    val tsocket: TSocket = new TSocket("127.0.0.1", 40032)
    tsocket.setTimeout(3000)
    val transport: TTransport = tsocket
    transport.open()

    val callRpcResult = Try {
      val protocol: TProtocol = new TBinaryProtocol(transport)
      val syncStub: HelloWorld.Client = new HelloWorld.Client(protocol)

      val greeter = new ToBeGreeted()
      greeter.setPerson(new Person("Doris"))
      greeter.setMsg("remote greetings!")

      val response: Greeting = syncStub.sayHello(greeter)
      response
    }

    transport.close()

    callRpcResult match {
      case Success(response) => println(s"${response.getMessage()}")
      case Failure(e) => throw e
    }
  }
}

构建工具

因为没有插件支持 mill 来根据 Thrift 服务定义生成相应代码,所以在 mill 里自定义了 Command 类型的 Task ,命令行调用 Apache Thrift compiler 来生成 Java 代码。

build.sc 内容:


import mill._
import mill.scalalib._


trait ScalaThriftExample extends ScalaModule {
  def scalaVersion = "2.13.3"
  def thriftVersion = "0.14.1"

  override def ivyDeps = T {
    super.ivyDeps() ++ Agg(
      ivy"org.apache.thrift:libthrift:${thriftVersion}",
    )
  }

  def thriftTool = os.pwd / 'lib / s"thrift-${thriftVersion}.exe"
  def sharedThriftInterface = "hello.thrift"
  def projectRoot = os.pwd
  def thriftPath = projectRoot / 'resources / sharedThriftInterface
  def genThriftPath = projectRoot / 'src


  def genThrift() = T.command {
    os.proc(thriftTool,
            "--out", genThriftPath,
            "--gen", "java",
            "-r", thriftPath
           ).call()
  }
}

// lib\thrift-0.14.1.exe --out ServerExample\src --gen java \
\\    -r ServerExample\resources\hello.thrift
object ServerExample extends ScalaThriftExample {
  override def projectRoot = os.pwd / 'ServerExample
}

// lib\thrift-0.14.1.exe --out ClientExample\src --gen java \
//    -r ClientExample\resources\hello.thrift
object ClientExample extends ScalaThriftExample {
  override def projectRoot = os.pwd / 'ClientExample
}

测试

首先,在 ThriftExample 下执行 mill -i ServerExample.genThrift,在 ServerExample/src 下就会生成对应 Thrift 定义的 Java 代码。再执行 mill -i ServerExample.run 就会编译并执行服务端代码。

在 ThriftExample 下执行 mill -i ClientExample.genThrift,在 ClientExample/src 下就会生成对应 Thrift 定义的 Java 代码。再执行 mill -i ClientExample.run 就会编译并执行客户端代码。可以客户端调用 sayHello 的结果:

1
Hello Doris, remote greetings!
本文链接: https://paxinla.github.io/posts/2021/05/thrift-zai-scala-zhong-shi-yong-thrift-rpc.html

知识共享许可协议 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可,欢迎转载、演绎,
但是必须保留本文的署名 Charles(包含链接),且不得用于商业目的。