热爱生活,

热爱工作,

热爱编程,

爱自己。

——《实干日志》


第23关 第一个微服务 用户服务

  • ​​23-1 新建虚拟环境和项目​​
  • ​​23-2 user表结构设计和生成​​
  • ​​23-3 md5信息摘要​​
  • ​​23-4 md5盐值加密解决用户密码安全问题​​
  • ​​23-5 proto接口定义和生成​​
  • ​​23-6 用户列表接口​​
  • ​​23-7 启动grpc服务​​
  • ​​23-8 日志库选型 - loguru​​
  • ​​23-9 优雅退出server​​
  • ​​23-10 通过argparse解析传递进入的参数​​
  • ​​23-11 通过id和mobile查询用户是否存在​​
  • ​​23-12 新建用户接口​​
  • ​​23-13 更新用户​​

23-1 新建虚拟环境和项目

本小节我们就进入到实战项目的开发阶段,

首先我们要开发的第一个‌‌服务,那就是我们的用户服务。

在我们开发系统的时候,因为我们会面临着后台的管理系统,‌‌以及我们的电商的系统,所以说这里边会牵扯到一个问题,就是用户登录的问题,‌‌有些接口我们需要验证用户是否登录才能访问,所以说在这里边我们首先就来开发用户的相关系统,‌‌用户相关的微服务,这个微服务对于我们来说非常的重要,因为这一个服务牵扯到我们整个微服务开发过程中一些通用的设计,‌‌所以说整个后续的服务都会依赖着我们服务来展开。‌

‌首先在开始写服务之前,我们来为我们的‌‌系统新建一个虚拟环境和新建一个项目,这里边我们来新建一个虚拟环境。

mkdir mxshop_srv && cd mxshop_srv
pipenv --python 3.7
pipenv shell

接下来我们就来新建一个Python的项目,来完成我们‌‌用户服务的相关开发。

第23关 第一个微服务 用户服务_mysql

接下来要做的事就是新建一个目录结构。‌

第23关 第一个微服务 用户服务_数据库_02

​然后进入下一小节。

23-2 user表结构设计和生成

本小节我们就开始开发的第一个阶段——就是数据库的设计。

数据库的设计在我们开发过程中基本上属于第一步,数据库设计完成之后,

我们要做的事就是来开始编写我们的proto文件以及编写我们的业务代码等等。

接下来开始进行数据库的设计。

新建models.py,我们将使用peewee来完成与数据库的交互。‌

第23关 第一个微服务 用户服务_Go微服务_03

数据库的配置放到​​settings.py​​中。

​url = "https://pypi.douban.com/simple"​

第23关 第一个微服务 用户服务_python_04

然后执行:

​pipenv install peewee​

第23关 第一个微服务 用户服务_数据库_05

代码0:

​settings/__init__.py​

import pymysql

pymysql.install_as_MySQLdb()

​settings/settings.py​

from playhouse.pool import PooledMySQLDatabase
from playhouse.shortcuts import ReconnectMixin


# 使用peewee的连接池, 使用ReconnectMixin来防止出现连接断开查询失败
class ReconnectMysqlDatabase(ReconnectMixin, PooledMySQLDatabase):
pass


MYSQL_DB = "mxshop_user_srv"
MYSQL_HOST = "192.168.0.123"
MYSQL_PORT = 3306
MYSQL_USER = "root"
MYSQL_PASSWORD = "root"

DB = ReconnectMysqlDatabase(MYSQL_DB, host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASSWORD)

代码1:

​model/models.py​

from peewee import *
from user_srv.settings import settings


class BaseModel(Model):
class Meta:
database = settings.DB


class User(BaseModel):
"""用户模型"""

GENDER_CHOICES = (
("female", "女"),
("male", "男")
)

ROLE_CHOICES = (
(1, "普通用户"),
(2, "管理员")
)

mobile = CharField(max_length=11, index=True, unique=True, verbose_name="手机号码")
password = CharField(max_length=100, verbose_name="密码") # 1. 密文 2. 密文不可反解
nick_name = CharField(max_length=20, null=True, verbose_name="昵称")
head_url = CharField(max_length=200, null=True, verbose_name="头像")
birthday = DateField(null=True, verbose_name="生日")
address = CharField(max_length=200, null=True, verbose_name="地址")
desc = TextField(null=True, verbose_name="个人简介")
gender = CharField(max_length=6, choices=GENDER_CHOICES, null=True, verbose_name="性别")
role = IntegerField(default=1, choices=ROLE_CHOICES, verbose_name="用户角色")


if __name__ == "__main__":
settings.DB.create_tables([User])

代码2:

​docker-compose.yml​

最后的准备工作:

如果不做会报错:

​peewee.OperationalError: (1049, "Unknown database 'mxshop_user_srv'")​

用​​docker-compose.yml​​部署好MySQL数据库的基本信息配置。

version: '3'

services:

db:
image: mysql:5.7.4
restart: always
container_name: peewee
ports:
- 3306:3306
environment:
TZ: Asia/Shanghai
MYSQL_USER: root
MYSQL_PASSWORD: root
# 此变量是必需变量,它指定将为MySQLroot超级用户帐户设置的密码
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: mxshop_user_srv


phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
restart: unless-stopped
depends_on:
- db
ports:
- 8888:80

后续应该把数据库配置即​​docker-compose.yml​​放到common文件夹下,方便统一管理。

先完成—>再完美。

然后

第23关 第一个微服务 用户服务_数据库_06

结果:

第23关 第一个微服务 用户服务_Go微服务_07

解读代码0:

在这里边我们使用数据库的连接池。

‌我们一般都不直接使用数据库连接,我们一般实际的开发过程中都会使用数据库的连接池,

它放在​​playhouse.pool​​这个地方的,‌‌​​playhouse​​它会随着我们安装​​peewee​​的时候,‌‌自动给我们生成这样的package。‌

但是这里边有一个很重要的点,如果我们使用这个连接池,我们再去连接MySQL的时候,‌‌当我一段时间不操作的时候,连接会被关闭掉,

关闭掉之后我们后边再去使用它进行一些select查询,‌‌它会抛出异常的,所以说这里边还有另外的配置,我们一定要把它给配置上来,在实际的开发过程中这些东西是很重要的。‌

‌在​​playhouse​​里面有一个​​shortcuts​​,‌‌这里边注意到有一个​​ReconnectMixin​​,当我们数据库里边过一段时间被关闭之后,‌‌

当我再次访问的话,如果这个连接被关闭,它会重新去连接的。

所以说这两个地方很重要,贴下代码:

from playhouse.pool import PooledMySQLDatabase
from playhouse.shortcuts import ReconnectMixin

配置上之后在使用时候我们会有一些差异,这个差异是什么?

见代码:

# 使用peewee的连接池, 使用ReconnectMixin来防止出现连接断开查询失败
class ReconnectMysqlDatabase(ReconnectMixin, PooledMySQLDatabase):
pass

​ReconnectMysqlDatabase​​​会继承​​ReconnectMixin 和 PooledMySQLDatabase​​,然后代码块里面什么都不用写,直接写一个pass。‌

‌这里面还涉及到 MRO继承的顺序【,解决父类中同名方法的二义性问题】

MRO是方法解析顺序,

比如说我继承了a,同时又继承了b,a和b中都有同名的方法,我调用方法/属性时候,是调用a的还是b的?

于是就规定了一种顺序,即MRO,按遍历结果调用第一个类,以此类推,直到最终的object类。

代码演示:

class Person():
name = "keegan"
age = 18

def drink(self):
print("Person-来喝...")

class Teacher(Person):
def drink(self):
print("Teacher-大佬喝茶...")

class Student(Person):
def drink(self):
print("Student-喝水...")

class Tutor(Teacher, Student):
pass

ins = Tutor()
print(Tutor.__mro__)
# (<class '__main__.Tutor'>, <class '__main__.Teacher'>, <class '__main__.Student'>, <class '__main__.Person'>, <class 'object'>)
print(ins.drink())
# Teacher-大佬喝茶...
"""
代码解释如下:
mro算法实现的原理是:
将class后面的括号()里的类顺序,
按照从本身开始,
从左到右的顺序进行单独遍历,
如果它们中有很混乱的继承,
就按遍历的结果取出第一个拿到的类,【这就解释了为什么打印的是 Teacher-大佬喝茶...】
以此类推,直到最终的object类。
写东西就是想东西,想明白才能写明白。
"""

