Python高级

Python 高级

网络编程

socket(套接字),位于应用层和传输层的一个接口,用于实现网络通信。

socket 初步

详见网络原理,或者Windows 网络编程

UDP 编程

UDP 不严格区分客户端和服务器。

from socket import *
s = socket(AF_INET, SOCK_DGRAM)
s.bind(('', 8080)) # 若为客户端,这不需要绑定端口
addr = ('192.168.56.1', 7788)
send_data = input('发送内容')
s.sendto(send_data.endcode('utf-8'), addr)
recv_data = s.recvfrom(1024)
print(recv_data[0].decode('utf-8'), recv_data[1])
s.close()

TCP 编程

TCP 编程严格区分客户端和服务器,这是由于 TCP 协议性质决定的。

客户端
from socket import *
s = socket(AF_INET, SOCK_STREAM)
dest_addr = ('192.168.56.1', 7788)
s.connect(dest_addr)
send_data = input('发送内容')
s.send(send_data.endcode('utf-8'))
recv_data = s.recv(1024)
print(recv_data.decode('utf-8'))
s.close()
服务器
from socket import *
s = socket(AF_INET, SOCK_STREAM)
addr = ('', 8080)
s.bind(addr)
s.listen(10)

while True:
    new_client, new_addr = s.accept()
    send_data = 'welcom!'
    new_client.send(send_data.encode('utf-8'))
    while True:
        recv_data = new_client.recv(1024)
        if recv_data:
            print(recv_data.decode('utf-8'))
        else:
            new_client.close()

s.close()
  • 当客户端close后,服务器的recv会阻塞,并且返回空字符串。故可以通过这个判断客户端断开连接。
  • 当服务器close后,即四次挥手之后并不会立即释放已有的资源,会等待 2 倍 MSL 时间。可以通过设置s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)来让端口立即得到复用。

非阻塞模式

  • socket 默认处于阻塞模式,当未接受到数据时会处于阻塞状态直到有数据到来。通过设置s.setblocking(False)可以将其改变为非阻塞模式。
  • 当 socket 处于非阻塞模式时,没有数据到达各种方法会立即报错,所以需要捕获异常和人为轮询。
  • 设置非阻塞模式也是一种多任务 socket 编程模型
from socket import *
s = socket(AF_INET, SOCK_STREAM)
# 端口复用
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 设置非阻塞模式
s.setblocking(False)
addr = ('', 8080)
s.bind(addr)
s.listen(10)

client_list = []
while True:
    try:
        new_client, new_addr = s.accept()
    except Exception:
        pass
    else:
        print('新的客户端到来')
        new_client.setblocking(False)
        client_list.append(new_client)
        send_data = 'welcom!'
        new_client.send(send_data.encode('utf-8'))

    for client in client_list:
        try:
            recv_data = client.recv(1024)
        except Exception:
            pass
        else:
            if recv_data:
                print(recv_data.decode('utf-8'))
            else:
                print('客户端断开')
                client.close()
                client_list.remove(client)

s.close()

多任务

  • 在 Python 中实现多任务一般有三种方式:多进程,多线程,协程。
  • 多线程由于有GIL的存在,导致性能受限,但仍不失它的优点。
  • 协程时 Python 中独有的一种多任务方式,它底层使用yield实现。更一般地有封装yeildgreenletgevent模块,目前常用的是通过gevent实现。

多进程

多进程的实现

多进程的实现方法有很多,主流的有 2 种方式:Process 类和进程池。这两种方式目前在新版中都需要在main函数中使用!!!

  • import multiprocessing,创建多进程。
    • os.getpid():可以查看当前运行的进程的 ID 号,这个与端口号不同。
    • 在 Python 新版本中Process必须在 main 函数中使用!!!
import multiprocessing

def f(n):
    while True:
        print(n)

def main():
    p1 = multiprocessing.Process(target=f, args=(1,))
    p2 = multiprocessing.Process(target=f, args=(2,))

    p1.start()
    p2.start()

if __name__ == '__main__':
    main()
  • 使用进程池:
    • 进程池创建时可以设置最大可以管理的进程数。
    • 利用进程池创建子进程,其主进程不会等待所有的子进程结束。所以需要使用po.join()阻塞主线程在此不结束。
    • 进程池易错点
      • 进程池无法使用通信队列;
      • 在类中无法实现进程池;
      • 在 Python 新版本中进程池必须在 main 函数中使用!!!
import multiprocessing

def f(n):
    while True:
        print(n)

def main():
    po = multiprocessing.Pool(5)
    for i in range(4):
        po.apply_async(f, (i,))
    po.close() # 关闭进程池,不在接受新进程
    po.join() # 等待所有子进程完成,必须放在close之后

if __name__ == '__main__':
    main()
进程通信

由于进程之间不共享全局变量,故需要引入进程通信机制。常见的有 3 中方式:socket机制硬盘缓冲区内存缓冲区

  • socket:网络间进程通信的机制。
  • 硬盘缓冲区:利用文件的读写实现进程通信。
  • 内存缓冲区:利用队列实现进程通信。该队列对象需要使用multprocessing模块中的队列。
    • q = multiprocessing.Queue(1000),定义一个大小为 1000 的通信队列
    • q.put(xx), q.get(), q.full(), q.empty(), q.qsize()
import multiprocessing

def f1(q):
    for i in range(100):
        q.put(i)

def f2(q):
    while not q.empty():
        print(q.get())

def main():
    q = multiprocessing.Queue(100)
    p1 = multiprocessing.Process(target=f1, args=(q,))
    p2 = multiprocessing.Process(target=f2, args=(q,))
    p1.start()
    p2.start()

if __name__ == '__main__':
    main()

多线程

多线程的实现
  • import threading,创建多线程。
import threading

def f1(n):
    while True:
        print(n)

