文章目录
- 一、 RESTful API 服务
- 1. RESTful API 定义
- 1.1 HTTP Method
- 1.2 RESTful API 规范
- 2. RESTful API 风格示例
- 3. RESTful JSON API
- 4. Gin 框架
- 4.1 导入 Gin 框架
- 4.2 使用 Gin 框架
- 4.2.1 获取特定的用户(GET)
- 4.2.2 新增一个用户(POST)
- 4.2.3 删除一个用户(DELETE)
- 4.2.4 修改一个用户的名字(PATCH)
- 二、 RPC 跨平台服务
- 1. 定义
- 2. Go语言中的RPC
- 2.1 服务端
- 2.2 客户端
- 3. 基于 HTTP 的RPC
- 3. JSON RPC 跨平台通信
- 3.1 基于 TCP 的 JSON RPC
- 3.2 基于 HTTP的JSON RPC
- 三、Go 语言的发展前景
一、 RESTful API 服务
在做项目开发的时候,要善于借助已经有的轮子,让自己的开发更有效率,也更容易实现。
1. RESTful API 定义
RESTful API 是一套规范,它可以规范如何对服务器上的资源进行操作。和 RESTful API 和密不可分的是 HTTP Method。
1.1 HTTP Method
HTTP Method最常见的就是POST和GET,其实最早在 HTTP 0.9 版本中,只有一个GET方法,该方法是一个幂等方法,用于获取服务器上的资源。
在 HTTP 1.0 版本中又增加了HEAD和POST方法,其中常用的是 POST 方法,一般用于给服务端提交一个资源,导致服务器的资源发生变化。
随着网络越来越复杂,在 HTTP1.1 版本的时候,支持的方法增加到了 9 个,新增的方法有 HEAD、OPTIONS、PUT、DELETE、TRACE、PATCH 和 CONNECT。下面是它们各自的作用:
- GET 方法可请求一个指定资源的表示形式,使用 GET 的请求应该只被用于获取数据。
- HEAD 方法用于请求一个与 GET 请求的响应相同的响应,但没有响应体。
- POST 方法用于将实体提交到指定的资源,通常导致服务器上的状态变化或副作用。
- PUT 方法用于请求有效载荷替换目标资源的所有当前表示。
- DELETE 方法用于删除指定的资源。
- CONNECT 方法用于建立一个到由目标资源标识的服务器的隧道。
- OPTIONS 方法用于描述目标资源的通信选项。
- TRACE 方法用于沿着到目标资源的路径执行一个消息环回测试。
- PATCH 方法用于对资源应用部分修改。
HTTP 规范针对每个方法都给出了明确的定义,所以使用的时候也要尽可能地遵循这些定义,这样在开发中才可以更好地协作。
1.2 RESTful API 规范
RESTful API 规范就是基于 HTTP Method 规范对服务器资源的操作,同时规范了 URL 的样式和 HTTP Status Code。
在 RESTful API 中,使用的主要是以下五种 HTTP 方法:
- GET,表示读取服务器上的资源;
- POST,表示在服务器上创建资源;
- PUT,表示更新或者替换服务器上的资源;
- DELETE,表示删除服务器上的资源;
- PATCH,表示更新 / 修改资源的一部分。
以上 HTTP 方法在 RESTful API 规范中是一个操作,操作的就是服务器的资源,服务器的资源通过特定的 URL 表示。
(1)GET 方法的示例
HTTP GET https://www.flysnow.org/users
HTTP GET https://www.flysnow.org/users/123
上例中
- 第一个表示获取所有用户的信息;
- 第二个表示获取 ID 为 123 用户的信息。
(2) POST 方法的示例
HTTP POST https://www.flysnow.org/users
该示例表示创建一个用户,通过 POST 方法给服务器提供创建这个用户所需的全部信息。
这里 users 是个复数。
(3)PUT 方法的示例
HTTP PUT https://www.flysnow.org/users/123
该示例表示要更新 / 替换 ID 为 123 的这个用户,在更新的时候,会通过 PUT 方法提供更新这个用户需要的全部用户信息。这里 PUT 方法和 POST 方法不太一样的是,从 URL 上看,PUT 方法操作的是单个资源,比如这里 ID 为 123 的用户。
如果要更新一个用户的部分信息,使用 PATCH 方法更恰当。
(4)DELETE 方法的示例
HTTP DELETE https://www.flysnow.org/users/123
DELETE 方法的使用和 PUT 方法一样,也是操作单个资源,这里是删除 ID 为 123 的这个用户。
2. RESTful API 风格示例
Go 语言的一个很大的优势,就是可以很容易地开发出网络后台服务,而且性能快、效率高。Golang 提供了内置的 net/http 包处理 HTTP 请求,让开发者可以比较方便地开发一个 HTTP 服务。
一个简单的 HTTP 服务的 Go 语言实现代码如下所示:
func main() {
http.HandleFunc("/users",handleUsers)
http.ListenAndServe(":8080", nil)
}
func handleUsers(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w,"ID:1,Name:张三")
fmt.Fprintln(w,"ID:2,Name:李四")
fmt.Fprintln(w,"ID:3,Name:王五")
}
这个示例运行后,在浏览器中输入 http://localhost:8080/users, 就可以看到如下内容信息:
ID:1,Name:张三
ID:2,Name:李四
ID:3,Name:王五
这并不是一个 RESTful API,因为使用者不仅可以通过 HTTP GET 方法获得所有的用户信息,还可以通过 POST、DELETE、PUT 等 HTTP 方法获得所有的用户信息,这显然不符合 RESTful API 的规范。
对以上示例进行修改,使它符合 RESTful API 的规范,修改后的示例代码如下所示:
func handleUsers(w http.ResponseWriter, r *http.Request){
switch r.Method {
case "GET":
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w,"ID:1,Name:张三")
fmt.Fprintln(w,"ID:2,Name:李四")
fmt.Fprintln(w,"ID:3,Name:王五")
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w,"not found")
}
}
该示例修改了 handleUsers 函数,在该函数中增加了只在使用 GET 方法时,才获得所有用户的信息的判断,其他情况返回 not found。
3. RESTful JSON API
在项目中最常见的是使用 JSON 格式传输信息,也就是提供的 RESTful API 要返回 JSON 内容给使用者。
用上面的示例改造成可以返回 JSON 内容的方式,示例代码如下所示:
//数据源,类似MySQL中的数据
var users = []User{
{ID: 1,Name: "张三"},
{ID: 2,Name: "李四"},
{ID: 3,Name: "王五"},
}
func handleUsers(w http.ResponseWriter, r *http.Request){
switch r.Method {
case "GET":
users,err:=json.Marshal(users)
if err!=nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w,"{\"message\": \""+err.Error()+"\"}")
}else {
w.WriteHeader(http.StatusOK)
w.Write(users)
}
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w,"{\"message\": \"not found\"}")
}
}
//用户
type User struct {
ID int
Name string
}
从以上代码可以看到,这次的改造主要是新建了一个 User 结构体,并且使用 users 这个切片存储所有的用户,然后在 handleUsers 函数中把它转化为一个 JSON 数组返回。这样,就实现了基于 JSON 数据格式的 RESTful API。
运行这个示例,在浏览器中输入 http://localhost:8080/users,可以看到如下信息:
[{"ID":1,"Name":"张三"},{"ID":2,"Name":"李四"},{"ID":3,"Name":"王五"}]
4. Gin 框架
虽然 Go 语言自带的 net/http 包,可以比较容易地创建 HTTP 服务,但是它也有很多不足:
- 不能单独地对请求方法(POST、GET 等)注册特定的处理函数;
- 不支持 Path 变量参数;
- 不能自动对 Path 进行校准;
- 性能一般;
- 扩展性不足;
- ……
基于以上这些不足,出现了很多 Golang Web 框架,如 Mux,Gin、Fiber 等,其中使用最多是 Gin 框架。
4.1 导入 Gin 框架
Gin 框架是一个在 Github 上开源的 Web 框架,封装了很多 Web 开发需要的通用功能,并且性能也非常高,可以很容易地写出 RESTful API。
Gin 框架其实是一个模块,也就是 Go Mod,所以采用 Go Mod 的方法引入即可。
安装代码如下:
$ go get -u github.com/gin-gonic/gin
导入代码如下:
import "github.com/gin-gonic/gin"
4.2 使用 Gin 框架
用 Gin 框架重写上面的示例,修改的代码如下所示:
func main() {
r:=gin.Default()
r.GET("/users", listUser)
r.Run(":8080")
}
func listUser(c *gin.Context) {
c.JSON(200,users)
}
相比 net/http 包,Gin 框架的代码非常简单,通过它的 GET 方法就可以创建一个只处理 HTTP GET 方法的服务,而且输出 JSON 格式的数据也非常简单,使用 c.JSON 方法即可。
最后通过 Run 方法启动 HTTP 服务,监听在 8080 端口。运行这个示例,在浏览器中输入 http://localhost:8080/users,看到的信息和通过 net/http 包实现的效果是一样的。
4.2.1 获取特定的用户(GET)
如果要获得特定用户的信息,需要使用的是 GET 方法,并且 URL 格式如下所示:
http://localhost:8080/users/2
以上示例中的 2 是用户的 ID,也就是通过 ID 来获取特定的用户。
通过 Gin 框架 Path 路径参数可以实现这个功能,示例代码如下:
func main() {
//省略没有改动的代码
r.GET("/users/:id", getUser)
}
func getUser(c *gin.Context) {
id := c.Param("id")
var user User
found := false
//类似于数据库的SQL查询
for _, u := range users {
if strings.EqualFold(id, strconv.Itoa(u.ID)) {
user = u
found = true
break
}
}
if found {
c.JSON(200, user)
} else {
c.JSON(404, gin.H{
"message": "用户不存在",
})
}
}
在 Gin 框架中,路径中使用冒号表示 Path 路径参数,比如示例中的 :id,然后在 getUser 函数中可以通过 c.Param(“id”) 获取需要查询用户的 ID 值。
Param 方法的参数要和 Path 路径参数中的一致,比如示例中都是 ID。
运行这个示例,通过浏览器访问 http://localhost:8080/users/2,就可以获得 ID 为 2 的用户,输出信息如下所示:
{"ID":2,"Name":"李四"}
假如我们访问一个不存在的 ID,得到的结果如下所示:
➜ curl http://localhost:8080/users/99
{"message":"用户不存在"}%
4.2.2 新增一个用户(POST)
根据 RESTful API 规范,实现新增使用的是 POST 方法,并且 URL 的格式为 http://localhost:8080/users ,向这个 URL 发送数据,就可以新增一个用户,然后返回创建的用户信息。
使用 Gin 框架实现新增一个用户,示例代码如下:
func main() {
//省略没有改动的代码
r.POST("/users", createUser)
}
func createUser(c *gin.Context) {
name := c.DefaultPostForm("name", "")
if name != "" {
u := User{ID: len(users) + 1, Name: name}
users = append(users, u)
c.JSON(http.StatusCreated,u)
} else {
c.JSON(http.StatusOK, gin.H{
"message": "请输入用户名称",
})
}
}
以上新增用户的主要逻辑是获取客户端上传的 name 值,然后生成一个 User 用户,最后把它存储到 users 集合中,达到新增用户的目的。
在这个示例中,使用 POST 方法来新增用户,所以只能通过 POST 方法才能新增用户成功。
运行这个示例,通过如下命令发送一个新增用户的请求,查看结果:
➜ curl -X POST -d 'name=小明' http://localhost:8080/users
{"ID":4,"Name":"小明"}
4.2.3 删除一个用户(DELETE)
删除一个用户比较简单,它的 API 格式和获取一个用户一样,但是 HTTP 方法换成了DELETE。示例代码如下所示:
func main() {
//省略没有修改的代码
r.DELETE("/users/:id", deleteUser)
}
func deleteUser(c *gin.Context) {
id := c.Param("id")
i := -1
//类似于数据库的SQL查询
for index, u := range users {
if strings.EqualFold(id, strconv.Itoa(u.ID)) {
i = index
break
}
}
if i >= 0 {
users = append(users[:i], users[i+1:]...)
c.JSON(http.StatusNoContent, "")
} else {
c.JSON(http.StatusNotFound, gin.H{
"message": "用户不存在",
})
}
}
这个示例的逻辑就是注册 DELETE 方法,达到删除用户的目的。删除用户的逻辑是通过ID 查询:
- 如果可以找到要删除的用户,记录索引并跳出循环,然后根据索引删除该用户;
- 如果找不到要删除的用户,则返回 404。
4.2.4 修改一个用户的名字(PATCH)
修改和删除一个用户非常像,实现代码如下所示:
func main() {
//省略没有修改的代码
r.PATCH("/users/:id",updateUserName)
}
func updateUserName(c *gin.Context) {
id := c.Param("id")
i := -1
//类似于数据库的SQL查询
for index, u := range users {
if strings.EqualFold(id, strconv.Itoa(u.ID)) {
i = index
break
}
}
if i >= 0 {
users[i].Name = c.DefaultPostForm("name",users[i].Name)
c.JSON(http.StatusOK, users[i])
} else {
c.JSON(http.StatusNotFound, gin.H{
"message": "用户不存在",
})
}
}
逻辑和删除的差不多的,只不过这里使用的是 PATCH方法。
二、 RPC 跨平台服务
1. 定义
RPC,也就是远程过程调用,是分布式系统中不同节点调用的方式(进程间通信),属于 C/S 模式。RPC 由客户端发起,调用服务端的方法进行通信,然后服务端把结果返回给客户端。
RPC的核心有两个:通信协议和序列化。在 HTTP 2 之前,一般采用自定义 TCP 协议的方式进行通信,HTTP 2 出来后,也有采用该协议的,比如流行的gRPC。
序列化和反序列化是一种把传输内容编码和解码的方式,常见的编解码方式有 JSON、Protobuf 等。
在大多数 RPC的架构设计中,都有Client、Client Stub、Server、Server Stub这四个组件,Client 和 Server 之间通过 Socket 进行通信。RPC 架构如下图所示:
RPC 调用的流程:
- 客户端(Client)调用客户端存根(Client Stub),同时把参数传给客户端存根;
- 客户端存根将参数打包编码,并通过系统调用发送到服务端;
- 客户端本地系统发送信息到服务器;
- 服务器系统将信息发送到服务端存根(Server Stub);
- 服务端存根解析信息,也就是解码;
- 服务端存根调用真正的服务端程序(Sever);
- 服务端(Server)处理后,通过同样的方式,把结果再返回给客户端(Client)。
RPC 调用常用于大型项目,也就是常说的微服务,而且还会包含服务注册、治理、监控等功能,是一套完整的体系。
2. Go语言中的RPC
在 Go SDK 中,已经内置了 net/rpc 包来帮助开发者实现 RPC。简单来说,net/rpc 包提供了通过网络访问服务端对象方法的能力。
在实际的项目开发中,使用Go 语言自带的 RPC 框架并不多,比较常用的是Google的gRPC 框架,它是通过Protobuf 序列化的,是基于 HTTP/2 协议的二进制传输,并且支持很多编程语言,效率也比较高。
2.1 服务端
一个 RPC 示例的服务端代码如下所示:
package server
type MathService struct {
}
type Args struct {
A, B int
}
func (m *MathService) Add(args Args, reply *int) error {
*reply = args.A + args.B
return nil
}
在以上代码中:
- 定义了MathService,用于表示一个远程服务对象;
- Args 结构体用于表示参数;
- Add 这个方法实现了加法的功能,加法的结果通过 replay这个指针变量返回。
定义好服务对象就可以把它注册到暴露的服务列表中,以供其他客户端使用了。在Go 语言中,要注册一个RPC 服务对象可以通过 RegisterName 方法,示例代码如下所示:
package main
import (
"server"
"log"
"net"
"net/rpc"
)
func main() {
rpc.RegisterName("MathService",new(server.MathService))
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
rpc.Accept(l)
}
以上示例代码中,通过 RegisterName 函数注册了一个服务对象,该函数接收两个参数:
- 服务名称(MathService);
- 具体的服务对象,也就是刚刚定义好的MathService 这个结构体。
然后通过 net.Listen 函数建立一个TCP 链接,在 1234 端口进行监听,最后通过 rpc.Accept 函数在该 TCP 链接上提供 MathService 这个 RPC 服务。现在客户端就可以看到MathService这个服务以及它的Add 方法了。
在 net/rpc 这个RPC框架时,要想把一个对象注册为 RPC 服务,可以让客户端远程访问,那么该对象(类型)的方法必须满足如下条件:
- 方法的类型是可导出的(公开的);
- 方法本身也是可导出的;
- 方法必须有 2 个参数,并且参数类型是可导出或者内建的;
- 方法必须返回一个 error 类型。
总结来说该方法的格式如下所示:
func (t *T) MethodName(argType T1, replyType *T2) error
这里面的 T1、T2都是可以被 encoding/gob 序列化的。
- 第一个参数 argType 是调用者(客户端)提供的;
- 第二个参数 replyType是返回给调用者结果,必须是指针类型。
2.2 客户端
代码如下所示:
package main
import (
"fmt"
"server"
"log"
"net/rpc"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
args := server.Args{A:7,B:8}
var reply int
err = client.Call("MathService.Add", args, &reply)
if err != nil {
log.Fatal("MathService.Add error:", err)
}
fmt.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)
}
在以上实例代码中,首先通过 rpc.Dial 函数建立 TCP 链接。TCP 链接建立成功后,就需要准备远程方法需要的参数,也就是示例中的args 和 reply。参数准备好之后,就可以通过 Call 方法调用远程的RPC 服务了。Call 方法有 3 个参数,它们的作用分别如下所示:
- 调用的远程方法的名字,这里是MathService.Add,点前面的部分是注册的服务的名称,点后面的部分是该服务的方法;
- 客户端为了调用远程方法提供的参数,示例中是args;
- 为了接收远程方法返回的结果,必须是一个指针,也就是示例中的 &reply,这样客户端就可以获得服务端返回的结果了。
3. 基于 HTTP 的RPC
RPC 除了可以通过 TCP 协议调用之外,还可以通过HTTP 协议进行调用,而且内置的net/rpc 包已经支持,修改以上示例代码支持 HTTP 协议的调用,服务端代码如下所示:
func main() {
rpc.RegisterName("MathService", new(server.MathService))
rpc.HandleHTTP()//新增的
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
http.Serve(l, nil)//换成http的服务
}
客户端修改的代码如下所示:
func main() {
client, err := rpc.DialHTTP("tcp", "localhost:1234")
//省略了其他没有修改的代码
}
可以看到,只需要把建立链接的方法从 Dial 换成 DialHTTP 即可。
Go 语言 net/rpc 包提供的 HTTP 协议的 RPC 还有一个调试的 URL,运行服务端代码后,在浏览器中输入 http://localhost:1234/debug/rpc 回车,即可看到服务端注册的RPC 服务,以及每个服务的方法,如下图所示:
如上图所示,注册的 RPC 服务、方法的签名、已经被调用的次数都可以看到。
3. JSON RPC 跨平台通信
以上实现的RPC 服务是基于 gob 编码的,这种编码在跨语言调用的时候比较困难,当前在微服务架构中,RPC 服务的实现者和调用者都可能是不同的编程语言,因此实现的 RPC 服务要支持多语言的调用。
3.1 基于 TCP 的 JSON RPC
实现跨语言 RPC 服务的核心在于选择一个通用的编码,这样大多数语言都支持,比如常用的JSON。在 Go 语言中,实现一个 JSON RPC 服务非常简单,只需要使用 net/rpc/jsonrpc 包即可。
以上面的示例为例,改造成支持 JSON的RPC 服务,服务端代码如下所示:
func main() {
rpc.RegisterName("MathService", new(server.MathService))
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
for {
conn, err := l.Accept()
if err != nil {
log.Println("jsonrpc.Serve: accept:", err.Error())
return
}
//json rpc
go jsonrpc.ServeConn(conn)
}
}
从以上代码可以看到,相比 gob 编码的RPC 服务,JSON 的 RPC 服务是把链接交给了jsonrpc.ServeConn这个函数处理,达到了基于 JSON 进行 RPC 调用的目的。
JSON RPC 的客户端代码修改的部分如下所示:
func main() {
client, err := jsonrpc.Dial("tcp", "localhost:1234")
//省略了其他没有修改的代码
}
从以上代码可以看到,只需要把建立链接的 Dial方法换成 jsonrpc 包中的即可。
3.2 基于 HTTP的JSON RPC
Go 语言内置的jsonrpc 并没有实现基于 HTTP的传输,需要自己实现,这里参考 gob 编码的HTTP RPC 实现方式,来实现基于 HTTP的JSON RPC 服务。
RPC 服务端代码如下所示:
func main() {
rpc.RegisterName("MathService", new(server.MathService))
//注册一个path,用于提供基于http的json rpc服务
http.HandleFunc(rpc.DefaultRPCPath, func(rw http.ResponseWriter, r *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack()
if err != nil {
log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())
return
}
var connected = "200 Connected to JSON RPC"
io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
jsonrpc.ServeConn(conn)
})
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
http.Serve(l, nil)//换成http的服务
}
以上代码的实现基于 HTTP 协议的核心,即使用 http.HandleFunc 注册了一个 path,对外提供基于 HTTP 的 JSON RPC 服务。在这个 HTTP 服务的实现中,通过Hijack方法劫持链接,然后转交给 jsonrpc 处理,这样就实现了基于 HTTP 协议的 JSON RPC 服务。
客户端调用的代码如下所示:
func main() {
client, err := DialHTTP("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
args := server.Args{A:7,B:8}
var reply int
err = client.Call("MathService.Add", args, &reply)
if err != nil {
log.Fatal("MathService.Add error:", err)
}
fmt.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)
}
// DialHTTP connects to an HTTP RPC server at the specified network address
// listening on the default HTTP RPC path.
func DialHTTP(network, address string) (*rpc.Client, error) {
return DialHTTPPath(network, address, rpc.DefaultRPCPath)
}
// DialHTTPPath connects to an HTTP RPC server
// at the specified network address and path.
func DialHTTPPath(network, address, path string) (*rpc.Client, error) {
var err error
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
io.WriteString(conn, "GET "+path+" HTTP/1.0\n\n")
// Require successful HTTP response
// before switching to RPC protocol.
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "GET"})
connected := "200 Connected to JSON RPC"
if err == nil && resp.Status == connected {
return jsonrpc.NewClient(conn), nil
}
if err == nil {
err = errors.New("unexpected HTTP response: " + resp.Status)
}
conn.Close()
return nil, &net.OpError{
Op: "dial-http",
Net: network + " " + address,
Addr: nil,
Err: err,
}
}
以上这段代码的核心在于通过建立好的TCP 链接,发送 HTTP 请求调用远程的HTTP JSON RPC 服务,这里使用的是 HTTP GET 方法。
三、Go 语言的发展前景
Go 语言就是为云而生的编程语言,所以在云原生的时代,它就具备了天生的优势:易于学习、天然的并发、高效的网络支持、跨平台的二进制文件编译等。
CNCF(云原生计算基金会)对云原生的定义是:
- 应用容器化;
- 面向微服务架构;
- 应用支持容器的编排调度。
对于这三点有代表性的 Docker、K8s 以及 istio 都是采用 Go 语言编写的,所以 Go 语言在云原生中发挥了极大的优势。