常见网络通信协议


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 默认实现了 QueryMutation 两个操作。而 CreateTodoTodos 中只有一个 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
}

现在进入官方提供的面板进行一次名为 createTodoMutation

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)和特定后端语言深度绑定,所以难以作为前后端的通信协议,适合处理不同服务端的通信。

参考

  1. GraphQL Getting Started
  2. RPC原理与Go RP