有了这块的了解之后,再往下走。

我们首先为当前的用户服务新建一个数据库。

我们说过在微服务的开发过程当中,我们数据库都是‌‌互相独立的,也就是说每一个微服务‌‌我们都会给它新建一个数据库。‌【之前的架构图】

首先我们需要开发用户服务的话,我们就得在​​docker-compose​​新建一个数据库,‌‌有了这个数据库之后,我们现在就可以开始配置了:

第23关 第一个微服务 用户服务_数据库_08

解读代码1:

现在我们来设置user,首先要继承的就是database,

接着我们就来给它定义用户它的基本信息,用户的基本信息它实际上是要有一个id的,‌‌这个id我们可以不用管它,因为我们说过如果我们不去指明它的一个‌‌ primary_key的话,它会自动帮我们生成一个id。‌

‌当然如果你想自己去定义一个主键,又不想让它叫id的话,‌‌比如说这里边叫user_id,大家可以这样做:

​user_id = AutoField(primary_key=True)​

它有一个AutoField,它有一个primary_key=True,‌‌它会自动增长数字的,

我们这里就不这样设置了。‌我们就采用默认的id为主键就好了。

我们直接来定义用户相关的一些字段。

第一个字段mobile,‌‌mobile的话是CharField,是一个字符类型,‌‌然后我们给它设置max_length=11,

手机号码默认为11位,所以说这里边最大长度给它设置为11位就行了。‌

‌我们在这里边给它设置一个index=True,为什么?‌‌

因为我们后边在做用户登录,用户注册的时候,我们会先查询一下这个手机号码的用户是否存在,‌‌所以说这里边我们需要一个索引,否则的话用户量一大我们会很慢的。

而且这里边还有一个隐含的条件,‌‌我们如果只支持手机号码注册的话,我给它添加unique=True,那就是防止它重复,‌‌也就是说如果这个用户已经注册了,你想再注册,‌‌那是不行的,数据库它的检测就会出错。‌

‌我给它设置一个​​verbose_name="手机号码"​​,方便后边查看他的信息。

紧接着我们就来看一下第二个字段password,password我们仍然采用CharField类型,‌‌这个类型我们一定要注意它的长度,这个长度你可能会认为‌‌你的密码不会超过20位的,

‌你可能就会这样想,‌‌我们一般谁会把密码超过20位,这个地方我们不要随便去设置它的最大长度,为什么?

因为实际我们在存密码的时候,我们不会‌‌我们是不会存明文密码的,所以说这个密码我会进行一系列的转换,这个长度它就需要我们‌‌对它进行一个了解。‌

‌我们这里边先给它设置成200位,或者设成100位。

我们在设置它的时候,这两个字段都是必填的,所以说不要去给它设置为一个null等于True。‌

在实际开发过程中,‌‌密码它必须满足两个特性,第一个就是密码密文,第二个‌‌必须是加密的,

因为之前像很多大型的系统都出现过,‌‌数据库被破解,导致用户密码泄露的问题,‌‌所以说我们一定要纯密文,第二个密文不可反解,‌‌

也就不能获取到它的明文密码。‌

‌然后是nickname,它的昵称这个也是‌‌一般都会用到的,然后它是CharField,然后在这里边我们来看一下,‌‌这个长度我们可以预测,比如说20个不能太长,‌‌然后这里边注意到nickname是可以为null的,默认的话我们如果不填null等于True的话,它在数据库里边是不能为null的。‌

‌我现在设置一个null等于True,就证明它可以为null,因为我们用户在注册的时候,他实际上是没有填nickname的,不能说你没有填 nickname,‌‌我这个注册信息就添加不成功,所以这里边我们需要这样说明一下,后期的话大家在个人中心添加nickname就可以了。‌

‌我们再给他来一个比如说头像,head_url,‌‌它是CharField类型,它是一个URL,所以说我们可以给它添长一点,‌‌这个也是一样的,一开始注册是并没有的,当然大家也可以给它生成默认的都行,没有的话我们就设置一个‌‌可以设置为null等于True。

‌birthday的话它是一个什么类型?它是DateField 类型,‌‌同样也设置为null等于True。‌

比如说用户的住址,address,‌‌这里边它有一个住址,我们给它设置成​​max_length=200, null=True, verbose_name="地址"​​。

个人简介可能会比较长,所以说我们就给它设置成一个​​TextField​​,同样也设置为null等于True,

性别,我们可以设置CharField类型,‌‌也可以设置成​​IntegerField​​,都没有问题。甚至是布尔类型,因为性别只有男女两种。

这里边我们设置一个最大的长度,比如说它叫6,

性别我们一般是从男女里边选,peewee有个参数叫choices,‌‌我们可以在这里边指定只能选男女,这个里边它放的是元组,注意到它可以放多个元组,也就是 元组 里面 放 元组。‌

‌为了更清楚说明,我们这样来写:

第23关 第一个微服务 用户服务_Go微服务_09第一个它放的是我们具体的存到数据库里面的值叫female,‌‌但实际上我们在显示的时候,我们肯定不希望给它显示成一个female,我们期望给它显示成女,

第一个值是用来做数据库保存用的,

第二个值是用来做显示用的,

如果只是一些选择类型的字段来说,直接把它配置过来就行了,‌‌这样就其实会更加的清楚,别人一眼就能看懂你这里边有哪些值可用:

​gender = CharField(max_length=6, choices=GENDER_CHOICES, null=True, verbose_name="性别")​

一开始同样也设置为null等于True。

比如说用户他能够使用我们的‌‌电商系统,但是他不能登录后台管理系统,所以说这里边我们给它他做一个简单的区别,叫role,表明一下它是一个什么类型,‌‌是不是一个管理员?

我们把role设置成 ​​IntegerField​​​,设置一个default值为1,我们用1和2来标识他是​​普通用户​​​还是​​管理员​​。‌

表结构设计完成之后,我们就来生成一下表。‌

第23关 第一个微服务 用户服务_mysql_10

我们来数据库里边看一下,可以看到这个表结构已经有了,我们直接来点击表结构来设计表:

第23关 第一个微服务 用户服务_mysql_11

‌这里边它自动帮我们生成了一个id ,然后这里边还有一个索引,它是唯一的。【见图片索引的第四列】

它的名称是user_mobile,‌‌但是它的字段是mobile。

回顾一下我们做了哪些事情?

(1)在​​settings.py​​中主要解决了两个问题,

第一个问题就是使用peewee的‌‌连接池,

第二个就是使用​​ReconnectMixin​​来防止‌‌出现连接断开导致查询失败。‌‌

然后做了数据库基本的配置项。

‌这就是数据库的建表及新建数据的完成。‌


23-3 md5信息摘要

在上一小节中,我们定义了用户相关的表以及生成的相关的表,在开始‍‍具体的完成接口之前,我们还有一个问题需要解决,这个问题就是我们的加密,‍‍这个加密的话我们要做一个特殊的说明,什么说明?也就是说我们实际上在保存数据,特别是保存密码的时候,‍‍我们不能够将明文密码保存起来,这里边就涉及到我们的密码,在进行密文保存的时候,‍‍我们要涉及到哪些方案?‍‍

在密码保存的时候,实际上我们加密有两种方式,第一种方式就是我们的‍‍对称加密,还有一种非对称加密。‍‍

如果我们将我们的密码‍‍把它设置成一种明文的,如果我们的数据库一旦被被盗,‍‍这样的话就意味着我们大量的用户的密码会被攻击者发现,这是非常重大的事故,‍‍所以说实际上我们的系统如果想要安全,我们的密码一定是要加密的,‍‍

这里边非对称加密对我们来说,也就是说它实际上在生成密码的时候,我们用一个密钥去对它密码进行生成,‍‍

但是意味着另外一个问题,就是我们如果拿到它的密钥,我们可以对密码进行反解/破解,这其实也是非常危险的行为。‍‍

所以说对于我们在做加密的时候,密码我们必须要把它做成不能够反解/破解,不能够反解/破解就意味着一个问题,就是说‍我如果不能够反解/破解,‍‍我怎么去验证用户的密码是否正确?‍‍

这里边对于我们来说验证还是比较简单的,也就是说‍‍如果我有一种加密方法,同一个字符串,我在对它进行加密的时候,它生成的密文是一样的,‍‍这样的话我就可以拿着用户传输过来的密码,我对它进行一个加密,‍‍然后我拿着加密的字符串和数据库里边保存的字符串做一下对比,我就能知道这个密码是否正确。

