---
# System prepended metadata

title: Protocol
tags: [GraphQL, RPC, 分散式系統, JSON, essay]

---

{%hackmd SybccZ6XD %}
<style>
.toc > ul > li:first-child {
  list-style: none;
}
.toc > ul > li:first-child > a {
  display: none;
}
</style>

Protocol
===

###### tags: `essay` `分散式系統` `JSON` `GraphQL` `RPC`

> [name=Shueh Chou Lu][time=June, 2021]

[TOC]

---

> 分散式系統中，很重要的一塊是「不同的服務間，彼此的溝通方式是什麼？」
> 比起撰寫完整又清楚的 API 文件，有沒有什麼好方法可以讓服務和服務之間同步 API？

以下討論皆假設：溝通是透過網路，且以 HTTP 協定為基礎。

## OSI (Open Systems Interconnection)

- 應用層（Application Layer）
- 表達層（Presentation Layer）
- 會議層（Session Layer）
- 傳輸層（Transport Layer）
- 網路層（Network Layer）
- 資料連結層（Data Link Layer）
- 實體層（Physical Layer）

```text
┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                             Data Frame                                                │
├─────────────────┬─────────┬───────────┬───────────┬───────────────────────────────────────┬───────────┤
│        DLH      │    H3   │     H4    │     H5    │      H6      │ H7 │       DATA        │    DLT    │
│                 │         │           │           │         ┌────┐         ┌────┐         │           │
│                 │         │           │           │ chunk 1 │Sync│ chunk 2 │Sync│ chunk N │           │
│                 │         │           │           └─────────┘    └─────────┘    └─────────┤           │
│                 │         │           │                                                   │           │
├─────────────────┼─────────┼───────────┼───────────┬──────────────┬────────────────────────┼───────────┤
│    Data Link    │ Network │ Transport │  Session  │ Presentation │       Application      │ Data Link │
├─────────────────┼─────────┼───────────┼───────────┼──────────────┼────────────────────────┼───────────┤
│                 │         │           │ Uncurrent │  Uncurrent   │                        │           │
├─────────────────┼─────────┼───────────┼───────────┴──────────────┴────────────────────────┼───────────┤
│ Error Detection │    IP   │    TCP    │                        HTTP                       │   Wi─Fi   │
│ Error Tolerance │         │    UDP    │───────────┬──────────────┬────────────────────────┤  Ethernet │
│                 │         │           │   Socket  │     ASCII    │                        │           │
│                 │         │           │           │     UTF─8    │                        │           │
│                 │         │           ├───────────┼──────────────┘                        │           │
│                 │         │           │           │                   FTP                 │           │
│                 │         │           └───────────┴──────────────┬────────────────────────┴───────────┤
│                 │         │                 TLS                  │                                    │
└─────────────────┴─────────┴──────────────────────────────────────┴────────────────────────────────────┘
```

## HTTP

```text
POST / HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 15

{"name":"evan.lu"}
```

空行後的下一行即為代表本次*請求*的 **body**，範例中的 **body** 是常見的 `JSON` 格式。

由此，可以想像 `JSON` 格式是在*應用層*之上的**第八層**。

### JSON

單純透過 `JSON` 傳遞有什麼缺點？

1. 正確的資料格式應該要長什麼樣子？
2. 使用者需要閱讀相關文件，有辦法讓機器自動處理嗎？

為了解決上述問題，就會有其他 protocol 需要被引入。

不過除了用其他協定，也有一些方式可以舒緩（降低）上述發生的問題，如：

