第37章:微服务,第二部分¶
在上一章中,你了解了微服务的基础知识,以及如何将该架构应用于TIL应用。在这一章中,你将学习API网关,以及如何让客户访问微服务。最后,你将学习如何使用Docker和Docker Compose来启动整个应用。
API网关¶
上一章介绍了TIL应用程序的两个微服务,一个用于缩写,一个用于用户。在一个真实的应用中,你可能会有更多的服务用于你的应用的各个不同方面。客户端很难与一个由如此多的微服务组成的应用程序集成。每个客户端都需要知道每个微服务做什么,以及每个服务的URL。客户端甚至可能要为每个服务使用不同的认证方法。微服务架构让人很难把一个服务拆成独立的服务。例如,将认证从TIL应用中的用户服务中移出,就需要对所有客户端进行更新。
这个问题的一个解决方案是API网关。一个API网关可以聚合来自客户的请求,并将其分配给所有需要的服务。此外,API网关可以从多个服务中检索结果,并将其合并为一个响应。
大多数云供应商提供API网关解决方案来管理大量的微服务,但你也可以轻松地创建自己的。在本章中,你就可以做到这一点。
下载本章的启动项目。TILAppUsers和TILAppAcronyms项目与前一章的最终项目相同。还有一个新的TILAppAPI项目,包含API网关的骨架。
启动服务¶
在终端,打开三个独立的标签。确保MySQL、PostgreSQL和Redis Docker容器在前一章中运行。在终端中,输入以下内容:
docker ps
这个命令显示了当前正在运行的容器。你应该看到三个正在运行的容器:

接下来,在第一个选项卡中,导航到TILAppUsers目录并运行以下命令:
swift run
这将启动TILAppUsers服务。在第二个选项卡中,导航到TILAppAcronyms并运行以下命令:
swift run
这将启动TILAppAcronyms服务。最后,在第三个标签中,导航到TILAppAPI目录,并输入这个命令:
open Package.swift
这将打开Xcode项目并开始下载依赖项。
转发请求¶
在TILAppAPI的Xcode项目中,打开UsersController.swift。在boot(route:)下面输入以下内容:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.get("\(userServiceURL)/users")
}
// 2
func getHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
let id = try req.parameters.require("userID", as: UUID.self)
return req.client.get("\(userServiceURL)/users/\(id)")
}
// 3
func createHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.post("\(userServiceURL)/users") {
createRequest in
// 4
try createRequest.content.encode(
req.content.decode(CreateUserData.self))
}
}
以下是新代码中的情况:
- 创建一个路由处理程序来获取所有的用户。简单地返回来自
TILAppUsers微服务的/users路由的响应。 - 创建一个路由处理程序来获取单个用户。从请求的参数中获取用户的
UUID,并从TILAppUsers微服务中返回该用户的响应。 - 创建一个路由处理程序来创建一个用户。向
TILAppUsers服务的users路由发送一个POST请求并返回响应。 - 在你发送请求之前,将请求到
API网关的数据编码到TILAppUsers服务的请求中。这是创建一个用户所需的数据。
要注册新的路由,请在boot(routes:)的末尾添加以下内容:
// 1
routeGroup.get(use: getAllHandler)
// 2
routeGroup.get(":userID", use: getHandler)
// 3
routeGroup.post(use: createHandler)
下面是这个的作用:
- 将一个到
/api/users/的GET请求路由到`getAllHandler(_:)'。 - 将
/api/users/<USER_ID>的GET请求路由到getHandler(_:),使用userID作为动态参数名。 - 将
/api/users/的POST请求路由到`createHandler(_:)'。
这些请求不需要任何认证或多重服务。你可以把它们直接转发到TILAppUsers微服务上。
打开AcronymsController.swift,为GET请求做同样的事情。在boot(routes:)下面添加以下内容:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
return req.client.get("\(acronymsServiceURL)/")
}
// 2
func getHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
let id =
try req.parameters.require("acronymID", as: UUID.self)
return req.client.get("\(acronymsServiceURL)/\(id)")
}
以下是新代码的作用:
- 创建一个路由处理程序来获取所有的首字母缩写。简单地从
TILAppAcronyms微服务的/路由返回响应。 - 创建一个路由处理程序来获取一个首字母缩写。从请求的参数中获取首字母缩写的
UUID。从TILAppronyms微服务中返回该首字母缩写的响应。
要注册新的路由,请在boot(routes:)的末尾添加以下内容:
// 1
acronymsGroup.get(use: getAllHandler)
// 2
acronymsGroup.get(":acronymID", use: getHandler)
下面是这个的作用:
- 将一个
GET请求路由到/api/acronyms/的getAllHandler(_:)。 - 将
GET请求路由到/api/acronyms/<ACRONYM_ID>到getHandler(_:)。
建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/users
- method: GET
点击Send Request,你会看到TILAppUsers微服务中的所有用户:

API认证¶
登录¶
API网关的认证方式与微服务的认证方式完全相同。首先,你必须允许一个用户登录。
在Xcode中,打开UsersController.swift。在createHandler(_:)下面,添加一个新的路由处理程序来处理登录:
func loginHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
// 1
return req.client.post("\(userServiceURL)/auth/login") {
loginRequest in
// 2
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 3
loginRequest.headers.add(
name: .authorization,
value: authHeader)
}
}
以下是新的路径处理程序的作用:
- 发送一个
POST请求给TILAppUsers微服务,以登录用户。 - 确保传入的请求包含一个
Authorization头。否则,返回一个401 Unauthorized的响应。 - 用传入请求中的授权头对传出请求进行编码。这个头包含用户的
HTTP基本认证信息。
要注册路由,请在boot(routes:)的末尾添加以下内容:
routeGroup.post("login", use: loginHandler)
这将一个POST请求发送到/api/users/login的路由到loginHandler(_:)。建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/users/login
- method: POST
点击Authorization,输入上一章创建的用户的用户名和密码。勾选Present Before Authentication Challenge,然后点击OK。
点击Send Request,你会收到该用户的令牌。复制令牌的值:

访问受保护的路由¶
回到Xcode中,打开AcronymsController.swift。在getHandler(_:)下面,创建一个新的路由处理程序来创建一个首字母缩写:
func createHandler(_ req: Request)
-> EventLoopFuture<ClientResponse> {
// 1
return req.client.post("\(acronymsServiceURL)/") {
createRequest in
// 2
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 3
createRequest.headers.add(
name: .authorization,
value: authHeader)
// 4
try createRequest.content.encode(
req.content.decode(CreateAcronymData.self))
}
}
以下是代码的作用:
- 向
TILAppAcronyms微服务发送一个POST请求,创建一个新的缩写。 - 确保传入的请求包含一个
Authorization头,否则返回一个401 Unauthorized的响应。 - 将
Authorization头添加到传出的请求中,到TILAppAcronyms微服务。 - 用数据对传出请求的主体进行编码,以创建一个首字母缩写。数据来自于传入的请求。
在boot(routes:)下面的acronymsGroup.get(":acronymID", use: getHandler)中注册路由处理器:
acronymsGroup.post(use: createHandler)
这将一个POST请求路由到/api/acronyms到createHandler(_:)。建立并运行应用程序,并返回到RESTed。点击Authorization,取消Present Before Authentication Challenge,以阻止RESTed在头中发送HTTP Basic Authentication凭证。
然后,配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/acronyms/
- method: POST
- Parameter encoding: JSON-encoded
添加两个带有名称和值的参数:
- short: IRL
- long: In Real Life
为Authorization创建一个新的头域,其值为Bearer <TOKEN STRING>,使用你之前复制的令牌字符串。
点击Send Request,你会看到通过API网关在TILAppAcronyms微服务中创建的缩写:

