Golang 微服務教程(六)

譯文連接:wuYin/blog
原文連接:ewanvalentine.io,翻譯已獲做者 Ewan Valentine 受權。css

本文完整代碼:GitHubhtml

在上節中咱們使用 go-micro 搭建了微服務的事件驅動架構。本節將揭曉從 web 客戶端的角度出發如何與微服務進行調用交互。react

微服務與 web 端交互

參考 go-micro 文檔,可看到 go-micro 實現了爲 web 客戶端代理請求 RPC 方法的機制。git

內部調用

微服務 A 調用微服務 B 的方法,須要先實例化再調用:bClient.CallRPC(args...),數據做爲參數傳遞,屬於內部調用github

外部調用

web 端瀏覽器是經過 HTTP 請求去調用微服務的方法,go-micro 就作了中間層,調用方法的數據以 HTTP 請求的方式提交,屬於外部調用golang

REST vs RPC

REST 風格多年來在 web 開發領域獨領風騷,經常使用於客戶端與服務端進行資源管理,應用場景比 RPC 和 SOAP 都要廣得多。更多參考:知乎:RPC 與 RESTful API 對比web

REST

REST 風格對資源的管理既簡單又規範,它將 HTTP 請求方法對應到資源的增刪改查上,同時還可使用 HTTP 錯誤碼來描述響應狀態,在大多數 web 開發中 REST 都是優秀的解決方案。docker

RPC

不過近年來 RPC 風格也乘着微服務的順風車逐漸普及開來。REST 適合同時管理不一樣的資源,不過通常微服務只專一管理單一的資源,使用 RPC 風格能讓 web 開發專一於各微服務的實現與交互。shell

Micro 工具箱

咱們從第二節開始就一直在使用 go-micro 框架,如今來看看它的 API 網關。go-micro 提供 API 網關給微服務作代理。API 網關把微服務 RPC 方法代理成 web 請求,將 web 端使用到的 URL 開放出來,更多參考:go-micro toolkitsgo-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

API 網關

如今把 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.authshippy.email,默認值是 go.micro.api,若是不指定而使用默認值將沒法生效。

web 端建立用戶

如今外部能夠像這樣調用 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

效果以下:

image-20180530160604137

在這個 HTTP 請求中,咱們把 user-service Create 方法所需參數以 JSON 字段值的形式給出,API 網關會幫咱們自動調用,並一樣以 JSON 格式返回方法的處理結果。

web 端認證用戶

$ 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

運行效果以下:

image-20180530200732645

用戶界面

如今將上邊的 API 作成 web 端調用,咱們這裏使用 React 的 react-create-app 庫。先安裝:$ npm install -g react-create-app,最後建立項目:$ react-create-app shippy-ui

App.js

// 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;

接下來添加用戶認證、貨物託運的兩個組件。

Authenticate 用戶認證組件

// 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;

CreateConsignment 貨物託運組件

// 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,效果以下:

image-20180530180558510

打開 Chrome 的 Application 能看到在註冊或登陸時,RPC 成功調用:

image-20180530210325184

總結

本節使用 go-micro 本身的 API 網關,完成了 web 端對微服務函數的調用,可看出函數參數的入參出參都是以 JSON 給出的,對應於第一節 Protobuf 部分說與瀏覽器交互 JSON 是隻選的。此外,做者代碼與譯者代碼有少數出入,望讀者注意,感謝。

下節咱們將引入 Google Cloud 雲平臺來託管咱們的微服務項目,並使用 Terraform 進行管理。

相關文章
相關標籤/搜索