- [JSON API](https://jsonapi.org/format/)
- [JSON Schema](http://json-schema.org/specification.html)
- [OpenAPI (Swagger)](https://swagger.io/specification/)

> 上述僅是制定一些規範，讓使用者在閱讀相關 API 文件時，能較快進入狀況。

### GraphQL

GraphQL 讓*使用者*在跟*服務*要取資料的時候能指定特定資料，這有幾個好處：

- 可以拿到最準確的資料，減少網路傳輸
- 把多種服務的資料在一次請求中要齊

這也讓 GraphQL 通常成為 *facade services*，也就是在眾多服務中的首個接觸點，並作為對外溝通的唯一渠道。

> GraphQL 並不限定在要 HTTP 上執行，也能執行如 TCP 等協定之上。

> 雖然請求時送出的是類似 Query 的語法，但 Response 並無指定，只要能代表其階層式的結果就行，如 `JSON`。

#### 規範

```graphql=
type RecipeRoot { 
  recipe(id: ID): Recipe
  pid: Int
}
type Recipe { 
  id: ID!
  name: String!
  steps: String
  ingredients: [Ingredient]! 
}
type Ingredient {
  id: ID!
  name: String!
  quantity: String
}
```

這份檔案是可以對外公開的，幫助使用者依此撰寫程式，類似上述提到的 OpenAPI。

#### 請求

這時，我們可以依照上述的規範送出請求：

```
{
  pid
}
```

```json=
{
  "data": {
    "pid": 9372
  }
}
```

```
{
  recipe(id: 42) {
    name
    ingredients {
      name
      quantity
    }
  }
}
```

```json=
{
  "data": {
    "recipe": {
      "name": "Chicken Tikka Masala",
      "ingredients": [
        { "name": "Chicken", "quantity": "1 lb" },
        { "name": "Sauce", "quantity": "2 cups" }
      ]
    }
  }
}
```

----

#### Code Demo

下列則是以 Node.js 為基礎的範例：

- [web-api 原始碼](https://github.com/evan361425/distributed-node/blob/master/src/web-api/consumer-graphql.ts)

```typescript=
// 僅展示請求的範例，這裡的 `kitchenSink` 是自定義名稱，方便 debug 用的
const query = `query kitchenSink ($id:ID) {
  recipe(id: $id) {
    id name
    ingredients {
      name quantity
    }
  }
  pid
}`;
const variables = { id: '42' };

return got(`http://${TARGET}/graphql`, {
  method: 'POST',
  json: { query, variables },
})
```

- [recipe-api 原始碼](https://github.com/evan361425/distributed-node/blob/master/src/recipe-api/producer-graphql.ts)

```typescript=
import { GraphQLID, GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql';

// 僅展示 RecipeRoot 的建置方式
const recipeRoot = new GraphQLObjectType({
  name: 'RecipeRoot',
  fields: {
    pid: {
      type: GraphQLInt,
      resolve: resolvers.RecipeRoot.pid,
    },
    recipe: {
      type: recipeQuery,
      args: { id: { type: GraphQLID } },
      resolve: resolvers.RecipeRoot.recipe,
    },
  },
});
return new GraphQLSchema({ query: rootQuery });
```

#### Live Demo

http://localhost:4000/graphql

### gRPC

像是 REST 或 GraphQL 都是建立在資料之上，而透過 CRUD 的方式去執行行為，這裡就可以注意到其限制：

> 大量的名詞，而僅有少量的動詞

舉例：
若有一個 API endpoint 是用來建立發票，今欲新增一附帶條件：**是否同時寄送信箱通知**。
有什麼樣的方式？

- 再建立一個 endpoint 專門做這件事： 過多 API，難管理和理解
- 在該 endpoint 新增變數：`need_send_email`： 讓該 endpoint 越來越複雜

`Remote Procedure Call` 就是來解決此事的！

> gRPC 為 Google 建立的 RPC 標準

gRPC 預設即非使用 JSON 格式進行資訊的傳遞，而是以 `Protocol Buffers`（ProtoBufs）的方式進行傳遞。

有幾個條件：

- 所有格式皆須預先設定好，副檔名為 `.proto`，且需要讓 client 擁有。
- 各值需給定順序，且之後不建議修改。
- 數字有多型別：`int32`，`int64`，`float`，`double` 等等。

這些條件有幾個好處：

- 效能、體積的最優化，binary serialize/deserialize

```text
{"id":42} v.s. 42
```

- 向後相容

```text
v1 需要 arg1 arg2
v2 需要 arg1 arg2 arg3
若 client 僅拿到 v1 的 proto，程式上會自動忽略 arg2 後的參數
```

----

#### Code Demo

- gRPC proto

```proto=
syntax = "proto3";
package recipe;
service RecipeService { 
  rpc GetRecipe(RecipeRequest) returns (Recipe) {}
  rpc GetMetaData(Empty) returns (Meta) {}
}
message Recipe {
  int32 id = 1; 
  string name = 2;
  string steps = 3;
  repeated Ingredient ingredients = 4; 
}
message Ingredient {
  int32 id = 1;
  string name = 2;
  string quantity = 3;
}
message RecipeRequest {
  int32 id = 1;
}
message Meta { 
  int32 pid = 2;
}
message Empty {}
```

- 建立 service，[原始碼](https://github.com/evan361425/distributed-node/blob/master/src/recipe-api/producer-grpc.ts)

```javascript=
import { loadPackageDefinition, Server } from '@grpc/grpc-js';
import { loadSync } from '@grpc/proto-loader';

// 讀取 proto 檔
const def = loadSync(__dirname + '/grpc.proto');
const proto = loadPackageDefinition(def);

// 建立處理邏輯
// handlers = ...;
const server = new Server();
server.addService(
  proto.recipe.RecipeService.service,
  handlers,
);

// 建立對外連線
// credentials = ...; for https
const cb = () => server.start();
server.bindAsync(`${HOST}:${PORT}`, credentials, cb);

// 建立 handlers
const handlers = {
  GetMetaData: (_call, cb) => {
    cb(null, { // error = null
      pid: process.pid,
    });
  },
  GetRecipe: (call, cb) => ({}) // if (call.request.id === 42)
}
```

- 建立 client，[原始碼](https://github.com/evan361425/distributed-node/blob/master/src/web-api/consumer-grpc.ts)

```javascript=
import { loadPackageDefinition } from '@grpc/grpc-js';
import { loadSync } from '@grpc/proto-loader';

// 讀取 proto 檔
const def = loadSync(__dirname + '/grpc.proto');
const proto = loadPackageDefinition(def);

// credentials = ...; for https
const client = new proto.recipe.RecipeService(TARGET, credentials);

client.getMetaData({}, cb);
client.getRecipe({ id: 42 }, cb);

```

#### Live Demo

http://localhost:3001

---

## Alternatives

除了 gRPC 還有什麼類似的東西？

**ProtoBufs**

*MessagePack*

雖然同為 binary representation of hierarchical object data，但
- 有 field
- 不需要額外檔案（如 `.proto`）去描述

**gRPC**

- *Apache Thrift*
- *JSON RPC*

關於 gRPC 推薦的文章：

- [比較 gRPC 服務與 HTTP API](https://docs.microsoft.com/zh-tw/aspnet/core/grpc/comparison?view=aspnetcore-5.0)

## 結論

1. JSON
你需要一個 http client 來呼叫眾多 API Endpoint，訊息格式也需要有額外的 Schema 定義
2.  GraphQL
你還是需要一個 http client，但是這次只需要對應一個端點，而且可以自己組織查詢內容
3. gRPC
你連 http client 都不用，套件會幫你產出這些呼叫的程式邏輯，而你只需要像寫一般 function 一樣呼叫即可