def f2(n):
    while True:
        print(n)

t1 = threading.Thread(target=f1, args=(1,))
t2= threading.Thread(target=f2, args=(2,))
t1.start()
t2.start()
  • 定义一个类,继承threading.Thread类,重写run方法。比较复杂,一般不用
import threading

class MyThread1(threading.Thread):
    def run(self):
        while True:
            print(1)

class MyThread2(threading.Thread):
    def run(self):
        while True:
            print(2)

t1 = MyThread1()
t2 = MyThread2()
t1.start() # start会自动调用run方法
t2.start()
多线程共享全局变量

多线程共享主线程(即整个进程)的所有数据,故会造成多个线程之间争夺临界资源的情况,使得程序运行异常。

  • 问题的引入。A, B 两个线程同时对一个全局变量(初始值为 0)增加 10000 次,理论上最终结果为 20000。但是由于 CPU 是时间片轮转运行的,导致两个进程对+1 操作不一致。例如,A 还未将结果保存时,B 就占领 CPU 执行,导致+1 失败。
import threading

sum = 0

def f():
    global sum
    for i in range(100000):
        sum += 1

t1 = threading.Thread(target=f)
t2= threading.Thread(target=f)
t1.start()
t2.start()
t1.join()
t2.join()

print(sum) # sum的结果并不是200000
  • 互斥锁:原子操作。
mutex = threading.Lock()
mutex.acquire() # 加锁
mutex.release() # 解锁
死锁
  • 死锁是指两个或两个以上的多任务在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁任务。
  • 解决死锁有很多种方法,比如破坏死锁的四个必要条件。但是使用得最普遍的是银行家算法
  • 银行家算法:先进行预分配,看分配后能否找到一个安全序列,则真正地分配之。通过银行家算法可以保证系统总是保持在安全状态。
GIL(全局解释器锁)
GIL 介绍
  • GIL 并不属于 Python 内部知识,仅仅是由于历史原因在 Python 官方解释器(CPython)中难以移除 GIL。其他版本的解释器(如 JPython 等)中没有 GIL。
  • 每个线程在执行前都要获取 GIL,每当执行 100 行字节码或者遇到阻塞时便释放 GIL 让其他线程运行。即 Python 保证同一时刻只有一个线程运行。
  • 虽然同一时刻只有一个线程真正地运行,但是多线程仍然比单线程效率高。因为多线程在线程发生阻塞时会主动释放 GIL,让其他线程运行,很大地提高了 CPU 的利用率。
  • GIL 仅仅针对于多线程而言,对多进程、协程毫无影响。
    • 单线程死循环:单个 CPU 跑到 100%。
    • 两个线程死循环:两个 CPU 各占 50%。
    • 两个进程死循环:两个 CPU 各占 100%。
解决 GIL
  • 使用其他版本的解释器。
  • 使用 C 语言。(Python 是胶水语言)
    • 用 c 语言编写’haha.c’文件。内容如下:
    • gcc haha.c -shared -o libhaha.so编译之。
    • 在 Python 主文件中,内容如下:
haha.c
void loop()
{
	while(1){;}
}
main.py
from ctypes import *
from threading import Thread
lib = cdll.LoadLibrary('./libhaha.so') # 加载C语言动态库
t = Thread(target=lib.loop)
t.start()

while True:
    pass

协程

考虑到 CPU 和 IO 之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待 IO 操作,浪费时间。但是现代 OS 对 IO 操作已经做了巨大的改进,最大的特点就是支持异步 IO,可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx 就是支持异步 IO 的 Web 服务器。它在单核 CPU 上采用单进程模型就可以高效地支持多任务。由于系统总的进程数量十分有限,因此操作系统调度非常高效。

协程的实现
  • 在 Python 语言,单进程的异步编程模型称为协程。协程利用程序的空余时间(阻塞,IO 操作等)去执行其他代码,有效地提高了 CPU 的利用率,即如果没有遇到阻塞操作,协程是不会运行的
  • 调用gevent第三方库实现协程。gevent库高度封装了yeild
import gevent

def f(n):
    for i in range(n):
        print(i)

print('-------1------')
g1 = gevent.spawn(f, 5) # 由于没有阻塞,g1并不会立即执行
print('-------2------')
g2 = gevent.spawn(f, 5)
print('-------3------')
g3 = gevent.spawn(f, 5)
print('-------4------')

g1.join() # 主线程发生阻塞,g1开始执行
g2.join()
g3.join()
monkey 补丁
  • gevent中,普通的延时和函数阻塞是没有效果的,要使用gevent中封装的各种函数才可以,例如gevent.sleep(), gevent.recv()等。
  • 一般的代码若需要使用协程需要全部重构,但是太浪费时间了。打monkey 补丁monkey.patch_all()
  • monkey补丁的书写的位置:monkey补丁必须写在需要替换的模块之前,要不然没有效果。最好放在最开头,第一行
  • 到底需不需要使用join:如果后续有阻塞代码则不用加,否则需要加。
import gevent
from gevent import monkey
monkey.patch_all()
// 不能使用gevent.monkey.patch_all(),不知道为什么
import random
import time

def work(name):
    for i in range(10):
        print(name, i)
        time.sleep(random.random())

gevent.joinall([
    gevent.spawn(work, 'bob'),
    gevent.spawn(work, 'alice'),
    gevent.spawn(work, 'mike')
])

进程、线程和协程对比

  • 进程是资源分配的基本单位,线程是独立运行的基本单位,协程是依附于线程的一种多任务机制。
  • 一个进程有且只有一个主线程,可以有多个子线程。一个线程可以拥有多个协程。
  • 协程调度的消耗最小,效率也最高。进程或线程根据 CPU 的不同可能是并行的,但协程一定是并发的。
  • 车间效率提高类比:
    • 多进程类似于增加多个车间。
    • 多线程类似于每个车间增加多个工人。
    • 协程类似于所有工人都不休息,一直干!

