first commit

This commit is contained in:
2025-08-27 14:39:37 +08:00
commit 81ebee9cf3
97 changed files with 14191 additions and 0 deletions

View File

@@ -0,0 +1,849 @@
# 网络编程基础
## 操作系统基础
操作系统:(Operating System简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。
![image-20210725220525763](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220525763.png)
操作系统应该分成两部分功能
1. 隐藏了丑陋的硬件调用接口(键盘、鼠标、音箱等等怎么实现的,就不需要你管了),为应用程序员提供调用硬件资源的更好,更简单,更清晰的模型(系统调用接口)。应用程序员有了这些接口后,就不用再考虑操作硬件的细节,专心开发自己的应用程序即可。
例如:操作系统提供了文件这个抽象概念,对文件的操作就是对磁盘的操作,有了文件我们无需再去考虑关于磁盘的读写控制(比如控制磁盘转动,移动磁头读写数据等细节),
2. 将应用程序对硬件资源的竞态请求变得有序化
例如很多应用软件其实是共享一套计算机硬件比方说有可能有三个应用程序同时需要申请打印机来输出内容那么a程序竞争到了打印机资源就打印然后可能是b竞争到打印机资源也可能是c这就导致了无序打印机可能打印一段a的内容然后又去打印c...,操作系统的一个功能就是将这种无序变得有序。
## socket
回顾一下五层通讯流程
![image-20210725220535013](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220535013.png)
但实际上从传输层开始以及以下,都是操作系统帮咱们完成的,下面的各种包头封装的过程
![image-20210725220541285](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220541285.png)
Socket又称为套接字它是应用层与TCP/IP协议族通信的中间软件抽象层它是一组接口。在设计模式中Socket其实就是一个门面模式它把复杂的TCP/IP协议族隐藏在Socket接口后面对用户来说一组简单的接口就是全部让Socket去组织数据以符合指定的协议。当我们使用不同的协议进行通信时就得使用不同的接口还得处理不同协议的各种细节这就增加了开发的难度软件也不易于扩展(就像我们开发一套公司管理系统一样,报账、会议预定、请假等功能不需要单独写系统,而是一个系统上多个功能接口,不需要知道每个功能如何去实现的)。于是UNIX BSD就发明了socket这种东西socket屏蔽了各个协议的通信细节使得程序员无需关注协议本身直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用通过系统调用我们可以方便的使用磁盘文件操作使用内存而无需自己去进行磁盘读写内存管理。socket其实也是一样的东西就是提供了tcp/ip协议的抽象对外提供了一套接口同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。
其实站在你的角度上看socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port因为ip是用来标识互联网中的一台主机的位置而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序并且使用socket模块来与之通信。
## 套接字家族
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种或者称为有两个种族,分别是基于文件型的和基于网络型的。
### 基于文件类型的套接字家族
套接字家族的名字AF_UNIX
unix一切皆文件基于文件的套接字调用的就是底层的文件系统来取数据两个套接字进程运行在同一机器可以通过访问同一个文件系统间接完成通信
### 基于网络类型的套接字家族
套接字家族的名字AF_INET
(还有AF_INET6被用于ipv6还有一些其他的地址家族不过他们要么是只用于某个平台要么就是已经被废弃或者是很少被使用或者是根本没有实现所有地址家族中AF_INET是使用最广泛的一个python支持很多种地址家族但是由于我们只关心网络编程所以大部分时候我么只使用AF_INET)
## 套接字的工作流程基于TCP和 UDP两个协议
### TCP和UDP对比
- TCPTransmission Control Protocol
- 可靠的、面向连接的协议eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存、面向字节流。使用TCP的应用Web浏览器文件传输程序。
- UDPUser Datagram Protocol
- 不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包)尽最大努力服务无拥塞控制。使用UDP的应用域名系统 (DNS)视频流IP语音(VoIP)。
### TCP协议下的socket
![image-20210725220552513](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220552513.png)
服务器端先初始化Socket然后与端口绑定(bind),对端口进行监听(listen)调用accept阻塞等待客户端连接。在这时如果有个客户端初始化一个Socket然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
```python
import socket
socket.socket(socket_family,socket_type,protocal=0)
# socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
# 例如
tcpSock = socket(AF_INET, SOCK_STREAM)
```
服务端套接字函数
| s.bind() | 绑定(主机,端口号)到套接字 |
| ---------- | -------------------------------------------- |
| s.listen() | 开始TCP监听 |
| s.accept() | 被动接受TCP客户的连接,(阻塞式)等待连接的到来 |
客户端套接字函数
| s.connect() | 主动初始化TCP服务器连接 |
| -------------- | ------------------------------------------------------- |
| s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数
| s.recv() | 接收TCP数据 |
| --------------- | ------------------------------------------------------------ |
| s.send() | 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) |
| s.sendall() | 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) |
| s.recvfrom() | 接收UDP数据 |
| s.sendto() | 发送UDP数据 |
| s.getpeername() | 连接到当前套接字的远端的地址 |
| s.getsockname() | 当前套接字的地址 |
| s.getsockopt() | 返回指定套接字的参数 |
| s.setsockopt() | 设置指定套接字的参数 |
| s.close() | 关闭套接字 |
面向锁的套接字方法
| s.setblocking() | 设置套接字的阻塞与非阻塞模式 |
| --------------- | ---------------------------- |
| s.settimeout() | 设置阻塞套接字操作的超时时间 |
| s.gettimeout() | 得到阻塞套接字操作的超时时间 |
面向文件的套接字的函数
| s.fileno() | 套接字的文件描述符 |
| ------------ | ---------------------------- |
| s.makefile() | 创建一个与该套接字相关的文件 |
第一版,单个客户端与服务端通信
服务端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.bind(('127.0.0.1',8080)) # 0 ~ 65535 1024之前系统分配好的端口 绑定电话卡
phone.listen(5) # 同一时刻有5个请求但是可以有N多个链接。 开机。
conn, client_addr = phone.accept() # 接电话
print(conn, client_addr, sep='\n')
from_client_data = conn.recv(1024) # 一次接收的最大限制 bytes
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper())
conn.close() # 挂电话
phone.close() # 关机
```
客户端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
phone.send('hello'.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data)
phone.close() # 挂电话
```
第二版,通信循环
服务端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
conn,client_addr = phone.accept()
print(conn,client_addr,sep='\n')
while 1:
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper())
except ConnectionResetError:
break
conn.close()
phone.close()
```
客户端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while 1:
client_data = input('>>> ')
phone.send(client_data.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close()
```
第三版, 通信,连接循环
服务端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while 1:
conn,client_addr = phone.accept()
print(conn,client_addr,sep='\n')
while 1:
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper())
except:
break
conn.close()
phone.close()
```
客户端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
while 1:
client_data = input('>>> ')
phone.send(client_data.encode('utf-8'))
if client_data == 'q':break
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close()
```
远程执行命令的示例:
```python
import socket
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
```
客户端
```python
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1', 8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close() # 挂电话
```
### UDP协议下的socket
**udp是无链接的先启动哪一端都不会报错**
![image-20210725220708133](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220708133.png)
服务器端先初始化Socket然后与端口绑定(bind)recvform接收消息这个消息有两项消息内容和对方客户端的地址然后回复消息时也要带着你收到的这个客户端的地址发送回去最后关闭连接一次交互结束
服务端
```python
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr) # 对话(接收与发送)
udp_sk.close() # 关闭服务器套接字
```
客户端
```python
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
```
类似于qq聊天的代码示例
服务端
```python
import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #DGRAM:datagram 数据报文的意思象征着UDP协议的通信方式
udp_server_sock.bind(ip_port)#你对外提供服务的端口就是这一个,所有的客户端都是通过这个端口和你进行通信的
while True:
qq_msg,addr=udp_server_sock.recvfrom(1024)# 阻塞状态,等待接收消息
print('来自[%s:%s]的一条消息:\033[1;34;43m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
back_msg=input('回复消息: ').strip()
udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
```
客户端
```python
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
qq_name_dic={
'taibai':('127.0.0.1',8081),
'Jedan':('127.0.0.1',8081),
'Jack':('127.0.0.1',8081),
'John':('127.0.0.1',8081),
}
while True:
while 1:
qq_name=input('请选择聊天对象: ').strip()
if qq_name not in qq_name_dic:
print('没有这个聊天对象')
continue
break
while True:
msg='发给'+ qq_name + ': ' + input('请输入消息,回车发送,输入q结束和他的聊天: ').strip()
if msg == 'q':break
if not msg:continue
udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])# 必须带着自己的地址这就是UDP不一样的地方不需要建立连接但是要带着自己的地址给服务端否则服务端无法判断是谁给我发的消息并且不知道该把消息回复到什么地方因为我们之间没有建立连接通道
back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)# 同样也是阻塞状态,等待接收消息
print('来自[%s:%s]的一条消息:\033[1;34;43m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
udp_client_socket.close()
```
自制时间服务器
服务端
```python
from socket import *
from time import strftime
import time
ip_port = ('127.0.0.1', 9000)
bufsize = 1024
tcp_server = socket(AF_INET, SOCK_DGRAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)
while True:
msg, addr = tcp_server.recvfrom(bufsize)
print('===>', msg.decode('utf-8'))
stru_time = time.localtime() #当前的结构化时间
if not msg:
time_fmt = '%Y-%m-%d %X'
else:
time_fmt = msg.decode('utf-8')
back_msg = strftime(time_fmt,stru_time)
print(back_msg,type(back_msg))
tcp_server.sendto(back_msg.encode('utf-8'), addr)
tcp_server.close()
```
客户端
```python
from socket import *
ip_port=('127.0.0.1',9000)
bufsize=1024
tcp_client=socket(AF_INET,SOCK_DGRAM)
while True:
msg=input('请输入时间格式(例%Y %m %d)>>: ').strip()
tcp_client.sendto(msg.encode('utf-8'),ip_port)
data=tcp_client.recv(bufsize)
print('当前日期:',str(data,encoding='utf-8'))
```
## 粘包
![image-20210725220726010](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220726010.png)
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据而是先将数据写入缓冲区中再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区函数就可以成功返回不管它们有没有到达目标机器也不管它们何时被发送到网络这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
这些I/O缓冲区特性可整理如下
1. I/O缓冲区在每个TCP套接字中单独存在
2. I/O缓冲区在创建套接字时自动生成
3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4. 关闭套接字将丢失输入缓冲区中的数据。
## 两种情况下会发生粘包
1. 接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
服务端
```python
import socket
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
```
客户端
```python
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close()
```
2. 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
服务端
```python
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
conn, client_addr = phone.accept()
frist_data = conn.recv(1024)
print('1:',frist_data.decode('utf-8')) # 1: helloworld
second_data = conn.recv(1024)
print('2:',second_data.decode('utf-8'))
conn.close()
phone.close()
```
客户端
```python
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
phone.send(b'hello')
phone.send(b'world')
phone.close()
```
## 粘包的解决方案
### struct模块
该模块可以把一个类型如数字转成固定长度的bytes
![image-20210725220750620](01.%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/image-20210725220750620.png)
```python
import struct
# 将一个数字转化成等长度的bytes类型。
ret = struct.pack('i', 183346)
print(ret, type(ret), len(ret))
# 通过unpack反解回来
ret1 = struct.unpack('i',ret)[0]
print(ret1, type(ret1), len(ret1))
# 但是通过struct 处理不能处理太大
```
方案一:
```python
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1:
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
# 1 制作固定报头
total_size = len(correct_msg) + len(error_msg)
header = struct.pack('i', total_size)
# 2 发送报头
conn.send(header)
# 发送真实数据:
conn.send(correct_msg)
conn.send(error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
## 但是low版本有问题
## 1报头不只有总数据大小而是还应该有MD5数据文件名等等一些数据。
## 2通过struct模块直接数据处理不能处理太大。
```
客户端
```python
import socket
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while 1:
cmd = input('>>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 1接收固定报头
header = phone.recv(4)
# 2解析报头
total_size = struct.unpack('i', header)[0]
# 3根据报头信息接收真实数据
recv_size = 0
res = b''
while recv_size < total_size:
recv_data = phone.recv(1024)
res += recv_data
recv_size += len(recv_data)
print(res.decode('gbk'))
phone.close()
```
方案二:可自定制报头
整个流程的大致解释:
我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的)然后json序列化然后用struck将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据 都叫做数据包数据包里的所有数据都叫做报文报文里面不止有你的数据还有ip地址、mac地址、端口号等等其实所有的报文都有报头这个报头是协议规定的看一下
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容
服务端
```python
import socket
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1:
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
# 1 制作固定报头
total_size = len(correct_msg) + len(error_msg)
header_dict = {
'md5': 'fdsaf2143254f',
'file_name': 'f1.txt',
'total_size': total_size,
}
header_dict_json = json.dumps(header_dict) # str
bytes_headers = header_dict_json.encode('utf-8')
header_size = len(bytes_headers)
header = struct.pack('i', header_size)
# 2 发送报头长度
conn.send(header)
# 3 发送报头
conn.send(bytes_headers)
# 4 发送真实数据:
conn.send(correct_msg)
conn.send(error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
```
客户端
```python
import socket
import struct
import json
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while 1:
cmd = input('>>>').strip()
if not cmd: continue
phone.send(cmd.encode('utf-8'))
# 1接收固定报头
header_size = struct.unpack('i', phone.recv(4))[0]
# 2解析报头长度
header_bytes = phone.recv(header_size)
header_dict = json.loads(header_bytes.decode('utf-8'))
# 3,收取报头
total_size = header_dict['total_size']
# 3根据报头信息接收真实数据
recv_size = 0
res = b''
while recv_size < total_size:
recv_data = phone.recv(1024)
res += recv_data
recv_size += len(recv_data)
print(res.decode('gbk'))
phone.close()
```
FTP上传下载文件的代码简单版
服务端
```python
import socket
import struct
import json
sk = socket.socket()
## buffer = 4096 # 当双方的这个接收发送的大小比较大的时候就像这个4096就会丢数据这个等我查一下再告诉大家改小了就ok的在linux上也是ok的。
buffer = 1024 #每次接收数据的大小
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()
#接收
head_len = conn.recv(4)
head_len = struct.unpack('i',head_len)[0] #解包
json_head = conn.recv(head_len).decode('utf-8') #反序列化
head = json.loads(json_head)
filesize = head['filesize']
with open(head['filename'],'wb') as f:
while filesize:
if filesize >= buffer: #>=是因为如果刚好等于的情况出现也是可以的。
content = conn.recv(buffer)
f.write(content)
filesize -= buffer
else:
content = conn.recv(buffer)
f.write(content)
break
conn.close()
sk.close()
```
客户端
```python
import os
import json
import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
buffer = 1024 #读取文件的时候,每次读取的大小
head = {
'filepath':r'C:\Users\Aaron\Desktop\新建文件夹', #需要下载的文件路径,也就是文件所在的文件夹
'filename':'config', #改成上面filepath下的一个文件
'filesize':None,
}
file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize
# json_head = json.dumps(head,ensure_ascii=False) #字典转换成字符串
json_head = json.dumps(head) #字典转换成字符串
bytes_head = json_head.encode('utf-8') #字符串转换成bytes类型
print(json_head)
print(bytes_head)
#计算head的长度因为接收端先接收我们自己定制的报头对吧
head_len = len(bytes_head) #报头长度
pack_len = struct.pack('i',head_len)
print(head_len)
print(pack_len)
sk.send(pack_len) #先发送报头长度
sk.send(bytes_head) #再发送bytes类型的报头
#即便是视频文件也是可以按行来读取的也可以readline也可以for循环但是读取出来的数据大小就不固定了影响效率有可能读的比较小也可能很大像视频文件一般都是一行的二进制字节流。
#所有我们可以用read设定一个一次读取内容的大小一边读一边发一边收一边写
with open(file_path,'rb') as f:
while filesize:
if filesize >= buffer: #>=是因为如果刚好等于的情况出现也是可以的。
content = f.read(buffer) #每次读取出来的内容
sk.send(content)
filesize -= buffer #每次减去读取的大小
else: #那么说明剩余的不够一次读取的大小了,那么只要把剩下的读取出来发送过去就行了
content = f.read(filesize)
sk.send(content)
break
sk.close()
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -0,0 +1,174 @@
# 操作系统发展史
## 手工操作 —— 穿孔卡片
1946年第一台计算机诞生--20世纪50年代中期计算机工作还在采用手工操作方式。此时还没有操作系统的概念。
![image-20210725220908985](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725220908985.png)
![image-20210725220917981](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725220917981.png)
程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把程序和数据输入计算机内存,接着通过控制台开关启动程序针对数据运行;计算完毕,打印机输出计算结果;用户取走结果并卸下纸带(或卡片)后,才让下一个用户上机。
手工操作方式两个特点:
1. 用户独占全机。不会出现因资源已被其他用户占用而等待的现象,但资源的利用率低。
2. CPU 等待手工操作。CPU的利用不充分。
20世纪50年代后期出现人机矛盾手工操作的慢速度和计算机的高速度之间形成了尖锐矛盾手工操作方式已严重损害了系统资源的利用率使资源利用率降为百分之几甚至更低不能容忍。唯一的解决办法只有摆脱人的手工操作实现作业的自动过渡。这样就出现了成批处理。
## 批处理 —— 磁带存储
批处理系统:加载在计算机上的一个系统软件,在它的控制下,计算机能够自动地、成批地处理一个或多个用户的作业(这作业包括程序、数据和命令)。
### 联机批处理系统
首先出现的是联机批处理系统,即作业的输入/输出由CPU来处理。
![image-20210725220928953](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725220928953.png)
主机与输入机之间增加一个存储设备——磁带,在运行于主机上的监督程序的自动控制下,计算机可自动完成:成批地把输入机上的用户作业读入磁带,依次把磁带上的用户作业读入主机内存并执行并把计算结果向输出机输出。完成了上一批作业后,监督程序又从输入机上输入另一批作业,保存在磁带上,并按上述步骤重复处理。
监督程序不停地处理各个作业,从而实现了作业到作业的自动转接,减少了作业建立时间和手工操作时间,有效克服了人机矛盾,提高了计算机的利用率。
但是在作业输入和结果输出时主机的高速CPU仍处于空闲状态等待慢速的输入/输出设备完成工作: 主机处于“忙等”状态。
### 脱机批处理系统
为克服与缓解高速主机与慢速外设的矛盾提高CPU的利用率又引入了脱机批处理系统即输入/输出脱离主机控制。
![image-20210725220936340](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725220936340.png)
卫星机:一台不与主机直接相连而专门用于与输入/输出设备打交道的。
1. 从输入机上读取用户作业并放到输入磁带上。
2. 从输出磁带上读取执行结果并传给输出机
不足:每次主机内存中仅存放一道作业,每当它运行期间发出输入/输出I/O请求后高速的CPU便处于等待低速的I/O完成状态致使CPU空闲。
## 多道程序系统
### 多道程序设计技术
所谓多道程序设计技术就是指允许多个程序同时进入内存并运行。即同时把多个程序放入内存并允许它们交替在CPU中运行它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时CPU便立即转去运行另一道程序。
![image-20210725220944897](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725220944897.png)
在A程序计算时I/O空闲 A程序I/O操作时CPU空闲B程序也是同样必须A工作完成后B才能进入内存中开始工作两者是串行的全部完成共需时间=T1+T2。
![image-20210725220951109](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725220951109.png)
将A、B两道程序同时存放在内存中它们在系统的控制下可相互穿插、交替地在CPU上运行当A程序因请求I/O操作而放弃CPU时B程序就可占用CPU运行这样 CPU不再空闲而正进行A I/O操作的I/O设备也不空闲显然CPU和I/O设备都处于“忙”状态大大提高了资源的利用率从而也提高了系统的效率A、B全部完成所需时间<<T1+T2
单处理机系统中多道程序运行时的特点
1. 多道计算机内存中同时存放几道相互独立的程序
2. 宏观上并行同时进入系统的几道程序都处于运行过程中即它们先后开始了各自的运行但都未运行完毕
3. 微观上串行实际上各道程序轮流地用CPU并交替运行
多道程序系统的出现标志着操作系统渐趋成熟的阶段先后出现了作业调度管理处理机管理存储器管理外部设备管理文件系统管理等功能
由于多个程序同时在计算机中运行开始有了**空间隔离**的概念只有内存空间的隔离才能让数据更加安全稳定
出了空间隔离之外多道技术还第一次体现了**时空复用**的特点遇到IO操作就切换程序使得cpu的利用率提高了计算机的工作效率也随之提高
### 多道批处理系统
20世纪60年代中期在前述的批处理系统中引入多道程序设计技术后形成多道批处理系统简称批处理系统)。
 它有两个特点
1. 多道系统内可同时容纳多个作业这些作业放在外存中组成一个后备队列系统按一定的调度原则每次从后备作业队列中选取一个或多个作业进入内存运行运行作业结束退出运行和后备作业进入运行均由系统自动实现从而在系统中形成一个自动转接的连续的作业流
2. 成批在系统运行过程中不允许用户与其作业发生交互作用作业一旦进入系统用户就不能直接干预其作业的运行
- 批处理系统的追求目标提高系统资源利用率和系统吞吐量以及作业流程的自动化
- 批处理系统的一个重要缺点不提供人机交互能力给用户使用计算机带来不便
- 虽然用户独占全机资源并且直接控制程序的运行可以随时了解程序运行情况但这种工作方式因独占全机造成资源效率极低
- 一种新的追求目标既能保证计算机效率又能方便用户使用计算机 20世纪60年代中期计算机技术和软件技术的发展使这种追求成为可能
## 分时系统
由于CPU速度不断提高和采用分时技术一台计算机可同时连接多个用户终端而每个用户可在自己的终端上联机使用计算机好象自己独占机器一样
![image-20210725221001607](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725221001607.png)
![image-20210725221010282](02.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%8F%91%E5%B1%95%E5%8F%B2/image-20210725221010282.png)
分时技术把处理机的运行时间分成很短的时间片按时间片轮流把处理机分配给各联机作业使用
若某个作业在分配给它的时间片内不能完成其计算则该作业暂时中断把处理机让给另一作业使用等待下一轮时再继续其运行由于计算机速度很快作业运行轮转得很快给每个用户的印象是好象他独占了一台计算机而每个用户可以通过自己的终端向系统发出各种操作控制命令在充分的人机交互情况下完成作业的运行
具有上述特征的计算机系统称为分时系统它允许多个用户同时联机使用计算机
特点
1. 多路性若干个用户同时使用一台计算机微观上看是各用户轮流使用计算机宏观上看是各用户并行工作
2. 交互性用户可根据系统对请求的响应结果进一步向系统提出新的请求这种能使用户与系统进行人机对话的工作方式明显地有别于批处理系统因而分时系统又被称为交互式系统
3. 独立性用户之间可以相互独立操作互不干扰系统保证各用户程序运行的完整性不会发生相互混淆或破坏现象
4. 及时性系统可对用户的输入及时作出响应分时系统性能的主要指标之一是响应时间它是指从终端发出命令到系统予以应答所需的时间
**分时系统的主要目标**对用户响应的及时性即不至于用户等待每一个命令的处理时间过长
分时系统可以同时接纳数十个甚至上百个用户由于内存空间有限往往采用对换又称交换方式的存储方法即将未轮到的作业放入磁盘一旦轮到”,再将其调入内存而时间片用完后又将作业存回磁盘俗称滚进”、“滚出使同一存储区域轮流为多个用户服务
**注意分时系统的分时间片工作在没有遇到IO操作的时候就用完了自己的时间片被切走了这样的切换工作其实并没有提高cpu的效率反而使得计算机的效率降低了。但是我们牺牲了一点效率却实现了多个程序共同执行的效果这样你就可以在计算机上一边听音乐一边聊qq了。**
## 实时系统
虽然多道批处理系统和分时系统能获得较令人满意的资源利用率和系统响应时间但却不能满足实时控制与实时信息处理两个应用领域的需求于是就产生了实时系统即系统能够及时响应随机发生的外部事件并在严格的时间范围内完成对该事件的处理
实时系统在一个特定的应用中常作为一种控制设备来使用
**实时系统可分成两类**
1. 实时控制系统当用于飞机飞行导弹发射等的自动控制时要求计算机能尽快处理测量系统测得的数据及时地对飞机或导弹进行控制或将有关信息通过显示终端提供给决策人员当用于轧钢石化等工业生产过程控制时也要求计算机能及时处理由各类传感器送来的数据然后控制相应的执行机构
2. 实时信息处理系统当用于预定飞机票查询有关航班航线票价等事宜时或当用于银行系统情报检索系统时都要求计算机能对终端设备发来的服务请求及时予以正确的回答此类对响应及时性的要求稍弱于第一类
**实时操作系统的主要特点**
3. 及时响应每一个信息接收分析处理和发送的过程必须在严格的时间限制内完成
4. 高可靠性需采取冗余措施双机系统前后台工作也包括必要的保密措施等
分时——现在流行的PC服务器都是采用这种运行模式即把CPU的运行分成若干时间片分别处理不同的运算请求 linux系统
实时——一般用于单片机上PLC等比如电梯的上下控制中对于按键等动作要求进行实时处理
## 通用操作系统
操作系统的三种基本类型多道批处理系统分时系统实时系统
- 通用操作系统具有多种类型操作特征的操作系统可以同时兼有多道批处理分时实时处理的功能或其中两种以上的功能
- 例如实时处理+批处理=实时批处理系统。首先保证优先处理实时任务,插空进行批处理作业。常把实时任务称为前台作业,批作业称为后台作业。
- 再如分时处理+批处理=分时批处理系统。即:时间要求不强的作业放入“后台”(批处理)处理,需频繁交互的作业在“前台”(分时)处理,处理机优先运行“前台”作业。
从上世纪60年代中期国际上开始研制一些大型的通用操作系统这些系统试图达到功能齐全可适应各种应用范围和操作方式变化多端的环境的目标但是这些系统过于复杂和庞大不仅付出了巨大的代价且在解决其可靠性可维护性和可理解性方面都遇到很大的困难
相比之下UNIX操作系统却是一个例外这是一个通用的多用户分时交互型的操作系统它首先建立的是一个精干的核心而其功能却足以与许多大型的操作系统相媲美在核心层以外可以支持庞大的软件系统它很快得到应用和推广并不断完善对现代操作系统有着重大的影响
至此操作系统的基本概念功能基本结构和组成都已形成并渐趋完善
## 操作系统的进一步发展
进入20世纪80年代大规模集成电路工艺技术的飞跃发展微处理机的出现和发展掀起了计算机大发展大普及的浪潮一方面迎来了个人计算机的时代同时又向计算机网络分布式处理巨型计算机和智能化方向发展于是操作系统有了进一步的发展个人计算机操作系统网络操作系统分布式操作系统等
### 个人计算机操作系统
个人计算机上的操作系统是联机交互的单用户操作系统它提供的联机交互功能与通用分时系统提供的功能很相似
由于是个人专用因此一些功能会简单得多然而由于个人计算机的应用普及对于提供更方便友好的用户接口和丰富功能的文件系统的要求会愈来愈迫切
### 网络操作系统
计算机网络通过通信设施将地理上分散的具有自治功能的多个计算机系统互连起来实现信息交换资源共享互操作和协作处理的系统
网络操作系统在原来各自计算机操作系统上按照网络体系结构的各个协议标准增加网络管理模块其中包括通信资源共享系统安全和各种网络应用服务
### 分布式操作系统
表面上看分布式系统与计算机网络系统没有多大区别分布式操作系统也是通过通信网络将地理上分散的具有自治功能的数据处理系统或计算机系统互连起来实现信息交换和资源共享协作完成任务。——硬件连接相同
但有如下一些明显的区别
1. 分布式系统要求一个统一的操作系统实现系统操作的统一性
2. 分布式操作系统管理分布式系统中的所有资源它负责全系统的资源分配和调度任务划分信息传输和控制协调工作并为用户提供一个统一的界面
3. 用户通过这一界面实现所需要的操作和使用系统资源至于操作定在哪一台计算机上执行或使用哪台计算机的资源则是操作系统完成的用户不必知道此谓系统的透明性
4. 分布式系统更强调分布式计算和处理因此对于多机合作和系统重构坚强性和容错能力有更高的要求希望系统有更短的响应时间高吞吐量和高可靠性
## 操作系统的作用
现代的计算机系统主要是由一个或者多个处理器主存硬盘键盘鼠标显示器打印机网络接口及其他输入输出设备组成
一般而言现代计算机系统是一个复杂的系统
其一如果每位应用程序员都必须掌握该系统所有的细节那就不可能再编写代码了严重影响了程序员的开发效率全部掌握这些细节可能需要一万年....
其二并且管理这些部件并加以优化使用是一件极富挑战性的工作于是计算安装了一层软件系统软件称为操作系统它的任务就是为用户程序提供一个更好更简单更清晰的计算机模型并管理刚才提到的所有设备
**总结:**
1. 程序员无法把所有的硬件操作细节都了解到管理这些硬件并且加以优化使用是非常繁琐的工作这个繁琐的工作就是操作系统来干的有了他程序员就从这些繁琐的工作中解脱了出来只需要考虑自己的应用软件的编写就可以了应用软件直接使用操作系统提供的功能来间接使用硬件
2. 精简的说的话操作系统就是一个协调管理和控制计算机硬件资源和软件资源的控制程序

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,636 @@
# 多线程
## 操作系统线程理论
### 进程
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
### 线程
60年代在OS中能拥有资源和独立运行的基本单位是进程然而随着计算机技术的发展进程出现了很多弊端
1. 是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;
2. 是由于对称多处理机SMP出现可以满足多个运行单位而多个进程并行开销过大。
因此在80年代出现了能独立运行的基本单位——线程Threads
注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.每一个进程中至少有一个线程。 
### 进程和线程的关系
![image-20210725221459812](04.%E5%A4%9A%E7%BA%BF%E7%A8%8B/image-20210725221459812.png)
线程与进程的区别可以归纳为以下4点
1. 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2. 通信进程间通信IPC线程间可以直接读写进程数据段如全局变量来进行通信——需要进程同步和互斥手段的辅助以保证数据的一致性。
3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
4. 在多线程操作系统中,进程不是一个可执行的实体。
### 使用线程的实际场景
开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
### 内存中的线程
![image-20210725221509219](04.%E5%A4%9A%E7%BA%BF%E7%A8%8B/image-20210725221509219.png)
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程
2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?
因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。
## python使用线程
### 全局解释器锁GIL
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
  对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
  在多线程环境中Python 虚拟机按以下方式执行:
1. 设置 GIL
2. 切换到一个线程去运行;
3. 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0))
4. 把线程设置为睡眠状态;
5. 解锁 GIL
6. 再次重复以上所有步骤。
在调用外部代码(如 C/C++扩展函数)的时候GIL将会被锁定直到这个函数结束为止(由于在这期间没有Python的字节码被运行所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
### python线程模块的选择
Python提供了几个用于多线程编程的模块包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
避免使用thread模块因为更高级别的threading模块更为先进对线程的支持更为完善而且使用thread模块里的属性有可能会与threading出现冲突其次低级别的thread模块的同步原语很少(实际上只有一个)而threading模块则有很多再者thread模块中当主线程结束时所有的线程都会被强制结束掉没有警告也不会有正常的清除工作至少threading模块能确保重要的子线程退出后进程才退出。
thread模块不支持守护线程当主线程退出时所有的子线程不论它们是否还在工作都会被强行退出。而threading模块支持守护线程守护线程一般是一个等待客户请求的服务器如果没有客户提出请求它就在那等着如果设定一个线程为守护线程就表示这个线程是不重要的在进程退出的时候不用等待这个线程退出。
## threading模块
### 线程的创建
```python
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('aaron',))
t.start()
print('主线程')
```
另一种创建进程的方式
```python
from threading import Thread
import time
class Sayhi(Thread):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
time.sleep(2)
print('%s say hello' % self.name)
if __name__ == '__main__':
t = Sayhi('aaron')
t.start()
print('主线程')
```
### 多线程与多进程
```python
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello',os.getpid())
if __name__ == '__main__':
#part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
t1=Thread(target=work)
t2=Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid',os.getpid())
#part2:开多个进程,每个进程都有不同的pid
p1=Process(target=work)
p2=Process(target=work)
p1.start()
p2.start()
print('主线程/主进程pid',os.getpid())
```
效率对比
```python
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello')
if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()
print('主线程/主进程')
'''
打印结果:
hello
主线程/主进程
'''
#在主进程下开启子进程
t=Process(target=work)
t.start()
print('主线程/主进程')
```
内存数据共享
```python
from threading import Thread
from multiprocessing import Process
from threading import Thread
import os
def work():
global n
n=0
if __name__ == '__main__':
# n=100
# p=Process(target=work)
# p.start()
# p.join()
# print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
n=1
t=Thread(target=work)
t.start()
t.join()
print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
```
### 多线程实现socket
服务端
```python
import multiprocessing
import threading
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
def action(conn):
while True:
data=conn.recv(1024)
print(data)
conn.send(data.upper())
if __name__ == '__main__':
while True:
conn,addr=s.accept()
p=threading.Thread(target=action,args=(conn,))
p.start()
```
客户端
```python
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))
while True:
msg=input('>>: ').strip()
if not msg:continue
s.send(msg.encode('utf-8'))
data=s.recv(1024)
print(data)
```
### Thread类的其他方法
- Thread实例对象的方法
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
- threading模块提供的一些方法
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量与len(threading.enumerate())有相同的结果。
```python
from threading import Thread
import threading
from multiprocessing import Process
import os
def work():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()
print(threading.current_thread().getName())
print(threading.current_thread()) #主线程
print(threading.enumerate()) #连同主线程在内有两个运行的线程
print(threading.active_count())
print('主线程/主进程')
```
使用join
```python
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('aaron',))
t.start()
t.join()
print('主线程')
print(t.is_alive())
```
### 守护线程
无论是进程还是线程都遵循守护xx会等待主xx运行完毕后被销毁。需要强调的是运行完毕并非终止运行
1. 对主进程来说,运行完毕指的是主进程代码运行完毕
主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
2. 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
```python
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)
if __name__ == '__main__':
t=Thread(target=sayhi,args=('aaron',))
t.setDaemon(True) #必须在t.start()之前设置
t.start()
print('主线程')
print(t.is_alive())
```
```python
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(3)
print("end456")
if __name__ == '__main__':
t1=Thread(target=foo)
t2=Thread(target=bar)
t1.daemon=True
t1.start()
t2.start()
print("main")
```
## 锁
### 同步锁
没有锁的情况下
```python
from threading import Thread
import os,time
def work():
global n
temp=n
time.sleep(0.1)
n=temp-1
if __name__ == '__main__':
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n)
```
同步锁
```python
import threading
R=threading.Lock()
R.acquire()
'''
对公共数据的操作
'''
R.release()
```
```python
from threading import Thread,Lock
import os,time
def work():
global n
lock.acquire()
temp=n
time.sleep(0.1)
n=temp-1
lock.release()
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n) #结果肯定为0由原来的并发执行变成串行牺牲了执行效率保证了数据安全
```
```python
#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
global n
print('%s is running' %current_thread().getName())
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
```
```python
#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
#未加锁的代码并发运行
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
#加锁的代码串行运行
lock.acquire()
temp=n
time.sleep(0.5)
n=temp-1
lock.release()
if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
```
有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
```python
from threading import current_thread,Thread,Lock
import os,time
def task():
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
temp=n
time.sleep(0.5)
n=temp-1
if __name__ == '__main__':
n=100
lock=Lock()
start_time=time.time()
for i in range(100):
t=Thread(target=task)
t.start()
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n))
```
### 死锁与递归锁
两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为**死锁进程**
```python
from threading import Lock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire() # 上面已经拿过一次key了这边就拿不到了
print(123)
mutexA.release()
mutexA.release()
```
解决方法递归锁在Python中为了支持在同一线程中多次请求同一资源python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量counter记录了acquire的次数从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release其他的线程才能获得资源。上面的例子如果使用RLock代替Lock则不会发生死锁
```python
from threading import RLock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()
```
吃面的问题
```python
import time
from threading import Thread,Lock
noodle_lock = Lock()
fork_lock = Lock()
def eat1(name):
noodle_lock.acquire()
print('%s 抢到了面条'%name)
fork_lock.acquire()
print('%s 抢到了叉子'%name)
print('%s 吃面'%name)
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print('%s 抢到了叉子' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s 抢到了面条' % name)
print('%s 吃面' % name)
noodle_lock.release()
fork_lock.release()
for name in ['顾客1','顾客2','顾客3']:
t1 = Thread(target=eat1,args=(name,))
t2 = Thread(target=eat2,args=(name,))
t1.start()
t2.start()
```
使用递归锁解决问题
```python
import time
from threading import Thread,RLock
noodle_lock = fork_lock = RLock()
def eat1(name):
noodle_lock.acquire()
print('%s 抢到了面条'%name)
fork_lock.acquire()
print('%s 抢到了叉子'%name)
print('%s 吃面'%name)
fork_lock.release()
noodle_lock.release()
def eat2(name):
fork_lock.acquire()
print('%s 抢到了叉子' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s 抢到了面条' % name)
print('%s 吃面' % name)
noodle_lock.release()
fork_lock.release()
for name in ['顾客1','顾客2','顾客3']:
t1 = Thread(target=eat1,args=(name,))
t2 = Thread(target=eat2,args=(name,))
t1.start()
t2.start()
```
### 线程队列
queue队列 使用import queue用法与进程Queue一样
先进先出
```python
import queue
q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
```
先进后出
```python
import queue
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
```
优先级队列
```python
import queue
q=queue.PriorityQueue()
##put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))
print(q.get())
print(q.get())
print(q.get())
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,425 @@
# 多协程
## 协程理论
进程是资源分配的最小单位,线程是CPU调度的最小单位
无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。
随着我们对于效率的追求不断提高基于单线程来实现并发又成为一个新的课题即只用一个主线程很明显可利用的cpu只有一个情况下实现并发。这样就可以节省创建线进程所消耗的时间。
cpu正在运行一个任务会在两种情况下切走去执行其他的任务切换由操作系统强制控制一种情况是该任务发生了阻塞另外一种情况是该任务计算的时间过长
![image-20210725221729223](05.%E5%A4%9A%E5%8D%8F%E7%A8%8B/image-20210725221729223.png)
其中第二种情况并不能提升效率只是为了让cpu能够雨露均沾实现看起来所有任务都被“同时”执行的效果如果多个任务都是纯计算的这种切换反而会降低效率。
为此我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法
1. yield可以保存状态yield的状态保存与操作系统的保存线程状态很像但是yield是代码级别控制的更轻量级
2. send可以把一个函数的结果传给另外一个函数以此实现单线程内程序之间的切换
```python
#串行执行
import time
def consumer(res):
'''任务1:接收数据,处理数据'''
pass
def producer():
'''任务2:生产数据'''
res=[]
for i in range(10000000):
res.append(i)
return res
start=time.time()
#串行执行
res=producer()
consumer(res) #写成consumer(producer())会降低执行效率
stop=time.time()
print(stop-start) #1.5536692142486572
#基于yield并发执行
import time
def consumer():
'''任务1:接收数据,处理数据'''
while True:
x=yield
def producer():
'''任务2:生产数据'''
g=consumer()
next(g)
for i in range(10000000):
g.send(i)
start=time.time()
#基于yield保存状态,实现两个任务直接来回切换,即并发的效果
#PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的.
producer()
stop=time.time()
print(stop-start) #2.0272178649902344
```
第一种情况的切换。在任务一遇到io情况下切到任务二去执行这样就可以利用任务一阻塞的时间完成任务二的计算效率的提升就在于此。
```python
import time
def consumer():
'''任务1:接收数据,处理数据'''
while True:
x=yield
def producer():
'''任务2:生产数据'''
g=consumer()
next(g)
for i in range(10000000):
g.send(i)
time.sleep(2)
start=time.time()
producer() #并发执行,但是任务producer遇到io就会阻塞住,并不会切到该线程内的其他任务去执行
stop=time.time()
print(stop-start)
```
1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
2. 作为1的补充可以检测io操作在遇到io操作的情况下才发生切换
## 协程
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。、
1. python的线程属于内核级别的即由操作系统控制调度如单线程遇到io或执行时间过长就会被迫交出cpu执行权限切换其他线程运行
2. 单线程内开启协程一旦遇到io就会从应用程序级别而非操作系统控制切换以此来提升效率非io操作的切换与效率无关
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果最大限度地利用cpu
缺点:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程特点:
1. 必须在只有一个单线程里实现并发
2. 修改共享数据不需加锁
3. 用户程序里自己保存多个控制流的上下文栈
4. 附加一个协程遇到IO操作自动切换到其它协程如何实现检测IOyield、greenlet都无法实现就用到了gevent模块select机制
## Greenlet模块
pip install greenlet
```python
from greenlet import greenlet
def eat(name):
print('%s eat 1' %name)
g2.switch('aaron')
print('%s eat 2' %name)
g2.switch()
def play(name):
print('%s play 1' %name)
g1.switch()
print('%s play 2' %name)
g1=greenlet(eat)
g2=greenlet(play)
g1.switch('aaron') # 可以在第一次switch时传入参数以后都不需要
```
单纯的切换在没有io的情况下或者没有重复开辟内存空间的操作反而会降低程序的执行速度
```python
#顺序执行
import time
def f1():
res=1
for i in range(100000000):
res+=i
def f2():
res=1
for i in range(100000000):
res*=i
start=time.time()
f1()
f2()
stop=time.time()
print('run time is %s' %(stop-start))
#切换
from greenlet import greenlet
import time
def f1():
res=1
for i in range(100000000):
res+=i
g2.switch()
def f2():
res=1
for i in range(100000000):
res*=i
g1.switch()
start=time.time()
g1=greenlet(f1)
g2=greenlet(f2)
g1.switch()
stop=time.time()
print('run time is %s' %(stop-start))
```
greenlet只是提供了一种比generator更加便捷的切换方式当切到一个任务执行时如果遇到io那就原地阻塞仍然是没有解决遇到IO自动切换来提升效率的问题。
单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作我们完全可以在执行任务1时遇到阻塞就利用阻塞的时间去执行任务2。。。。如此才能提高效率这就用到了Gevent模块。
## Gevent模块
pip install gevent
Gevent 是一个第三方库可以轻松通过gevent实现并发同步或异步编程在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部但它们被协作式地调度。
用法介绍
```python
g1=gevent.spawn(func,1,2,3,x=4,y=5) # 创建一个协程对象g1spawn括号内第一个参数是函数名如eat后面可以有多个参数可以是位置实参或关键字实参都是传给函数eat的
g2=gevent.spawn(func2)
g1.join() #等待g1结束
g2.join() #等待g2结束
#或者上述两步合作一步gevent.joinall([g1,g2])
g1.value#拿到func1的返回值
```
```python
import gevent
def eat(name):
print('%s eat 1' %name)
gevent.sleep(2)
print('%s eat 2' %name)
def play(name):
print('%s play 1' %name)
gevent.sleep(1)
print('%s play 2' %name)
g1=gevent.spawn(eat,'aaron')
g2=gevent.spawn(play,name='aaron')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')
```
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面如timesocket模块之前
或者我们干脆记忆成要用gevent需要将from gevent import monkey;monkey.patch_all()放到文件的开头
```python
from gevent import monkey;monkey.patch_all()
import gevent
import time
def eat():
print('eat food 1')
time.sleep(2)
print('eat food 2')
def play():
print('play 1')
time.sleep(1)
print('play 2')
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print('主')
```
用threading.current_thread().getName()来查看每个g1和g2查看的结果为DummyThread-n即假线程
```python
from gevent import monkey;monkey.patch_all()
import threading
import gevent
import time
def eat():
print(threading.current_thread().getName())
print('eat food 1')
time.sleep(2)
print('eat food 2')
def play():
print(threading.current_thread().getName())
print('play 1')
time.sleep(1)
print('play 2')
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print('主')
```
## Gevent之同步与异步
```python
from gevent import spawn,joinall,monkey;monkey.patch_all()
import time
def task(pid):
"""
Some non-deterministic task
"""
time.sleep(0.5)
print('Task %s done' % pid)
def synchronous(): # 同步
for i in range(10):
task(i)
def asynchronous(): # 异步
g_l=[spawn(task,i) for i in range(10)]
joinall(g_l)
print('DONE')
if __name__ == '__main__':
print('Synchronous:')
synchronous()
print('Asynchronous:')
asynchronous()
# 上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。
# 初始化的greenlet列表存放在数组threads中此数组被传给gevent.joinall 函数,
# 后者阻塞当前流程并执行所有给定的greenlet任务。执行流程只会在 所有greenlet执行完后才会继续向下走。
```
## Gevent之应用举例一
```python
from gevent import monkey;monkey.patch_all()
import gevent
import requests
import time
def get_page(url):
print('GET: %s' %url)
response=requests.get(url)
if response.status_code == 200:
print('%d bytes received from %s' %(len(response.text),url))
start_time=time.time()
gevent.joinall([
gevent.spawn(get_page,'https://www.python.org/'),
gevent.spawn(get_page,'https://www.yahoo.com/'),
gevent.spawn(get_page,'https://github.com/'),
])
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))
```
## Gevent之应用举例二
通过gevent实现单线程下的socket并发
注意 from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前否则gevent无法识别socket的阻塞
服务端
```python
from gevent import monkey;monkey.patch_all()
from socket import *
import gevent
#如果不想用money.patch_all()打补丁,可以用gevent自带的socket
# from gevent import socket
# s=socket.socket()
def server(server_ip,port):
s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind((server_ip,port))
s.listen(5)
while True:
conn,addr=s.accept()
gevent.spawn(talk,conn,addr)
def talk(conn,addr):
try:
while True:
res=conn.recv(1024)
print('client %s:%s msg: %s' %(addr[0],addr[1],res))
conn.send(res.upper())
except Exception as e:
print(e)
finally:
conn.close()
if __name__ == '__main__':
server('127.0.0.1',8088)
```
客户端
```python
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
msg=input('>>: ').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
msg=client.recv(1024)
print(msg.decode('utf-8'))
```
多线程并发客户端
```python
from threading import Thread
from socket import *
import threading
def client(server_ip,port):
c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
c.connect((server_ip,port))
count=0
while True:
c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8'))
msg=c.recv(1024)
print(msg.decode('utf-8'))
count+=1
if __name__ == '__main__':
for i in range(500):
t=Thread(target=client,args=('127.0.0.1',8088))
t.start()
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB