优酷项目小结

优酷项目小结

任务需求

管理员功能

1.注册
2.登陆
3.上传视频
4.删除视频
5.发布公告

普通用户功能

1 注册
2 登录
3 冲会员
4 查看视频
5 下载免费视频
6 下载收费视频
7 查看观影记录
8 查看公告


  总结一下上面的所有需求, 管理员需要实现注册与登录功能, 这里需要注意用户名可以与管理员重名, 登录后需要保存用户的登录session, 即模拟浏览器的cookie,用户登录后只需发送由浏览器生成的随机串session就好了,管理员上传视频前需要比对md5值,如果服务器端存在相同文件, 则不用再重复上传; 删除文件同样需要注意只是逻辑上的删除,在数据库中数据由0改成1而已, 不会真正的删除原始文件. 用户注册与登录都需要与管理员共用一个接口,只需要在是否是管理员这一个字段区分就可以了.用户登录后,根据是否是管理员会弹出一条最新公告(模拟广告), 用户下载电影也会根据不同的级别有不通过的表现.

框架

       框架整体流程是客户端发送需求,通过socket传输到服务端, 期间所有的发送信息都是以字典的形式请求的, 服务端接收到来自客户端的请求后, 根据需要执行的命令操作, 再跳转到相应接口函数里面去执行, 涉及到需要增查改数据等等的操作, 利用事先写好的ORM(对象关系映射)来与数据库交互, 最后把相应信息发送给客户端, 这就完成了客户端与服务端的通过socket的操作.

写的过程遇到的BUG

1. 第一个错误是自己太粗心导致的, 发送文件之前需要发送文件的信息字典(包括大小,文件名等等信息),而我却先发送文件过去了, 很低级的错误.

def send_back_msg(client, send_dic, is_file=False):
    """
    客户端发送并获取来自服务端的消息, 并返回一个字典, 当传入第三个参数时, 继续接收文件信息
    :param client: 客户端接口
    :param send_dic: 发送的信息
    :param is_file: 是否需要发送文件信息
    :return: 来自服务端的字典消息
    """
    # 自定义报头
    # 发送字典长度, 再发送字典信息
    send_bytes = json.dumps(send_dic, ensure_ascii=False).encode('utf-8')
    send_len = struct.pack('i', len(send_bytes))

    client.send(send_len)
    client.send(send_bytes)

    if is_file:
        # 如果是传送文件的话, 就继续上传文件
        path = send_dic.get('path')
        with open(path, 'rb') as f:
            for line in f:
                client.send(line)

    # 接收信息

    recv_len = struct.unpack('i', client.recv(4))[0]
    recv_dic = json.loads(client.recv(recv_len))
    return recv_dic

2. 第二个常见错误是网络传输与hashlib模块md5等等update(更新)都需要接收字节串, 而我总是没有第一时间想起, 报错了才记得这点.

3. 文件在网络传输中, 需要知道文件大小, 分多次传输, 而不能一次性全部发送.

4. 名称上的bug,命名上的不规范, 导致的发生引用不明的, 比如自己不小心与内置模块, 与重要第三方模块重名, 从而导致调用失败等等. 例如这次项目中, 就发生了我的一个模块名与其变量同名,而我在程序中两个不同文件用到了这个变量, 可是这却导致, 其中一个文件引用这个变量无效, 即解释器把其中一个当做模块, 而把另一个当做变量名称, 两个不一致导致冲突. 

5. 循环导入的问题, 两个模块间循环导入,这时候利用第三个模块,就能解决问题, 即把冲突的变量,函数等等写到第三个模块中就可以解决.

书写中的小技巧

6. 这次实现了orm, 中间还有许多问题, 例如当查找的id超过了上限, 或是查找的字段压根就不在表中, 需要在里面把错误信息打印出来, 错误封装起来了, 会导致查找bug很难找到, 只能一步步调试到这,才知道, 不然直接就知道了是数据库查找的问题. 

7. 这次也涉及到了前后端参数接口需要统一的问题, 即客户端传入什么参数, 调用什么接口, 服务端需与其保持一致, 此外,相互之间的通信协议也要一致, 当客户端发来请求后, 服务端不管请求正确与否都要发送一条确认信息过去.

8. 存储进数据库中是时间戳格式, 当pymysql获取到时, 返回的是一个datetime类, 这时候如果要通过socket传输,就需要利用strftime方法进行转换后才能传输.

9. 元类本身需要继承type, 继承元类时也需要指定metaclass=元类,元类的new方法需要调用父类的new方法, 返回构建好的类