MySQL 数据库

安装 MySQL 数据库

sudo apt install mysql-server # 安装MySQL

# 配置root密码
sudo -s
mysql
use mysql;
update user set authentication_string=password('123456'),plugin='mysql_native_password' where user='root';
flush privileges;
  • mysql 相关命令:
    • 开启服务:sudo service mysql start
    • 停止服务:sudo service mysql stop
    • 配置 mysql:/etc/mysql/mysql.cnf
  • 客户端软件
    • Navicat 软件。(Windows 主机登录 Ubuntu 虚拟机的 MySQL 失败:
      • 修改数据库为远程可登录:sudo gedit /etc/mysql/mysql.conf.d/mysqld.cnf,注释掉文中的bind-address = 127.0.0.1: -> #bind-address = 127.0.0.1:
      • 修改登录权限:修改 mysql 数据库中的 user 表中的 root 为%:update user set host = '%' where user = 'root';
      • 重启服务器。
    • 命令行:sudo apt install mysql-client

数据库理论

数据库基础
  • 现代化记录数据的方式有文件和数据库等,但是文件对于容量很大时效率很低且不易扩展。数据库是长期存储在计算机内,有组织、可共享的大量数据的集合
  • 数据库是一个特殊的文件,冗余低、速度快, 数据库一般使用 DBMS 软件进行管理。MySQL 是一种主流的关系型数据库,使用 RDMBS 进行管理。
    • 关系型数据库:MySQL, sqlite, sql server, oracle等。
    • 非关系型数据库:mongodb等(无需提前建表,存储无关系数据)。
  • 重要概念:
    • 记录、字段、域、数据表、数据库、视图、查询表、冗余。
    • 主键与外键。
    • 完整性约束:实体完整性、参照完整性、用户自定义完整性。
    • SQL(结构化查询语言),不区分大小写
      • 数据定义:create, drop, alter
      • 数据操作:insert, delete, update
      • 数据查询:select
      • 数据控制:grant, revoke
数据库高级
  • 视图:视图是一个查询表,由 select 语句执行后返回的结果构成的续表。
    • 创建:create view 视图名 as 查询
    • 删除:drop view 视图名
  • 事务:事务广泛运用于订单系统、银行系统等多种场景。事务是一个操作序列,要么全做要么全部做。
    • 事务的四大特性(ACID):原子性、一致性、隔离型、永久性。
      • 原子性(Atomicity):事务是不可分割的最小单元。成功则提交,失败者回滚。
      • 一致性(Consistency):事务保证数据库总是一个状态到另一个状态,不会因系统崩溃而状态模棱两可。
      • 隔离性(Isolation):一个事务在提交之前,对其他事务不可见,即加锁。
      • 持久性(Durability):事务一旦提交,修改会永久保存。
    • 开启事务:pymysql默认开启事务,当增删改时需要commit();命令行默认关闭事务。若需要开启事务:start transaction或者begin,加锁直至commit()才释放锁。
  • 索引:当数据库中数据量很大时,查找数据会变得很慢。索引是一种特殊的文件(InnoDB 引擎的数据表上的索引是表空间的一个组成部分),它们包含着对数据表记录的引用指针。通俗的说,数据库索引就好比是一本书前面的目录,能加快数据库的查询速度。
    • 创建索引:create index 索引名 on 表名(列名)。若列为字符串类型,需要指明长度。例如:create index haha on Student(name(10))
    • 索引的优缺点:优点是提高了查询速度,缺点是降低了插入速度。
  • 账户管理:一般不会直接使用 root 账户操作数据库,因为权限太大。
    • 所有的用户和权限信息都存储在mysql数据库中的user表中,表有些比较重要的字段:
      • host:表示可以访问的主机。localhost表示只能主机登录,%表示可以任意主机登录。
      • user:用户名。
      • authentication_string:密码的摘要。
    • 创建用户和授权:常用的权限有create, alter, drop, insert, delete, update, select等,所有权限为all privileges
      • grant 权限列表 on 数据库.数据表 | 数据库.* to '用户名'@'主机名' identified by '密码'。例如:grant select on jing_dong.goods to 'laowang'@'localhost' identified by '123456'
    • 查看用户有哪些权限:show grants for 用户名@主机名
    • 修改权限:grant 权限 on 数据库.数据表 to 用户名@主机名 with grant option。然后,flush privileges
    • 修改密码:update user set authentication_string=password('新密码') where user='用户名'。然后,flush privileges
    • 删除用户:drop user '用户名'@'主机名'
  • MySQL 主从同步配置:设置一个或多个自动同步的备份数据库,而且还可以做到服务器的读写分离,读可以在从服务器完成,写在主服务器完成。
    • 备份主数据库的数据:mysqldump -uroot -pmysql --all-databases --lock-all-tables > ~/master_db.sql
    • 在从数据库上恢复数据:mysql –uroot –pmysql < master_db.sql
    • 配置主数据库:
      • 编辑设置mysqld的配置文件,设置log_bin=/var/log/mysql/mysql-bin.logserver-id=1sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
      • 重启 mysql 服务:sudo service mysql restart
      • 登入主服务器 Ubuntu 中的 mysql,创建用于从服务器同步数据使用的帐号:GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%' identified by 'slave',FLUSH PRIVILEGES
      • 获取主服务器的二进制日志信息:show master status。记录下FilePosition字段,用于配置从数据库。
    • 配置从数据库:
      • 编辑设置 mysqld 的配置文件,设置server-id=2sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
      • 设置连接到主数据库:change master to master_host='10.211.55.5', master_user='slave', master_password='slave',master_log_file='mysql-bin.000006', master_log_pos=590
      • 开启同步:start slave
      • 查看同步状态:show slave status \G

命令行命令

MySQL 的命令行命令以’;'作为结束的标志。

  • show databases; show tables;
  • use 库名;
  • select now(); select version(); select database();
  • desc 表名; 查看表的结构。
  • 数据库备份和还原
    • 备份:mysqldump –uroot –p 数据库名 > python.sql;
    • 还原:mysql -uroot –p 数据库名(该数据库存在且同名) < python.sql;

数据库设计

三范式
  • 第一范式(1NF):每一个分量都是不可分割的数据项。
  • 第二范式(2NF):消除非主属性对码(主码和候选码)的部分函数依赖。
  • 第三范式(3NF):消除非主属性对码的传递依赖。
ER 图

ER 图:实体关系图。是设计数据库必要的前步骤。实体可以是表,关系也可以构成一个表。

  • 1 对 1:在任意一个实体中增加一个外键。
  • 1 对多:在多的实体中增加一个外键。
  • 多对多:需要对关系新建一个基本表。

数据定义

  • 定义数据库:create database 库名 charset=utf8
  • 定义数据表:
    • 列级完整性约束:primary key, not null, unique, auto_increasement, default
    • 表级完整性约束:
      • primary key(列名1, 列名2)
      • foreign key(列名) references 表名(列名)
    • 数据类型:char(x), varchar(x), text, tinyint, int, int unsigned, smallint, bigint, float, double, boolean, date, time, datatime, enum(x, y, z...), bit(x),对于图片、音频、视频等文件,不存储在数据库中,而是上传到服务器上保存其路径。
create table 表名(
    字段名1, 数据类型 [,列级完整性约束],
    字段名2, 数据类型 [,列级完整性约束],
    ...
    [表级完整性约束]
)
  • 删除数据表:drop table 表名
  • 修改数据表:alter table 表名 [add 列名 数据类型 列级完整性约束 | change 旧列名 新列名 数据类型 列级完整性约束 | drop 列名]

数据操作

  • 增:
    • insert into 表名[(列名)] values(值)
    • insert into 表名[(列名)] 子查询
    • insert into 表名 values(default, 'haha', 10, default)default用于表明使用默认值和自增的占位符。
  • 删:delete from 表名 where 条件
  • 改:update 表名 set 列名1=值1 [,列名2=值2] where 条件

数据查询

  • select * from country; select name, Continent from country;
  • select name Name, Continent haha from country hehe;
  • 条件查询:
    • 逻辑和比较。
    • 模糊查询:·like(%, _), rlike 正则表达式
    • 范围查询。
    • 空判断。
  • 排序,支持多重排序
    • 升序:asc
    • 降序:desc
    • order by height desc, id desc
  • 聚合函数:sum, count, max, min, avg, round(xx, 2)等
  • 分组:细化聚合函数的作用对象,分组一般都要和聚合函数配合使用。
    • 分组的查询结果必须是每个分组共有的字段。select gender, count(*) from students group by gender
    • 查看每个分组中的内容:select gender, group_concat(name, age) from students group by gender
    • 分组配合wherehavingselect gender from students where gender=1 group by gender having xx
      • where对原始数据进行条件判断。
      • having对分组进行条件判断,一般条件是聚合函数。
  • 分页:limit start, count
  • 连接查询:多个表查询。select * from 表1 inner或left或right join 表2 on 表1.列=表2.列
    • 内连接:查询的结果为两个表匹配到的数据,笛卡尔乘积,默认
    • 左外连接:查询的结果为两个表匹配到的数据,左表特有的数据,对于右表中不存在的用 null。
    • 右外连接:查询的结果为两个表匹配到的数据,右表特有的数据,对于左表中不存在的用 null。
    • 自连接:需要为表增加别名。
  • 子查询:查询中还有查询。

Python 和 MySQL 交互

在 Python3 中使用pymysql操作 MySQL 数据库。

Connection 对象
  • 用于建立与数据库的连接。
  • conn = connect(host='localhost', port=3306, user='root', password='123456', database='jingdong', charset='utf8')
    • host:连接的 mysql 主机,如果本机是’localhost’
    • port:连接的 mysql 主机的端口,默认是 3306
    • database:数据库的名称
    • user:连接的用户名
    • password:连接的密码
    • charset:通信采用的编码方式,推荐使用 utf8
  • 方法:
    • close():关闭。
    • commit():提交,增删改必须要commit()
    • rollback():回滚。
Cursor 对象
  • 用于执行 sql 语句,使用频度最高的语句为select、insert、update、delete
  • cs = conn.cursor()
  • 方法:
    • close():关闭。
    • fetchone():返回查询结果的一行数据。
    • fetchall():返回查询结果的所有数据,以元组封装。
    • fetchmany(n):返回查询结果的 n 条结果。
from pymysql import *

# 创建Connection连接
conn = connect(host='192.168.56.4', port=3306, user='root', password='123456', database='jingdong', charset='utf8')
# 获得Cursor对象
cs = conn.cursor()
# 执行一条sql语句
sql = 'select * from goods'
count = cs.execute(sql)
# 提交操作
conn.commit()
# conn.rollback()
# 打印受影响的条数
print(count)

for i in range(count):
    # 获取查询结果
    result = cs.fetchone()
    print(result)

# 关闭Cursor对象
cs.close()
conn.close()

SQL 注入与参数化

  • 如果 SQL 不好好写,其他人不用密码也能盗用数据库的数据。
  • sql = "select * from goods where name='%s'" % (haha),如果haha = ' or 1=1 '1,即会发生SQL 注入
  • 在一定程度上防止 SQL 注入。
    • sql = "select * from goods where name=%s
    • parmas = ['笔记本']
    • cs.excute(sql, params) # 让excute自动替换参数

web 服务器

HTTP 协议

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。HTTP是一个客户端和服务器端请求和应答的标准(TCP)。客户端是终端用户,服务器端是网站。通过使用 Web 浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为 80)的HTTP请求。

  • HTTP/1.0 这是第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用,特别是在代理服务器中。
  • HTTP/1.1 当前版本。持久连接被默认采用,并能很好地配合代理服务器工作。还支持以管道方式同时发送多个请求,以便降低线路负载,提高传输速度。
