Golang 下的微服务 - 第 6 部分 - Web Clients

zhangyang9 · 2018-04-14 16:52:06 · 3424 次点击 · 预计阅读时间 16 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-04-14 16:52:06 的文章,其中的信息可能已经有所发展或是发生改变。

在之前的文章中,我们看了一些使用 go-micro 和 go 语言的生成的各种事件驱动的方法。 在本篇文章,我们将深入到客户端,探究一下如何创建一个能够与我们之前创建的平台交互的 Web 客户端。

这篇文章会介绍如何使用 micro 工具包生成 web 客户端从外部代理内部 rpc 方法。

我们会创建一个 user 接口用于生成平台的登录界面、还会创建一个接口用于使用我们的 consignments。该界面包含了创建用户、登录、和创建 consignments 等功能。 本系列的前几篇文章已经介绍过其中的部分代码了,在这篇文章我会带大家深入了解一下。

所以让我们开始吧!

RPC 复兴

REST 已经在网络上服务了很多年了,并且迅速成为管理客户端和服务器之间资源的途径。REST 正在逐渐取代已经过时的 RPC 和 SOAP。曾经必须写一个 wsdl 文件的时代已经过去了。

REST 向我们承诺了一种实用,简单和标准化的资源管理方法。 REST 使用 http 协议明确了正在执行的具体 web 动作类型。REST 鼓励我们使用 http 错误响应码来更精确地描述服务器的响应状态。而且大多数情况下,这种方法运行良好,并没有问题。但是像所有好东西一样,REST有许多不足和缺点,我不打算在这里详细介绍。大家有兴趣可以参考这篇文章

但是!随着微服务的出现,RPC 正在卷土重来。

REST 对于管理不同的资源非常有用,但微服务通常只处理单一资源,这一性质导致我们不需要在微服务的上下文中使用 RESTful 术语。相反,我们可以专注于每个服务的单一的具体操作和交互。

Micro

我们已经在本系列教程中广泛使用了 go-micro,现在我们将介绍 micro cli 这个工具包。这个 micro 工具包提供了的功能包括 API 网关、 sidecar、Web 代理以及其他一些很酷的功能。但是这篇文章我们使用到的功能主要是 API 网关。

API 网关将允许我们将 rpc 调用代理为 Web 友好的 javascriptON rpc 调用,然后将客户端应用程序中使用的 url 暴露出来。

那么以上这些炫酷功能是如何工作的?

首先要确保安装了 micro 工具包:

$ go get -u github.com/micro/micro

Docker环境下使用 Micro 更好的方法还是建议大家使用Docker镜像:

$ docker pull microhq/micro

接下来可以看一下 user 服务的代码,我对 user 服务的代码做了一些错误处理和命名约定方面的修改:

// shippy-user-service/main.go
package main

import (
    "log"

    pb "github.com/EwanValentine/shippy-user-service/proto/auth"
    "github.com/micro/go-micro"
    _ "github.com/micro/go-plugins/registry/mdns"
)

func main() {

    // 创建了一个数据库 connection 
    // main 方法结束之前要关闭数据库连接
    db, err := CreateConnection()
    defer db.Close()

    if err != nil {
        log.Fatalf("Could not connect to DB: %v", err)
    }

    // 将 user 结构类型自动移植到数据库类型中。此操作在服务每一次重启时都会做一次检测
    db.AutoMigrate(&pb.User{})

    repo := &UserRepository{db}

    tokenService := &TokenService{repo}

    // 创建一个新的服务
    srv := micro.NewService(

        // 这个名字必须于你在protobuf definition定义的包名匹配
        micro.Name("shippy.auth"),
    )

    // Init 用于初始化命令行参数
    srv.Init()

        // Will comment this out for now to save having to run this locally... 
    // publisher := micro.NewPublisher("user.created", srv.Client())

    // 注册 handler
    pb.RegisterAuthHandler(srv.Server(), &service{repo, tokenService, publisher})

    // 启动 server
    if err := srv.Run(); err != nil {
        log.Fatal(err)
    }
}
// shippy-user-service/proto/auth/auth.proto
syntax = "proto3";

