Go 使用中间件和 RPC详解

在本章中,我们将研究中间件功能。什么是中间件,我们如何从头开始构建它?接下来,我们将转向为我们编写的更好的中间件解决方案,称为 Gorilla 处理程序。然后,我们将尝试了解一些中间件有帮助的用例。之后,我们将开始使用 Go 的内部 RPC 和 JSON RPC 构建我们的 RPC 服务。然后我们将转向一个高级 RPC 框架,比如 Gorilla HTTP RPC。

我们在本章中介绍的主题包括:

本章的所有代码可在上找到 https://github.com/narenaryan/gorestful/tree/master/chapter3 。请参考第 1 章REST API 开发入门,以设置 Go 项目并运行程序。最好从 GitHub 克隆整个gorestful存储库。

中间件是连接到服务器的请求/响应处理的实体。中间件可以在许多组件中定义。每个组件都有要执行的特定功能。无论何时为 URL 模式定义处理程序(如上一章中所述),请求都会命中处理程序并执行业务逻辑。因此,实际上所有中间件都应该按顺序执行这些功能:

  1. 在点击处理程序之前处理请求(函数)
  2. 处理处理程序函数
  3. 在将响应提交给客户之前,请先对其进行处理

我们可以通过可视插图的形式看到前面的几点:

如果我们仔细观察图表,请求的过程从客户端开始。在没有中间件的应用程序中,请求到达 API 服务器,并由某个函数处理程序处理。响应立即从服务器发回,客户端接收响应。但在使用中间件的应用程序中,它会经过一系列阶段,如日志记录、认证、会话验证等,然后进入业务逻辑。这是为了过滤与业务逻辑交互的错误请求。最常见的用例是:

  • 使用日志记录器记录每个命中 RESTAPI 的请求
  • 验证用户会话并保持通信活动
  • 如果未识别,则对用户进行认证
  • 写入自定义逻辑以废弃请求数据
  • 为客户端提供服务时将属性附加到响应

在中间件的帮助下,我们可以将内务处理工作(如认证)保持在适当的位置。让我们创建一个基本的中间件,并在 Go 中篡改 HTTP 请求。

当需要为每个请求或 HTTP 请求的子集执行一段代码时,应该定义中间件功能。没有它们,我们需要在每个处理程序中复制逻辑。

构建中间件既简单又直观。让我们根据从第二章获得的知识构建一个程序。如果您不熟悉闭包函数,则闭包函数将返回另一个函数。这个原则帮助我们编写中间件。我们应该做的第一件事是实现一个满足 http.Handler 接口的函数。

一个名为closure.go的示例闭包如下所示:

package main
import (
    "fmt"
)
func main() {
    numGenerator := generator()
    for i := 0; i < 5; i++ {
        fmt.Print(numGenerator(), "\t")
    }
}
// This function returns another function
func generator() func() int {
    var i = 0
    return func() int {
        i++
        return i
    }
}

如果我们运行此代码:

go run closure.go

将使用选项卡空间生成和打印数字:

1 2 3 4 5

我们正在创建一个名为 generator 的闭包函数,并调用它来获取一个新的数字。生成器模式每次根据给定的条件生成一个新项。返回的内部函数是一个匿名函数,没有参数,返回类型为整数。在外部函数中定义的变量i可用于匿名函数,这使得它在将来计算逻辑时非常有用。闭包的另一个好例子是创建一个计数器。您可以通过遵循前面代码中应用的相同逻辑来实现它

在 Go 中,外部函数的函数签名应该与匿名函数的签名完全匹配。在前面的示例中,func() int是外部和内部函数的签名。

给出这个例子是为了理解闭包在 Go 中是如何工作的。现在,让我们用这个概念来组成我们的第一个中间件:

package main
import (
    "fmt"
    "net/http"
)
func middleware(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("Executing middleware before request phase!")
        // Pass control back to the handler
        handler.ServeHTTP(w, r)
        fmt.Println("Executing middleware after response phase!")
    })
}
func mainLogic(w http.ResponseWriter, r *http.Request) {
    // Business logic goes here
    fmt.Println("Executing mainHandler...")
    w.Write([]byte("OK"))
}
func main() {
    // HandlerFunc returns a HTTP Handler
    mainLogicHandler := http.HandlerFunc(mainLogic)
    http.Handle("/", middleware(mainLogicHandler))
    http.ListenAndServe(":8000", nil)
}