请求报文
GET / HTTP/1.1 # 方法,路径,版本
Host: www.baidu.com # 主机,域名或ip地址
Connection: keep-alive # 长连接
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36 # 用户代理信息
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 # 浏览器可接受格式
Accept-Encoding: gzip, deflate, br # 浏览器可接受编码
Accept-Language: zh-CN,zh;q=0.9 # 浏览器可接受语言
Cookie: BAIDUID=65A437EC89EC8AC9F11FBDC675C4FB4C;  # cookie
响应报文
HTTP/1.1 200 OK # 版本,状态码,状态号
Bdpagetype: 2
Bdqid: 0xb5a51d18000bda5b
Cache-Control: private
Connection: Keep-Alive # 长连接
Content-Encoding: gzip # 内容的编码
Content-Type: text/html;charset=utf-8 # 内容的类型
Date: Mon, 25 Mar 2019 12:54:53 GMT # 日期
Expires: Mon, 25 Mar 2019 12:54:52 GMT # 过期时间
Server: BWS/1.1 # 服务器信息
Set-Cookie: BDSVRTM=343; path=/ # 设置cookie
Set-Cookie: BD_HOME=1; path=/
Set-Cookie: H_PS_PSSID=28631_1436_21111_28723_28557_28697_28584_28638_26350_28603_28625_22160; path=/; domain=.baidu.com
Strict-Transport-Security: max-age=172800
X-Ua-Compatible: IE=Edge,chrome=1
Transfer-Encoding: chunked
其他
  • URL(Union Resourse Location):统一资源定位符。<协议>://<主机>:<端口>/路径
  • 点击一个链接所发生的事情:
    • 浏览器分析 URL,获得主机名和端口号。
    • 浏览器发送 DNS 请求,获取该主机名对应的 IP 地址。
    • 浏览器与该 IP 地址建立 TCP 连接。
    • 浏览器发送请求报文。
    • 浏览器接受响应报文,判断是否断开连接,是长连接还是短连接。
    • 浏览器解析数据。
  • 长连接与短连接:
    • 短连接:HTTP 1.0。一次 TCP 连接只发送一次数据。Connection: Close。一个页面有 10 张图片:11 次 GET,11 次建立连接
    • 长连接:HTTP 1.1。一次 TCP 连接可以发送很多次数据。Connection: Keep-alive。一个页面有 10 张图片:11 次 GET,1 次建立连接
    • 实现长连接:由于 HTTP 1.1 已经指明是长连接了,所以浏览器在没有收到该包已经结束的标志之前,不会再次发送新的请求报文。所以需要在返回响应报文中添加一个字段:Content-Length:body的长度,以此指明当前的响应报文已经发送结束,浏览器可以发送下一个请求了。
  • GET、POST、PUT、DEL
    • GET的参数放在 url 中。
    • POST的参数放在 Body 中。