这里边会有一个问题,什么问题?

也就是说无法知道原始密码是什么,‍‍即除了用户之外,没有人能够知道‍‍他的密码是什么,包括我们的开发者。

我们也是无法知道的。‍‍我们能做的事就是把用户传递过来的密码‍‍进行一个加密,然后和加密字符串两者做一下对比来确认密码是否正确。

以前的web系统当中,经常会有一个功能就是用户找回密码,但是找回密码,‍‍有的系统会把你的原密码发给你,如果我们采用了这种方案,原密码我们是无法知道的,‍‍如果用户一旦忘记了,我们就无法知道原始密码是什么,

这个里面会存在着一个问题,也就是说‍‍如果用户忘记密码怎么办?‍‍

这个对于我们来说,其实我们不需要知道他的原始密码,我们只要给他一个安全的url,让他在url当中去修改密码就行了。‍‍

虽然我们无法知道他的原始密码,但是我给他的链接只要是安全的,他通过这个链接,我们修改数据库,‍‍重新使用它的新密码生成密文并保存,就能够达到一个找回密码的功能。‍‍

我们关键是来解决非对称加密,非对称加密里边我们最常用的一种模式就是采用MD5,‍‍MD5严格意义上讲,它不是一种加密算法,它是一个信息摘要的算法。‍‍

MD5是什么?

MD5它是一个信息摘要的算法,‍‍可以将任意长度的‍‍字符把它生成一段指定长度的字符串,比如说限制在50个字符以内,

在比对的时候就很简单了,只要比对它的‍‍生成的摘要的字符串那就可以了。

比如这个场景即百度的秒传功能,‍‍比如说我本地有一个非常大的文件,可能上G。

我很有可能在一秒钟之内或者几秒钟之内就能上传完,‍‍这个是怎么做的?

实际上百度它自己有一个文档,也就是别人上传过的整个文档的MD5库,任何一个文件,哪怕你是一个G的,‍‍我也能把它生成到50个字符,甚至少于50个字符以内的字符串。‍‍

这样的话你上传一个文件,我只需要把MD5的字符串和我的数据库里面的字符串做一个比较,

如果我发现我的百度的网盘里边已经有这个文件存在了,‍‍我直接在你的网盘里面添加一个指向,那指向我的新的文件就可以了,我就没有必要把你本机的文件再上传。

这是典型的应用场景。‍‍

我们来看一下MD5信息摘要算法,它有哪些特性?

第一个特性就是压缩性,‍‍任意长度的数据计算出MD5值的长度是固定的,这什么意思?也就是说只要你的字符一样,你‍‍不管什么时候拿来计算,它的MD5值都是固定的,也就是它都是一样的,这对于我们来说生成密码很重要,‍‍因为如果用户拿着他的原始密码,他输入多次,我要在数据库里面比对,假设我这个算法导致了‍‍比如说我今天算和明天算它的MD5值不一样,这就麻烦了。

我们的MD5算法它就能达到这样一个要求,‍‍你任何字符只要来进行一个MD5值的计算,‍‍它的值都是固定的。‍‍

第二个特性是容易计算,从原始数据计算出MD5值是很容易的。‍‍

第三点就是抗修改性,就是你对原始数据进行任何修改,‍‍哪怕一个字节 Md5值的差异都很大,就从一个角度去防止了它会被破解。‍‍

试想一下,如果两个字符它很相近,它生成的MD5值很相近的话,以后别人来破解你的时候,比如说‍‍他已经破解到了一个密码,

然后他发现你的另外一个字符串跟它很相似,‍‍这样的话他在做暴力破解就很容易了,

因为他知道两者的MD5值,

如果它的差异不大,它们的原始密码‍‍它的差异也不大,就很容易被破解,

如果它的差异很大,也就是说你在原始字符算‍修改了任何一个字符,它最终的MD5差异很大,它就无从破解。‍‍

第四点就是强碰撞,‍‍想找到两个不同的数据,使它们具有相同的MD5值,很困难,这就是一个很重要的特性,‍‍对于我们来说这也是非常重要的,只有不同的字符,生成的MD5值不一样才能够更安全。‍‍

第五点它是不可逆的,‍‍不可反解。也就是说你无法拿着你的生成的MD5值的字符串去破解它生成原始的密码,‍‍这是不可能的,这些特性就使得去使用MD5值去解决我们的密码存储问题变得很容易了,‍‍我们就使用MD5只来计算就行了。‍‍

但是实际上我们在开发的过程中,我们在生成MD5值的话,我们实际上是有一个‍‍ MD5加盐的一个加密算法。

什么叫MD5加盐?

在生成MD5值的时候给它加一段随机的字符串,‍‍使得生成的 MD5 的值更加不容易被破解。

第二个就是数据库同时存储MD5值和salt的值,验证‍‍正确性使用salt进行MD5。‍‍

我们来测试一下MD5它是怎么使用的。

首先我们来看一下在Python中使用MD5加盐是怎么做的?‍‍

在Python如何对一段字符串计算它的MD5值呢?‍‍

import hashlib

m = hashlib.md5()
m.update(b"123456")
# 暴力破解 彩虹表
print(m.hexdigest())
# e10adc3949ba59abbe56e057f20f883e

MD5反解:https://www.cmd5.com/

我们可以在网上找一个破解工具,‍‍我们把上面Python代码生成的MD5字符串​​e10adc3949ba59abbe56e057f20f883e​​拷贝过来,‍‍

第23关 第一个微服务 用户服务_Go微服务_12

可以看到,现在这里边出现了它的解密。

我们之前已经说了MD5是不能反解的,为什么现在有人能够反解?

实际上不是因为它通过算法反解出来的,它是通过暴力破解,什么意思?‍‍‍‍

实际上对于MD5的破解,为了解决这个问题,它有一张彩虹表,‍‍什么叫彩虹表?

也就是说它实际上它会拿着很多字符常见的密码,比如说我们常见的123456,‍‍这就是非常常见的1个密码,

它会拿到这个密码去生成它的MD5值,‍‍然后这个时候它就会有一张很大的一张表,我们把它叫做彩虹表。‍‍

然后这个时候当你要反向解的时候,‍‍它不是通过算法破解出来的,它是通过查这张表给你破解出来的,

这对于我们来说其实就是很要命的,因为意味着我们的常用密码很容易被人破解,如何解决这个问题呢?‍‍

这就是MD5进行一个加密的盐值的问题。‍‍什么叫MD5加密的盐值问题?‍‍

我在将这个原始字符串生成MD5的时候,我给它加一个随机数,它通过这个随机数来‍‍将我的密码变得更加不可破解,也就是说你要破解这个密码,你要除了知道‍‍原始的字符串以外,你还要知道我的随机数是什么,那随机数是放在我服务器的,他就无法知道。

这样的话它的破解难度就很大,无法破解了。

所以说对于我们来说,我们要采用一个盐值的做法。

见下一小节。

23-4 md5盐值加密解决用户密码安全问题

第23关 第一个微服务 用户服务_数据库_13

比如说我拿着123456‌‌,然后我拿着MD5算法,然后它将我的123456变成了新的一段文本​​e10adc3949ba59abbe56e057f20f883e​​,‌‌

当你要通过文本​​e10adc3949ba59abbe56e057f20f883e​​反向来找123456‌‌的时候,你无法通过算法来得知‌‌怎么办?你可以通过这张彩虹表‌‌查询一下,

查询之后你就会知道​​e10adc3949ba59abbe56e057f20f883e​​是123456,但是对于一些复杂的密码,不常用的密码它仍然是算不出来的,‌‌除非它这张表非常大,几乎能够列举所有的密码,否则的话它算不出来的。‌

这种加密方式它实际上并不安全,‌‌因为对于我们用户来说有一些常见的密码,有的用户可能仍然会使用。这是第一点;

‌第二点,你无法知道‌‌用户的密码是不是常用密码,因为它这种彩虹表‌‌可能会尝试很多密码,这种彩虹表‌‌可能很大,

所以说这种加密算法如果只是普通的使用MD5的话,它并不安全。‌‌我们怎么来做?

这样来做,我们在加密的时候,MD5它给我们提供了一个叫做随机数加一个盐值。‌

‌你在生成 MD5 值的时候加一个随机数,当然这个随机数是由你自己指定的,它保证了你‌‌这个MD5是通过盐值加上你的原始字符串生成的,‌‌这样你就无法得知它的原始字符串是多少。