package auth;

service Auth {
    rpc Create(User) returns (Response) {}
    rpc Get(User) returns (Response) {}
    rpc GetAll(Request) returns (Response) {}
    rpc Auth(User) returns (Token) {}
    rpc ValidateToken(Token) returns (Token) {}
}

message User {
    string id = 1;
    string name = 2;
    string company = 3;
    string email = 4;
    string password = 5;
}

message Request {}

message Response {
    User user = 1;
    repeated User users = 2;
    repeated Error errors = 3;
}

message Token {
    string token = 1;
    bool valid = 2;
    repeated Error errors = 3;
}

message Error {
    int32 code = 1;
    string description = 2;
}
// shippy-user-service/handler.go
package main

import (
    "errors"
    "fmt"
    "log"

    pb "github.com/EwanValentine/shippy-user-service/proto/auth"
    micro "github.com/micro/go-micro"
    "golang.org/x/crypto/bcrypt"
    "golang.org/x/net/context"
)

const topic = "user.created"

type service struct {
    repo         Repository
    tokenService Authable
    Publisher    micro.Publisher
}

func (srv *service) Get(ctx context.Context, req *pb.User, res *pb.Response) error {
    user, err := srv.repo.Get(req.Id)
    if err != nil {
        return err
    }
    res.User = user
    return nil
}

func (srv *service) GetAll(ctx context.Context, req *pb.Request, res *pb.Response) error {
    users, err := srv.repo.GetAll()
    if err != nil {
        return err
    }
    res.Users = users
    return nil
}

func (srv *service) Auth(ctx context.Context, req *pb.User, res *pb.Token) error {
    log.Println("Logging in with:", req.Email, req.Password)
    user, err := srv.repo.GetByEmail(req.Email)
    log.Println(user, err)
    if err != nil {
        return err
    }

    // 比较输入的密码与存储在数据库里的哈希密码
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        return err
    }

    token, err := srv.tokenService.Encode(user)
    if err != nil {
        return err
    }
    res.Token = token
    return nil
}

func (srv *service) Create(ctx context.Context, req *pb.User, res *pb.Response) error {

    log.Println("Creating user: ", req)

    // 为我们的密码生成一个哈希值
    hashedPass, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    if err != nil {
        return errors.New(fmt.Sprintf("error hashing password: %v", err))
    }

    req.Password = string(hashedPass)
    if err := srv.repo.Create(req); err != nil {
        return errors.New(fmt.Sprintf("error creating user: %v", err))
    }

    res.User = req
    if err := srv.Publisher.Publish(ctx, req); err != nil {
        return errors.New(fmt.Sprintf("error publishing event: %v", err))
    }

    return nil
}

func (srv *service) ValidateToken(ctx context.Context, req *pb.Token, res *pb.Token) error {

    // Decode token
    claims, err := srv.tokenService.Decode(req.Token)

    if err != nil {
        return err
    }

    if claims.User.Id == "" {
        return errors.New("invalid user")
    }

    res.Valid = true

    return nil
}

现在运行 $ make build && make run。 然后转到 shippy-email-service 运行$ make build && make run。 一旦这两个服务都运行,运行:

$ docker run -p 8080:8080 \ 
        -e MICRO_REGISTRY=mdns \
        microhq/micro api \
        --handler=rpc \
        --address=:8080 \
        --namespace=shippy

这将在 Docker 容器中开一个 8080 端口上,该端口将 micro api-gateway 作为 rpc 处理程序暴露出来,使用 mdns 作为本地的注册表,使用命名空间 shippy,shippy 是我们所有服务名称的第一部分。例如 shippy.auth 或 shippy.email。设置它是很重要的,因为它默认为 go.micro.api,在默认情况下,go.micro.api 是无法找到我们需要的特定服务来进行代理的。