让我们运行代码:

go run customMiddleware.go

如果您执行卷曲请求或在浏览器中看到http://localhost:8000,控制台将收到以下消息:

Executing middleware before request phase!
Executing mainHandler...
Executing middleware after response phase!

如果您观察前面提供的中间件示意图,右箭头指向请求阶段,而响应是左箭头。这个程序实际上是最右边矩形中的程序,即CustomMiddleware

在简单的步骤中,前面的程序分解为:

  • 通过将主处理函数(mainLogic传递给http.HandlerFunc()来创建处理函数。
  • 创建一个接受处理程序并返回处理程序的中间件函数。
  • 方法ServeHTTP允许处理程序执行作为 mainLogic 的处理程序逻辑。
  • http.Handle函数需要一个 HTTP 处理程序。考虑到这一点,我们以这样一种方式结束了我们的逻辑,即最终返回一个处理程序,但修改了执行。
  • 我们正在将主处理程序传递到中间件中。然后中间件接受它并返回一个函数,同时将这个主处理程序逻辑嵌入其中。这使得到达处理程序的所有请求都通过中间件逻辑。
  • print 语句的顺序解释了请求的过程。
  • 最后,我们在8000端口上为服务器提供服务。

像 Martini、Gin 这样的 Go web 框架默认提供中间件。我们将在接下来的章节中看到更多关于它们的内容。对于开发人员来说,了解中间件的底层细节是很好的。

下图可以帮助您了解中间件中的逻辑流是如何发生的:

在上一节中,我们构建了一个中间件,用于在请求到达处理程序之前或之后执行操作。也可以链接一组中间件。为了做到这一点,我们应该遵循与前一节相同的闭包逻辑。让我们创建一个城市 API 来保存城市详细信息。为了简单起见,API 将有一个 POST 方法,主体由两个字段组成:城市名称和城市区域。

让我们考虑一个场景,在这个场景中,API 开发人员只允许来自客户端的 JSON 媒体类型,并且还需要为每个请求将服务器时间(UTC)发送回客户端。使用中间件,我们可以做到这一点。

两个中间件的功能是:

  • 在第一个中间件中,检查内容类型是否为 JSON。如果没有,则不允许请求继续
  • 在第二个中间件中,向响应 cookie 添加一个名为服务器时间(UTC)的时间戳

首先,让我们创建POSTAPI:

package main

 import (
     "encoding/json"
     "fmt"
     "net/http"
 )

 type city struct {
     Name string
     Area uint64
 }

 func mainLogic(w http.ResponseWriter, r *http.Request) {
     // Check if method is POST
     if r.Method == "POST" {
         var tempCity city
         decoder := json.NewDecoder(r.Body)
         err := decoder.Decode(&tempCity)
         if err != nil {
             panic(err)
         }
         defer r.Body.Close()
         // Your resource creation logic goes here. For now it is plain print to console
         fmt.Printf("Got %s city with area of %d sq miles!\n", tempCity.Name, tempCity.Area)
         // Tell everything is fine
         w.WriteHeader(http.StatusOK)
         w.Write([]byte("201 - Created"))
     } else {
         // Say method not allowed
         w.WriteHeader(http.StatusMethodNotAllowed)
         w.Write([]byte("405 - Method Not Allowed"))
     }
 }

 func main() {
     http.HandleFunc("/city", mainLogic)
     http.ListenAndServe(":8000", nil)
 }

如果我们运行这个:

go run cityAPI.go

然后给出一个 CURL 请求:

curl -H "Content-Type: application/json" -X POST http://localhost:8000/city -d '{"name":"New York", "area":304}'

curl -H "Content-Type: application/json" -X POST http://localhost:8000/city -d '{"name":"Boston", "area":89}'

Go 为我们提供了以下信息:

Got New York city with area of 304 sq miles!
Got Boston city with area of 89 sq miles!

回答将是:

201 - Created
201 - Created

为了链接,我们需要在多个中间件之间传递处理程序。

以下是程序的简单步骤:

  • 我们创建了一个 RESTAPI,允许使用 POST 方法。它不完整,因为我们没有将数据存储到数据库或文件中。
  • 我们导入了 json 包,并使用它来解码客户端提供的 POST 正文。接下来,我们创建了一个映射 JSON 主体的结构。
  • 然后,JSON 被解码并将信息打印到控制台。

前面的示例中只涉及一个处理程序。但是现在,对于即将到来的任务,我们的想法是将主处理程序传递给多个中间件处理程序。完整的代码如下所示:

package main
import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "time"
)
type city struct {
    Name string
    Area uint64
}
// Middleware to check content type as JSON
func filterContentType(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("Currently in the check content type middleware")
        // Filtering requests by MIME type
        if r.Header.Get("Content-type") != "application/json" {
            w.WriteHeader(http.StatusUnsupportedMediaType)
            w.Write([]byte("415 - Unsupported Media Type. Please send JSON"))
            return
        }
        handler.ServeHTTP(w, r)
    })
}
// Middleware to add server timestamp for response cookie
func setServerTimeCookie(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        handler.ServeHTTP(w, r)
        // Setting cookie to each and every response
        cookie := http.Cookie{Name: "Server-Time(UTC)", Value: strconv.FormatInt(time.Now().Unix(), 10)}
        http.SetCookie(w, &cookie)
        log.Println("Currently in the set server time middleware")
    })
}
func mainLogic(w http.ResponseWriter, r *http.Request) {
    // Check if method is POST
    if r.Method == "POST" {
        var tempCity city
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&tempCity)
        if err != nil {
            panic(err)
        }
        defer r.Body.Close()
        // Your resource creation logic goes here. For now it is plain print to console
        log.Printf("Got %s city with area of %d sq miles!\n", tempCity.Name, tempCity.Area)
        // Tell everything is fine
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("201 - Created"))
    } else {
        // Say method not allowed
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte("405 - Method Not Allowed"))
    }
}
func main() {
    mainLogicHandler := http.HandlerFunc(mainLogic)
    http.Handle("/city", filterContentType(setServerTimeCookie(mainLogicHandler)))
    http.ListenAndServe(":8000", nil)
}