回到Xcode中,创建用于更新和删除首字母缩写的路由处理程序。在createHandler(_:)下面,添加以下内容:
func updateHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 1
let acronymID =
try req.parameters.require("acronymID", as: UUID.self)
// 2
return req.client
.put("\(acronymsServiceURL)/\(acronymID)") {
updateRequest in
// 3
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 4
updateRequest.headers.add(
name: .authorization,
value: authHeader)
// 5
try updateRequest.content.encode(
req.content.decode(CreateAcronymData.self))
}
}
func deleteHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 6
let acronymID =
try req.parameters.require("acronymID", as: UUID.self)
// 7
return req.client
.delete("\(acronymsServiceURL)/\(acronymID)") {
deleteRequest in
// 8
guard let authHeader =
req.headers[.authorization].first else {
throw Abort(.unauthorized)
}
// 9
deleteRequest.headers.add(
name: .authorization,
value: authHeader)
}
}
以下是新代码的作用:
- 从请求的参数中获取首字母缩写的
ID。 - 向
TILAppAcronyms微服务发送请求以更新该首字母缩写。返回响应。 - 在发送请求之前,确保传入的请求包含一个
Authorization头。如果没有,则返回一个401 Unauthorized的响应。 - 在发出的请求中添加
Authorization头。 - 用数据对发出的请求的正文进行编码,以更新缩写。这些数据来自传入的请求。
- 从请求的参数中获取首字母缩写的
ID。 - 向
TILAppAcronyms微服务发送请求,删除该首字母缩写。返回响应。 - 在发送请求之前,确保传入的请求包含一个
Authorization头。如果没有,则返回一个401 Unauthorized的响应。 - 在发出的请求中添加
Authorization头。
最后,为了注册新的路由,在boot(routes:)的末尾添加以下内容:
// 1
acronymsGroup.put(":acronymID", use: updateHandler)
// 2
acronymsGroup.delete(":acronymID", use: deleteHandler)
下面是这个的作用:
- 将一个
PUT请求路由到/api/acronyms/<ID>到updateHandler(_:)。 - 将一个
DELETE请求路由到/api/acronyms/<ID>到deleteHandler(_:)。
处理关系¶
在上一章中,你看到了关系如何在微服务中发挥作用。对于微服务架构中的客户端来说,获取不同模型的关系是很困难的。你可以使用API网关来帮助简化这个问题。
获取用户的首字母缩写¶
在Xcode中,打开UsersController.swift。在loginHandler(_:)下面,添加一个新的路由处理程序来获取用户的首字母缩写:
func getAcronyms(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 1
let userID =
try req.parameters.require("userID", as: UUID.self)
// 2
return req.client
.get("\(acronymsServiceURL)/user/\(userID)")
}
以下是发生的情况:
- 从请求的参数中获取用户的
ID。 - 向
TILAppAcronyms微服务发送请求,以获得该用户的所有首字母缩写,并返回响应。
要注册新的路由,请在boot(routes:)的末尾添加以下内容:
routeGroup.get(":userID", "acronyms", use: getAcronyms)
这将一个GET请求路由到/api/users/<user_ID>/acronyms到getAcronyms(_:)。
获取一个首字母缩写的用户¶
获取一个用户的首字母缩写词看起来和微服务中的其他请求一样,因为客户端知道用户的ID。获取某个特定首字母缩写的用户则比较复杂。打开AcronymsController.swift,在deleteHandler(_:)下面添加一个新的路由处理程序来完成这个任务:
func getUserHandler(_ req: Request) throws
-> EventLoopFuture<ClientResponse> {
// 1
let acronymID =
try req.parameters.require("acronymID", as: UUID.self)
// 2
return req
.client
.get("\(acronymsServiceURL)/\(acronymID)")
.flatMapThrowing { response in
// 3
return try response.content.decode(Acronym.self)
// 4
}.flatMap { acronym in
// 5
return req
.client
.get("\(userServiceURL)/users/\(acronym.userID)")
}
}
以下是新的路径处理程序的作用:
- 从请求的参数中获取首字母缩写的
ID。 - 向
TILAppAcronyms发出请求,以获得该首字母缩写的详细信息。 - 将响应解码为一个
Acronym并返回结果。 - 使用
flatMap(_:)从前一个期货链中获得Acronym并将其传递到另一个链中。通过期货链,你可以避免在catch语句中包裹任何try。 - 使用首字母缩写的用户
ID向TILAppUsers发出请求。
这个路由处理程序需要对两个微服务进行请求。API网关使客户的请求变得简单,就像单体的TIL应用一样。在boot(routes:)下面的acronymsGroup.delete(":acronymID", use: deleteHandler)中注册该路由:
acronymsGroup.get(":acronymID", "user", use: getUserHandler)
这就把对/api/acronyms/<ACRONYM_ID>/user的GET请求路由到getUserHandler(_:)。建立并运行应用程序,并启动RESTed。配置一个新的请求,如下所示:
- URL: http://localhost:8080/api/acronyms/
/user - method: GET
点击Send Request。API网关向所有的微服务发出必要的请求,以获得该ID的缩写的用户。你会看到返回的用户信息:

最后,在Xcode中停止TILAppAPI应用程序。
在Docker中运行一切¶
现在你有三个微服务,组成了你的TIL应用程序。这些微服务还需要另外三个数据库来工作。如果你正在开发一个客户端应用程序,或另一个微服务,有很多东西需要运行才能开始。你可能还想在Linux中运行一切,以检查你的服务部署是否正确。就像在第11章"测试"中,你将使用Docker Compose来运行一切。
注入服务URLs¶
目前,应用程序将不同微服务的URL硬编码为localhost。你必须改变这一点以在Docker Compose中运行它们。回到Xcode的TILAppAPI,打开AcronymsController.swift。将userServiceURL和acronymsServiceURL的定义替换为以下内容:
let acronymsServiceURL: String
let userServiceURL: String
init(
acronymsServiceHostname: String,
userServiceHostname: String) {
acronymsServiceURL =
"http://\(acronymsServiceHostname):8082"
userServiceURL = "http://\(userServiceHostname):8081"
}
这样你就可以为不同的服务注入主机名。打开UsersController.swift,再次将userServiceURL和acronymsServiceURL的定义替换为以下内容:
let userServiceURL: String
let acronymsServiceURL: String
init(
userServiceHostname: String,
acronymsServiceHostname: String) {
userServiceURL = "http://\(userServiceHostname):8081"
acronymsServiceURL =
"http://\(acronymsServiceHostname):8082"
}
最后,打开routes.swift,用以下内容替换routes(_:)的主体:
let usersHostname: String
let acronymsHostname: String
// 1
if let users = Environment.get("USERS_HOSTNAME") {
usersHostname = users
} else {
usersHostname = "localhost"
}
// 2
if let acronyms = Environment.get("ACRONYMS_HOSTNAME") {
acronymsHostname = acronyms
} else {
acronymsHostname = "localhost"
}
// 3
try app.register(collection: UsersController(
userServiceHostname: usersHostname,
acronymsServiceHostname: acronymsHostname))
try app.register(collection: AcronymsController(
acronymsServiceHostname: acronymsHostname,
userServiceHostname: usersHostname))
以下是变化的内容:
- 如果环境变量存在,使用
USERS_HOSTNAME作为用户微服务的主机名。否则,默认为localhost。 - 如果环境变量存在,使用
ACRONYMS_HOSTNAME作为缩略语微服务的主机名。否则,默认为`localhost'。 - 将
UsersController和AcronymsController注册为RouteCollection,注入主机名。
构建项目以确保所有东西都能编译并关闭Xcode。现在,用TILAppUsers打开标签,用Control-C停止应用程序,因为你不再需要一个独立的实例运行。
接下来,打开TILAppAcronyms的标签,用Control-C停止该应用程序。在Xcode中打开该项目,并打开UserAuthMiddleware.swift。在respond(to:chainingTo:)之前添加以下内容:
let authHostname: String
init(authHostname: String) {
self.authHostname = authHostname
}
这允许你传入TILAppUsers微服务的主机名。接下来,将中间件发出请求的URL--http://localhost:8081/auth/authenticate--替换为以下内容:
"http://\(authHostname):8081/auth/authenticate"
这使用了传入的主机名来进行请求。最后,打开AcronymsController.swift,在boot(routes:)中,将let authGroup = routes.grouped(UserAuthMiddleware())改为以下内容:
let authHostname: String
// 1
if let host = Environment.get("AUTH_HOSTNAME") {
authHostname = host
} else {
authHostname = "localhost"
}
// 2
let authGroup = routes.grouped(
UserAuthMiddleware(authHostname: authHostname))
以下是新代码的作用:
- 检查是否有
AUTH_HOSTNAME环境变量,并使用authHostname的值。如果环境变量不存在,则默认为localhost。 - 使用
UserAuthMiddleware创建一个路由组,并传入authHostname。
构建项目以确保代码的编译。
Docker Compose文件¶
在包含所有三个项目的根目录下,创建一个名为docker-compose.yml的新文件,并在你选择的编辑器中打开它。添加以下内容来定义版本和数据库服务:
# 1
version: '3'
services:
# 2
postgres:
image: "postgres"
environment:
- POSTGRES_DB=vapor_database
- POSTGRES_USER=vapor_username
- POSTGRES_PASSWORD=vapor_password
# 3
mysql:
image: "mysql"
environment:
- MYSQL_USER=vapor_username
- MYSQL_PASSWORD=vapor_password
- MYSQL_DATABASE=vapor_database
- MYSQL_RANDOM_ROOT_PASSWORD=yes
# 4
redis:
image: "redis"
以下是正在发生的事情:
- 设置
Docker Compose文件的版本号。 - 为
PostgreSQL数据库定义一个服务。使用postgres图像和与你的本地Docker容器相同的环境变量。 - 为
MySQL数据库定义一个服务。使用mysql图像和与你的本地Docker容器相同的环境变量。 - 为
Redis数据库定义一个服务。使用redis镜像。
在文件的最后,为TILAppUsers微服务添加以下内容:
# 1
til-users:
# 2
depends_on:
- postgres
- redis
# 3
build:
context: ./TILAppUsers
dockerfile: Dockerfile
# 4
environment:
- DATABASE_HOST=postgres
- REDIS_HOSTNAME=redis
- PORT=8081
- ENVIRONMENT=production
Note
缩进必须与定义的其他服务相匹配。
下面是新代码的作用:
- 为
TILAppUsers定义一个服务。 - 告诉
Docker Compose这个服务依赖于postgres和redis容器。Docker Compose将在TILAppUsers之前启动这些服务。 - 告诉
Docker Compose该服务的工作目录和要使用的Docker文件。默认的Vapor模板包含一个兼容的Dockerfile。 - 为服务设置必要的环境变量。这些定义了数据库以及环境和端口所需的变量。
你可能注意到这个服务没有在Docker Compose之外暴露任何端口。因为你通过API网关路由一切,没有必要暴露其他微服务。
在文件的最后,添加TILAppAcronyms的规范:
# 1
til-acronyms:
# 2
depends_on:
- mysql
- til-users
# 3
build:
context: ./TILAppAcronyms
dockerfile: Dockerfile
# 4
environment:
- DATABASE_HOST=mysql
- PORT=8082
- ENVIRONMENT=production
- AUTH_HOSTNAME=til-users
以下是新规范的作用:
- 为
TILAppAcronyms定义一个服务。 - 告诉
Docker Compose这个服务依赖于mysql和til-users容器。Docker Compose会在TILAppAcronyms之前启动这些服务。 - 告诉
Docker Compose该服务的工作目录和要使用的Docker文件。默认的Vapor模板包含一个兼容的Dockerfile。 - 为服务设置必要的环境变量。这些定义了数据库所需的变量以及环境和端口。这也设置了
AUTH_HOSTNAME环境变量,所以这个服务可以向TILAppUsers发送请求。
最后,在文件的最后,添加TILAppAPI的规范:
# 1
til-api:
# 2
depends_on:
- til-users
- til-acronyms
# 3
ports:
- "8080:8080"
# 4
build:
context: ./TILAppAPI
dockerfile: Dockerfile
# 5
environment:
- USERS_HOSTNAME=til-users
- ACRONYMS_HOSTNAME=til-acronyms
- PORT=8080
- ENVIRONMENT=production
以下是新规范的作用:
- 为
TILAppAPI定义一个服务。 - 告诉
Docker Compose这个服务依赖于til-users和til-acronyms容器。Docker Compose会在TILAppAcronyms之前启动这些服务。 - 将容器的
8080端口暴露给你的本地机器,端口为8080。这允许你连接到该容器。 - 告诉
Docker Compose服务的工作目录和要使用的Docker文件。默认的Vapor模板包含一个兼容的Dockerfile。 - 为服务设置必要的环境变量。这定义了环境和端口。这也设置了
USERS_HOSTNAME和ACRONYMS_HOSTNAME环境变量,因此这个服务可以向TILAppUsers和TILAppAcronyms发送请求。
修改Dockerfiles¶
在你可以运行一切之前,你必须修改Dockerfiles。Docker Compose按要求的顺序启动不同的容器,但不会等待它们准备好接受连接。如果你的Vapor应用程序试图在数据库准备好之前连接到数据库,这将导致问题。在TILAppAcronyms中,打开Dockerfile并替换:
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
为下面内容:
ENTRYPOINT sleep 20 && \
./Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT
这告诉容器在启动Vapor应用程序之前要等待20秒。这应该给数据库足够的时间来启动。在实际应用中,你可能要考虑把这个放在一个脚本中,在启动Vapor应用之前测试数据库。你也可以参考第33章,"用Docker部署",以获得更强大的解决方案。
在TILAppUsers中,打开Dockerfile,做与上面相同的修改。
运行一切¶
现在你已经准备好在Docker Compose中启动你的应用程序了。在Terminal中,在包含docker-compose.yml的目录中,输入以下内容:
docker-compose up
这将下载和构建docker-compose.yml中指定的所有容器,并启动它们。请注意,构建所有的微服务可能需要一些时间。
当一切都启动和运行时,你会看到类似的东西:

然后你可以像以前一样打开RESTed并提出请求。
接下来去哪?¶
在本章中,你学到了如何使用Vapor来创建一个API网关。这使得客户与你的不同微服务的交互变得简单。你学会了如何在不同的微服务之间发送请求并返回单一的响应。你还学会了如何使用Docker Compose来构建和启动所有的微服务,并将它们连接起来。
你现在已经掌握了编写强大的微服务所需的基本知识。你可以用消息队列、协议缓冲区和远程程序性调用来进一步加强。你现在可以构建的应用是没有限制的!