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
实现。更一般地有封装yeild
的greenlet
和gevent
模块,目前常用的是通过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
。
- Navicat 软件。(Windows 主机登录 Ubuntu 虚拟机的 MySQL 失败:
数据库理论
数据库基础
- 现代化记录数据的方式有文件和数据库等,但是文件对于容量很大时效率很低且不易扩展。数据库是长期存储在计算机内,有组织、可共享的大量数据的集合。
- 数据库是一个特殊的文件,冗余低、速度快, 数据库一般使用 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()
才释放锁。
- 事务的四大特性(ACID):原子性、一致性、隔离型、永久性。
- 索引:当数据库中数据量很大时,查找数据会变得很慢。索引是一种特殊的文件(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.log
和server-id=1
:sudo 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
。记录下File
和Position
字段,用于配置从数据库。
- 编辑设置
- 配置从数据库:
- 编辑设置 mysqld 的配置文件,设置
server-id=2
:sudo 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
- 编辑设置 mysqld 的配置文件,设置
- 备份主数据库的数据:
命令行命令
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
。 - 分组配合
where
和having
:select 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 主机的端口,默认是 3306database
:数据库的名称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的长度
,以此指明当前的响应报文已经发送结束,浏览器可以发送下一个请求了。
- 短连接:HTTP 1.0。一次 TCP 连接只发送一次数据。
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 页面就是静态的;
动态服务器
服务器本身只是资源的调用工具。无论是静态的还是动态的都另外写,使得服务器高度解耦。这时候可以使用很多框架。如Django
、Flask
等很成熟的框架。
- 服务器软件只负责调用资源:如果是动态的那就导入
".php"、".py"
等文件,如果是静态的那就读".html"
文件等。 - 服务器软件:
Apache
、Nginx
等。现在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
:列表,里面每一项为一个元组,里面包含key
和value
。
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.quote
和parse.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
- 原因是系统同时安装了
python2
和python3
,而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