比如

第23关 第一个微服务 用户服务_mysql_14

比如说同样的米饭,米饭1和米饭2,这两个对于我们来说都是一样的米饭,

但是在给到顾客手中之前,撒盐哥现在给米饭撒了一把盐,

这把盐对于顾客来说实际上是不知道我们撒了多少盐,

顾客也不知道撒盐哥撒的是什么盐‌‌导致的,

这个时候顾客同样的米饭 生成饭的味道,它是不一样的,

‌即使你的加工过程是一样的,‌‌但是撒的这把盐不一样的,

比如说盐的数量,盐的粗细,盐的品牌,‌‌这些都会导致饭的味道不一样。‌

‌所以说我们现在加一把盐,我们在生成密钥的时候,我们实际上可以直接这样做。‌

如果你要生成随机数,‌‌你就得把随机数给保存下来,这个随机数对于我们来说可以是一个CharField类型,‌‌这个CharField类型最简单的一种做法是什么?

就是拿当前的时间戳来生成随机数,我把这个随机数保存起来,‌‌这样的话就导致了我生成的密码是不一样的。‌

‌首先我们来看一下加盐,加盐的话对于我们来说其实也并不复杂,‌‌我们使用

第23关 第一个微服务 用户服务_数据库_15

​m.hexdigest()​​的方法可以生成它的MD5字符串。

import hashlib

m = hashlib.md5()
password = "123456"
salt = "keegan9527"
m.update((salt+password).encode("utf8"))
# 暴力破解 彩虹表
print(m.hexdigest())
# 88176346c997e5f49acc77d0c44dba61

现在假设我现在有一段盐值叫​​keegan9527​​,然后我怎么做?

我把 password 和 salt 这两个字符串给它拼接一下,‌‌然后我拿到新的字符串,‌‌再生成它的MD5文本​​88176346c997e5f49acc77d0c44dba61​​,

第23关 第一个微服务 用户服务_mysql_16

此时就无法破解了。

如果要这样做,我们就必须要把随机的盐值放到我们的数据库当中,‌‌因为每一个用户他的盐值可能都不一样,这样做其实比较麻烦,

为了保存这个密码,你把盐值保存起来,‌‌这种对于我们来说,这些方法我们可以直接‌‌使用另外一个库,这个库叫passlib,

文档:https://passlib.readthedocs.io/en/stable/

这是Python中非常方便的加密的库。‌

​pip install passlib​

安装好了之后,如何来使用它?

我们来看一下官方文档,官方文档里边这里边其实说的还是比较清楚的,‌‌大家可以看到整个使用过程很简单:

>>> # import the hash algorithm
>>> from passlib.hash import pbkdf2_sha256

>>> # generate new salt, and hash a password
>>> hash = pbkdf2_sha256.hash("toomanysecrets")
>>> hash
'$pbkdf2-sha256$29000$N2YMIWQsBWBMae09x1jrPQ$1t8iyB2A.WF/Z5JZv.lfCIhXXN33N23OSgQYThBYRfk'

>>> # verifying the password
>>> pbkdf2_sha256.verify("toomanysecrets", hash)
True
>>> pbkdf2_sha256.verify("joshua", hash)
False
from passlib.hash import pbkdf2_sha256
hash = pbkdf2_sha256.hash("123456")
print(hash)
# $pbkdf2-sha256$29000$EKI0ZowxhpCyds65t1aKUQ$NYBlsEuamP7dDHVcGTRSjDY//Qmx9KVeCEpzKxF35xk

仍然使用​​123456​​,不需要再把它变成bytes类型了,因为它内部会自己转换的,总之好用。

注意到它把整个盐值加密,包括生成随机数,它内部已经帮我们完全做了,我们什么都不用做,

但是我们要知道它内部的原理大概是这个样子。

​$pbkdf2-sha256$29000$EKI0ZowxhpCyds65t1aKUQ$NYBlsEuamP7dDHVcGTRSjDY//Qmx9KVeCEpzKxF35xk​​没法破解:

第23关 第一个微服务 用户服务_python_17

有了这块之后,‌‌我们现在在校验密码的时候,我们得使用​​pbkdf2_sha256.verify​​的方法,

​pbkdf2_sha256.verify​​方法专门让我们来校验这个密码:

from passlib.hash import pbkdf2_sha256
hash = pbkdf2_sha256.hash("123456")
print(hash)
# $pbkdf2-sha256$29000$EKI0ZowxhpCyds65t1aKUQ$NYBlsEuamP7dDHVcGTRSjDY//Qmx9KVeCEpzKxF35xk
print(pbkdf2_sha256.verify("123456",hash)) # True
print(pbkdf2_sha256.verify("123123",hash)) # False

到这里,对于我们来说密码加密的问题就解决了,‌‌我们就能够防止这个密码被别人破解。

然后就是我们仍然可以完成密码的校验,‌‌后边密码校验一概使用​​pbkdf2_sha256.verify​​方法就ok了。

接下来我们新建几个用户,然后‌‌用户的密码肯定是要先使用​​passlib​​那一套来生成。

代码如下:

models.py

from peewee import *
from user_srv.settings import settings


class BaseModel(Model):
class Meta:
database = settings.DB


class User(BaseModel):
"""用户模型"""

GENDER_CHOICES = (
("female", "女"),
("male", "男")
)

ROLE_CHOICES = (
(1, "普通用户"),
(2, "管理员")
)

mobile = CharField(max_length=11, index=True, unique=True, verbose_name="手机号码")
password = CharField(max_length=100, verbose_name="密码") # 1. 密文 2. 密文不可反解
nick_name = CharField(max_length=20, null=True, verbose_name="昵称")
head_url = CharField(max_length=200, null=True, verbose_name="头像")
birthday = DateField(null=True, verbose_name="生日")
address = CharField(max_length=200, null=True, verbose_name="地址")
desc = TextField(null=True, verbose_name="个人简介")
gender = CharField(max_length=6, choices=GENDER_CHOICES, null=True, verbose_name="性别")
role = IntegerField(default=1, choices=ROLE_CHOICES, verbose_name="用户角色")


if __name__ == "__main__":
settings.DB.create_tables([User])
from passlib.hash import pbkdf2_sha256
for i in range(10):
user = User()
user.nick_name = f"keegan{i}"
user.mobile = f"1385562061{i}"
user.password = pbkdf2_sha256.hash("ZXCzxc123")
user.save()

第23关 第一个微服务 用户服务_python_18

password注意到它的密钥是不一样的,‌‌它的密码虽然是一样的,但是它生成的 password它是不一样的,‌‌

对于很多人来说可能是一个很难理解的地方,我们刚刚说了,那MD5它是将同样的字符串,‌‌生成的MD5值应该是一样的。

‌我们刚才说过有盐值的存在,导致了它这里边的值可能不一样,‌‌但是passlib内部做了一整套管理,所以说我们不用担心这些。

第23关 第一个微服务 用户服务_python_19

‌所以说我们不用担心在后端的保存的时候,它们的值不一样的问题,它内部都已经帮我们完成了。‌


23-5 proto接口定义和生成

本小节我们就开始来定义‌‌用户服务的grpc的接口。

​user.proto​

syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";

service User {
rpc GetUserList(PageInfo) returns (UserListResonse); //用户列表
rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse); //通过mobile查询用户
rpc GetUserById(IdRequest) returns (UserInfoResponse); //通过id查询用户
rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); //添加用户
rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty); // 更新用户
rpc CheckPassWord (PasswordCheckInfo) returns (CheckResponse); //检查密码
}

message PasswordCheckInfo {
string password = 1;
string encryptedPassword = 2;
}

message CheckResponse {
bool success = 1;
}

message PageInfo {
uint32 pn = 1;
uint32 pSize = 2;
}

message MobileRequest {
string mobile = 1;
}

message IdRequest {
int32 id = 1;
}

message CreateUserInfo {
string nickName = 1;
string passWord = 2;
string mobile = 3;
}

message UpdateUserInfo {
int32 id = 1;
string nickName = 2;
string gender = 3;
uint64 birthDay = 4;
}


message UserInfoResponse {
int32 id = 1;
string passWord = 2;
string mobile = 3;
string nickName = 4;
uint64 birthDay = 5;
string gender = 6;
int32 role = 7;
}

