并发网络编程
好处
建立了统一的通信标准,万物互联
降低开发难度,每层功能明确,各司其职
七层模型实际规定了每一层的任务,该完成什么事情
TCP/IP模型
七层模型过于理想,结构细节太复杂
在工程中应用实践难度大
实际工作中以TCP/IP模型为工作标准流程
注:应用层功能包含,规定应用功能,压缩优化加密,选择传输服务
网络协议
什么是网络协议:在网络数据传输中,都遵循的执行规则。
网络协议实际上规定了每一层在完成自己的任务时应该遵循什么规范。
需要应用工程师做的工作 : 编写应用ss功能,明确对方地址,选择传输服务。
IP地址
IP地址 : 即在网络中标识一台计算机的地址编号。
IP地址分类
IPv4 特点(2^32)
IPv6 特点(了解)(2^128)
IP地址相关命令
公网IP和内网IP
端口号
服务端(Server):服务端是为客户端服务的,服务的内容诸如向客户端提供资源,保存客户端数据,处理客户端请求等。
客户端(Client) :也称为用户端,是指与服务端相对应,为客户提供一定应用功能的程序,我们平时使用的手机或者电脑上的程序基本都是客户端程序。
sockfd=socket.socket(socket_family,socket_type,proto=0)
功能:创建套接字
参数:socket_family 网络地址类型 AF_INET表示ipv4,AF_INET6表示ipv6
socket_type 套接字类型 SOCK_DGRAM 表示udp套接字 (也叫数据报套接字)
proto 通常为0 选择子协议
返回值: 套接字对象
eg:
#创建一个udp套接字:
import socket
udp_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sockfd.bind(addr)
功能: 绑定本机网络地址
参数: 二元元组 (ip,port) ('0.0.0.0',8888)
eg:udp_socket.bind(('176.233.29.14',8888))
data,addr = sockfd.recvfrom(buffersize) #阻塞函数
功能: 接收UDP消息
参数: 每次最多接收多少字节
返回值: data 接收到的内容
addr 消息发送方地址
n = sockfd.sendto(data,addr)
功能: 发送UDP消息
参数: data 发送的内容 bytes格式
addr 目标地址
返回值:发送的字节数
sockfd.close()
功能:关闭套接字
面向连接的传输服务
三次握手(建立连接)
四次挥手(断开连接)
sockfd=socket.socket(socket_family,socket_type,proto=0)
功能:创建套接字
参数:socket_family 网络地址类型 AF_INET表示ipv4
socket_type 套接字类型 SOCK_STREAM 表示tcp套接字 (也叫流式套接字)
proto 通常为0 选择子协议
返回值: 套接字对象
sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小
connfd,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求
返回值: connfd 客户端连接套接字
addr 连接的客户端地址
data = connfd.recv(buffersize) #阻塞函数
功能 : 接受客户端消息
参数 :每次最多接收消息的大小
返回值: 接收到的内容
n = connfd.send(data)
功能 : 发送消息
参数 :要发送的内容 bytes格式
返回值: 发送的字节数
sockfd.connect(server_addr)
功能:连接服务器
参数:元组 服务器地址
注意: 防止两端都阻塞,recv send要配合
tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
tcp连接中如果一端已经不存在,仍然试图通过send向其发送数据则会产生BrokenPipeError
一个服务端可以同时连接多个客户端,也能够重复被连接
tcp粘包问题
传输特征
套接字编程区别
使用场景
源端口和目的端口 各占2个字节,分别写入源端口和目的端口。1字节=8bit
序号 占4字节。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号是301,而接待的数据共有100字节。这就表明本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。
确认号 占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
确认ACK(ACKnowledgment) 仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。
同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。
终止FIN(FINis,意思是“完”“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
多任务
即操作系统中可以同时运行多个任务。比如我们可以同时挂着qq,听音乐,同时上网浏览网页。这是我们看得到的任务,在系统中还有很多系统任务在执行,现在的操作系统基本都是多任务操作系统,具备运行多任务的能力。
计算机原理
CPU:计算机硬件的核心部件,用于对任务进行执行运算。
操作系统调用CPU执行任务
cpu轮训机制 : cpu都在多个任务之间快速的切换执行,切换速度在微秒级别,其实cpu同时只执行一个任务,但是因为切换太快了,从应用层看好像所有任务同时在执行。(cpu在同一时刻只执行一个任务)
多核CPU:现在的计算机一般都是多核CPU,比如四核,八核,我们可以理解为由多个单核CPU的集合。这时候在执行任务时就有了选择,可以将多个任务分配给某一个cpu核心,也可以将多个任务分配给多个cpu核心,操作系统会自动根据任务的复杂程度选择最优的分配方案。
什么是多任务编程
多任务编程即一个程序中编写多个任务,在程序运行时让这多个任务一起运行,而不是一个一个的顺次执行。
比如微信视频聊天,这时候在微信运行过程中既用到了视频任务也用到了音频任务,甚至同时还能发消息。这就是典型的多任务。而实际的开发过程中这样的情况比比皆是。
多任务意义
定义: 程序在计算机中的一次执行过程。
进程状态
三态
就绪态 : 进程具备执行条件,等待系统调度分配cpu资源
运行态 : 进程占有cpu正在运行
等待态 : 进程阻塞等待,此时会让出cpu
五态 (在三态基础上增加新建和终止)
新建 : 创建一个进程,获取资源的过程
终止 : 进程结束,释放资源的过程
进程命令
使用模块 : multiprocessing
创建流程
【1】 将需要新进程执行的事件封装为函数
【2】 通过模块的Process类创建进程对象,关联函数
【3】 可以通过进程对象设置进程信息及属性
【4】 通过进程对象调用start启动进程
【5】 通过进程对象调用join回收进程资源
主要类和函数使用
Process()
功能 : 创建进程对象
参数 : target 绑定要执行的目标函数
args 元组,用于给target函数位置传参
kwargs 字典,给target函数键值传参
p.start()
功能 : 启动进程
注意 : 启动进程此时target绑定函数开始执行,该函数作为新进程执行内容,此时进程真正被创建
p.join([timeout]) #阻塞函数
功能:阻塞等待回收进程
参数:超时时间
进程执行现象理解 (难点)
#子进程拷贝父进程的全部内存空间,父子进程谁先执行不确定
进程对象属性
os.getpid()
功能: 获取一个进程的PID值
返回值: 返回当前进程的PID
os.getppid()
功能: 获取父进程的PID号
返回值: 返回父进程PID
sys.exit(info)
功能:退出进程
参数:字符串 表示退出时打印内容
孤儿和僵尸
孤儿进程 : 父进程先于子进程退出,此时子进程成为孤儿进程。
僵尸进程 : 子进程先于父进程退出,父进程又没有处理子进程的退出状态,此时子进程就会称为僵尸进程。
特点: 僵尸进程虽然结束,但是会存留部分进程信息资源在内存中,大量的僵尸进程会浪费系统的内存资源。
如何避免僵尸进程产生
使用join()回收
在父进程中使用signal方法处理(只适用于Linux,unix)
from signal import *
signal(SIGCHLD,SIG_IGN)
进程的基本创建方法将子进程执行的内容封装为函数。如果我们更热衷于面向对象的编程思想,也可以使用类来封装进程内容。
创建步骤
【1】 继承Process类
【2】 重写__init__
方法添加自己的属性,使用super()加载父类属性
【3】 重写run()方法
使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
【3】 调用join回收进程
"""
自定义进程类
"""
from multiprocessing import Process
# 自定义类
class MyProcess(Process):
def __init__(self,val):
self.val = val
super().__init__() # 加载父类属性
def fun1(self):
print("步骤1:假设很复杂",self.val)
def fun2(self):
print("步骤2:假设也很复杂",self.val)
# 重写run,将其作为一个新进程的执行内容
def run(self):
self.fun1()
self.fun2()
process = MyProcess(3)
process.start() # 启动进程 执行run() #调用start自动执行run方法
process.join()
"""
练习1 : 编写一个程序
* 使用单进程 求100000以内质数之和 记录所用时间
* 使用4个进程,将100000拆分为4份,分别求每部分中质数之和 记录时间
* 使用10个进程,将100000拆分为10份,分别求每部分中质数之和 记录时间
"""
import time
from multiprocessing import Process
# 求函数运行时间
def timeis(f):
def wrapper(*args,**kwargs):
start_time = time.time()
res = f(*args,**kwargs)
end_time = time.time()
print("执行时间:",end_time - start_time)
return res
return wrapper
# 判断一个数是不是质数
def isPrime(n):
if n <= 1:
return False
for i in range(2,n):
if n % i == 0:
return False
return True
# @timeis
# def no_process():
# prime = []
# for i in range(1,100001):
# if isPrime(i):
# prime.append(i) # 将质数加入列表
# print(sum(prime))
#
# no_process() # 执行时间: 25.926925897598267
class Prime(Process):
def __init__(self,begin,end):
"""
:param begin: 开始数值
:param end: 结尾数值
"""
self.begin = begin
self.end = end
super().__init__()
def run(self):
prime = []
for i in range(self.begin,self.end):
if isPrime(i):
prime.append(i)
print(sum(prime))
@timeis
def process_4():
jobs = []
for i in range(1,100001,25000):
p = Prime(i,i+25000)
jobs.append(p)
p.start()
for i in jobs:
i.join()
process_4() # 执行时间: 15.002186059951782
必要性
【1】 进程的创建和销毁过程消耗的资源较多
【2】 当任务量众多,每个任务在很短时间内完成时,需要频繁的创建和销毁进程。此时对计算机压力较大
【3】 进程池技术很好的解决了以上问题。
原理
创建一定数量的进程来处理事件,事件处理完进程不退出而是继续处理其他事件,直到所有事件全都处理完毕统一销毁。增加进程的重复利用,降低资源消耗。
进程池实现
from multiprocessing import Pool
Pool(processes)
功能: 创建进程池对象
参数: 指定进程数量,默认根据系统自动判定(默认跟系统内核数量保持一致)
pool.apply_async(func,args,kwds)
功能: 使用进程池执行 func事件
参数: func 事件函数
args 元组 给func按位置传参
kwds 字典 给func按照键值传参
pool.close()
功能: 关闭进程池
pool.join()
功能: 回收进程池中进程
eg:# 如果父进程退出,进程池自动销毁;关闭进程池,不能添加新的事件;事件函数声明要在创建进程池之前
"""
进程池使用示例
* 如果父进程退出,进程池自动销毁
* 事件函数的声明要在创建进程池之前
"""
from multiprocessing import Pool
from time import sleep,ctime
# 进程池执行事件
def worker(msg,sec):
print(ctime(),"---",msg)
sleep(sec)
# 创建进程池
pool=Pool(4)
# 向进程池中加入事件
for i in range(10) :
msg="tedu-%d"%i
pool.apply_async(func=worker,args=(msg,2)) # 事件开始执行
pool.close() # 关闭进程池,不能添加新的事件
pool.join() # 等事件都执行完,回收进程池
"""
练习2 : 拷贝一个目录
编写程序完成,将一个文件夹拷贝一份
* 假设文件夹中只有普通文件
* 将每个文件的拷贝作为一个拷贝事件
* 使用进程池完成事件
提示 : os.mkdir('name')
"""
from multiprocessing import Pool
import os,sys
# 拷贝一个文件
def copy(file,old_folder,new_folder):
fr = open(old_folder+'/'+file,'rb')
fw = open(new_folder+'/'+file,'wb')
while True:
data = fr.read(1024)
if not data:
break
fw.write(data)
fr.close()
fw.close()
# 使用进程池
def main():
old_folder = input("你要拷贝的目录:")
new_folder = old_folder + "-备份"
try:
os.mkdir(new_folder)
except:
sys.exit("该目录已存在")
# 创建进程池
pool = Pool()
# 遍历目录,确定要拷贝的文件
for file in os.listdir(old_folder):
pool.apply_async(func=copy,args=(file,old_folder,new_folder))
pool.close()
pool.join()
if __name__ == '__main__':
main()
必要性: 进程间空间独立,资源不共享,此时在需要进程间数据传输时就需要特定的手段进行数据通信。
常用进程间通信方法:消息队列,套接字等。
消息队列使用
通信原理: 在内存中开辟空间,建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信。
实现方法
from multiprocessing import Queue
q = Queue(maxsize=0)
功能: 创建队列对象
参数:最多存放消息个数
返回值:队列对象
q.put(data,[block,timeout])
功能:向队列存入消息
参数:data 要存入的内容
block 设置是否阻塞 False为非阻塞(默认情况下为阻塞)
timeout 超时检测
q.get([block,timeout])
功能:从队列取出消息
参数:block 设置是否阻塞 False为非阻塞(默认情况下为阻塞)
timeout 超时检测
返回值: 返回获取到的内容
q.full() 判断队列是否为满
q.empty() 判断队列是否为空
q.qsize() 获取队列中消息个数
q.close() 关闭队列
**群聊聊天室 **
功能 : 类似qq群功能
【1】 有人进入聊天室需要输入姓名,姓名不能重复
【2】 有人进入聊天室时,其他人会收到通知:xxx 进入了聊天室
【3】 一个人发消息,其他人会收到:xxx : xxxxxxxxxxx
【4】 有人退出聊天室,则其他人也会收到通知:xxx退出了聊天室
【5】 扩展功能:服务器可以向所有用户发送公告:管理员消息: xxxxxxxxx
什么是线程
【1】 线程被称为轻量级的进程,也是多任务编程方式
【2】 也可以利用计算机的多cpu资源
【3】 线程可以理解为进程中再开辟的分支任务
线程特征
【1】 一个进程中可以包含多个线程
【2】 线程也是一个运行行为,消耗计算机资源
【3】 一个进程中的所有线程共享这个进程的资源
【4】 多个线程之间的运行同样互不影响各自运行
【5】 线程的创建和销毁消耗资源远小于进程
*没有线程的进程成为单线程
创建方法
【1】 创建线程对象
from threading import Thread
t = Thread()
功能:创建线程对象
参数:target 绑定线程函数
args 元组 给线程函数位置传参
kwargs 字典 给线程函数键值传参
【2】 启动线程
t.start()
【3】 回收线程
t.join([timeout]) #立即将线程回收,释放线程所占用的进程资源
#timeout :表示超时时间
"""
无参数线程创建示例
*在分支线程中修改全局变量,原变量随之改变
"""
import os
import threading
import time
#全局变量
a=1
# 线程函数
def music():
for i in range(3):
time.sleep(2)
print(os.getpid(),"hahaha")
global a
a=122
#创建线程对象
t=threading.Thread(target=music)
#启动线程
t.start()
for i in range(3):
time.sleep(4)
print(os.getpid(),"葫芦娃")
print(a) #122
# 回收线程
t.join()
"""
含参数多线程创建示例
"""
from threading import Thread
import time
# 线程函数
def fun(sec,name):
print("含有参数的线程")
time.sleep(sec)
print("%s hahaha"%name)
jobs=[]
#创建多个线程对象
for i in range(6):
t=Thread(target=fun,args=(2,),kwargs={"name":"T%d"%i})
jobs.append(t)
#启动线程
t.start()
# 回收线程
[x.join() for x in jobs]
线程对象属性
t.setName() 设置线程名称
t.getName() 获取线程名称
t.is_alive() 查看线程是否在生命周期
t.setDaemon() 设置daemon属性值
*t.setDaemon(True) #参数为True时表示分支线程随之退出
t.isDaemon() 查看daemon属性值
"""
练习1: 模拟售票系统
现有500 张票 记为 T1--T500 放在一个列表
有10个窗口一起买票 记为 w1 -- w10 ,每张票卖出需要0.1秒
创建10个线程 模拟10个窗口,票的售出顺序必须是1--500
每张票卖出时 打印 w2----T203
编程创建10个 线程模拟这个过程
"""
from threading import Thread
from time import sleep
# 存储票
ticket = ["T%d" % x for x in range(1, 501)]
# 模拟每个窗口的买票情况 w 窗口编号
def sell(w):
while ticket:
print("%s --- %s"%(w,ticket.pop(0)))
sleep(0.1)
jobs = []
for i in range(1,11):
t = Thread(target=sell,args=("w%d"%i,))
jobs.append(t)
t.start()
[i.join() for i in jobs] # 回收
创建步骤
【1】 继承Thread类
【2】 重写__init__
方法添加自己的属性,使用super()加载父类属性
【3】 重写run()方法
使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
【3】 调用join回收线程
"""
自定义线程类演示
"""
from threading import Thread
import time
class MyThread(Thread):
def __init__(self,song):
self.song = song
super().__init__() # 加载父类属性
def run(self):
for i in range(3):
print("Playing %s:%s"%(self.song,time.ctime()))
time.sleep(2)
t = MyThread("小幸运")
t.start() # run作为线程执行
t.join()
线程通信方法: 线程间使用全局变量进行通信
共享资源争夺
同步互斥机制
同步 : 同步是一种协作关系,为完成操作,多进程或者线程间形成一种协调,按照必要的步骤有序执行操作。
互斥 : 互斥是一种制约关系,当一个进程或者线程占有资源时会进行加锁处理,此时其他进程线程就无法操作该资源,直到解锁后才能操作。
线程Event
from threading import Event
e = Event() 创建线程event对象
e.wait([timeout]) 阻塞等待e被set #设置阻塞
e.set() 设置e,使wait结束阻塞 #打开,结束阻塞
e.clear() 使e回到未被设置状态
e.is_set() 查看当前e是否被设置
"""
event 线程同步互斥演示
"""
from threading import Thread,Event
msg = None # 线程通信
e = Event() # event对象
def 杨子荣():
print("杨子荣前来拜山头")
global msg
msg = "天王盖地虎"
e.set() # 打开阻塞
t = Thread(target=杨子荣)
t.start()
# 主线程中认证
print("说对口令才是自己人")
e.wait() # 验证前阻塞
if msg == "天王盖地虎":
print("宝塔镇河妖")
print("确认过眼神,你是对的人")
else:
print("打死他.....无情啊.....")
t.join()
from threading import Lock
lock = Lock() 创建锁对象
lock.acquire() 上锁 如果lock已经上锁再调用会阻塞
lock.release() 解锁
with lock: 上锁
...
...
with代码块结束自动解锁
"""
线程同步互斥 Lock方法
"""
from threading import Thread,Lock
lock = Lock() # 创建锁
# 共享资源
a = b = 0
def value():
while True:
lock.acquire() # 上锁
if a != b:
print("a = %d,b = %d"%(a,b))
lock.release() # 解锁
t = Thread(target=value)
t.start()
while True:
lock.acquire() # 上锁
a += 1
b += 1
lock.release() # 解锁
t.join()
"""
练习1: 一个线程打印1--52 另一个线程打印A--Z
两个线程一起启动,要求打印的结果
12A34B56C.....5152Z
提示: 使用同步互斥方法控制线程执行
程序中不一定只有一个锁
"""
from threading import Thread,Lock
lock1 = Lock()
lock2 = Lock()
def print_num():
# 每次循环打印2个数字
for i in range(1,53,2):
lock1.acquire()
print(i)
print(i+1)
lock2.release()
def print_char():
for i in range(6 5,91):
lock2.acquire()
print(chr(i))
lock1.release()
t1 = Thread(target=print_num)
t2 = Thread(target=print_char)
lock2.acquire() # 让数字先执行
t1.start()
t2.start()
t1.join()
t2.join()
什么是死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁产生条件
互斥条件:指线程使用了互斥方法,使用一个资源时其他线程无法使用。
请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,在获取到新的资源前不会释放自己保持的资源。
不剥夺条件:不会受到线程外部的干扰,如系统强制终止线程等。
环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,如 T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。
如何避免死锁
什么是GIL问题 (全局解释器锁)
由于python解释器设计中加入了解释器锁,导致python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。
导致后果
因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行多阻塞任务时可以提升程序效率,其他情况并不能对效率有所提升。
GIL问题建议
尽量使用进程完成无阻塞的并发行为
不使用c作为解释器 (Java C#)
结论
什么是网络并发
在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。
循环网络模型问题
循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。
多任务并发模型具体指多进程多线程网络并发模型,即每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程,多任务并发模型也是实际工作中最为常用的服务端处理模型。
"""
基于多进程的 tcp 网络并发模型
重点代码!!
"""
from socket import *
from multiprocessing import Process
from signal import *
# 全局变量
HOST = '0.0.0.0'
PORT = 8888
ADDR = (HOST,PORT) # 服务器地址
# 处理客户端请求
def handle(connfd):
while True:
data = connfd.recv(1024)
# 另外一端不存在了,recv会返回空字节
if not data:
break
print("Recv:",data.decode())
connfd.send(b"Thanks")
connfd.close()
def main():
# tcp套接字创建
sock = socket()
sock.bind(ADDR)
sock.listen(5)
print("Listen the port %s"%PORT)
signal(SIGCHLD,SIG_IGN) # 处理僵尸进程
# 循环连接客户端
while True:
try:
connfd,addr = sock.accept()
print("Connect from",addr)
except KeyboardInterrupt:
sock.close()
return
# 为连接进来的客户端创建单独的子进程
p = Process(target = handle,args=(connfd,))
p.daemon = True # 父进程退出,子进程终止服务
p.start()
if __name__ == '__main__':
main()
"""
基于多线程的 tcp 网络并发模型
重点代码!!
"""
from socket import *
from threading import Thread
# 全局变量
HOST = '0.0.0.0'
PORT = 8888
ADDR = (HOST,PORT) # 服务器地址
# 处理客户端请求
class MyThread(Thread):
def __init__(self,connfd):
super().__init__()
self.connfd = connfd
def run(self):
while True:
data = self.connfd.recv(1024)
# 另外一端不存在了,recv会返回空字节
if not data:
break
print("Recv:",data.decode())
self.connfd.send(b"Thanks")
self.connfd.close()
def main():
# tcp套接字创建
sock = socket()
sock.bind(ADDR)
sock.listen(5)
print("Listen the port %s"%PORT)
# 循环连接客户端
while True:
try:
connfd,addr = sock.accept()
print("Connect from",addr)
except KeyboardInterrupt:
sock.close()
return
# 为连接进来的客户端创建单独的线程
t = MyThread(connfd) # 使用自定义线程类创建线程
t.setDaemon(True) # 主线程退出,分之线程终止服务
t.start()
if __name__ == '__main__':
main()
ftp 文件服务器
【1】 分为服务端和客户端,要求可以有多个客户端同时操作。
【2】 客户端可以查看服务器文件库中有什么文件。
【3】 客户端可以从文件库中下载文件到本地。
【4】 客户端可以上传一个本地文件到文件库。
【5】 使用print在客户端打印命令输入提示,引导操作
注:ftp是一个文件传输协议
什么是IO
在程序中存在读写数据操作行为的事件均是IO行为,比如终端输入输出 ,文件读写,数据库修改和网络消息收发等。
程序分类
IO分类:阻塞IO ,非阻塞IO,IO多路复用,异步IO等。
设置套接字为非阻塞IO
sockfd.setblocking(bool)
功能:设置套接字为非阻塞IO
参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞
超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。
sockfd.settimeout(sec)
功能:设置套接字的超时时间
参数:设置的时间
from socket import *
import time
#打开日志文件
f=open("my.log","w")
#创建套接字
sockfd=socket()
sockfd.bind(("0.0.0.0",7845))
sockfd.listen(5)
# #设置套接字的非阻塞
# sockfd.setblocking(False)
# 超时检测
sockfd.settimeout(3)
while True:
print("waiting for connect")
try:
connfd,addr=sockfd.accept() #阻塞等待
print("connect from",addr)
except BlockingIOError as e:
#客户端未连接,每隔两秒写入日志
msg = "%s : %s\n" % (time.ctime(), e)
f.write(msg)
time.sleep(2)
except timeout as e:
msg = "%s : %s\n" % (time.ctime(), e)
f.write(msg)
else:
#正常客户连接
data=connfd.recv(1024)
print(data.decode())
定义
同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
具体方案
rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能: 监控IO事件,阻塞等待IO发生
参数:rlist 列表 读IO列表,添加等待发生的或者可读的IO事件
wlist 列表 写IO列表,存放要可以主动处理的或者可写的IO事件
xlist 列表 异常IO列表,存放出现异常要处理的IO事件
timeout 超时时间
返回值: rs 列表 rlist中准备就绪的IO
ws 列表 wlist中准备就绪的IO
xs 列表 xlist中准备就绪的IO
注:IO对象:文件对象,数据库对象,套接字对象
读事件:获取数据的事件
UDP套接字有读写事件;TCP:监听套接字没有写事件,只有读事件;连接套接字有读有写;文件对象读写事件都有
"""
select io 多路复用
"""
from socket import *
from select import select
from time import sleep
# 创建三个对象,帮助监控
tcp_sock = socket()
tcp_sock.bind(('0.0.0.0',8888))
tcp_sock.listen(5)
print(tcp_sock)
udp_sock = socket(AF_INET,SOCK_DGRAM)
udp_sock.bind(('0.0.0.0',8866))
f = open("my.log",'rb')
# 开始监控这些IO
print("监控IO发生")
sleep(5)
rs,ws,xs = select([tcp_sock],[f,udp_sock],[])
print("rs :",rs)
print("ws :",ws)
print("xs :",xs)
p = select.poll()
功能 : 创建poll对象
返回值: poll对象
p.register(fd,event)
功能: 注册关注的IO事件
参数:fd 要关注的IO
event 要关注的IO事件类型
常用类型:POLLIN 读IO事件(rlist)
POLLOUT 写IO事件 (wlist)
POLLERR 异常IO (xlist)
POLLHUP 断开连接
e.g. p.register(sockfd,POLLIN|POLLERR)
p.unregister(fd)
功能:取消对IO的关注
参数:IO对象或者IO对象的fileno
events = p.poll()
功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IO
events格式 [(fileno,event),()....]
每个元组为一个就绪IO,元组第一项是该IO的fileno(文件编号或者说文件描述符),第二项为该IO就绪的事件类型
#文件描述符:每个IO在系统中都有一个系统分配的>=0 的整数编号;特点:不重复,每个文件描述符对应一个IO对象
# 查看 : IO对象.fileno()
#IO就绪的事件类型:
1. POLLOUT ---> 4
2.POLLIN ---> 3
"""
poll io 多路复用
"""
from socket import *
from select import *
from time import sleep
# 创建三个对象,帮助监控
tcp_sock = socket()
tcp_sock.bind(('0.0.0.0',8888))
tcp_sock.listen(5)
udp_sock = socket(AF_INET,SOCK_DGRAM)
udp_sock.bind(('0.0.0.0',8866))
f = open("my.log",'rb')
# 开始监控这些IO
print("监控IO发生")
p = poll()
# 关注
p.register(tcp_sock,POLLIN)
p.register(f,POLLOUT)
p.register(udp_sock,POLLOUT|POLLIN)
print("tcp_sock:",tcp_sock.fileno())
print("udp_sock:",udp_sock.fileno())
print("file:",f.fileno())
# 准备工作
map = {
tcp_sock.fileno():tcp_sock,
udp_sock.fileno():udp_sock,
f.fileno():f
}
events = p.poll() # 进行监控
print(events)
p.unregister(f) # 取消关注
epoll方法
使用方法 : 基本与poll相同
生成对象改为 epoll()
将所有事件类型改为EPOLL类型
select:
支持的操作系统最多,windows,Linux,Unix
同时监控io数量 1024
执行效率 一般
poll:
支持的操作系统Linux,Unix
同时监控io数量 无限制
执行效率 一般
epoll:
支持的操作系统Linux
同时监控io数量 无限制
执行效率 较高0
利用IO多路复用等技术,同时处理多个客户端IO请求。
优点 : 资源消耗少,能同时高效处理多个IO行为
缺点 : 只针对处理并发产生的IO事件
适用情况:HTTP请求,网络传输等都是IO行为,可以通过IO多路复用监控多个客户端的IO请求。
并发服务实现过程
【1】将关注的IO准备好,通常设置为非阻塞状态。
【2】通过IO多路复用方法提交,进行IO监控。
【3】阻塞等待,当监控的IO有发生时,结束阻塞。
【4】遍历返回值列表,确定就绪IO事件。
【5】处理发生的IO事件。
【6】继续循环监控IO发生。
"""
基于select 的IO多路复用并发模型
重点代码 !
"""
from socket import *
from select import select
# 全局变量
HOST = "0.0.0.0"
PORT = 8889
ADDR = (HOST,PORT)
# 创建tcp套接字
tcp_socket = socket()
tcp_socket.bind(ADDR)
tcp_socket.listen(5)
# 设置为非阻塞
tcp_socket.setblocking(False)
# IO对象监控列表
rlist = [tcp_socket] # 初始监听对象
wlist = []
xlist = []
# 循环监听
while True:
# 对关注的IO进行监控
rs,ws,xs = select(rlist,wlist,xlist)
# 对返回值rs 分情况讨论 监听套接字 客户端连接套接字
for r in rs:
if r is tcp_socket:
# 处理客户端连接
connfd, addr = r.accept()
print("Connect from", addr)
connfd.setblocking(False) # 设置非阻塞
rlist.append(connfd) # 添加到监控列表
else:
# 收消息
data = r.recv(1024)
if not data:
# 客户端退出
rlist.remove(r) # 移除关注
r.close()
continue
print(data.decode())
# r.send(b'OK')
wlist.append(r) # 放入写列表
for w in ws:
w.send(b"OK") # 发送消息
wlist.remove(w) # 如果不移除会不断的写
"""
基于poll方法实现IO并发
"""
from socket import *
from select import *
# 全局变量
HOST = "0.0.0.0"
PORT = 8889
ADDR = (HOST,PORT)
# 创建tcp套接字
tcp_socket = socket()
tcp_socket.bind(ADDR)
tcp_socket.listen(5)
# 设置为非阻塞
tcp_socket.setblocking(False)
p = poll() # 建立poll对象
p.register(tcp_socket,POLLIN) # 初始监听对象
# 准备工作,建立文件描述符 和 IO对象对应的字典 时刻与register的IO一致
map = {tcp_socket.fileno():tcp_socket}
# 循环监听
while True:
# 对关注的IO进行监控
events = p.poll()
# events--> [(fileno,event),()....]
for fd,event in events:
# 分情况讨论
if fd == tcp_socket.fileno():
# 处理客户端连接
connfd, addr = map[fd].accept()
print("Connect from", addr)
connfd.setblocking(False) # 设置非阻塞
p.register(connfd,POLLIN|POLLERR) # 添加到监控
map[connfd.fileno()] = connfd # 同时维护字典
elif event == POLLIN:
# 收消息
data = map[fd].recv(1024)
if not data:
# 客户端退出
p.unregister(fd) # 移除关注
map[fd].close()
del map[fd] # 从字典也移除
continue
print(data.decode())
map[fd].send(b'OK')
"""
基于epoll方法实现IO并发
重点代码 !
"""
from socket import *
from select import *
# 全局变量
HOST = "0.0.0.0"
PORT = 8889
ADDR = (HOST,PORT)
# 创建tcp套接字
tcp_socket = socket()
tcp_socket.bind(ADDR)
tcp_socket.listen(5)
# 设置为非阻塞
tcp_socket.setblocking(False)
p = epoll() # 建立epoll对象
p.register(tcp_socket,EPOLLIN) # 初始监听对象
# 准备工作,建立文件描述符 和 IO对象对应的字典 时刻与register的IO一致
map = {tcp_socket.fileno():tcp_socket}
# 循环监听
while True:
# 对关注的IO进行监控
events = p.poll()
# events--> [(fileno,event),()....]
for fd,event in events:
# 分情况讨论
if fd == tcp_socket.fileno():
# 处理客户端连接
connfd, addr = map[fd].accept()
print("Connect from", addr)
connfd.setblocking(False) # 设置非阻塞
p.register(connfd,EPOLLIN|EPOLLERR) # 添加到监控
map[connfd.fileno()] = connfd # 同时维护字典
elif event == EPOLLIN:
# 收消息
data = map[fd].recv(1024)
if not data:
# 客户端退出
p.unregister(fd) # 移除关注
map[fd].close()
del map[fd] # 从字典也移除
continue
print(data.decode())
map[fd].send(b'OK')
超文本传输协议 #应用层协议
请求行和空行必须有
请求行 : 具体的请求类别和请求内容
换行:\r\n
GET / HTTP/1.1
请求类别 请求内容 协议版本
请求类别:每个请求类别表示要做不同的事情
GET : 获取网络资源
POST :提交一定的信息,得到反馈 例如;注册登录信息
HEAD : 只获取网络资源的响应头
PUT : 更新服务器资源
DELETE : 删除服务器资源
Accept-Encoding: gzip
HTTP/1.1 200 OK
版本信息 响应码 附加信息
响应码 : 告知客户端响应状态
1xx 提示信息,表示请求被接收
2xx 响应成功 # 200:服务器已成功处理了请求
3xx 响应需要进一步操作,重定向
4xx 客户端错误 #404:服务器找不到请求的网页
5xx 服务器错误 #500:服务器内部错误
Content-Type: text/html #响应的格式为什么格式
Content-Length:109\r\n #响应的数据是多大
"""
http 请求响应演示
"""
from socket import *
# 创建tcp套接字
s = socket()
s.bind(("0.0.0.0",8888))
s.listen(5)
c,addr = s.accept()
print("Connect from",addr) # 浏览器连接
data = c.recv(4096) # 接收的是http请求
print(data.decode())
# http响应格式
html = """HTTP/1.1 404 Not Found
Content-Type:text/html
Sorry....
"""
c.send(html.encode()) # 发送响应给客户端
c.close()
s.close()
【2】 解析客户端发送的请求
【3】 根据请求组织数据内容
【4】 将数据内容形成http响应格式返回给浏览器
特点 :
【1】 采用IO并发,可以满足多个客户端同时发起请求情况
【2】 通过类接口形式进行功能封装
【3】 做基本的请求解析,根据具体请求返回具体内容,同时处理客户端的非网页请求行为
"""
Web 服务程序
假定 : 用户有一组网页,希望使用我们提供的类快速搭建一个服务,实现
自己网页的展示浏览
IO 多路复用和 http训练
"""
from socket import *
from select import select
import re
class WebServer:
def __init__(self, host='0.0.0.0', port=80, html=None):
self.host = host
self.port = port
self.html = html
# 做IO多路复用并发模型准备
self.__rlist = []
self.__wlist = []
self.__xlist = []
self.create_socket()
self.bind()
# 创建套接字
def create_socket(self):
self.sock = socket()
self.sock.setblocking(False)
def bind(self):
self.address = (self.host, self.port)
self.sock.bind(self.address)
# 启动服务
def start(self):
self.sock.listen(5)
print("Listen the port %d" % self.port)
# IO多路复用并发模型
self.__rlist.append(self.sock)
while True:
# 循环监听IO
rs, ws, xs = select(self.__rlist, self.__wlist, self.__xlist)
for r in rs:
if r is self.sock:
# 有浏览器连接
connfd, addr = self.sock.accept()
connfd.setblocking(False)
self.__rlist.append(connfd)
else:
# 有客户端发送请求
try:
self.handle(r)
except:
self.__rlist.remove(r)
r.close()
# 处理客户端请求
def handle(self, connfd):
# 浏览器发送了HTTP请求
request = connfd.recv(1024 * 10).decode()
# print(request)
# 使用正则提取请求内容
pattern = "[A-Z]+\s+(?P/\S*)"
result = re.match(pattern, request) # match对象 None
if result:
info = result.group('info') # 提取请求内容
print("请求内容:", info)
# 发送响应内容
self.send_response(connfd, info)
else:
# 没有获取请求断开客户端
self.__rlist.remove(connfd)
connfd.close()
# 根据请求组织响应内容,发送给浏览器
def send_response(self, connfd, info):
if info == '/':
# 主页
filename = self.html + "/index.html"
else:
filename = self.html + info
try:
fd = open(filename,'rb') # 有可能有文本还有图片
except:
# 请求的文件不存在
response = "HTTP/1.1 404 Not Found\r\n"
response += "Content-Type:text/html\r\n"
response += "\r\n"
response += "Sorry....
"
response = response.encode()
else:
# 请求的文件存在
data = fd.read()
response = "HTTP/1.1 200 OK\r\n"
response += "Content-Type:text/html\r\n"
response += "Content-Length:%d\r\n"%len(data) # 发送图片
response += "\r\n"
response =response.encode() + data # 转换字节拼接
finally:
# 给客户端发送响应
connfd.send(response)
if __name__ == '__main__':
"""
思考问题 : 1. 使用流程
2. 那些量需要用户决定,怎么传入
哪组网页? 服务端地址?
"""
# 实例化对象
httpd = WebServer(host='0.0.0.0', port=8000, html="./static")
httpd.start() # 启动服务
衡量高并发的关键指标
响应时间(Response Time) : 接收请求后处理的时间
吞吐量(Throughput): 响应时间+QPS+同时在线用户数量
每秒查询率QPS(Query Per Second): 每秒接收请求的次数
每秒事务处理量TPS(Transaction Per Second):每秒处理请求的次数(包含接收,处理,响应)
同时在线用户数量:同时连接服务器的用户的数量
多大的并发量算是高并发
没有最高,只要更高
比如在一个小公司可能QPS2000+就不错了,在一个需要频繁访问的门户网站可能要达到QPS5W+
C10K问题:(C是客户端的意思)
早先服务器都是单纯基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程占用操作系统资源多,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的,这就是著名的C10k问题。创建的进程线程多了,数据拷贝频繁, 进程/线程切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!
为了解决C10K问题,现在高并发的实现已经是一个更加综合的架构艺术。涉及到进程线程编程,IO处理,数据库处理,缓存,队列,负载均衡等等,这些我们在后面的阶段还会学习。此外还有硬件的设计,服务器集群的部署,服务器负载,网络流量的处理等。
实际工作中,应对更庞大的任务场景,网络并发模型的使用有时也并不单一。比如多进程网络并发中每个进程再开辟线程,或者在每个进程中也可以使用多路复用的IO处理方法。
if result:
info = result.group('info') # 提取请求内容
print("请求内容:", info)
# 发送响应内容
self.send_response(connfd, info)
else:
# 没有获取请求断开客户端
self.__rlist.remove(connfd)
connfd.close()
# 根据请求组织响应内容,发送给浏览器
def send_response(self, connfd, info):
if info == '/':
# 主页
filename = self.html + "/index.html"
else:
filename = self.html + info
try:
fd = open(filename,'rb') # 有可能有文本还有图片
except:
# 请求的文件不存在
response = "HTTP/1.1 404 Not Found\r\n"
response += "Content-Type:text/html\r\n"
response += "\r\n"
response += "Sorry....
"
response = response.encode()
else:
# 请求的文件存在
data = fd.read()
response = "HTTP/1.1 200 OK\r\n"
response += "Content-Type:text/html\r\n"
response += "Content-Length:%d\r\n"%len(data) # 发送图片
response += "\r\n"
response =response.encode() + data # 转换字节拼接
finally:
# 给客户端发送响应
connfd.send(response)
if name == ‘main’:
“”"
思考问题 : 1. 使用流程
2. 那些量需要用户决定,怎么传入
哪组网页? 服务端地址?
“”"
# 实例化对象
httpd = WebServer(host=‘0.0.0.0’, port=8000, html="./static")
httpd.start() # 启动服务
## 并发技术探讨(扩展)
### 高并发问题dle(r)
* 衡量高并发的关键指标
- 响应时间(Response Time) : 接收请求后处理的时间
- 吞吐量(Throughput): 响应时间+QPS+同时在线用户数量
- 每秒查询率QPS(Query Per Second): 每秒接收请求的次数
- 每秒事务处理量TPS(Transaction Per Second):每秒处理请求的次数(包含接收,处理,响应)
- 同时在线用户数量:同时连接服务器的用户的数量
* 多大的并发量算是高并发
* 没有最高,只要更高
比如在一个小公司可能QPS2000+就不错了,在一个需要频繁访问的门户网站可能要达到QPS5W+
* C10K问题:(C是客户端的意思)
早先服务器都是单纯基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程占用操作系统资源多,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的,这就是著名的C10k问题。创建的进程线程多了,数据拷贝频繁, 进程/线程切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!
### 更高并发的实现
为了解决C10K问题,现在高并发的实现已经是一个更加综合的架构艺术。涉及到进程线程编程,IO处理,数据库处理,缓存,队列,负载均衡等等,这些我们在后面的阶段还会学习。此外还有硬件的设计,服务器集群的部署,服务器负载,网络流量的处理等。
![\[外链图片转存中...(img-m3153ebe-1616418798830)\]](https://img-blog.csdnimg.cn/20210322232946902.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzI0NjE1MQ==,size_16,color_FFFFFF,t_70)
实际工作中,应对更庞大的任务场景,网络并发模型的使用有时也并不单一。比如多进程网络并发中每个进程再开辟线程,或者在每个进程中也可以使用多路复用的IO处理方法。