现在,如果我们运行这个:

go run multipleMiddleware.go

然后对 CURL 命令运行以下命令:

curl -i -H "Content-Type: application/json" -X POST http://localhost:8000/city -d '{"name":"Boston", "area":89}'

输出为:

HTTP/1.1 200 OK
Date: Sat, 27 May 2017 14:35:46 GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8

201 - Created

但是如果我们试图从 CURL 命令中删除Content-Type:application/json,中间件会阻止我们执行主处理程序:

curl -i -X POST http://localhost:8000/city -d '{"name":"New York", "area":304}' 
HTTP/1.1 415 Unsupported Media Type
Date: Sat, 27 May 2017 15:36:58 GMT
Content-Length: 46
Content-Type: text/plain; charset=utf-8

415 - Unsupported Media Type. Please send JSON

cookie 将从另一个中间件设置。

在前面的程序中,我们使用 log 而不是 fmt 包。即使两者都做相同的事情,log 通过附加日志的时间戳来格式化输出。它还可以很容易地定向到文件

这个节目中有一些有趣的东西。我们定义的中间件函数具有非常常见的用例。我们可以扩展它们来执行任何操作。程序由许多元素组成。如果你一个函数一个函数地读,逻辑就很容易解开。请看以下几点:

  • 创建了一个名为 city 的结构来存储城市详细信息,如上一个示例所示。
  • filterContentType是我们添加的第一个中间件。它实际上检查请求的内容类型,并允许或阻止请求进一步进行。为了检查,我们使用了r.Header.GET(内容类型)。如果是 application/json,我们允许请求调用handler.ServeHTTP函数,该函数执行mainLogicHandler代码。
  • setServerTimeCookie是我们设计的第二个中间件,用于向响应中添加 cookie,其值为服务器时间。我们使用 Go 的时间包来查找 Unix 时代中的当前 UTC 时间。
  • 对于 cookie,我们正在设置NameValue,cookie 还接受另一个名为Expire,的参数,该参数告诉 cookie 的到期时间。
  • 如果内容类型不是 application/json,我们的应用程序将返回 415 Media type not supported 状态代码。
  • 在 mainhandler 中,我们使用json.NewDecoder解析 JSON 并将其填充到city结构中。
  • strconv.FormatInt允许我们将int64数字转换为字符串。如果是正常的int,那么我们使用strconv.Itoa
  • 201 是操作成功时返回的正确状态代码。对于所有其他方法,我们返回 405,即不允许的方法。