message UserListResonse {
int32 total = 1;
repeated UserInfoResponse data = 2;
}

然后:

python -m pip install grpcio
python -m pip install grpcio-tools
(mxshop_srv) proto python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. user.proto
# 注意是在哪个路径下执行的


23-6 用户列表接口

上一小节我们定义了proto接口文件,以及生成了相应的代码,‌‌本小节 我们来写一下这里边的业务逻辑代码,‌‌我们把业务逻辑代码放到我们handler当中,然后这里边建一个handler的user。

​handler/user.py​

import time

from user_srv.model.models import User
from user_srv.proto import user_pb2, user_pb2_grpc


class UserServicer(user_pb2_grpc.UserServicer):
def GetUserList(self, request: user_pb2.PageInfo, context):
# 获取用户列表
rsp = user_pb2.UserListResponse()

users = User.select()
rsp.total = users.count()

start = 0
page = 1
per_page_nums = 10

if request.pSize:
per_page_nums = request.pSize
if request.pn:
start = per_page_nums * (request.pn - 1)

users = users.limit(per_page_nums).offset(start)

for user in users:
user_info_rsp = user_pb2.UserInfoResponse()

user_info_rsp.id = user.id
user_info_rsp.passWord = user.password
user_info_rsp.mobile = user.mobile
user_info_rsp.role = user.role

if user.nick_name:
user_info_rsp.nickName = user.nick_name
if user.gender:
user_info_rsp.gender = user.gender
if user.birthday:
user_info_rsp.birthDay = int(time.mktime(user.birthday.timetuple()))

rsp.data.append(user_info_rsp)

return rsp

我们接下来要做的事‌‌是写一个测试,client端来连接它。


23-7 启动grpc服务

​server.py​

import grpc
import logging

from concurrent import futures

from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer


def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 注册用户服务
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port('[::]:50051')
print(f"启动服务:127.0.0.1:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

启动服务成功之后会有以下输出:

第23关 第一个微服务 用户服务_mysql_20

​test/user.py​

import grpc
from user_srv.proto import user_pb2_grpc, user_pb2


class UserTest:
def __init__(self):
# 连接grpc服务器
channel = grpc.insecure_channel("127.0.0.1:50051")
self.stub = user_pb2_grpc.UserStub(channel)

def user_list(self):
print(f"{self.stub.GetUserList(user_pb2.PageInfo())}")
rsp: user_pb2.UserListResponse = self.stub.GetUserList(user_pb2.PageInfo())
print(rsp.total)

for user in rsp.data:
print(user.mobile, user.birthDay)


if __name__ == '__main__':
user = UserTest()
user.user_list()

没问题的话,效果如下:

第23关 第一个微服务 用户服务_数据库_21

有可能你也会遇到以下问题。

如果遇到了以下问题:

AttributeError: ‘_FieldProperty’ object has no attribute ‘append’

解决方法如下:

pip uninstall protobuf
pip install protobuf

问题分析:

就是protobuf的版本问题【别瞎折腾了,费曼先生】

参考链接:

https://stackoverflow.com/questions/58343293/attributeerror-google-protobuf-pyext-message-repeatedcompositeco-object-has

尝试将各种关键词在​​stackoverflow​​上尝试搜索,一定要相信我的问题别人早就遇到了。

将上述这部分代码修改:

第23关 第一个微服务 用户服务_Go微服务_22

也就是每一页展示2条数据,

xxx0 xxx1

xxx2 xxx3

那么第二页应该

xxx02 xxx03的样子

接着我们再来运行一下:【重启​​server.py和user.py​​】

第23关 第一个微服务 用户服务_mysql_23

可以看到ta从xxx2开始,跳过了xxx0和xxx1,从xxx2开始,然后返回了两个数xxx2和xxx3,‌‌

到这里用户的列表页的接口就算完成,‌‌

但实际上我们在整个的开发过程中,我们想要做一个完善一点的这样一个小的微服务,‌‌

我们仅仅只是启动它的server和启动它的一个接口,它并不是很完善,‌‌

所以说下一小节我们来解决一个第一个问题就是它的一个日志的问题。‌

‌日志在我们微服务当中它比较重要,因为我们服务比较多,如果日志不好的话,整个过程会比较麻烦。‌‌

见下一小节。

23-8 日志库选型 - loguru

日志在我们实际开发过程中比较常见,‌‌Python实际上它是内置了一个loggin的库,loggin它已经比较方便了。

‌但是还有另外更方便的一个库,叫loguru,‌‌这个库写日志会更加的方便,

文档:

https://loguru.readthedocs.io/en/stable/overview.html

安装:

​pip install loguru​

现在我们把它应用到我们的 server 当中,server当目前是通过print打印的,现在我不用print打印了,我直接这样打印:

第23关 第一个微服务 用户服务_mysql_24

它会打印时间,

打印是logger类型是INFO的信息。‌‌

在哪一个源码当中哪一行,

以及它的打印信息是什么。

我们如果仅仅只是想使用‌‌ logger 的话,可以把print的地方全部改成logger就行了,我们后面就尽量不再使用print了。‌

但是它不仅仅只是这么简单,‌‌比如说我现在想把它输出到某个文件当中【日志文件】,我们来使用这个方法:

​logger.add("logs/user_srv_{time}.log")​

文件的‌‌名称,文件的名称前面可以写死​​logs/user_srv_​​,后边使用这个符号​​{time}​​,它就会自动加上我们当前启动的时间,‌‌然后来一个点log,

就行了。‌

这样调用‌‌,后边的话日志文件它会放到我 user_srv的根目录下,我们之前建立了一个logs,

所以说我就给它配置logs这个路径就行了,把日志全部放到logs里边来。‌

‌重新启动​​server.py​​,‌‌如果是 log 文件的话,我们的PyCharm它会有一些插件支持。‌

如果你要配置日志的话,‌‌包括配置日志的名称,

第二就是我在加日志的时候,‌‌我们可以做一些其他的配置,

第23关 第一个微服务 用户服务_数据库_25

比如说 rotation 这样一个参数,就是当你的文件太大,比如说超过500兆的话,它就新生成一个文件。‌

‌第二个就是设置时间,‌‌当你每天12点的时候,它就给你生成一个文件,

往下看比如说一周,10天,‌‌甚至你可以压缩你的文件的格式,比如说成zip格式,它都会帮我们做,‌‌

所以说这个地方大家根据自己的需要配置就行了。‌‌

因为如果我们的系统一旦使用的时间过长,那文件过大肯定是不行的。

在实际的开发过程中,大家把这些给它加上,设置一下大小或者说设置一下时间都可以,

因为日志文件对于我们来说‌‌是具有很重要的数据分析的意义的。‌

‌重点是,我们可以给某个函数‌‌配置一个装饰器,那就是​​@logger.catch​​:

例如

第23关 第一个微服务 用户服务_Go微服务_26

当函数抛出异常的时候,它会打印日志。‌

‌比如我把x,y,z三个值都传为0,变成了1÷0,@logger.catch就会抛出经典的除0异常。

第23关 第一个微服务 用户服务_数据库_27

运行抛出异常的时候,‌‌把变量的值也显示出来了,然后是它的异常的信息,整体来说显示的很清楚。

还可以设置某些信息显示成什么颜色:

​logger.add(sys.stdout, colorize=True, format="<green>{time}</green> <level>{message}</level>")​

文档中都有详细的说明。

日志的‌‌重要程度不一样,它也会显示不同的颜色:

第23关 第一个微服务 用户服务_Go微服务_28

logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误信息")

第23关 第一个微服务 用户服务_mysql_29

当我这里边一旦有了异常之后,这里边的信息‌‌就能够被发现。‌

这就是日志库,它的功能比较多,有需要的话‌‌自己可查看文档。‌

https://loguru.readthedocs.io/en/stable/overview.html


23-9 优雅退出server

问题:

如何在Python程序中做到优雅退出?

代码1:

​server.py​

import grpc
import logging
import signal
import sys

from concurrent import futures
from datetime import time
from loguru import logger

from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer


def on_exit():
logger.info("""
==============
| 进程中断 |
==============
""")
sys.exit(0)


def serve():
logger.add("logs/user_srv_{time}.log")
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 注册用户服务
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port('[::]:50051')
# 优雅退出进程
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)
logger.info("启动服务:127.0.0.1:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

问题1:

第23关 第一个微服务 用户服务_python_30

解决问题的逻辑如下:

(1)认识问题

找不到模块

(2)

为啥在PyCharm中运行程序的时候不报这个错?

而在终端使用 python xxx.py 就报了这个错?

这两者有啥区别?

(3)

在终端使用 ​​python server.py​​ ,但是​​server.py​​所在的项目的路径是没有配置的。【熟悉Django的童鞋会知道在settings.py 有BASE_DIR的配置获取settings.py 文件的父级目录路径】,

所以python解释器也不知道在哪儿找​​user_srv​​。

而PyCharm会自动将我们当前项目的路径设置成python解释器可以获得的路径,

所以 在终端使用 ​​python server.py​​的时候,还得解决另外一个问题,

把当前项目的目录放到python解释器可以获得的目录之下。

我们要用到以下信息:

第23关 第一个微服务 用户服务_数据库_31

首先得把​​mxshop_srv​​路径找到,然后把它添加到模块搜索路径列表里面。

第23关 第一个微服务 用户服务_python_32

有些东西记不住没关系,我们能搜到它,能调用它即可。

我们要知道,有些问题我们应该怎么去解决它。【解决问题的能力】

至此,问题就解决了。

我先给出了答案,再给提出答案的解决问题的编程思维。

授人以鱼不如授人以渔,授人以渔不如让人自渔。

总之我希望读者看完我的文章有些东西可以忘,但是实用的思想不能忘。

第23关 第一个微服务 用户服务_python_33

开始讲解解决问题的完整的过程:

import os
# 返回当前文件的父目录
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
print(BASE_DIR)
# /Users/blex/Developer/PythonProject/mxshop_srv/user_srv

首先认识一下问题:

步骤1:问题:什么是父级目录?

目录是一个文件夹,目录即文件夹。
父目录就是当前文件所在文件夹的文件夹,即上一个文件夹。
比如C:\Windows\System32\a\b\1.txt
那么1.txt的当前目录就是b,当前所在文件夹就是b,
那么1.txt的父目录是什么?
是1.txt这个文件的当前目录的上一级,
目录b的上一级是目录a。
所以1.txt文件的父级目录是
C:\Windows\System32\a

根目录是指最开始的那个文件夹,比如C盘就是一个根目录,因为它没有上一个文件夹了。
*/

父级目录跟当前目录是不一样的概念。

结论:

一个文件的父级目录是这个文件的当前目录的上一级目录。

我们认识了问题,理解了问题之后,进入下一步:

提出解决方法。

步骤2:解决方法:将大问题拆分成小问题,逐个解决。

步骤3:验证解决方法:

我们从内到外一层层的逐个分析。

代码如下:

# 语法:os.path.dirname(path)
# 功能:去掉文件名,返回目录。也就是该文件的当前目录
import os

# 绝对路径。返回当前文件的绝对路径
BASE_DIR1 = os.path.abspath("settings.py")
print(BASE_DIR1)
# /Users/blex/Developer/PythonProject/mxshop_srv/user_srv/settings/settings.py

# 当前目录。返回当前文件的所在目录,即在绝对路径的基础上去掉文件名
BASE_DIR2 = os.path.dirname(os.path.abspath("settings.py"))
print(BASE_DIR2)
# /Users/blex/Developer/PythonProject/mxshop_srv/user_srv/settings

# 父级目录。当前目录的上一级目录
BASE_DIR3 = os.path.dirname(os.path.dirname(os.path.abspath("settings.py")))
print(BASE_DIR3)
# /Users/blex/Developer/PythonProject/mxshop_srv/user_srv

好,我们现在通过将

​os.path.dirname(os.path.dirname(os.path.abspath("settings.py")))​

拆分成3层,从最简单的一层,渐进式的再加一层,然后再加一层,最终理解了问题,也验证了分解方法的正确性和实用性。

到现在你应该知道了,单纯的说BASE_DIR是父级目录这种说法是不可靠的?

最终的结果取决于代码。然后你得懂点基础的计算机知识来理解代码的结果。

理论和实践是相辅相成的。

步骤4:总结问题

我们先认识了问题,理解了问题。【认识问题的过程就是求知的过程】

然后我们采用分解的方法,

接着通过代码运行结果验证了分解的方法的实用性,【验证就是实践的过程】

在后续遇到问题理不清头绪的时候,可以采用分解的方法。【分而治之】

我为什么要这么做?

因为我在搜的时候,发现有些人自己不懂的学习,不懂的分析问题,写的都是错的,这不是误人子弟吗?

截图如下:

第23关 第一个微服务 用户服务_mysql_34

现在你知道了图中标注的不是叫当前文件的父级目录,而是该文件的当前目录。

然后最后一个不是当前文件父目录的父目录,而是该文件的父目录。

所以有些东西需要代码验证的。不是别人说啥就当个宝一样,别人说的有可能是错的,但是代码的执行结果不会骗人。

认识问题的过程就是搜索信息的过程,也就是调用信息的过程。

这就是​​server.py​​优雅退出,以及如何去‌在terminal当中可以运行它。​

​server.py​

完整代码如下:

import grpc
import logging
import os
import signal
import sys

from concurrent import futures
from datetime import time
from loguru import logger

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)