epoll 服务器

  • epoll 是当今 Linux 服务器采用的主流方式,而没有采用多进程、所线程,协程等多任务机制,也没有采用非阻塞模式。例如在 Nginx 和 Apache 等均采用 epoll 模式。
  • epoll 采用单进程实现网络异步 IO,它是一种高并发方案。epoll 相对非阻塞而言,效率提高很多。因为 epoll 采用的是 os 级的自动检测网络 IO 接口是否有数据到来,当有网络 IO 结束时会主动通知,而非非阻塞是软件级的轮询。
import select
from socket import *

s = socket(AF_INET, SOCK_STREAM)
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s.setblocking(False)
addr = ('', 8080)
s.bind(addr)
s.listen(10)

epl = select.epoll()
epl.register(s.fileno(), select.EPOLLIN) # 将socket的文件描述符进行注册

client_list = {} # 有socket可以得到fileno,但是有fileno得不到socket。所以需要一个字典来记录。
while True:
    fd_event_list = epl.poll() # poll会默认阻塞,直至有网络IO,通过时间通知的方式解阻塞
    for fd, event in fd_event_list:
        if fd == s.fileno():
            new_client, new_addr = s.accept()
            epl.register(new_client.fileno(), select.EPOLLIN) # 注册用户端
            client_list[new_client.fileno()] = new_client
            send_data = 'welcom!'
            new_client.send(send_data.encode('utf-8'))
        elif event == select.EPOLLIN:
            recv_data = client_list[fd].recv(1024)
            if recv_data:
                print(recv_data.decode('utf-8'))
            else:
                client_list[fd].close()
                epl.unregister(fd)
                client_list.pop(fd)

epl.unregister(s.fileno())
s.close()

在 Windows 下运行会提示select模块没有epoll属性,在Linux下可以正常运行。

mini-web

之前用多任务构建的都是静态服务器,客户端请求什么,服务器只能将资源直接返回。但是在实际运用中,需要根据特定的请求,然后通过比如查询数据库等得到数据,来动态构建页面返回。

静态服务器

由文本编辑器直接编辑并生成静态的 HTML 页面,如果要修改 Web 页面的内容,就需要再次编辑 HTML 源文件,早期的互联网 Web 页面就是静态的;

动态服务器

服务器本身只是资源的调用工具。无论是静态的还是动态的都另外写,使得服务器高度解耦。这时候可以使用很多框架。如DjangoFlask等很成熟的框架。

  • 服务器软件只负责调用资源:如果是动态的那就导入".php"、".py"等文件,如果是静态的那就读".html"文件等。
  • 服务器软件:ApacheNginx等。现在Nginx逐渐比较主流,
  • 自己写的框架如何让其他主流的服务器适用。所以需要有一种协议,WSGI协议
  • WSGI协议是沟通服务器和网站框架的桥梁。只要网站的框架符合 WSGI 框架,那么就能在主流服务器上部署。
