原文链接:ewanvalentine.io ,翻译已获作者 Ewan Valentine 授权。
本文完整代码:GitHub
在上节中我们使用 go-micro 搭建了微服务的事件驱动架构。本节将揭晓从 web 客户端的角度出发如何与微服务进行调用交互。
微服务与 web 端交互 参考 go-micro 文档 ,可看到 go-micro 实现了为 web 客户端代理请求 RPC 方法的机制。
内部调用 微服务 A 调用微服务 B 的方法,需要先实例化再调用:bClient.CallRPC(args...)
,数据作为参数传递,属于内部调用 。
外部调用 web 端浏览器是通过 HTTP 请求去调用微服务的方法,go-micro 就做了中间层,调用方法的数据以 HTTP 请求的方式提交,属于外部调用 。
REST vs RPC REST 风格多年来在 web 开发领域独领风骚,常用于客户端与服务端进行资源管理,应用场景比 RPC 和 SOAP 都要广得多。更多参考:知乎:RPC 与 RESTful API 对比
REST REST 风格对资源的管理既简单又规范,它将 HTTP 请求方法对应到资源的增删改查上,同时还可以使用 HTTP 错误码来描述响应状态,在大多数 web 开发中 REST 都是优秀的解决方案。
RPC 不过近年来 RPC 风格也乘着微服务的顺风车逐渐普及开来。REST 适合同时管理不同的资源,不过一般微服务只专注管理单一的资源,使用 RPC 风格能让 web 开发专注于各微服务的实现与交互。
Micro 工具箱 我们从第二节开始就一直在使用 go-micro 框架,现在来看看它的 API 网关。go-micro 提供 API 网关给微服务做代理。API 网关把微服务 RPC 方法代理成 web 请求,将 web 端使用到的 URL 开放出来,更多参考:go-micro toolkits ,go-micro API example
使用 1 2 3 4 5 # 安装 go-micro 的工具箱: # $ go get -u github.com/micro/micro # 我们直接使用它的 Docker 镜像 $ docker pull microhq/micro
现在修改一下 user-service 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "log" pb "shippy/user-service/proto/user" "github.com/micro/go-micro" ) func main () { db, err := CreateConnection() defer db.Close() if err != nil { log.Fatalf("connect error: %v\n" , err) } repo := &UserRepository{db} db.AutoMigrate(&pb.User{}) srv := micro.NewService( micro.Name("go.micro.srv.user" ), micro.Version("latest" ), ) srv.Init() publisher := micro.NewPublisher(topic, srv.Client()) t := TokenService{repo} pb.RegisterUserServiceHandler(srv.Server(), &handler{repo, &t, publisher}) if err := srv.Run(); err != nil { log.Fatalf("user service error: %v\n" , err) } }
原代码仓库:shippy-user-service/tree/tutorial-6
API 网关 现在把 user-service 和 emil-service 像上节一样 make run
运行起来。之后再执行:
1 2 3 4 5 6 $ docker run -p 8080:8080 \ -e MICRO_REGISTRY=mdns \ microhq/micro api \ --handler=rpc \ --address=:8080 \ --namespace=shippy
API 网关现在运行在 8080 端口,同时告诉它和其他微服务一样使用 mdns 做服务发现,最后使用的命名空间是 shippy,它会作为我们服务名的前缀,比如 shippy.auth
,shippy.email
,默认值是 go.micro.api
,如果不指定而使用默认值将无法生效。
web 端创建用户 现在外部可以像这样调用 user-service 创建用户的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ curl -XPOST -H 'Content-Type: application/json' \ -d '{ "service": "shippy.auth", "method": "Auth.Create", "request": { "user": { "email": "ewan.valentine89@gmail.com", "password": "testing123", "name": "Ewan Valentine", "company": "BBC" } } }' \ http://localhost:8080/rpc
效果如下:
在这个 HTTP 请求中,我们把 user-service Create
方法所需参数以 JSON 字段值的形式给出,API 网关会帮我们自动调用,并同样以 JSON 格式返回方法的处理结果。
web 端认证用户 1 2 3 4 5 6 7 8 9 10 $ curl -XPOST -H 'Content-Type: application/json' \ -d '{ "service": "shippy.auth", "method": "Auth.Auth", "request": { "email": "your@email.com", "password": "SomePass" } }' \ http://localhost:8080/rpc
运行效果如下:
用户界面 现在将上边的 API 做成 web 端调用,我们这里使用 React 的 react-create-app
库。先安装:$ npm install -g react-create-app
,最后创建项目:$ react-create-app shippy-ui
App.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import React , { Component } from 'react' ;import './App.css' ;import CreateConsignment from './CreateConsignment' ;import Authenticate from './Authenticate' ;class App extends Component { state = { err : null , authenticated : false , } onAuth = (token ) => { this .setState ({ authenticated : true , }); } renderLogin = () => { return ( <Authenticate onAuth ={this.onAuth} /> ); } renderAuthenticated = () => { return ( <CreateConsignment /> ); } getToken = () => { return localStorage .getItem ('token' ) || false ; } isAuthenticated = () => { return this .state .authenticated || this .getToken () || false ; } render ( ) { const authenticated = this .isAuthenticated (); return ( <div className ="App" > <div className ="App-header" > <h2 > Shippy</h2 > </div > <div className ='App-intro container' > {(authenticated ? this.renderAuthenticated() : this.renderLogin())} </div > </div > ); } } export default App ;
接下来添加用户认证、货物托运的两个组件。
Authenticate 用户认证组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 import React from 'react' ;class Authenticate extends React.Component { constructor (props ) { super (props); } state = { authenticated : false , email : '' , password : '' , err : '' , } login = () => { fetch (`http://localhost:8080/rpc` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ request : { email : this .state .email , password : this .state .password , }, service : 'shippy.auth' , method : 'Auth.Auth' , }), }) .then (res => res.json ()) .then (res => { this .props .onAuth (res.token ); this .setState ({ token : res.token , authenticated : true , }); }) .catch (err => this .setState ({ err, authenticated : false , })); } signup = () => { fetch (`http://localhost:8080/rpc` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ request : { email : this .state .email , password : this .state .password , name : this .state .name , }, method : 'Auth.Create' , service : 'shippy.auth' , }), }) .then ((res ) => res.json ()) .then ((res ) => { this .props .onAuth (res.token .token ); this .setState ({ token : res.token .token , authenticated : true , }); localStorage .setItem ('token' , res.token .token ); }) .catch (err => this .setState ({ err, authenticated : false , })); } setEmail = e => { this .setState ({ email : e.target .value , }); } setPassword = e => { this .setState ({ password : e.target .value , }); } setName = e => { this .setState ({ name : e.target .value , }); } render ( ) { return ( <div className ='Authenticate' > <div className ='Login' > <div className ='form-group' > <input type ="email" onChange ={this.setEmail} placeholder ='E-Mail' className ='form-control' /> </div > <div className ='form-group' > <input type ="password" onChange ={this.setPassword} placeholder ='Password' className ='form-control' /> </div > <button className ='btn btn-primary' onClick ={this.login} > Login</button > <br /> <br /> </div > <div className ='Sign-up' > <div className ='form-group' > <input type ='input' onChange ={this.setName} placeholder ='Name' className ='form-control' /> </div > <div className ='form-group' > <input type ='email' onChange ={this.setEmail} placeholder ='E-Mail' className ='form-control' /> </div > <div className ='form-group' > <input type ='password' onChange ={this.setPassword} placeholder ='Password' className ='form-control' /> </div > <button className ='btn btn-primary' onClick ={this.signup} > Sign-up</button > </div > </div > ); } } export default Authenticate ;
CreateConsignment 货物托运组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 import React from 'react' ;import _ from 'lodash' ;class CreateConsignment extends React.Component { constructor (props ) { super (props); } state = { created : false , description : '' , weight : 0 , containers : [], consignments : [], } componentWillMount ( ) { fetch (`http://localhost:8080/rpc` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ service : 'shippy.consignment' , method : 'ConsignmentService.Get' , request : {}, }) }) .then (req => req.json ()) .then ((res ) => { this .setState ({ consignments : res.consignments , }); }); } create = () => { const consignment = this .state ; fetch (`http://localhost:8080/rpc` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , }, body : JSON .stringify ({ service : 'shippy.consignment' , method : 'ConsignmentService.Create' , request : _.omit (consignment, 'created' , 'consignments' ), }), }) .then ((res ) => res.json ()) .then ((res ) => { this .setState ({ created : res.created , consignments : [...this .state .consignments , consignment], }); }); } addContainer = e => { this .setState ({ containers : [...this .state .containers , e.target .value ], }); } setDescription = e => { this .setState ({ description : e.target .value , }); } setWeight = e => { this .setState ({ weight : Number (e.target .value ), }); } render ( ) { const { consignments, } = this .state ; return ( <div className ='consignment-screen' > <div className ='consignment-form container' > <br /> <div className ='form-group' > <textarea onChange ={this.setDescription} className ='form-control' placeholder ='Description' > </textarea > </div > <div className ='form-group' > <input onChange ={this.setWeight} type ='number' placeholder ='Weight' className ='form-control' /> </div > <div className ='form-control' > Add containers... </div > <br /> <button onClick ={this.create} className ='btn btn-primary' > Create</button > <br /> <hr /> </div > {(consignments && consignments.length > 0 ? <div className ='consignment-list' > <h2 > Consignments</h2 > {consignments.map((item) => ( <div > <p > Vessel id: {item.vessel_id}</p > <p > Consignment id: {item.id}</p > <p > Description: {item.description}</p > <p > Weight: {item.weight}</p > <hr /> </div > ))} </div > : false)} </div > ); } } export default CreateConsignment ;
UI 的完整代码可见:shippy-ui
现在执行 npm start
,效果如下:
打开 Chrome 的 Application 能看到在注册或登录时,RPC 成功调用:
总结 本节使用 go-micro 自己的 API 网关,完成了 web 端对微服务函数的调用,可看出函数参数的入参出参都是以 JSON 给出的,对应于第一节 Protobuf 部分说与浏览器交互 JSON 是只选的。此外,作者代码与译者代码有少数出入,望读者注意,感谢。
下节我们将引入 Google Cloud 云平台来托管我们的微服务项目,并使用 Terraform 进行管理。