# 为什么写在上面呢?因为是走到下面才报错:ModuleNotFoundError: No module named 'user_srv'
from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer

# 为什么添加signo和frame?否则会有TypeError: on_exit() takes 0 positional arguments but 2 were given
def on_exit(signo,frame):
logger.info("""
==============
| 进程中断 |
==============
""")
sys.exit(0)


def serve():
logger.add("logs/user_srv_{time}.log")
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 注册用户服务
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port('[::]:50051')
# 优雅退出进程
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)
logger.info("启动服务:127.0.0.1:50051")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

23-10 通过argparse解析传递进入的参数

第23关 第一个微服务 用户服务_python_35

我们这里监听的IP地址和端口号都是写死的,这对我们来说其实并不友好。‌

可以通过从配置文件里边去读。‌

也可以 使用 ​​python xxx.py --host ip_add -- port port_num​​这种形式。

完整代码如下:

​server.py​

import argparse
import grpc
import logging
import os
import signal
import sys

from concurrent import futures
from datetime import time
from loguru import logger

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)

# 为什么写在上面呢?因为是走到下面才报错:ModuleNotFoundError: No module named 'user_srv'
from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer

# 为什么添加signo和frame?否则会有TypeError: on_exit() takes 0 positional arguments but 2 were given
def on_exit(signo,frame):
logger.info("""
==============
| 进程中断 |
==============
""")
sys.exit(0)


def serve():
logger.add("logs/user_srv_{time}.log")
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 通过argparse解析传递 ip 和 port
parser = argparse.ArgumentParser()
parser.add_argument('--ip',
nargs="?",
type=str,
default="127.0.0.1",
help="binding ip"
)
parser.add_argument('--port',
nargs="?",
type=int,
default=50051,
help="the listening port"
)
args = parser.parse_args()
# 注册用户服务
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port(f'{args.ip}:{args.port}')
# 优雅退出进程
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)
logger.info(f"启动服务:{args.ip}:{args.port}")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

这样使用:

第23关 第一个微服务 用户服务_python_36

第23关 第一个微服务 用户服务_python_37

第23关 第一个微服务 用户服务_Go微服务_38

主要是用到了这个​​argparse​​模块。

这就是通过argparse如何去解析它传递进来的参数。‌‌

23-11 通过id和mobile查询用户是否存在

接下来继续来完成handler里边具体接口的完善。

​server.py​

import argparse
import grpc
import logging
import os
import signal
import sys

from concurrent import futures
from datetime import time
from loguru import logger

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)

# 为什么写在上面呢?因为是走到下面才报错:ModuleNotFoundError: No module named 'user_srv'
from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer

# 为什么添加signo和frame?否则会有TypeError: on_exit() takes 0 positional arguments but 2 were given
def on_exit(signo,frame):
logger.info("""
==============
| 进程中断 |
==============
""")
sys.exit(0)


def serve():
logger.add("logs/user_srv_{time}.log")
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 通过argparse解析传递 ip 和 port
parser = argparse.ArgumentParser()
parser.add_argument('--ip',
nargs="?",
type=str,
default="127.0.0.1",
help="binding ip"
)
parser.add_argument('--port',
nargs="?",
type=int,
default=50051,
help="the listening port"
)
args = parser.parse_args()
# 注册用户服务
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port(f'{args.ip}:{args.port}')
# 优雅退出进程
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)
logger.info(f"启动服务:{args.ip}:{args.port}")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