import sys
import threading
import time
from socket import *

from dynamic import mini_frame


class WebServer(object):
    def __init__(self, port):
        self.ss = socket(AF_INET, SOCK_STREAM)
        self.ss.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        self.ss.bind(('', port))
        self.ss.listen(10)
        self.env = {}

    def run(self):
        print('Sever is starting......')
        while True:
            new_client, new_addr = self.ss.accept()
            print('Welcom, the client', new_addr)
            t = threading.Thread(target=self.handle_client, args=(new_client,))
            t.start()

    def handle_client(self, client):
        while True:
            recv_data = client.recv(1024)
            if recv_data:
                self.parse_env(recv_data)
                path = self.env['path']
                if not path.endswith('.py'):
                    # static file
                    header, body = self.static_file()
                else:
                    # dynamic file
                    header, body = self.dynamic_file()
            else:
                print('Client has been closed.')
                client.close() # long connection, the server doesn't close first
                break

            client.send(header)
            client.send(body)

    def parse_env(self, recv_data):
        param = recv_data.decode('utf-8').splitlines()
        param.pop() # delete the space line
        self.env['method'], self.env['path'], self.env['protocol'] = param[0].split()
        for i in range(1, len(param)):
            k, v = param[i].split(':', 1)
            self.env[k] = v.strip()

    def static_file(self):
        path = self.env['path']
        flag = True # find the file or not
        try:
            if path == '/':
                path = '/index.html' # default page is the index.html
            with open('./WSGI/static'+path, 'rb') as f:
                body = f.read()
        except Exception:
            flag = False # not find
            body = b'<h1>404, not found</h1>'

        haha = [
            'Connection: Keep-Alive',
            'Content-Length: %d' % len(body)
        ]
        if flag:
            header = 'HTTP/1.1 200 OK\r\n' + '\r\n'.join(haha) + '\r\n\r\n'
        else:
            header = 'HTTP/1.1 404 NOT FOUND\r\n' + '\r\n'.join(haha) + '\r\n\r\n'
        return header.encode('utf-8'), body

    def dynamic_file(self):
        path = self.env['path']
        header, body = mini_frame.app(path)
        return header.encode('utf-8'), body.encode('utf-8')


def main():
    # run with the port, default is 8080
    if len(sys.argv) == 1:
        port = 8080
    else:
        port = sys.argv[1]
    web = WebServer(port)
    web.run()

if __name__ == '__main__':
    main()

WSGI 协议

定义一个服务器框架的模块,类似于上面自己写的mini_web框架,但是更加强大,适合大型服务器。

  • 项目完整代码见:WSGI 框架
  • WSGI协议的本质就是连接服务器和框架。服务器有Nginx, Apache等,框架有,Flask, Django等。
  • WSGI接口的定义,写在框架中,服务器调用:
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')]) # 处理响应头
    return 'hello world' # 处理响应体
  • 实现 WSGI:
    • application(env, start_response)函数在框架中实现。
      • env为一个包含所有 HTTP 请求信息的dict对象。返回响应头。
      • start_response为一个发送 HTTP 响应的函数。返回响应体。
    • start_response(status, headers)函数在服务器实现。
      • status:状态码。
      • headers:列表,里面每一项为一个元组,里面包含keyvalue
import sys
import multiprocessing
from socket import *

class Server(object):
    def __init__(self, port, app):
        # 套接字
        self.ss = socket(AF_INET, SOCK_STREAM)
        self.ss.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 开启端口立即复用
        self.ss.bind(('', port))
        self.ss.listen(10)

        self.app = app
        self.env = {}
        self.header = ''

    def __del__(self):
        print('Sever is closing......')
        self.ss.close()

    def run(self):
        '''运行服务器
        每来一个用户就创建子进程,调用handle_client处理之。
        '''
        print('Server is starting......')
        while True:
            new_client, new_addr = self.ss.accept()
            print('Welcom, the client', new_addr)
            # 每来一个客户端就创建一个进程处理之
            p = multiprocessing.Process(target=self.handle_client, args=(new_client,))
            p.start()
            # 因为多进程是资源的深拷贝,所以新的子进程也会有一个client套接字,故该套接字可以关闭
            new_client.close()

    def handle_client(self, client):
        '''处理用户请求
        一直循环状态,直到客户端关闭
        '''
        while True:
            recv_data = client.recv(1024)
            # 收到的数据为空时表明客户端已断开连接
            if recv_data:
                self.get_env(recv_data)
                # 由于body可能是图片、视频、文本等众多格式,故统一采用"rb"格式读取,返回的是bytes
                body = self.app(self.env, self.set_header)
                client.send(self.header.encode('utf-8'))
                client.send(body)
            else:
                print('The client has been closed.')
                client.close()
                break

    def get_env(self, recv_data):
        '''解析客户端的请求头
        获取请求头中的各个字段,如protocal, path, method等
        '''
        headers = recv_data.decode('utf-8').splitlines()
        headers.pop()  # header和body之间的空
        self.env['protocal'], self.env['path'], self.env['method'] = headers[0].split()
        for i in range(1, len(headers)):
            k,v = headers[i].split(':', 1)
            self.env[k] = v.strip()

    def set_header(self, status, headers):
        '''封装框架返回的状态码和响应头
        '''
        self.header = ''
        self.header += 'HTTP/1.1 ' + status + '\r\n'
        for temp in headers:
            self.header += '%s:%s\r\n'%temp
        # 增加与服务器有关的头
        self.header += 'Server: My server\r\n'
        self.header += '\r\n' # header和body之间的空行