我们在这里所做的链接形式对于两到三个中间件是可读的:

http.Handle("/city", filterContentType(setServerTimeCookie(mainLogicHandler)))

如果一个 API 服务器希望一个请求通过许多中间件,那么我们如何使链接简单易读呢?有一个叫做 Alice 的非常好的库来解决这个问题。它允许您对中间件进行语义排序并将其连接到主处理程序。我们将在下一章简要介绍。

当中间件列表很大时,Alice 库降低了链接中间件的复杂性。它为我们提供了一个干净的 API 来将处理程序传递给中间件。要安装它,请使用 go-get 命令,如下所示:

go get github.com/justinas/alice

现在我们可以在程序中导入 Alice 包并直接使用它。我们可以修改前面程序的各个部分,通过改进链接来实现相同的功能。在导入部分,添加github.com/justinas/alice,如下代码片段:

import (
    "encoding/json"
    "github.com/justinas/alice"
    "log"
    "net/http"
    "strconv"
    "time"
)

现在,在 main 函数中,我们可以如下修改处理程序部分:

func main() {
    mainLogicHandler := http.HandlerFunc(mainLogic)
    chain := alice.New(filterContentType, setServerTimeCookie).Then(mainLogicHandler)
    http.Handle("/city", chain)
    http.ListenAndServe(":8000", nil)
}

包含这些新增更改的完整代码可以在书的 GitHub 存储库的chapter 3文件夹中的multipleMiddlewareWithAlice.go文件中找到。了解上述概念后,让我们使用 Gorilla 工具包中名为 Handlers 的库构建一个日志中间件。

Gorilla Handlers 包为常见任务提供了各种中间件。列表中最重要的是:

  • LoggingHandler:用于以 Apache 通用日志格式登录
  • CompressionHandler:用于压缩响应
  • RecoveryHandler:用于从意外恐慌中恢复

这里,我们使用LoggingHandler执行 API 范围的日志记录。首先,使用 go get 安装此库:

go get "github.com/gorilla/handlers"

此日志服务器使我们能够创建一个类似于日志的服务器,其中包含时间和选项。例如,当您看到apache.log时,您会发现如下内容:

192.168.2.20 - - [28/Jul/2006:10:27:10 -0300] "GET /cgi-bin/try/ HTTP/1.0" 200 3395
127.0.0.1 - - [28/Jul/2006:10:22:04 -0300] "GET / HTTP/1.0" 200 2216

格式为IP-Date-Method:Endpoint-ResponseStatus,编写我们自己的中间件需要一些努力。但大猩猩处理程序已经为我们实现了它。请看以下代码段:

package main
import (
    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
    "log"
    "os"
    "net/http"
)
func mainLogic(w http.ResponseWriter, r *http.Request) {
    log.Println("Processing request!")
    w.Write([]byte("OK"))
    log.Println("Finished processing request")
}
func main() {
    r := mux.NewRouter()
    r.HandleFunc("/", mainLogic)
    loggedRouter := handlers.LoggingHandler(os.Stdout, r)
    http.ListenAndServe(":8000", loggedRouter)
}

现在运行服务器:

go run loggingMiddleware.go

现在,让我们在浏览器中打开http://127.0.0.1:8000或进行卷曲,您将看到以下输出:

2017/05/28 10:51:44 Processing request!
2017/05/28 10:51:44 Finished processing request
127.0.0.1 - - [28/May/2017:10:51:44 +0530] "GET / HTTP/1.1" 200 2
127.0.0.1 - - [28/May/2017:10:51:44 +0530] "GET /favicon.ico HTTP/1.1" 404 19