10. 覆盖双下getattr方法的时候,当属性不存在时, 需要给出明显的提示, 如None或抛出异常, 不然后面获取就会有问题., 会把问题隐藏., 例如后面的 values.append(self[k] if getattr(self, k) else v.default) 如果不返回None, 后面调用getattr方法就永远不会获取到后面的默认值.

11. 一个pymysql的字符串拼接的问题. pymysql 帮助我们拼接的原理是把字符串转换成两端带引号的形式, 把其他类型转换成本身的形式. 此处需要注意的是, 表的名称需要我们自己拼接,让pymysql拼接会在表名两端加引号就会报错.

12. 在其他线程中报的错误, 不会体现在终端中, 需要仔细寻找.

运用的设计技巧

 ORM设计.

       orm的设计十分的精妙, 先是为每一个字段单独设计一个类, 分别对应字段名称, 字段类型,是否是主键以及默认值, 这基本把大部分情况都包含了,然后下面这个元类的设计更是巧妙, 先是规定我们在类属性中定义自己的表的每一个字段(用预先定义的Field字段类), 表名, 主键. 有了这些信息我们就可以在类创建的时候动手脚, 在类创建出来之前就把表名,主键,以及所有字段确定, 因为原先的类属性写的比较分散, 主键信息也不好判断, 现在就可以利用元编程来在类创建出来就把这些信息确定, 并把所有字段放在一个mappings字典中,这样这张表就能很好的映射表中的数据了.

元类基类: 

class BaseModel(type):

    def __new__(mcs, name, base, attrs):
        """4个参数分别是元类本身, 类型, 基类, 以及类属性,包括方法和类属性"""
        # print(mcs, name, base, attrs)

        # 如果是Model表, 则不做任何事情, 直接返回
        if name == 'Model':
            return super().__new__(mcs, name, base, attrs)
        # 获取表名, 如果没有指定表名, 就用类名作为表名
        table_name = attrs.get('table_name', name)
        # 存储映射字段的字典
        mappings = {}
        primary_key = None
        # 遍历整个属性字典, 保存主键, 字段
        for k, v in attrs.items():
            if isinstance(v, Field):
                mappings[k] = v
                if v.primary_key:
                    if primary_key:
                        raise TypeError('一张表只能存在一个主键')
                    primary_key = k

        # 属性已将保存映射表里了,可以删除原来类里的属性
        for k in mappings:
            attrs.pop(k)

        if not primary_key:
            raise TypeError('一张表必须要指定主键')

        # 为类重新添加对应的属性
        attrs['primary_key'] = primary_key
        attrs['mappings'] = mappings
        attrs['table_name'] = table_name
        # 最后一定要调用父类方法创建类
        return super().__new__(mcs, name, base, attrs)

模型类

这个类是我们自己定义的表需要继承的父类, 它继承自字典和指定了前面书写的元类. 继承字典是因为sql中查询的数据也是类似字典键值对一样, 字段=值这种类型,这样就可以一一对应,直接让表里的数据与自己定义的模型类挂钩了.这个类先是重写了双下getattr和setattr这两个魔法方法, 让我们能用点语法操作对象属性,进入操作表里的字段值.下面主要实现3个简单的查询修改和增加的封装. 不过需要注意pymysql拼接sql语句的坑.