​handler/user.py​

import time

import grpc
from loguru import logger
from peewee import DoesNotExist

from user_srv.model.models import User
from user_srv.proto import user_pb2, user_pb2_grpc


class UserServicer(user_pb2_grpc.UserServicer):
def convert_user_to_rsp(self, user):
"""解决共用的代码块"""
# 将user的model对象转化为message对象
user_info_rsp = user_pb2.UserInfoResponse()
user_info_rsp.id = user.id
user_info_rsp.passWord = user.password
user_info_rsp.mobile = user.mobile
user_info_rsp.role = user.role

if user.nick_name:
user_info_rsp.nickName = user.nick_name
if user.gender:
user_info_rsp.gender = user.gender
if user.birthday:
user_info_rsp.birthDay = int(time.mktime(user.birthday.timetuple()))

return user_info_rsp

@logger.catch
def GetUserList(self, request: user_pb2.PageInfo, context):
# 获取用户列表
rsp = user_pb2.UserListResponse()

users = User.select()
rsp.total = users.count()

start = 0
per_page_nums = 10

if request.pSize:
per_page_nums = request.pSize
if request.pn:
start = per_page_nums * (request.pn - 1)

users = users.limit(per_page_nums).offset(start)

for user in users:
rsp.data.append(self.convert_user_to_rsp(user))

return rsp

@logger.catch
def GetUserById(self, request: user_pb2.IdRequest, context):
# 通过id来查询用户
try:
user = User.get(User.id == request.id)
return self.convert_user_to_rsp(user)
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

@logger.catch
def GetUserByMobile(self, request: user_pb2.MobileRequest, context):
# 通过mobile查询用户
try:
user = User.get(User.mobile == request.mobile)
return self.convert_user_to_rsp(user)
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

​test/user.py​

import grpc
from user_srv.proto import user_pb2_grpc, user_pb2


class UserTest:
def __init__(self):
# 连接grpc服务器
channel = grpc.insecure_channel("127.0.0.1:50051")
self.stub = user_pb2_grpc.UserStub(channel)

def user_list(self):
rsp: user_pb2.UserListResponse = self.stub.GetUserList(user_pb2.PageInfo(pn=2, pSize=2))
print(rsp.total)

for user in rsp.data:
print(user.mobile, user.birthDay)

def get_user_by_id(self, id):
rsp: user_pb2.UserInfoResponse = self.stub.GetUserById(user_pb2.IdRequest(id=id))
print(rsp.mobile)

def get_user_by_mobile(self, mobile):
rsp: user_pb2.UserInfoResponse = self.stub.GetUserByMobile(user_pb2.MobileRequest(mobile=mobile))
print(rsp.mobile)


if __name__ == '__main__':
user = UserTest()
# user.user_list()
user.get_user_by_id(1)
user.get_user_by_mobile("13855620613")

在查询用户的时候,我们可以通过它的select,‌‌我们也可以通过它的一个get方法来查询。

‌然后get方法指明通过它的id来查询,‌‌注意到get方法它是有可能要抛出异常的,只要用get方法你就得处理异常,那这块异常我们‌‌它是我们叫​​DoesNotExist​​。‌

如果这里边已经查询到用户了,我们要做的事就是把用户信息‌‌给它构造出来,构造出对应的message,我们具体给它赋值来完成的。

同时,我们发现构造message的过程中有重复的代码,我们把它单独的抽出来封装到​​convert_user_to_rsp​​方法【类中的函数叫方法,变量叫属性】中提高代码的复用率。减少重复的代码。

然后我们先启动​​server.py​​,再运行​​test/user.py​​:

先看正常的返回结果:

第23关 第一个微服务 用户服务_python_39

我们再故意传不存在的数据,看返回的情况:

比如我给号码传xxx99,id也传99,数据库没有这样的数据的。【白色图片就10条记录】

第23关 第一个微服务 用户服务_mysql_40

可以看到,如果用户不存在,我们后端的返回信息是没有问题的。

这就是通过‌‌ id 和 mobile 来查询用户。‌都是通用的业务逻辑。​

23-12 新建用户接口

​handler/user.py​

import time
import grpc

from loguru import logger
from peewee import DoesNotExist
from passlib.hash import pbkdf2_sha256

from user_srv.model.models import User
from user_srv.proto import user_pb2, user_pb2_grpc


class UserServicer(user_pb2_grpc.UserServicer):
def convert_user_to_rsp(self, user):
"""解决共用的代码块"""
# 将user的model对象转化为message对象
user_info_rsp = user_pb2.UserInfoResponse()
user_info_rsp.id = user.id
user_info_rsp.passWord = user.password
user_info_rsp.mobile = user.mobile
user_info_rsp.role = user.role

if user.nick_name:
user_info_rsp.nickName = user.nick_name
if user.gender:
user_info_rsp.gender = user.gender
if user.birthday:
user_info_rsp.birthDay = int(time.mktime(user.birthday.timetuple()))

return user_info_rsp

@logger.catch
def GetUserList(self, request: user_pb2.PageInfo, context):
# 获取用户列表
rsp = user_pb2.UserListResponse()

users = User.select()
rsp.total = users.count()

start = 0
per_page_nums = 10

if request.pSize:
per_page_nums = request.pSize
if request.pn:
start = per_page_nums * (request.pn - 1)

users = users.limit(per_page_nums).offset(start)

for user in users:
rsp.data.append(self.convert_user_to_rsp(user))

return rsp

@logger.catch
def GetUserById(self, request: user_pb2.IdRequest, context):
# 通过id来查询用户
try:
user = User.get(User.id == request.id)
return self.convert_user_to_rsp(user)
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

@logger.catch
def GetUserByMobile(self, request: user_pb2.MobileRequest, context):
# 通过mobile查询用户
try:
user = User.get(User.mobile == request.mobile)
return self.convert_user_to_rsp(user)
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

@logger.catch
def CreateUser(self, request: user_pb2.CreateUserInfo, context):
# 新建用户
try:
User.get(User.mobile == request.mobile)

context.set_code(grpc.StatusCode.ALREADY_EXISTS)
context.set_details("用于已存在")
return user_pb2.UserInfoResponse()

except DoesNotExist as e:
pass

user = User()
user.nick_name = request.nickName
user.mobile = request.mobile
user.password = pbkdf2_sha256.hash(request.passWord)
user.save()

return self.convert_user_to_rsp(user)

​server.py​

import argparse
import grpc
import logging
import os
import signal
import sys

from concurrent import futures
from datetime import time
from loguru import logger

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)

# 为什么写在上面呢?因为是走到下面才报错:ModuleNotFoundError: No module named 'user_srv'
from user_srv.proto import user_pb2_grpc
from user_srv.handler.user import UserServicer

# 为什么添加signo和frame?否则会有TypeError: on_exit() takes 0 positional arguments but 2 were given
def on_exit(signo,frame):
logger.info("""
==============
| 进程中断 |
==============
""")
sys.exit(0)


def serve():
logger.add("logs/user_srv_{time}.log")
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

# 通过argparse解析传递 ip 和 port
parser = argparse.ArgumentParser()
parser.add_argument('--ip',
nargs="?",
type=str,
default="127.0.0.1",
help="binding ip"
)
parser.add_argument('--port',
nargs="?",
type=int,
default=50051,
help="the listening port"
)
args = parser.parse_args()
# 注册用户服务
user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server)
server.add_insecure_port(f'{args.ip}:{args.port}')
# 优雅退出进程
signal.signal(signal.SIGINT, on_exit)
signal.signal(signal.SIGTERM, on_exit)
logger.info(f"启动服务:{args.ip}:{args.port}")
server.start()
server.wait_for_termination()


if __name__ == '__main__':
logging.basicConfig()
serve()

​test/user.py​

import grpc
from user_srv.proto import user_pb2_grpc, user_pb2


class UserTest:
def __init__(self):
# 连接grpc服务器
channel = grpc.insecure_channel("127.0.0.1:50051")
self.stub = user_pb2_grpc.UserStub(channel)

def user_list(self):
rsp: user_pb2.UserListResponse = self.stub.GetUserList(user_pb2.PageInfo(pn=2, pSize=2))
print(rsp.total)

for user in rsp.data:
print(user.mobile, user.birthDay)

def get_user_by_id(self, id):
rsp: user_pb2.UserInfoResponse = self.stub.GetUserById(user_pb2.IdRequest(id=id))
print(rsp.mobile)