def main():
    # python3 web.py 8080 mini_frame:application
    try:
        port = int(sys.argv[1])
        frame_name, app_name = sys.argv[2].split(':')

        sys.path.append('./dynamic')
        frame = __import__(frame_name)
        app = getattr(frame, app_name)
    except Exception as ret:
        print(ret)
        print('run as "python3 web.py 8080 mini_frame:application"')
    else:
        server = Server(port, app)
        server.run()

if __name__ == '__main__':
    main()
使用装饰器实现路由功能

使用带参数的装饰器对每一个功能模块进行装饰,参数是请求的路径。可以使用一个url-func的字典来保存路径和路径的一一映射。使用的装饰器如下:

def route(url):
    def set_func(func):
        URL_FUNC[url] = func
        def call_func(*args, **kwargs):
            return func(*args, **kwargs)
        return call_func
    return set_func
静态、动态与伪静态
  • 静态 URL:类似域名/news/2012-2-18/110.html,称之为真静态URL,每一个网页都是一个真是的物理路径。
    • 优点:网站打开速度快,网址结构清晰。
    • 缺点:如果网站的页面过多,占用空间大且不好管理。
  • 动态 URL:类似域名/news/more.asp?id=5或者login.php?id=7,带有?号的称为动态网址,每个 URL 只是一个逻辑地址,并不是真实地存储在硬盘中。
    • 优点:适合中大型网站,修改页面很方便,因为是逻辑地址,故占用空间小。
    • 缺点:因为需要进行计算,打开速度慢于静态,但是随着技术的发展差距很小。最大的缺点是 URL 结构复杂。
  • 静态 URL 对SEO(搜索引擎的优化)较好,相同条件下,搜索引擎对静态的网页排名靠前。所以现在出现了一种伪静态 URL。
  • 伪静态 URL:类似域名/course/74.html,通过伪静态规则把动态 URL 伪装成静态 URL。
为路由支持正则表达式
  • 在实际开发过程中,url 中往往会带有很多参数,例如/add/007.html中的007就是参数。
  • 如果没有正则的话,就需要编写 N 次@route来进行添加 url 对应的函数到字典中,此时会造成字典空间的浪费。
  • 采用正则就可以很好地解决这一问题。只需要编写一次@route就可以完成多个 url,例如/add/001.html, /add/002.html等对应同一个add函数。
  • 由于装饰器的参数发生变化,所以对应的application函数调用方法也需要进行相应的改变。
@route(r'/add/(\d+)\.html')
@use_mysql
def add(ret):
    sql = 'select * from focus, info where focus.info_id=info.id and info.id=%s'
    params = [ret.group(1)]
    CS.execute(sql, params)
    data = CS.fetchall()
    if data:
        return 'already focused!'.encode('utf-8')
    else:
        sql = 'insert into focus values(default, "", %s)'
        try:
            CS.execute(sql, params)
        except Exception as ret:
            print(ret)
            CONN.rollback()
            return 'failed to add!'.encode('utf-8')
        else:
            CONN.commit()
            return 'add successfully!'.encode('utf-8')

def application(env, start_response):
    path = env['path']
    flag = True
    if path == '/':
        path = '/index.html' # default page is the index.html

    print(path)
    try:
        for url, func in URL_FUNC.items():
            ret = re.match(url, path)
            if ret:
                body = func(ret)
                break
        else:
            f = open('./static'+path, 'rb')
            body = f.read()
    except Exception as ret:
        print(ret)
        body = '<h1>404, not found</h1>'.encode('utf-8')
        status = '404 NOT FOUND'
    else:
        status = '200 OK'

    headers = [
        ('Connection', 'Keep-Alive'),
        # ('Content-Type', 'text/html'),
        ('Content-Length', '%d'%len(body))
    ]
    # Connection:Keep-Alive保持长连接,close关闭长连接。
    # Content-Type:text/html;charset=utf-8,网页显示中文,但是其他编码出问题,所以不通用。
    # Content-Length:body的长度。要实现长连接需要加上此,否则浏览器会一直转圈。

    start_response(status, headers)
    # 返回的body必须是bytes类型
    return body
url 编码问题
  • 在 url 中出现非英文(包括标点符号)时,浏览器会把字符进行编码以保证浏览器解析 url 时不会出错。如/update/033223/中国.html会变成update/033223/%E4%B8%AD%E5%9B%BD.html
  • 利用 Python 中的urllib官方库中的parse.quoteparse.unquote进行编解码。
WSGI 完整代码

以下是 WSGI 协议的框架中的完整代码:

import time
import pymysql
import re
from urllib import parse

# 路由字典,path-func
URL_FUNC = dict()
CONN = None
CS = None

# 路由装饰器
def route(url):
    def set_func(func):
        URL_FUNC[url] = func
        def call_func(*args, **kwargs):
            return func(*args, **kwargs)
        return call_func
    return set_func

# 使用数据库
def use_mysql(func):
    def call_func(*args, **kwargs):
        global CONN
        global CS
        CONN = pymysql.connect(host='192.168.56.104', port=3306, user='root', password='123456', database='stock_db', charset='utf8')
        CS = CONN.cursor()
        ret = func(*args, **kwargs)
        CS.close()
        CONN.close()
        return ret
    return call_func

@route(r'/index\.html')
@use_mysql
def index(ret):
    with open('./templates/index.html', 'rb') as f:
        content = f.read().decode('utf-8')

    # connect_to_mysql()

    sql = 'select * from info;'
    params = []
    CS.execute(sql, params)
    data = CS.fetchall()
    tr_template = '''
        <tr>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>
                <input type="button" value="添加" id="toAdd" name="toAdd" systemidvaule="%s">
            </td>
        </tr>
	'''
    haha = ''
    for info in data:
        haha += tr_template%(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7], info[0])
    content = content.replace('{%content%}', haha)

    return content.encode('utf-8')

