提示:本节课最终代码为:feature/s10

本节课,我们来看下如何实现一个最简单的 Web 服务。实现一个最简单的 Web 服务可以让你把整个代码框架搭建起来,后面就可以基于这个简单的 Web 服务,添加更多功能并测试。

如何实现一个 Web 服务?

从事 Go 语言开发,其实绝大多数时间你是在开发一个 Web 服务,Web 服务中实现了多个 API 接口,用来实现不同的业务逻辑,外部用户通过调用 API 接口来使用 Web 服务提供的能力。

要开发一个 Web 服务,你通常需要考虑做 2 个技术选型:选择 API 风格和选择数据交换格式。Go Web 服务中最常用的 API 风格是:REST 和 RPC。REST 底层使用的是 HTTP 协议,RPC 底层使用的是 RPC 协议。每种协议,又有其适配的数据交换格式。这种适配,你可以理解为一种事实上的标准,如无特殊需求,无需打破这种适配关系:REST API 风格采用 JSON 数据格式,RPC API 风格采用 Protobuf 数据格式

API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数或者接口,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无须访问源码,或理解内部工作机制的细节。

REST 和 RPC 又有其适配的场景,在企业应用开发中,通常会采用 2 种通信协议,一起构建一个优秀的 Go 应用。

  • 对外: REST + JSON 的组合。因为 API 接口规范、数据格式直观、易懂、开发调试方法,再加上客户端和服务端通过 HTTP 协议通信时,无需使用相同的编程语言,所以 REST + JSON 更适合对外提供 API 接口;

  • 对内: RPC + Protobuf 的组合。因为 RPC 协议调用方便,Protobuf 格式数据传输效率更高,从而使得 API 接口性能更好,所以 RPC + Probobuf 的组合更适合对内提供接口。

REST+JSON、RPC + Protobuf 这 2 种组合,在企业级应用中应用的都非常广泛,二者不是相互取代的关系,二者有各自的适应场景,相辅相成。在企业应用中,REST 和 RPC 组合方式通常如下图所示:

9. 基础功能:如何开发一个简单的 Web 服务? - 图1

还有不少 Go 应用,采用了一种更加强大的构建方法:在一个 Web Server 中同时实现了 REST 接口和 RPC 接口,外部客户调用 REST 接口,内部客户调用 RPC 接口,REST API 通过代理,转发到内部的 RPC 接口。这样,我们只需要实现一份 RPC 接口,通过代理也能够对外提供 REST 接口。

Go 生态中,最受欢迎的实现是 grpc-gateway。其原理如下图所示:

9. 基础功能:如何开发一个简单的 Web 服务? - 图2

借助 grpc-gateway 插件,可以基于 proto 文件生成反向代理(Reverse Proxy)的代码,这个反向代理运行起来后,对外提供 REST 服务,收到 REST 请求后通过 gRPC 调用原来的 gRPC 服务。

miniblog 项目虽小,但并不简单,miniblog 同时实现了 2 种组合。但本节课,我们只讨论 REST + JSON 这种组合。

提示:后面会详细介绍 RPC + Protobuf 的组合介绍及实现。

REST API 风格,底层使用了 HTTP 协议,HTTP 协议内容很多,本节课不会介绍。但会介绍 HTTP 请求处理流程,通过对 HTTP 请求处理流程的理解,能够让你清晰请求路径,更好地实现 HTTP 服务。

在实际项目开发中,我们经常需要解析 HTTP 请求包、返回业务处理结果。所以,我们有必要知道 HTTP 的请求和响应格式。

另外,REST 是一种 API 规范,要想实现一个 REST Web 服务,就需要按照 REST 规范的方式去实现,所以本节课也会介绍 REST 规范的核心内容。

所以接下来,我会详细介绍以下 3 点内容,方便大家更好理解和实现 REST Web Server:

  1. HTTP 请求处理流程;

  2. HTTP 请求和响应格式介绍;

  3. REST API 介绍。

HTTP 请求处理流程

HTTP 请求处理流程如下图所示(网图):

9. 基础功能:如何开发一个简单的 Web 服务? - 图3

一次完整的 HTTP 请求处理流程如上图所示(图片出自《HTTP 权威指南》,推荐想全面理解 HTTP 的读者阅读此书),具体分为以下 4 步:

  1. 建立连接

