REST
REpresentational State Transfer,REST 并不是某种具体的通信协议,而是一种设计风格,RESTful 即为符合这种风格的意思。为了保证前后段分离,定义了 6 个约束条件
- 客户端-服务器
- 无状态
- 可缓存
- 统一接口
- 分层系统
- 按需代码
在 分层系统 中,客户端与服务端中可以存在中间层如网关,代理服务器,负载均衡等,这对客户端来说是不可见的。按需代码 则是服务端可以向客户端提供例如 JavaScript 代码以改变页面的渲染,这确实很常见,但是在没有良好的文档时,会令人十分困惑。
由于服务端是无状态的,所以牺牲了很多服务端的灵活性,客户端获取到的数据可能并不是最新的,也可能带有许多无意义的数据。
GraphQL
RESTful 在起步阶段非常方便,但随着复杂度增加,页面的渲染可能会请求几十次不同的 API,这会导致后端的网络开销过大,基于木桶效应来说,这会降低后端的服务能力。GrahpQL 允许客户端指定需要的数据字段,服务端只返回这些字段,将多个请求的内容整合到一个请求中,这样可以大幅减少网络开销,提高服务端的能力。
mkdir gqlgen-todos
cd gqlgen-todos
go mod init github.com/username/gqlgen-todos
go get -tool github.com/99designs/gqlgen@latest
go tool gqlgen init
完成初始化后可以在 graph 路径下找到 schema.graphqls,其中定义了几个简单的实体
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
todos: [Todo!]!
}
input NewTodo {
text: String!
userId: String!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
}
在 schema.resolvers.go 默认实现了 Query 和 Mutation 两个操作。而 CreateTodo 和 Todos 中只有一个 panic,按照官方示例,可以实现这两个方法为
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
randNumber, _ := rand.Int(rand.Reader, big.NewInt(100))
todo := &model.Todo{
Text: input.Text,
ID: fmt.Sprintf("T%d", randNumber),
User: &model.User{ID: input.UserID, Name: "user " + input.UserID},
}
r.todos = append(r.todos, todo)
return todo, nil
}
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
return r.todos, nil
}
现在进入官方提供的面板进行一次名为 createTodo 的 Mutation
mutation createTodo {
createTodo(input: { text: "todo", userId: "1" }) {
user {
id
}
text
done
}
}
服务端返回了
{
"data": {
"createTodo": {
"user": {
"id": "1"
},
"text": "todo",
"done": false
}
}
}
再进行一次名为 findtodos 的查询
query findtodos {
todos {
text
done
user {
name
}
}
}
服务端返回了
{
"data": {
"todos": [
{
"text": "todo",
"done": false,
"user": {
"name": "user 1"
}
}
]
}
}
GraphQL 并不是 RESTful 的上位替代,因为许多实体或者说对象在一开始并没有如此夸张的复杂度,这时易于实现的 RESTful 不失为一种更加便利的选择。
RPC
Remote Procedure Call,由 Google 开发的 gRPC 是当前最常见的 RPC 协议,RPC 的核心思想是将远程的服务调用封装成本地的函数调用,相较于 RESTful,RPC 在传输方式上可以使用 TCP 而不是 HTTP,数据格式上可以使用字节流而不是 JSON,这意味着更高的性能和更低的网络开销。服务端和客户端代码示例如下
// rpc_demo/server2.go
package main
import (
"log"
"net"
"net/rpc"
)
func main() {
service := new(ServiceA)
rpc.Register(service) // 注册RPC服务
l, e := net.Listen("tcp", ":9091")
if e != nil {
log.Fatal("listen error:", e)
}
for {
conn, _ := l.Accept()
rpc.ServeConn(conn)
}
}
// rpc demo/client2.go
package main
import (
"fmt"
"log"
"net/rpc"
)
func main() {
// 建立TCP连接
client, err := rpc.Dial("tcp", "127.0.0.1:9091")
if err != nil {
log.Fatal("dialing:", err)
}
// 同步调用
args := &Args{10, 20}
var reply int
err = client.Call("ServiceA.Add", args, &reply)
if err != nil {
log.Fatal("ServiceA.Add error:", err)
}
fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)
// 异步调用
var reply2 int
divCall := client.Go("ServiceA.Add", args, &reply2, nil)
replyCall := <-divCall.Done // 接收调用结果
fmt.Println(replyCall.Error)
fmt.Println(reply2)
}
在实际编写代码时只需要关注注册服务和调用服务,库文件封装了实现 RPC 的 编码,传输,解码等细节,RPC 的性能优势在于它的二进制协议和持久连接,适合高性能的微服务通信。
由于 gRPC 通常与微服务架构、网络协议(HTTP/2)和特定后端语言深度绑定,所以难以作为前后端的通信协议,适合处理不同服务端的通信。