@route(r'/center\.html')
@use_mysql
def center(ret):
    with open('./templates/center.html', 'rb') as f:
        content = f.read().decode('utf-8')

    sql = 'select code, short, chg, turnover, price, highs, note_info from info, focus where focus.info_id=info.id'
    params = []
    CS.execute(sql, params)
    data = CS.fetchall()
    tr_template = '''
        <tr>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>
                <a type="button" class="btn btn-default btn-xs" href="/update/%s.html"> <span class="glyphicon glyphicon-star" aria-hidden="true"></span> 无修改 </a>
            </td>
            <td>
                <input type="button" value="删除" id="toDel" name="toDel" systemidvaule="%s">
            </td>
        </tr>
	'''
    haha = ''
    for info in data:
        haha += tr_template%(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[0], info[0])
    content = content.replace('{%content%}', haha)

    return content.encode('utf-8')

@route(r'/login\.html')
def login(ret):
    return '<h1>login</h1>'.encode('utf-8')

@route(r'/register\.html')
def register(ret):
    return '<h1>register</h1>'.encode('utf-8')

@route(r'/add/(\d+)\.html')
@use_mysql
def add(ret):
    sql = 'select * from focus, info where focus.info_id=info.id and info.id=%s'
    params = [ret.group(1)]
    CS.execute(sql, params)
    data = CS.fetchall()
    if data:
        return 'already focused!'.encode('utf-8')
    else:
        sql = 'insert into focus values(default, "", %s)'
        try:
            CS.execute(sql, params)
        except Exception as ret:
            print(ret)
            CONN.rollback()
            return 'failed to add!'.encode('utf-8')
        else:
            CONN.commit()
            return 'add successfully!'.encode('utf-8')

@route(r'/delete/(\d+)\.html')
@use_mysql
def delete(ret):
    sql = 'delete from focus where info_id=(select id from info where code=%s)'
    params = [ret.group(1)]
    try:
        CS.execute(sql, params)
    except Exception:
        CONN.rollback()
        return 'failed to delete!'.encode('utf-8')
    else:
        CONN.commit()
        return 'delete successfully!'.encode('utf-8')

@route(r'/update/(\d+)\.html')
@use_mysql
def delete(ret):
    print(ret.group(1))
    with open('./templates/update.html', 'rb') as f:
        content = f.read().decode('utf-8')

    sql = 'select code, note_info from info, focus where info.id=focus.info_id and info.code=%s'
    params = [ret.group(1)]
    CS.execute(sql, params)
    code, info = CS.fetchone()

    content = content.replace('{%code%}', code)
    content = content.replace('{%note_info%}', info)
    return content.encode('utf-8')

@route(r'/update/(\d+)/(.*)\.html')
@use_mysql
def delete(ret):
    print(ret.group(1), ret.group(2))

    sql = 'update focus set note_info=%s where info_id=(select id from info where code=%s)'
    params = [parse.unquote(ret.group(2)), ret.group(1)] # url编码问题
    try:
        CS.execute(sql, params)
    except Exception:
        CONN.rollback()
        return 'failed to update!'.encode('utf-8')
    else:
        CONN.commit()
        return 'update successfully!'.encode('utf-8')


def application(env, start_response):
    path = env['path']
    flag = True
    if path == '/':
        path = '/index.html' # default page is the index.html

    print(path)
    try:
        for url, func in URL_FUNC.items():
            ret = re.match(url, path)
            if ret:
                body = func(ret)
                break
        else:
            f = open('./static'+path, 'rb')
            body = f.read()
    except Exception as ret:
        print(ret)
        body = '<h1>404, not found</h1>'.encode('utf-8')
        status = '404 NOT FOUND'
    else:
        status = '200 OK'

    headers = [
        ('Connection', 'Keep-Alive'),
        # ('Content-Type', 'text/html'),
        ('Content-Length', '%d'%len(body))
    ]
    # Connection:Keep-Alive保持长连接,close关闭长连接。
    # Content-Type:text/html;charset=utf-8,网页显示中文,但是其他编码出问题,所以不通用。
    # Content-Length:body的长度。要实现长连接需要加上此,否则浏览器会一直转圈。

    start_response(status, headers)
    # 返回的body必须是bytes类型
    return body

虚拟环境

在项目工程中,为了避免包的混乱和版本的冲突,可以创建一个虚拟环境。虚拟环境是 Python 解释器的副本,在虚拟环境中可以为单独的某个项目创建一个环境,保证项目包的独立和整洁。

虚拟环境的安装

  • sudo pip3 install virtualenv
  • sudo pip3 install virtualenvwarpper,在 Windows 下使用pip3 install virtualenvwrapper-win

配置环境变量

  • mkdir ~$HOME/.virtualenvs # 创建目录来存放虚拟环境
  • 打开~/.bashrc,添加如下:
    • export WORKON_HOME=$HOME/.virtualenvs
    • source /usr/local/bin/virtualenvwarpper.sh
  • 加载环境变量:source ~/.bashrc
  • 第一行:virtualenvwrapper 存放虚拟环境目录
     第二行:virtrualenvwrapper 会安装到 python 的 bin 目录下,所以该路径是 python 安装目录下 bin/virtualenvwrapper.sh
  • 报错:/usr/bin/python: No module named virtualenvwrapper
    • 原因是系统同时安装了python2python3,而virtualenvwrapper默认选用的是python,只要修改环境变量或者更改默认python版本即可。
    • export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3

创建虚拟环境

  • mkvirtualenv Flask_py
  • workon Flask_py
  • deactive Flask_py

安装依赖包和导出依赖包

  • 安装依赖包:pip install -r requirements.txt
  • 导出依赖包:pip freeze > requirements.txt
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值