客户端发送 HTTP 请求后,服务器会根据域名进行域名解析,就是将网站名称转变成 IP 地址:localhost -> 127.0.0.1,Linux hosts 文件、DNS 域名解析等可以实现这种功能。之后通过发起 TCP 的三次握手建立连接。TCP 三次连接请参考 TCP 三次握手详解及释放连接过程,建立连接之后就可以发送 HTTP 请求了。

  1. 接收请求

HTTP 服务器软件进程,这里指的是 API 服务器,在接收到请求之后,首先根据 HTTP 请求行的信息来解析到 HTTP 方法和路径。在上图所示的报文中,方法是 GET,路径是 /index.html,之后根据 API 服务器注册的路由信息(大概可以理解为:HTTP 方法 + 路径和具体处理函数的映射)找到具体的处理函数。

  1. 处理请求

在接收到请求之后,API 通常会解析 HTTP 请求报文获取请求头和消息体,然后根据这些信息进行相应的业务处理,HTTP 框架一般都有自带的解析函数,只需要输入 HTTP 请求报文,就可以解析到需要的请求头和消息体。通常情况下,业务逻辑处理可以分为两种:包含对数据库的操作和不包含对数据库的操作。大型系统中通常两种都会有:

  • 包含对数据库的操作: 需要访问数据库(增删改查),然后获取指定的数据,对数据处理后构建指定的响应结构体,返回响应包。数据库通常用的是 MySQL,因为免费,功能和性能也都能满足企业级应用的要求;

  • 不包含对数据库的操作: 进行业务逻辑处理后,构建指定的响应结构体,返回响应包。

  1. 记录事务处理过程

在业务逻辑处理过程中,需要记录一些关键信息,方便后期 Debug 用。在 Go 中有各种各样的日志包可以用来记录这些信息。

HTTP 请求和响应格式介绍

一个 HTTP 请求报文由请求行(request line)、请求头部(header)、空行和请求数据四部分组成,下图是请求报文的一般格式。

9. 基础功能:如何开发一个简单的 Web 服务? - 图4

  • 第一行必须是一个请求行(request line),用来说明请求类型、要访问的资源以及所使用的 HTTP 版本;

  • 紧接着是一个头部(header)小节,用来说明服务器要使用的附加信息;

  • 之后是一个空行;

  • 再后面可以添加任意的其他数据(称之为主体:body)。

HTTP 响应格式跟请求格式类似,也是由 4 个部分组成:状态行、消息报头、空行和响应数据。

REST API 介绍

REST 代表表现层状态转移(REpresentational State Transfer),由 Roy Fielding 在他的 论文 中提出。REST 是一种架构风格,指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

REST 规范把所有内容都视为资源,网络上一切皆资源。REST 架构对资源的操作包括获取、创建、修改和删除,资源的操作正好对应 HTTP 协议提供的 GETPOSTPUTDELETE 方法。HTTP 动词与 REST 风格 CRUD 对应关系:

HTTP 方法 行为 示例
GET 获取资源信息(通常是资源列表) http://127.0.0.1/v1/users
GET 获取某个特定资源的信息 http://127.0.0.1/v1/users/nosbelm
POST 创建新资源 http://127.0.0.1/v1/users
PUT 更新资源 http://127.0.0.1/v1/users/nosbelm
DELETE 删除资源 http://127.0.0.1/v1/users/nosbelm

对资源的操作应该满足安全性和幂等性。

  • 安全性: 不会改变资源状态,可以理解为只读的;

  • 幂等性: 执行 1 次和执行 N 次,对资源状态改变的效果是等价的。

miniblog 实现一个最简单的 REST Web Server

上面,我介绍了开发一个 REST Web Server 需要的一些核心知识,有了上面的基础知识铺垫之后,接下来,我们就可以编写代码,将 miniblog 变成一个具有最小功能的 REST Web Server。

REST Web 框架选择

要编写一个 RESTful 风格的 API 服务器,首先需要一个 RESTful Web 框架,我经过调研选择了 GitHub star 数最多的 Gin。采用轻量级的 Gin 框架,具有如下优点:高性能、扩展性强、稳定性强、相对而言比较简洁(查看 性能对比)。关于 Gin 的更多介绍可以参考 Gin 官方文档:Gin Web Framework

