软件开发架构
- 服务端:24小时不间断提供服务
- 客户端:需要使用服务时,发送请求连接服务端,并享受服务
C/S架构
client:客户端
server:服务端
- 优点:占用网络资源少,软件使用稳定
- 缺点:服务端更新后,客户端也跟着更新;需要使用多个软件,需要下载多个客户端
B/S架构
browser:浏览器(客户端)
server:服务端
- 优点:以浏览器充当客户端,无需下载多个软件、无需更新,直接访问
- 缺点:消耗网络资源大,网络不稳定会影响用户使用
网络编程
互联网协议OSI七层协议
应用层
表示层
会话层
传输层
网络层
数据链路层
物理连接层
一、物理连接层
物理连接层功能:基于电信号发送二进制数据
顾名思义就是一些物理的连接,比如网线、路由器等, 间传的是电信号,即010101...这些二进制位。
二、数据链路层
数据链路层功能:定义了电信号的分组
以太网协议
数据链路层是来对电信号来做分组的。为了统一标准,就有了以太网协议ethernet
MAC地址
每一台连接网线的电脑都必须要由一块 "网卡"。
- 网卡由不同厂商生产的,每块网卡都会有世界上独一无二12位的编号 "mac"地址,长度为48位2进制,通常由 12位16进制数表示(前六位是厂商编号,后六位是流水线号)
广播
使用交换机可以让多台电脑连接到一起,基于以太网协议和MAC地址发送数据。
-
单播
单播是一对一发送消息
-
广播
在同一个局域网内,可以一对多发送消息
广播的弊端:广播风暴;不能夸局域网通信
三、网络层
网络层功能:靠mac地址广播获取信息数据,并不合适,于是有了网络层,引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址
IP
IP地址功能:用于标识唯一的一台计算机(局域网)的地址。
使用的是点分十进制
最小值: 0.0.0.0
最大值: 255.255.255.255
ARP协议
arp协议功能:广播的方式发送数据包,获取目标主机的Mac地址并解析
四、传输层
传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为系统占用端口
- 有了Mac地址+IP地址+端口,我们就能确定世界上独一无二的一台计算机上的应用程序
TCP协议
TCP协议特点:TCP协议是流式协议;通信必须建立双向通道;接收到消息后一定会确认收到信息
双向通道的建立 -- 三次握手,四次挥手
双向通道特点:客户端往服务端发送请求获取数据,服务端务必返回数据,客户端确认收到,反则会反复发送,一直到某个时间段内,会停止发送
- 三次握手建立连接
- 客户端往服务端发送请求建立通道
- 服务端要确认客户端的请求,并且往客户端发送确认请求
- 客户端接收到服务端建立连接的请求,并返回确认
- 三次握手后 才能成功建立双向通道
- 四次挥手断连接
- 客户端向服务端发送“断开连接”请求
- 服务端返回“确认收到”信息
- 服务端再次发送“确认断开连接”请求
- 客户端返回“确认断开连接”信息
- 四次挥手后 最终确认断开连接
UDP协议
UDP协议特点:
- 数据不安全
2) 不需要建立双向通道
3) 传输速度快
4) 不会有粘包问题
5) 客户端发送数据,不需要服务端确认收到,爱收不收
五、应用层
应用层功能:规定应用程序的数据格式,因为数据多种多样,必须规定好数据的组织形式。
应用层协议:http、ftp等...
socket模块
socket用来写套接字客户端与服务端的模块,内部帮我们封装好了7层协议需要做的事情
socket套接字模板
# 服务端
import socket
# 默认指定TCP协议
server = socket.socket()
server.bind(
# ip + port
('127.0.0.1', 9527)
)
# 开机,等待接听
server.listen(5) # listen(5) 半连接池
# 监听是否有消息
# conn: 相当于服务端往客户端挖的管道
conn, addr = server.accept()
print(addr)
data = conn.recv(1024).decode('utf-8') # 可以接收1024字节数据
print(data)
# 发送消息
conn.send(b'hello xiao tank')
# 通道关闭
conn.close()
server.close()
# 客户端
import socket
client = socket.socket()
# 往服务端拨号
# client: 相当于客户端往服务端挖的管道
client.connect(
# ip + port: 寻找服务端
('127.0.0.1', 9527)
)
# 客户端向服务端说话
client.send('你好'.encode('utf-8'))
data = client.recv(1024)
print(data)
# 关闭连接
client.close()
subprocess模块
用来通过代码往cmd创建一个管道,并且发送命令和接收cmd返回的结果
# subprocess的简单使用
import subprocess
# 参数的固定写法
obj = subprocess.Popen(
'cmd命令',
shell=True,
# 接收正确结果
stdout=subprocess.PIPE,
# 接收错误结果
stderr=subprocess.PIPE
)
# 接收正确和错误的信息
success = obj.stdout.read()
error = obj.stderr.read()
# 拼接后输出
msg = success + error
print(msg)
粘包问题
只有TCP有粘包现象,UDP永远不会粘包
粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
TCP协议会将多次连续发送数据量小、并且时间间隔短的数据一次性打包发送,而导致粘包问题
struct模块解决粘包问题
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
- 下面是解决粘包问题,传输大文件的例子
import socket
import json
import struct
server = socket.socket()
server.bind(('127.0.0.1', 9527))
server.listen(5)
while True:
conn, addr = server.accept()
try:
# 先接收字典报头
headers = conn.recv(4)
# 解包获取字典真实数据长度
data_len = struct.unpack('i', headers)[0]
# 获取字典真实数据
bytes_data = conn.recv(data_len)
# 反序列得到字典
back_dic = json.loads(bytes_data.decode('utf-8'))
print(back_dic)
# 拿到字典的文件名,文件大小
file_name = back_dic.get('file_name')
file_size = back_dic.get('file_size')
init_data = 0
# 1.以文件名打开文件,准备写入
with open(file_name, 'wb') as f:
# 一点一点接收文件,并写入
while init_data < file_size:
data = conn.recv(1024)
# 2.开始写入视频文件
f.write(data)
init_data += len(data)
print(f'{file_name}接收完毕!')
except Exception as e:
print(e)
break
conn.close()
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 9527))
# 1.打开一个视频文件,获取视频数据大小
with open(r'D:aaa.mp4', 'rb') as f:
movie_bytes = f.read()
# 关闭文件
# 2.为视频文件组织一个字典,字典内有视频的名称,视频大小
send_dic = {
'file_name': 'aaa.mp4',
'file_size': len(movie_bytes) # 10G
}
# 3.先打包字典,发送headers报头,再发送真实字典数据
json_data = json.dumps(send_dic)
bytes_data = json_data.encode('utf-8')
headers = struct.pack('i', len(bytes_data))
# 发送报头
client.send(headers)
# 发送真实字典数据
client.send(bytes_data)
# 4.接着发送真实视频文件数据
init_data = 0
num = 1
with open(r'D:aaa.mp4', 'rb') as f:
while init_data < len(movie_bytes):
# 最后一次获取,有多少拿多少
send_data = f.read(1024)
print(send_data, num)
num += 1
# 每次发送1024数据
client.send(send_data)
# 为初始发送数据 + 已发送数据的长度
init_data += len(send_data)
socketserver模块
python内置模块,可以简化socket套接字服务端的代码。
- 简化TCP与UDP服务端代码
- 必须要创建一个类
# socketserver 服务端
import socketserver
# 定义类
# TCP: 必须继承BaseRequestHandler类
class MyTcpServer(socketserver.BaseRequestHandler):
# 必须重写父类的handle, 当客户端连接时会调用该方法
def handle(self):
print(self.client_address)
while True:
try:
# 1.接收消息
# request.recv(1024) == conn.recv(1024)
data = self.request.recv(1024).decode('utf-8')
self.request.send(send_msg.encode('utf-8'))
except Exception as e:
print(e)
break
if __name__ == '__main__':
# socketserver.TCPServer只能有一个服务端 服务 一个客户端
# server = socketserver.TCPServer(
# ('127.0.0.1', 8888), MyTcpServer
# )
# ThreadingTCPServer: 有多个服务端 服务 客户端
server = socketserver.ThreadingTCPServer(
('127.0.0.1', 8888), MyTcpServer
)
# 永久执行服务
server.serve_forever()
# 客户端
import socket
client = socket.socket()
client.connect(
('127.0.0.1', 8888)
)
while True:
send_msg = input('客户端: ')
client.send(send_msg.encode('utf-8'))
back_msg = client.recv(1024)
print(back_msg.decode('utf-8'))
并发编程
多道技术
-
空间上的复用
一个cpu可以提供给多个用户去使用
-
时间上的复用
遇到IO操作就会切换程序
程序占用CPU时间过长切换
并发和并行
并发: 看起来像同时运行,实际上是使用多道技术,不断的切换,保存状态
并行: 真正意义上的同时运行,在多核的情况下
进程
进程是资源单位,每创建一个进程都会生成一个名称空间,占用内存资源
程序与进程的区别
程序就是一堆代码
进程就是一堆代码运行的过程
进程调度
当下操作系统的进程调度大多是 时间片轮转法加分级反馈队列
时间片轮转法
10个进程,将固定时间,等分成10份时间片,分配给每一个进程
分级反馈队列
将执行优先分为多层级别
一级:优先级最高
二级:优先级第二
以此类推....
进程的三种状态
-
就绪态:
所有进程创建时都会进入就绪态,准备调度
-
运行态:
调度后的进程,进入运行态
-
阻塞态:
凡是遇到IO操作的进程,都会进入阻塞态
若IO结束,必须重新进入就绪态
同步和异步
指的是提交任务的方式
- 同步:若有两个任务需要提交,在提交第一个任务时必须等待该任务结束后才能继续提交执行第二个任务
- 异步:若有两个任务需要提交,在提交第一个任务时不需要等待
阻塞和非阻塞
-
阻塞
阻塞态,遇到IO一定会阻塞
-
非阻塞:
就绪态,运行态
创建进程的两种方式
# 一:
p = Process(target=任务, args=(任务的参数, ))
p.daemon = True # 必须放在start()前,否则报错
p.start() # 向操作系统提交创建进程的任务
p.join() # 向操作系统发送请求, 等所有子进程结束,父进程再结束
# 二:
class MyProcess(Process):
def run(self): # self == p
# 任务的过程
p = MyProcess()
p.daemon = True # 必须放在start()前,否则报错
p.start() # 向操作系统提交创建进程的任务
p.join() # 向操作系统发送请求, 等所有子进程结束,父进程再结束
回收进程的两种条件
- join,可以回收子进程与主进程
- 主进程正常结束,子进程与主进程也会被回收
僵尸进程与孤儿进程
僵尸进程
指的是子进程已经结束,但pid号还存在,未销毁
缺点:占用pid号,占用操作系统资源
孤儿进程
指的是子进程还在执行,但父进程意外结束
操作系统优化机制:
提供一个福利院,帮你回收没有父亲的子进程
守护进程
指的是主进程结束后,该主进程产生的所有子进程跟着结束,并回收
互斥锁
将并发变成串行,牺牲执行效率,保证数据安全
# 互斥锁的简单使用
from multiprocessing import Lock
mutex = Lock()
# 加锁
mutex.acquire()
# ...修改数据的代码
# 释放锁
mutex.release()
队列
队列是先进先出(FIFO)
相当于内存中产生一个队列空间,可以存放多个数据,先进去的数据排在前面
from multiprocessing import Queue
# 设置五个数据
q = Queue(5)
# 添加数据,若队列添加数据满了,则等待
q.put()
# 添加数据,若队列添加数据满了,直接报错
q.put_nowait()
# 获取队列中的数据
q.get() # 若队列中没数据,会卡住等待
q.get_nowait() # 若队列中没数据,会直接报错
堆栈
堆栈是先进后出 (LIFO)
IPC进程间通信
进程间数据时相互隔离的,若想实现进程间通信,可以利用队列
生产者与消费者模型
生产者:生产数据的
消费者:使用数据的
在程序中:通过队列,生产者把数据添加队列中,消费者从队列中获取数据
这样为了保证 供需平衡
线程
什么是线程
线程和进程都是虚拟单位,目的是为了更好的描述某件事物
- 进程:资源单位
- 线程:执行单位
使用线程的优点
节约资源开销
注意:线程间数据是共享的;线程不能实现并行,线程只能实现并发,进程可以实现并行
进程和线程的优缺点
- 进程
- 优点:多核下可以并行执行;计算密集型程序 使用进程可以提高效率
- 缺点:开销资源远高于线程
- 线程
- 优点:占用资源远比进程小;IO密集型程序 使用线程可以提高效率
- 缺点:无法利用多核优势
GIL全局解释器锁
只有Cpython才有自带一个GIL全局解释器锁,因为CPython的内存线程不是安全的
- GIL本质上是一个互斥锁
- GIL是为了阻止同一个进程内多个线程同时执行(并行)
- GIL的存在就是为了保证线程安全
注意:多个线程过来执行,一旦遇到IO操作,就会立马释放GIL解释器锁,交给下一个先进来的线程
多线程的好处
- 多线程:
IO密集型,提高效率 - 多进程
计算密集型,提高效率
死锁现象
死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程 ,解决方式就是递归锁
递归锁
解决死锁现象
mutex = Lock() # 只能引用1次
mutex1, mutex2 = RLock() # 可以引用多次
+1, 只要这把锁计数为0释放该锁, 让下一个人使用, 就不会出现死锁现象.
信号量
信号量也是一把锁, 可以让多个任务一起使用
互斥锁:
只能让一个任务使用
信号量:
可以让多个任务一起使用.
sm = Semaphore(5) 可以让5个任务使用
线程队列
使用场景:若线程间数据不安全情况下使用线程队列, 为了保证线程间数据的安全.
使用方式import queue
-
FIFO: 先进先出队列
queue.Queue()
-
LIFO: 后进先出队列
queue.LifoQueue()
-
优先级队列:
根据数字大小判断,判断出队优先级,进队数据是无序的
queue.PriorityQueue()
event事件
可以控制线程的执行,让一些线程控制另一些线程的执行.
e = Event()
-
线程1
e.set()
给线程2发送信号,让他执行 -
线程2
e.wait()
等待线程1的信号进程池和线程池
为了控制进程/线程创建的数量,保证了硬件能正常运行
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
pool1 = ProcessPoolExecutor() # 默认CPU个数
pool2 = ThreadPoolExecutor() # CPU个数 * 5
pool3 = ProcessPoolExecutor(100) # 100个
pool4 = ThreadPoolExecutor(200) # 200个
# 将函数地址的执行结果,给回调函数
pool4.submit(函数地址, 参数).add_done_callback(回调函数地址)
# 回调函数(必须接收一个参数res):
# 获取值
res2 = res.result()
协程
什么是协程
- 进程:资源单位
- 线程:执行单位
- 协程:在单线程下实现并发
注意:协程不是操作系统资源,他是程序起的名字,为了让单线程能实现并发
协程的目的
单线程下实现并发,节省资源,通过手动模拟操作系统“多道技术”,实现 切换+保存状态
协程的优缺点
- IO密集型下:
协程有优势 - 计算密集型下:
进程有优势
协程的实现
-
手动实现切换 + 保存状态
yield保存
next() 切换
-
yield不能监听IO操作的任务,gevent来实现监听IO操作
gevent模块
from gevent import monkey
monkey.patch_all() # 可以监听该程序下所有的IO操作
import time
from gevent import spawn, joinall # 用于做切换 + 保存状态
def func1():
print('1')
# IO操作
time.sleep(1)
def func2():
print('2')
time.sleep(3)
def func3():
print('3')
time.sleep(5)
start_time = time.time()
s1 = spawn(func1)
s2 = spawn(func2)
s3 = spawn(func3)
# 必须传序列类型
joinall([s1, s2, s3])
end_time = time.time()
print(end_time - start_time)
IO模型
- 阻塞IO
- 非阻塞IO
- 多路复用IO
- 异步IO