我们现在可以使用以下方式调用我们的 user 服务方法:

创建一个 user:

curl -XPOST -H 'Content-Type: application/javascripton' \
    -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

这个请求中包含了我们想要传送给的服务名、要使用的服务方法、以及服务数据。

验证用户:

$ curl -XPOST -H 'Content-Type: application/javascripton' \ 
    -d '{ "service": "shippy.auth", "method": "Auth.Auth", "request":  { "email": "your@email.com", "password": "SomePass" } }' \
    http://localhost:8080/rpc

Consignment service

现在再次启动我们的 consignment 服务,$ make build && make run。 我们不需要在这里改变任何东西,但是,运行 rpc 代理的话我们还应该 创建一个 consignment:

$ curl -XPOST -H 'Content-Type: application/javascripton' \ 
    -d '{
        "service": "shippy.consignment",
        "method": "ConsignmentService.Create",
        "request": {
            "description": "This is a test",
            "weight": "500",
            "containers": []
        }
    }' --url http://localhost:8080/rpc

Vessel service

最后为了测试用户接口界面,我们需要运行 vessel 服务,这里没有对代码有什么修改,直接运行 $ make build && make run 即可。

User interface

现在可以使用我们的刚刚创建的新 rpc 节点创建一个用户界面。本文使用了 React,当然如果你喜欢的话可以使用其余的架构。请求都是一样的。本文使用来自 Facebook 的 react-create-app 库:

$ npm install -g react-create-app

安装完成后,执行 $ react-create-app shippy-ui。 这将为您创建一个 React 应用程序的框架。

// shippy-ui/src/App.javascript
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 和 CreateConsignment:

// shippy-ui/src/Authenticate.javascript
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/javascripton',
            },
            body: javascriptON.stringify({
                request: {
                    email: this.state.email,
                    password: this.state.password,
                },
                service: 'shippy.auth',
                method: 'Auth.Auth',
            }),
        })
        .then(res => res.javascripton())
        .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/javascripton',
            },
            body: javascriptON.stringify({
                request: {
                    email: this.state.email,
                    password: this.state.password,
                    name: this.state.name,
                },
                method: 'Auth.Create',
                service: 'shippy.auth',
            }),
        })
        .then((res) => res.javascripton())
        .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;

and...

// shippy-ui/src/CreateConsignment.javascript
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/javascripton',
            },
            body: javascriptON.stringify({
                service: 'shippy.consignment',
                method: 'ConsignmentService.Get',
                request: {},
            })
        })
        .then(req => req.javascripton())
        .then((res) => {
            this.setState({
                consignments: res.consignments,
            });
        });
    }

    create = () => {
        const consignment = this.state;
        fetch(`http://localhost:8080/rpc`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/javascripton',
            },
            body: javascriptON.stringify({
                service: 'shippy.consignment',
                method: 'ConsignmentService.Create',
                request: _.omit(consignment, 'created', 'consignments'),
            }),
        })
        .then((res) => res.javascripton())
        .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;

注意:我还将 Twitter Bootstrap 添加到 /public/index.html 并更改了一些CSS。

现在运行用户界面 $ npm start。 之后应该浏览器会自动打开一个界面。您现在应该可以注册并登录并查看 consignment 表单,您可以在其中创建新 consignments。看看你的开发工具中的 network 选项,然后看看 rpc 方法从我们的不同微服务中触发和获取我们的数据。

第6部分到这里就结束了,如果您有任何反馈,请给我发一封电子邮件,我会尽快答复(有可能不会很及时,敬请见谅)。

如果你发现这个系列有用,并且你使用了一个广告拦截器。 请考虑为我的时间和努力赞助几块钱。十分感谢!! https://monzo.me/ewanvalentine


via: https://ewanvalentine.io/microservices-in-golang-part-6/

作者:André Carvalho  译者:zhangyang9  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3424 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传