初识thrift

thrift 是 facebook 于2007年开发的一款跨平台 RPC(Remote Procedure Call) 软件框架,
它可以在多种平台上进行无缝交互,数据传输使用二进制的方式,比XML和JSON体积更小,适合于内网的之间的数据进行交互。

thrift 结构

(参见https://www.ibm.com/developerworks/cn/java/j-lo-apachethrift/)

thrift 是由传输层、协议层和业务组成。用户选定的传输层类型、协议层类型之后,只需要关注业务代码即可,无需关注底层实现。

当生成了一套协议后,由客户端和和服务端根据协议文件生成 thrift 的接口库,
接口库会提供定义的service方法,直接调用方法,远程服务端会返回数据。

thrift类型

接口定义地址;
http://thrift.apache.org/docs/types

基本类型:

  • bool 对应js中boolean
  • byte 8位无符号整数
  • i16 16位无符号整数
  • i32 32位无符号整数
  • i64 64位无符号整数
  • double 64位符点型数字
  • string utf-8类型的字符串
  • binary 一系列基于string类型的编码的字符串

与Js对应的对象类型,

  • struct 结构体,可使用基本类型,强类型,需要定义不同的名称
    示例:
struct ListItem {
  1: i32 id,
  2: string content = \'\',
  3: Status status = 1,
  4: string author,
  5: i32 textLength
}

容器类型,是不能直接在外层定义的,需要在 struct 中定义或者在 service 中定义,
主要包括:
与Js的数组对应的类型:

  • list 一系列的数组元素

与Js中set对应的类型:

  • set 相当于不重复的数组

与Js中Map对应的类型

  • map 类似于字典模式

其他类型;
异常类型

  • exception

可调用接口

  • service
    示例:
service Todo {
  list<ListItem> getTodoList(),
  i32 getTotalLength(1: string author),
  i8 postTodo(1: PostItem item)
  ListItem doneArtical(1: i32 id)
  ListItem deleteArtical(1: i32 id)
}

使用thrift实现网页和nodejs服务端进行交互的实践

安装环境

为方便操作,使用vue进行html的开发。首先,需要安装thrift环境(在mac环境下,其他环境请参考http://thrift.apache.org/tutorial/):

brew install thrift

同时安装vue开发的环境,vue/cli,用于直接对单个文件进行开发(其实是为了省事,不想搭webpack环境)。

npm install -g @vue/cli
npm install -g @vue/cli-service-global

新建接口文件

接口文件是我们根据 thrift 定义的类型进行书写。其中除service类型外,其他定义都相当于定义成全局的类型,要注意名字的唯一性,service 是供我们调用的类型,就是接口。

创建的thrift文件如下:


enum Status {
  NORMAL = 1,
  DONE = 2,
  DELETED = 3
}

struct PostItem {
  1: string content = \'\',
  2: string author,
}

exception CodeError {
  1: i32 code = 0,
  2: string message = \'\'
}

struct ListItem {
  1: i32 id,
  2: string content = \'\',
  3: Status status = 1,
  4: string author,
  5: i32 textLength
}

service Todo {
  list<ListItem> getTodoList(),
  i32 getTotalLength(1: string author),
  i8 postTodo(1: PostItem item)
  ListItem doneArtical(1: i32 id)
  ListItem deleteArtical(1: i32 id)
}

Todo就是我们需要使用的类。

生成接口库文件

thrift -r --gen js:node todo.thrift && thrift -r --gen js todo.thrift

js:node 是供 Nodejs 调用的库文件,js 是浏览器环境的文件(貌似是需要使用grunt进行打包,反正我是用不习惯,只是个示例,直接中在html通过脚本引入了)。生成的文件保存在gen-jsgen/nodejs 两个文件夹下,一个是类型文件,一个是接口文件。

建立 server 端代码

由于浏览器和后台交互目前只支持 ajax 的方式,所以我们的服务端是需要搭建http服务器的。
使用 thrift 的 createWebServer即可(注意不要使用示例中的createServer,那个创建的是socket服务,不是Http服务)。同时设置好传输协议为json格式,传输层类型为buffer模式。为接口中的每个 service 添加实现方式。

const thrift = require(\'thrift\')

const Todo = require(\'./gen-nodejs/Todo\')
const tTypes = require(\'./gen-nodejs/todo_types\')

const data = []
let gid = 0

const actions = {
  getTodoList () {
    return data
  },
  getTotalLength () {
    return data.length
  },
  postTodo (item) {
    const result = new tTypes.ListItem({
      content: item.content,
      author: item.author,
      status: tTypes.Status.NORMAL,
      textLength: item.content.length,
      id: ++gid
    })
    data.push(result)
    return 0
  },
  doneArtical (id) {
    const result = data.find(item => item.id === id)
    if (!result) {
      throw new tTypes.CodeError({code: 1, message: \'请选择条目!\'})
    }
    result.status = tTypes.Status.DONE
    return result
  },
  deleteArtical (id) {
    const index = data.findIndex(item => item.id === id)
    const result = data[index]
    if (!~result) {
      throw new tTypes.CodeError({code: 1, message: \'请选择条目!\'})
    }
    data.splice(index, 1)
    return result
  }
}

const serverOptions = {
  // 静态文件服务器路径
  files: \'.\',
  // 设置跨域请求
  cors: {
    \'*\': true
  },
  services: {
    // 设置service
    \'/\': {
      // 传输层类型为buffer模式
      transport: thrift.TBufferedTransport,
      // 协议类型为json格式
      protocol: thrift.TJSONProtocol,
      processor: Todo,
      handler: actions,
    }
  }
}

const server = thrift.createWebServer(serverOptions)

server.listen(7878, () => {
  console.log(`监听端口:${7878}`)
})

创建浏览器端代码

浏览器代码就是写网页了。为了使用 vue 的 serve 功能,网页的名称需要设置为 App.vue,同时添加个自定义的 html 文件,添加引入 thrift 库脚本:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>新增文章</title>
  <meta name="viewport" id="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <link rel="shortcut icon" href="/favicon.ico">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="format-detection" content="telephone=no">
</head>

<body>
  <div id="app"></div>
  <script type=\'text/javascript\' src=\'http://localhost:7878/thrift-bundle.js\'></script>
</body>

</html>

vue文件内容为:

<template>
  <div>
    <section class="artical-list">
      <ul>
        <li
          v-for="(item, index) in list"
          :key="index">
          <p>{{item.content}}</p>
          <p>作者: {{item.author}}, 当前状态:{{item.status | status}}</p>
          <button @click="doneArtical(item)">设置为已阅</button>
          <button @click="deleteArtical(item)">删除</button>
        </li>
      </ul>
    </section>

    <section class="form-data">
      <textarea name="artical" v-model="artical" cols="30" rows="10"></textarea>
      <input type="text" name="author" v-model="author"/>
      <button @click="postArtical">提交</button>
    </section>
  
  </div>
</template>

<script>
/* eslint-disable */
export default {
  data () {
    return {
      list: [],
      artical: \'\',
      author: \'\',
    }
  },
  created () {
    this.init()
  },
  filters: {
    status (value) {
      const status = [\'无\', \'正常\', \'已阅\', \'删除\']
      return status[value]
    },
  },
  methods: {
    init () {
      const transport = new Thrift.Transport(\'http://localhost:7878\')
      const protocol = new Thrift.Protocol(transport)
      const client = new TodoClient(protocol)
      this.client = client
      this.getList()
    },
    getList () {
      this.client.getTodoList((result) => {
        this.list = result
      })
    },
    postArtical () {
      const result = new PostItem()
      result.content = this.artical
      result.author = this.author

      this.client.postTodo(result, (result) => {
        this.getList()
      })
    },
    doneArtical (item) {
      this.client.doneArtical(item.id, (result) => {
        if (result instanceof Thrift.TApplicationException) {
          alert(result.message)
          return
        }
        this.getList()
      })
    },
    deleteArtical (item) {
      this.client.deleteArtical(item.id, (result) => {
        if (result instanceof Thrift.TApplicationException) {
          alert(result.message)
          return
        }
        this.getList()
      })
    },
  },
}
</script>

主要思路是在初始化先创建接口的实例,设置 transport 的请求地址,然后使用我们定义的 service,
绑定实例在this上。每次要操作时直接调用实例的方法即可。看起来是不是和我们写普通封装好的axios一样?

运行

为方便使用,我们使用 nodemon 进行服务器开发自动重启,我们在 npm 包中添加以下脚本:

"scripts": {
  "start": "vue serve & node server.js",
  "dev": "vue serve & npm run compile && nodemon server.js",
  "compile": "npm run gen && npm run concat",
  "gen": "thrift -r --gen js:node todo.thrift && thrift -r --gen js todo.thrift",
  "concat": "concat -o thrift-bundle.js ./thrift.js ./gen-js/*.js"
},

这样,我们使用 npm start 启动已经构建好的服务,使用 npm run dev 进行开发,
使用 npm run compile 在改动了 thrift 接口文件后进行重新编译。

这样我们的网页就做好了:

总结

搭建一个简单的 thrift 项目还是很容易的,所有的代码已经放在我的github上https://github.com/wenlonghuo/code-test/tree/master/004_thrift
其他原理和总结有待后续挖掘。

版权声明:本文为dreamless原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dreamless/p/8494551.html