譯文連接:wuYin/blog
原文連接:ewanvalentine.io,翻譯已獲做者 Ewan Valentine 受權。css
本文完整代碼:GitHubhtml
在上節中咱們使用 go-micro 搭建了微服務的事件驅動架構。本節將揭曉從 web 客戶端的角度出發如何與微服務進行調用交互。react
參考 go-micro 文檔,可看到 go-micro 實現了爲 web 客戶端代理請求 RPC 方法的機制。git
微服務 A 調用微服務 B 的方法,須要先實例化再調用:bClient.CallRPC(args...)
,數據做爲參數傳遞,屬於內部調用。github
web 端瀏覽器是經過 HTTP 請求去調用微服務的方法,go-micro 就作了中間層,調用方法的數據以 HTTP 請求的方式提交,屬於外部調用。golang
REST 風格多年來在 web 開發領域獨領風騷,經常使用於客戶端與服務端進行資源管理,應用場景比 RPC 和 SOAP 都要廣得多。更多參考:知乎:RPC 與 RESTful API 對比web
REST 風格對資源的管理既簡單又規範,它將 HTTP 請求方法對應到資源的增刪改查上,同時還可使用 HTTP 錯誤碼來描述響應狀態,在大多數 web 開發中 REST 都是優秀的解決方案。docker
不過近年來 RPC 風格也乘着微服務的順風車逐漸普及開來。REST 適合同時管理不一樣的資源,不過通常微服務只專一管理單一的資源,使用 RPC 風格能讓 web 開發專一於各微服務的實現與交互。shell
咱們從第二節開始就一直在使用 go-micro 框架,如今來看看它的 API 網關。go-micro 提供 API 網關給微服務作代理。API 網關把微服務 RPC 方法代理成 web 請求,將 web 端使用到的 URL 開放出來,更多參考:go-micro toolkits,go-micro API example數據庫
# 安裝 go-micro 的工具箱: # $ go get -u github.com/micro/micro # 咱們直接使用它的 Docker 鏡像 $ docker pull microhq/micro
如今修改一下 user-service 的代碼:
package main import ( "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} // 自動檢查 User 結構是否變化 db.AutoMigrate(&pb.User{}) // 做者使用了新倉庫 shippy-user-service // 但 auth.proto 和 user.proto 定義的內容是一致的 // 修改 shippy.auth 爲 go.micro.srv.user 便可 // 注意 API 調用參數也需對應修改 srv := micro.NewService( micro.Name("go.micro.srv.user"), micro.Version("latest"), ) srv.Init() // 獲取 broker 實例 // pubSub := s.Server().Options().Broker 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
如今把 user-service 和 emil-service 像上節同樣 make run
運行起來。以後再執行:
$ 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
,若是不指定而使用默認值將沒法生效。
如今外部能夠像這樣調用 user-service 建立用戶的方法:
$ 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 格式返回方法的處理結果。
$ 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
// shippy-ui/src/App.js 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;
接下來添加用戶認證、貨物託運的兩個組件。
// shippy-ui/src/Authenticate.js 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, }, // 注意 // 做者的把 Auth 認證做爲了獨立的項目 // Auth 其實和 go.micro.srv.user 是同樣的 // 這裏做者和譯者的代碼略有不一樣 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;
// shippy-ui/src/CreateConsignment.js 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 進行管理。