class Model(dict, metaclass=BaseModel):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def __getattr__(self, item):
        """当找不到属性时,会触发这个方法,使用这个是为了能够让表对应的类能够
        支持. 运算符来获取对应的属性"""
        return self.get(item, None)

    def __setattr__(self, key, value):
        """设置这个方法是为了能够让这个继承自字典的类能够支持通过. 运算符来
        为表对应的属性做操作"""
        self[key] = value

    # 为类增添增改查操作
    @classmethod
    def select(cls, **kwargs):
        """支持多个属性查找"""
        # select * from teacher where id=5 and name='egon'
        ms = mysql.Mysql()
        # 默认不传参数, 查询表的所有数据
        if not kwargs:
            sql = 'select * from {}'.format(cls.table_name)
            res = ms.select(sql)
        else:
            values = []
            sql = 'select * from {} where '.format(cls.table_name)
            for i, (k, v) in enumerate(kwargs.items()):
                sql += '{}=? '.format(k) if i == 0 else 'and {}=? '.format(k)
                values.append(v)
            sql = sql.replace('?', '%s')
            # print(sql)
            res = ms.select(sql, values)
        return [cls(**r) for r in res]  # 返回模型类的实例对象

    def update_val(self):
        """更新自己的属性"""
        # update teacher set name='', age='' where id=primary_id

        values = []
        keys = []
        pri = self.primary_key  # 主键
        pri_val = self[pri]     # 主键对应的值
        for k, v in self.mappings.items():
            if not v.primary_key:
                keys.append('{}=?'.format(k))
                values.append(self[k] if getattr(self, k) else v.default)
        sql = 'update {} set {} where {}={}'.format(
            self.table_name, ','.join(keys), pri, pri_val)
        sql = sql.replace('?', '%s')
        # print(sql)
        mysql.Mysql().execute(sql, values)

    def save(self):
        """保存自己的属性"""
        # insert into teacher(name, age) values('zhang', 13)
        keys = []
        values = []
        for k, v in self.mappings.items():
            if not v.primary_key:
                keys.append(k)
                values.append(self[k] if getattr(self, k) else v.default)
        sql = 'insert into {}({}) values (?)'.format(self.table_name, ','.join(keys))
        sql = sql.replace('?', ','.join(['%s'] * len(keys)))
        # print(sql)
        mysql.Mysql().execute(sql, values)

 客户端服务端的通信思路

  由于tcp传输会发生黏包现象, 所以客户端和服务端的通信就需要遵守一套协议, 发送方每次先把自己要发送的信息组成json的形式, 然后每次先发送这个json信息的长度, 再发送自己的json字典, 而接受端也每次先接受4个固定长度的字节, 来了解接下来需要接受多长的信息.这样就解决了黏包的问题. 传输文件的思路也是一样,先把文件大小信息放在自己发送的报头信息中,让接受端能够稳定的接受完文件.

   这里有一个可以封装的点. 就是客户端每次都需要先发送字典信息,然后等待服务器的响应,返回一个json(字典).这些操作都是固定的, 所以可以封装成函数, 这样以后客户端只用关需要发送什么字典信息, 也简化了编程的步骤.

获取大文件的md5值的小技巧

  获取大文件的md5值, 就不需要把所有字节都更新一遍, 可以按照自己的规则截取一小部分来hash, 这样也能确定文件下载的正确性.

def get_file_md5(path):
    """获取大文件的md5值方法
    只截取文件的开头, 中间1/3处, 2/3处
    和末尾前处, 每次哈希10个字节
    """
    size = os.path.getsize(path)
    index_list = [0, size // 3, size // 3 * 2, size - 10]
    md5 = hashlib.md5()
    with open(path, 'rb') as f:
        for index in index_list:
            f.seek(index)
            md5.update(f.read(10))
    return md5.hexdigest()
View Code

存储用户的session信息

  用户登录后需要保存登录状态, 而服务端又需要保存用户的登录状态信息, 还需要保证值这个登录信息是唯一的, 所以可以利用用户的ip和端口, 以此作为查找的键, 来对应后面的利用md5生成的随机字符串,这个随机字符串在生成后会发送给用户作为cookie, 以后用户只要发送这个cookie串,就能表明自己的登录状态. 每当这个登录后的用户发送来请求后, 服务端都会先去根据对方的ip端口信息查找session, 并比对相应的信息,以此来确定是否登录.

 在保存用户的session信息的时候,我们还可以保存相应的用户的其他关键信息,来让后面的操作更加方便, 例如这个案例中, 几乎所有操作都需要用到用户id这个重要信息, 这就可以在登录过程中, 就把这个信息一起记录到session中.这样每次用户的请求字典中,我们都可以加入这个id来让相关的处理函数能够更方便的执行查询等等操作.

with user_session.lock:
      session = common.get_session(name)
      alive_user[back_dic.get('addr')] = [session, user.id]
      is_vip = user.is_vip

session信息查询的加锁

  由于要支持多线程, 这样就涉及到数据不安全的问题, 这就需要在关键地方加锁处理, 就如上面所示. 上面是登录操作中的为用户添加session信息的保护措施.

具有相似功能的函数可以多定义一个关键字参数

  例如上面的用户需要查看所有电影, 收费电影,和免费电影, 这种情况就可以在获取电影参数的时候多加一个参数, 来指定获取什么类型的电影, 这也能够达到扩展已经写好的函数的功能.

优化点

1. 

 

2. 

    

3. 

有些很多的if else嵌套写起来很难阅读, 可以分成几个函数, 或者前面先写否定, 这样可以减少缩进级别.

问题

Field字段如何做类型检查

查询怎样做到支持大于等于甚至更复杂的查询逻辑.

 

posted @ 2019-07-29 18:26  yscl  阅读(159)  评论(0)    收藏  举报