def get_user_by_mobile(self, mobile):
rsp: user_pb2.UserInfoResponse = self.stub.GetUserByMobile(user_pb2.MobileRequest(mobile=mobile))
print(rsp.mobile)

def create_user(self, nick_name, password, mobile):
rsp: user_pb2.UserInfoResponse = self.stub.CreateUser(user_pb2.CreateUserInfo(
nickName=nick_name,
passWord=password,
mobile=mobile
))
# print(rsp.id)
print(rsp.mobile)


if __name__ == '__main__':
user = UserTest()
user.create_user("keegan111", "ZXCzxc123", "13855620789")

效果如下:

第23关 第一个微服务 用户服务_数据库_41

创建用户没问题,我们再来一次,此时它会报​​用户已存在​​的异常。

第23关 第一个微服务 用户服务_数据库_42

第23关 第一个微服务 用户服务_python_43

我们要重新启动server,一定要记得重新启动,否则的话它是不生效的。

到数据库里边来看一下,我们可以看到密码加密是成功的,信息都是没有问题的,‌‌

到这里注册用户的接口已经完成。‌

在做web开发的时候,一般在‌‌往数据库里面写数据,都会面临着第一个问题就是表单验证。

对于我们来说有没有必要做表单验证?‌

第23关 第一个微服务 用户服务_mysql_44

我们这里实际上是没有必要做,为什么?‌‌

我们底层使用到的是Python来做的,那就是用户服务叫srv层,

然后它是向上提供的grpc的‌‌ 接口,这个接口是给用户服务的web层使用的。‌

在这里边它会有尽量多的业务逻辑,是在这里边处理完成的,下边接口尽量通用一些,‌‌因为它不光用户服务可以用,它还包括商品服务,订单服务都可以用。

所以说这里面的接口我们能通用就尽量通用,‌‌上边是web服务,它会跟业务尽量的挂钩,它对外提供的是一个restful http的接口。

‌在它调用的时候注意到,‌‌因为这是我们自己写的一个服务,所以说当浏览器来发起请求的时候,浏览器‌‌当然浏览器首先它会先经过网关,也就是API网关,‌‌网关最终它会打到我们这里边的服务上,这个服务是我们用go语言写的。

微服务任何一个‌‌服务,你都可以换任何语言的任何一个库或者框架来完成都是没有问题的。

在这里边‌‌他在做请求的时候它是一个HTTP的请求,网关走向API网关,然后来到web框架,web框架一定会先做什么?它一定会先做表单验证,‌‌

‌所以当它向下调用的时候,‌‌因为我对你是对内部的调用,所以说我只要在调用的地方 做好了表单验证,‌‌

我底层实际上是没有必要去做表单验证的。

但是如果底层做一个表单验证,上层再做一个表单验证,它们两边的表单验证规则不一样,对用户来说其实就是很懵逼的状态。‌

‌用户到时候怎么会出现这种情况,你上层表单验证通过了,底层表单验证又不通过,就会很奇怪。

具体的情况具体分析。‌

这就是新建用户和表单验证的问题。

23-13 更新用户

先查询一下‌‌用户是否存在,如果存在,就更新指定的字段信息。

如果不存在,后台就返回​​用户不存在​​的描述信息。

代码1:

​handler/user.py​

import time
from datetime import date

import grpc

from loguru import logger
from peewee import DoesNotExist
from passlib.hash import pbkdf2_sha256
from google.protobuf import empty_pb2

from user_srv.model.models import User
from user_srv.proto import user_pb2, user_pb2_grpc


class UserServicer(user_pb2_grpc.UserServicer):
def convert_user_to_rsp(self, user):
"""解决共用的代码块"""
# 将user的model对象转化为message对象
user_info_rsp = user_pb2.UserInfoResponse()
user_info_rsp.id = user.id
user_info_rsp.passWord = user.password
user_info_rsp.mobile = user.mobile
user_info_rsp.role = user.role

if user.nick_name:
user_info_rsp.nickName = user.nick_name
if user.gender:
user_info_rsp.gender = user.gender
if user.birthday:
user_info_rsp.birthDay = int(time.mktime(user.birthday.timetuple()))

return user_info_rsp

@logger.catch
def GetUserList(self, request: user_pb2.PageInfo, context):
# 获取用户列表
rsp = user_pb2.UserListResponse()

users = User.select()
rsp.total = users.count()

start = 0
per_page_nums = 10

if request.pSize:
per_page_nums = request.pSize
if request.pn:
start = per_page_nums * (request.pn - 1)

users = users.limit(per_page_nums).offset(start)

for user in users:
rsp.data.append(self.convert_user_to_rsp(user))

return rsp

@logger.catch
def GetUserById(self, request: user_pb2.IdRequest, context):
# 通过id来查询用户
try:
user = User.get(User.id == request.id)
return self.convert_user_to_rsp(user)
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

@logger.catch
def GetUserByMobile(self, request: user_pb2.MobileRequest, context):
# 通过mobile查询用户
try:
user = User.get(User.mobile == request.mobile)
return self.convert_user_to_rsp(user)
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

@logger.catch
def CreateUser(self, request: user_pb2.CreateUserInfo, context):
# 新建用户
try:
User.get(User.mobile == request.mobile)

context.set_code(grpc.StatusCode.ALREADY_EXISTS)
context.set_details("用于已存在")
return user_pb2.UserInfoResponse()

except DoesNotExist as e:
pass

user = User()
user.nick_name = request.nickName
user.mobile = request.mobile
user.password = pbkdf2_sha256.hash(request.passWord)
user.save()

return self.convert_user_to_rsp(user)

@logger.catch
def UpdateUser(self, request: user_pb2.UpdateUserInfo, context):
# 更新用户
try:
user = User.get(User.id == request.id)

user.nick_name = request.nickName
user.gender = request.gender
# 将时间戳转日期
user.birthday = date.fromtimestamp(request.birthDay)
user.save()
return empty_pb2.Empty()
except DoesNotExist as e:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("用户不存在")
return user_pb2.UserInfoResponse()

代码2:

​test/user.py​

import time

import grpc
from user_srv.proto import user_pb2_grpc, user_pb2


class UserTest:
def __init__(self):
# 连接grpc服务器
channel = grpc.insecure_channel("127.0.0.1:50051")
self.stub = user_pb2_grpc.UserStub(channel)

def user_list(self):
rsp: user_pb2.UserListResponse = self.stub.GetUserList(user_pb2.PageInfo(pn=2, pSize=2))
print(rsp.total)

for user in rsp.data:
print(user.mobile, user.birthDay)

def get_user_by_id(self, id):
rsp: user_pb2.UserInfoResponse = self.stub.GetUserById(user_pb2.IdRequest(id=id))
print(rsp.mobile)

def get_user_by_mobile(self, mobile):
rsp: user_pb2.UserInfoResponse = self.stub.GetUserByMobile(user_pb2.MobileRequest(mobile=mobile))
print(rsp.mobile)

def create_user(self, nick_name, password, mobile):
rsp: user_pb2.UserInfoResponse = self.stub.CreateUser(user_pb2.CreateUserInfo(
nickName=nick_name,
passWord=password,
mobile=mobile
))
# print(rsp.id)
print(rsp.mobile)

def update_user_by_id(self, id, nick_name, gender, dt):
# 将指定的日期转时间戳比如1998-10-24。因为birthDay是uint64的类型
birthday = int(time.mktime(time.strptime(dt, "%Y-%m-%d")))
rsp: user_pb2.UserInfoResponse = self.stub.UpdateUser(user_pb2.UpdateUserInfo(
id=id,
nickName=nick_name,
gender=gender,
birthDay=birthday
))
# grpc 返回值是空类型

if __name__ == '__main__':
user = UserTest()
user.update_user_by_id(4, "keegan1to33333", "男", "1998-10-24")

然后先启动​​server.py​​,再运行​​test/user.py​​,

刷新数据库,可以看到更新后的数据是:

第23关 第一个微服务 用户服务_python_45

到这里就完成了用户的更新。‌‌

小结:

这里遇到的问题我都解决了,总结解决问题过程:

一定要想发生的问题点和哪个地方有联系?

这个过程就是认识问题和理解问题的过程,

然后下一步就会进入想解决问题的方法,

然后下一步就会进入代码验证方法,

直到解决问题。

一环套一环,就这么简单。