如果您观察到,最后两个日志是由中间件生成的。大猩猩LoggingMiddleware在响应时间写入它们。

在上一个示例中,我们总是检查 localhost 上的 API。在这个示例中,我们明确指定将 localhost 替换为127.0.0.1,因为前者将在日志中显示为空 IP。

接下来,我们将导入 Gorilla Mux 路由器和 Gorilla 处理程序。然后我们将一个名为mainLogic的处理程序连接到路由器。接下来,我们将路由器包装在handlers.LoggingHandler中间件中。它还返回一个处理程序,我们可以安全地将其传递给 http.listenandservice。

您也可以尝试处理程序中的其他中间件。本节的座右铭是向您介绍大猩猩处理者。Go 还提供许多其他外部软件包。有一个值得一提的库可以直接在 net/http 上编写中间件,它是 Negroni(github.com/urfave/Negroni。它还提供了大猩猩日志处理程序 Alice 的功能。所以请看一看。

我们可以使用名为 go.uuid(github.com/satori/go.uuid的库)和 cookie 轻松构建基于 cookie 的认证中间件。

远程过程调用(RPC)是在各种分布式系统之间交换信息的进程间通信。一台名为 Alice 的计算机可以调用另一台名为 Bob 的计算机中的函数(过程),并以协议格式返回计算结果。如果不在本地实现该功能,我们可以从位于另一个地方或地理区域的网络请求内容。

整个过程可分为以下步骤:

  • 客户端准备要发送的函数名和参数
  • 客户端通过拨号连接将它们发送到 RPC 服务器
  • 服务器接收函数名和参数
  • 服务器执行远程进程
  • 消息将被发送回客户端
  • 客户端从请求中收集数据并适当地使用它

服务器需要公开其服务,以便客户端连接并请求远程过程。请看下图:

Go 提供了一个库来实现 RPC 服务器和 RPC 客户端。在上图中,RPC 客户端使用主机和端口等详细信息拨号连接。它随请求一起发送两件事情。一个是参数和回复指针。由于它是一个指针,服务器可以修改它并将其发送回。然后客户端可以使用填充到指针中的数据。Go 有两个库,net/rpc 和 net/rpc/jsonrpc,用于使用 rpc。让我们编写一个 RPC 服务器,它与客户端对话,并为服务器提供服务。

让我们创建一个简单的 RPC 服务器,将 UTC 服务器时间发送回 RPC 客户端。首先,我们从服务器开始。

RPC 服务器和 RPC 客户端应就两件事达成一致:

  1. 通过的论点
  2. 返回值

对于服务器和客户端,前两个参数的类型应完全匹配:

package main
import (
    "log"
    "net"
    "net/http"
    "net/rpc"
    "time"
)
type Args struct{}
type TimeServer int64
func (t *TimeServer) GiveServerTime(args *Args, reply *int64) error {
    // Fill reply pointer to send the data back
    *reply = time.Now().Unix()
    return nil
}
func main() {
    // Create a new RPC server
    timeserver := new(TimeServer)
    // Register RPC server
    rpc.Register(timeserver)
    rpc.HandleHTTP()
    // Listen for requests on port 1234
    l, e := net.Listen("tcp", ":1234")
    if e != nil {
        log.Fatal("listen error:", e)
    }
    http.Serve(l, nil)
}

我们首先创建 Args 结构。它保存有关从客户端(RPC)传递到服务器的参数的信息。然后,我们创建了一个TimeServer号码来注册rpc.Register。这里,服务器希望导出类型为TimeServer(int64)的对象。HandleHTTPDefaultServer注册 RPC 消息的 HTTP 处理程序。然后,我们启动了一个在端口 1234 上侦听的 TCP 服务器。http.Serve功能用于将其用作运行程序。GiveServerTime是客户端调用的函数,返回当前服务器时间

上一个例子中有几点需要注意:

  • GiveServerTimeArgs对象作为第一个参数和一个应答指针对象
  • 它设置回复指针对象,但不返回除错误以外的任何内容
  • 此处的Args结构没有字段,因为此服务器不希望客户端发送任何参数

在运行这个程序之前,让我们也编写 RPC 客户端。两者都可以同时运行。

现在,客户端也使用相同的 net/rpc 包,但使用不同的方法拨号到服务器并执行远程功能。获取数据的唯一方法是将应答指针对象与请求一起传递,如以下代码段所示:

package main
import (
    "log"
    "net/rpc"
)
type Args struct {
}
func main() {
    var reply int64
    args := Args{}
    client, err := rpc.DialHTTP("tcp", "localhost"+":1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    err = client.Call("TimeServer.GiveServerTime", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    log.Printf("%d", reply)}

客户端在此执行以下操作:

  1. 执行DialHTTP连接到 RPC 服务器,该服务器正在端口1234上的本地主机上运行。
  2. args调用Name:Function格式的Remote函数,并用指针对象进行应答。 将收集到的数据获取到reply对象中 Call功能本质上是顺序的。

**现在,我们可以同时运行服务器和客户端来查看它们的运行情况:

go run RPCServer.go

这将运行服务器。现在打开另一个 shell 选项卡并运行以下操作:

go run RPCClient.go 

现在,服务器控制台将输出以下 UNIX 时间字符串:

2017/05/28 19:26:31 1495979791

看到魔法了吗?客户端作为独立程序运行。在这里,两个程序都可以在不同的机器上,计算仍然可以共享。这是分布式系统的核心概念。任务被划分并分配给不同的 RPC 服务器。最后,客户端收集结果并将其用于进一步的操作

自定义 RPC 代码仅在客户端和服务器都用 Go 编写时有用。因此,为了让 RPC 服务器被多个服务使用,我们需要定义 JSON RPC over HTTP。然后,任何其他编程语言都可以发送 JSON 字符串并获得 JSON 作为结果。

RPC 应该是安全的,因为它正在执行远程功能。从客户端收集请求时需要授权。

我们看到 Gorilla 工具包通过提供许多有用的库来帮助我们。然后我们研究了 Mux、处理程序,现在是 Gorilla RPC 库。使用它,我们可以创建使用 JSON 而不是自定义回复指针进行会话的 RPC 服务器和客户端。让我们把前面的例子转换成一个更有用的例子。

考虑一下这个场景。服务器上有一个 JSON 文件,其中包含书籍的详细信息(姓名、ID、作者)。客户端通过发出 HTTP 请求来请求图书信息。当 RPC 服务器接收到请求时,它从文件系统读取文件并对其进行解析。如果给定的 ID 与任何书籍匹配,那么服务器将以 JSON 格式将信息发送回客户端。我们可以使用以下命令安装 Gorilla RPC:

go get github.com/gorilla/rpc

此包源自标准的net/rpc包,但每个调用使用一个 HTTP 请求,而不是持久连接。与net/rpc相比的其他差异:将在以下章节中解释。

可以在同一台服务器上注册多个编解码器。根据请求的Content-Type头选择一个编解码器。服务方式也将http.Request作为参数接收。这个软件包可以在谷歌应用程序引擎上使用。现在,让我们编写一个 rpcjson 服务器。这里我们实现的是 JSON1.0 规范。对于 2.0,您应该使用 Gorilla JSON2:

package main
import (
    jsonparse "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "github.com/gorilla/mux"
    "github.com/gorilla/rpc"
    "github.com/gorilla/rpc/json"
)
// Args holds arguments passed to JSON RPC service
type Args struct {
    Id string
}
// Book struct holds Book JSON structure
type Book struct {
    Id string `"json:string,omitempty"`
    Name string `"json:name,omitempty"`
    Author string `"json:author,omitempty"`
}
type JSONServer struct{}
// GiveBookDetail
func (t *JSONServer) GiveBookDetail(r *http.Request, args *Args, reply *Book) error {
    var books []Book
    // Read JSON file and load data
    raw, readerr := ioutil.ReadFile("./books.json")
    if readerr != nil {
        log.Println("error:", readerr)
        os.Exit(1)
    }
    // Unmarshal JSON raw data into books array
    marshalerr := jsonparse.Unmarshal(raw, &books)
    if marshalerr != nil {
        log.Println("error:", marshalerr)
        os.Exit(1)
    }
    // Iterate over each book to find the given book
    for _, book := range books {
        if book.Id == args.Id {
            // If book found, fill reply with it
            *reply = book
            break
        }
    }
    return nil
}
func main() {
    // Create a new RPC server
    s := rpc.NewServer()    // Register the type of data requested as JSON
    s.RegisterCodec(json.NewCodec(), "application/json")
    // Register the service by creating a new JSON server
    s.RegisterService(new(JSONServer), "")
    r := mux.NewRouter()
    r.Handle("/rpc", s)
    http.ListenAndServe(":1234", r)
}

此程序可能与前面的 RPC 服务器实现不同。这是因为包含了大猩猩Mux大猩猩rpcjsonrpc包。在解释发生了什么之前,让我们运行前面的程序。使用以下命令运行服务器:

go run jsonRPCServer.go

现在客户在哪里?在这里,客户端可以是 CURL 命令,因为 RPC 服务器通过 HTTP 提供请求。我们需要发布带有图书 ID 的 JSON 来获取详细信息。因此,启动另一个 shell 并执行此 CURL 请求:

curl -X POST \
 http://localhost:1234/rpc \
 -H 'cache-control: no-cache' \
 -H 'content-type: application/json' \
 -d '{
 "method": "JSONServer.GiveBookDetail",
 "params": [{
 "Id": "1234"
 }],
 "id": "1"
}'

输出将是漂亮的 JSON,直接从 JSON RPC 服务器提供:

{"result":{"Id":"1234","Name":"In the sunburned country","Author":"Bill Bryson"},"error":null,"id":"1"}

现在,来到这个节目,我们有很多要了解的。创建 RPC 服务的文档非常有限。因此,我们在程序中使用的技术可以应用于各种各样的用例。首先,我们正在创建ArgsBook结构,分别保存有关传递的 JSON 参数和 book 结构的信息。我们正在一个名为JSONServer的资源上定义一个名为GiveBookDetail的远程函数。此结构是为注册 RPC 服务器的RegisterService功能而创建的服务。如果您注意到,我们也将该编解码器注册为 JSON。

每当我们收到客户端的请求时,我们都会将名为books.json的 JSON 文件加载到内存中,然后使用 JSON 的Unmarshal方法加载到Book结构中。jsonparse是 Go 包encoding/json**的别名,因为 Gorilla 导入的 JSON 包具有相同的名称。为了消除冲突,我们使用了一个别名

reply引用被传递到远程功能。在远程功能中,我们使用匹配的书籍设置回复的值。如果客户端发送的 ID 与 JSON 中的任何书籍匹配,则会填充数据。如果不匹配,则 RPC 服务器将发回空数据。通过这种方式,可以创建一个 JSON RPC,以使客户端具有通用性。在这里,我们没有编写 Go 客户端。任何客户端都可以从服务访问数据。

当多个客户端技术需要连接到您的 RPC 服务时,首选 JSON RPC。

在本章中,我们首先研究了什么是中间件,包括中间件如何处理请求和响应。然后,我们用几个实际的例子探讨了中间件代码。之后,我们看到了如何通过将一个中间件传递给另一个中间件来链接中间件。然后,我们使用一个名为Alice的包进行直观链接。我们还查看了用于日志记录的 Gorilla 处理程序中间件。接下来,我们了解了什么是 RPC,以及如何构建 RPC 服务器和客户端。之后,我们解释了什么是 JSON RPC,并了解了如何使用 Gorilla 工具包创建 JSON RPC。我们介绍了许多用于中间件和 RPC 的第三方软件包,并举例说明。

在下一章中,我们将探讨几个著名的 web 框架,这些框架将进一步简化 RESTAPI 的创建。他们拥有内置中间件和 HTTP 路由器,包括电池。****

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

深入浅出gRPC -〔李林锋〕

软件测试52讲 -〔茹炳晟〕

从0开始学大数据 -〔李智慧〕

Kafka核心源码解读 -〔胡夕〕

动态规划面试宝典 -〔卢誉声〕

Spark性能调优实战 -〔吴磊〕

Redis源码剖析与实战 -〔蒋德钧〕

大厂广告产品心法 -〔郭谊〕

结构学习力 -〔李忠秋〕