一个最简单的 Gin REST Web Server 代码实现如下:

  1. package main
  2. import (
  3. "net/http"
  4. "github.com/gin-gonic/gin"
  5. )
  6. func main() {
  7. r := gin.Default()
  8. r.GET("/ping", func(c *gin.Context) {
  9. c.JSON(http.StatusOK, gin.H{
  10. "message": "pong",
  11. })
  12. })
  13. r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
  14. }

Gin 官网仓库还有大量的,满足不同场景的示例可供你学习:gin-gonic/examples

社区还有很多优秀的 Web 框架,例如:Beego、Echo、Iris、Revel、Martini 等,如果你感兴趣,可以自行学习。

使用框架开发 REST 服务

在我们学习了 Gin 官方文档之后,你很容易就能基于 miniblog 现有的代码,将 miniblog 变成一个 REST Web Server。

我们只需要在 internal/miniblog/miniblog.go 文件中的 run() 函数中,添加以下代码即可:

  1. // run 函数是实际的业务代码入口函数.
  2. func run() error {
  3. // 设置 Gin 模式
  4. gin.SetMode(viper.GetString("runmode"))
  5. // 创建 Gin 引擎
  6. g := gin.New()
  7. // 注册 404 Handler.
  8. g.NoRoute(func(c *gin.Context) {
  9. c.JSON(http.StatusOK, gin.H{"code": 10003, "message": "Page not found."})
  10. })
  11. // 注册 /healthz handler.
  12. g.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"})
  13. })
  14. // 创建 HTTP Server 实例
  15. httpsrv := &http.Server{Addr: viper.GetString("addr"), Handler: g}
  16. // 运行 HTTP 服务器
  17. // 打印一条日志,用来提示 HTTP 服务已经起来,方便排障
  18. log.Infow("Start to listening the incoming requests on http address", "addr", viper.GetString("addr"))
  19. if err := httpsrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
  20. log.Fatalw(err.Error())
  21. }
  22. return nil
  23. }

上述代码,我们 通过 gin.New() 创建了一个 *gin.Engine,之后的代码都是在配置 *gin.Engine

  • 使用 g.NoRoute 注册了一个 404 Handler,用来在请求没有匹配到路由时,返回指定的错误信息;

  • 使用 g.Get 注册了 /healthz 接口,这个接口是 miniblog 的健康检查接口。在真实的企业应用开发中,一个合格的 Web 服务,需要提供一个健康检查接口,用来供其他服务探测 Web 服务的健康状态。你可以根据需要在 /healthz 接口添加任意需要的判正逻辑;

  • *gin.Engine 实现了 net/http.Handler 接口,所以可以使用 *gin.Engine 创建一个 *http.Server 对象;

  • 使用 httpsrv.ListenAndServe() 启动 HTTP 服务器(或者 REST 服务器)。

因为服务的监听地址是一个值得配置的参数,所以,我们将 addr 配置在 miniblog 的配置文件中(见:miniblog.yaml#L8):

  1. addr: :8080 # HTTP 服务器监听地址

编译并测试

开发完成的完整代码见:feature/s10。执行以下命令编译、运行并测试 miniblog Web Server:

  1. $ make
  2. $ _output/miniblog -c configs/miniblog.yaml
  3. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  4. - using env: export GIN_MODE=release
  5. - using code: gin.SetMode(gin.ReleaseMode)
  6. [GIN-debug] GET /healthz --> github.com/marmotedu/miniblog/internal/miniblog.run.func2 (1 handlers)

打开另外一个 Linux 终端,并输入以下命令:

  1. $ curl http://127.0.0.1:8080/healthz
  2. {"status":"ok"}

可以看到 curl 命令输出:{"status":"ok"} 说明服务正常。

小结

本节课带着你实现了一个最简单的 Web 服务器,后面你就可以基于这个地基不断添加新的功能,最终将 miniblog 变成一个强大的 Web 服务器。

最后,给你布置一个小作业:学习 grpc-gateway,并使用 grpc-gateway 插件,开发一个 Web Server,同时对外提供 REST API 和 RPC API,开发完成后,编译并测试。

我也给你准备了一些比较好的 grpc-gateway 学习材料,供你学习参考: