first commit
849
03.网络编程与并发/01.网络编程基础.md
Normal file
@@ -0,0 +1,849 @@
|
||||
# 网络编程基础
|
||||
|
||||
## 操作系统基础
|
||||
|
||||
操作系统:(Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。
|
||||
|
||||

|
||||
|
||||
操作系统应该分成两部分功能
|
||||
|
||||
1. 隐藏了丑陋的硬件调用接口(键盘、鼠标、音箱等等怎么实现的,就不需要你管了),为应用程序员提供调用硬件资源的更好,更简单,更清晰的模型(系统调用接口)。应用程序员有了这些接口后,就不用再考虑操作硬件的细节,专心开发自己的应用程序即可。
|
||||
例如:操作系统提供了文件这个抽象概念,对文件的操作就是对磁盘的操作,有了文件我们无需再去考虑关于磁盘的读写控制(比如控制磁盘转动,移动磁头读写数据等细节),
|
||||
|
||||
2. 将应用程序对硬件资源的竞态请求变得有序化
|
||||
例如:很多应用软件其实是共享一套计算机硬件,比方说有可能有三个应用程序同时需要申请打印机来输出内容,那么a程序竞争到了打印机资源就打印,然后可能是b竞争到打印机资源,也可能是c,这就导致了无序,打印机可能打印一段a的内容然后又去打印c...,操作系统的一个功能就是将这种无序变得有序。
|
||||
|
||||
## socket
|
||||
|
||||
回顾一下五层通讯流程
|
||||
|
||||

|
||||
|
||||
但实际上从传输层开始以及以下,都是操作系统帮咱们完成的,下面的各种包头封装的过程
|
||||
|
||||

|
||||
|
||||
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对比
|
||||
|
||||
- TCP(Transmission Control Protocol)
|
||||
- 可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;文件传输程序。
|
||||
|
||||
- UDP(User Datagram Protocol)
|
||||
- 不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
|
||||
|
||||
### TCP协议下的socket
|
||||
|
||||

|
||||
|
||||
服务器端先初始化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是无链接的,先启动哪一端都不会报错**
|
||||
|
||||

|
||||
|
||||
服务器端先初始化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'))
|
||||
```
|
||||
|
||||
## 粘包
|
||||
|
||||

|
||||
|
||||
每个 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
|
||||
|
||||

|
||||
|
||||
```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()
|
||||
```
|
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220525763.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220535013.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220541285.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220552513.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220612103.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220622753.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220635963.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220641602.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220708133.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220726010.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
03.网络编程与并发/01.网络编程基础/image-20210725220750620.png
Normal file
After Width: | Height: | Size: 102 KiB |
174
03.网络编程与并发/02.操作系统发展史.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 操作系统发展史
|
||||
|
||||
## 手工操作 —— 穿孔卡片
|
||||
|
||||
1946年第一台计算机诞生--20世纪50年代中期,计算机工作还在采用手工操作方式。此时还没有操作系统的概念。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把程序和数据输入计算机内存,接着通过控制台开关启动程序针对数据运行;计算完毕,打印机输出计算结果;用户取走结果并卸下纸带(或卡片)后,才让下一个用户上机。
|
||||
|
||||
手工操作方式两个特点:
|
||||
|
||||
1. 用户独占全机。不会出现因资源已被其他用户占用而等待的现象,但资源的利用率低。
|
||||
2. CPU 等待手工操作。CPU的利用不充分。
|
||||
|
||||
20世纪50年代后期,出现人机矛盾:手工操作的慢速度和计算机的高速度之间形成了尖锐矛盾,手工操作方式已严重损害了系统资源的利用率(使资源利用率降为百分之几,甚至更低),不能容忍。唯一的解决办法:只有摆脱人的手工操作,实现作业的自动过渡。这样就出现了成批处理。
|
||||
|
||||
## 批处理 —— 磁带存储
|
||||
|
||||
批处理系统:加载在计算机上的一个系统软件,在它的控制下,计算机能够自动地、成批地处理一个或多个用户的作业(这作业包括程序、数据和命令)。
|
||||
|
||||
### 联机批处理系统
|
||||
|
||||
首先出现的是联机批处理系统,即作业的输入/输出由CPU来处理。
|
||||
|
||||

|
||||
|
||||
主机与输入机之间增加一个存储设备——磁带,在运行于主机上的监督程序的自动控制下,计算机可自动完成:成批地把输入机上的用户作业读入磁带,依次把磁带上的用户作业读入主机内存并执行并把计算结果向输出机输出。完成了上一批作业后,监督程序又从输入机上输入另一批作业,保存在磁带上,并按上述步骤重复处理。
|
||||
监督程序不停地处理各个作业,从而实现了作业到作业的自动转接,减少了作业建立时间和手工操作时间,有效克服了人机矛盾,提高了计算机的利用率。
|
||||
但是,在作业输入和结果输出时,主机的高速CPU仍处于空闲状态,等待慢速的输入/输出设备完成工作: 主机处于“忙等”状态。
|
||||
|
||||
### 脱机批处理系统
|
||||
|
||||
为克服与缓解:高速主机与慢速外设的矛盾,提高CPU的利用率,又引入了脱机批处理系统,即输入/输出脱离主机控制。
|
||||
|
||||

|
||||
|
||||
卫星机:一台不与主机直接相连而专门用于与输入/输出设备打交道的。
|
||||
|
||||
1. 从输入机上读取用户作业并放到输入磁带上。
|
||||
2. 从输出磁带上读取执行结果并传给输出机
|
||||
|
||||
不足:每次主机内存中仅存放一道作业,每当它运行期间发出输入/输出(I/O)请求后,高速的CPU便处于等待低速的I/O完成状态,致使CPU空闲。
|
||||
|
||||
## 多道程序系统
|
||||
|
||||
### 多道程序设计技术
|
||||
|
||||
所谓多道程序设计技术,就是指允许多个程序同时进入内存并运行。即同时把多个程序放入内存,并允许它们交替在CPU中运行,它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。
|
||||
|
||||

|
||||
|
||||
在A程序计算时,I/O空闲, A程序I/O操作时,CPU空闲(B程序也是同样);必须A工作完成后,B才能进入内存中开始工作,两者是串行的,全部完成共需时间=T1+T2。
|
||||
|
||||

|
||||
|
||||
将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速度不断提高和采用分时技术,一台计算机可同时连接多个用户终端,而每个用户可在自己的终端上联机使用计算机,好象自己独占机器一样。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
分时技术:把处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业使用。
|
||||
若某个作业在分配给它的时间片内不能完成其计算,则该作业暂时中断,把处理机让给另一作业使用,等待下一轮时再继续其运行。由于计算机速度很快,作业运行轮转得很快,给每个用户的印象是,好象他独占了一台计算机。而每个用户可以通过自己的终端向系统发出各种操作控制命令,在充分的人机交互情况下,完成作业的运行。
|
||||
具有上述特征的计算机系统称为分时系统,它允许多个用户同时联机使用计算机。
|
||||
|
||||
特点:
|
||||
|
||||
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. 精简的说的话,操作系统就是一个协调、管理和控制计算机硬件资源和软件资源的控制程序。
|
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725220908985.png
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725220917981.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725220928953.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725220936340.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725220944897.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725220951109.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725221001607.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
03.网络编程与并发/02.操作系统发展史/image-20210725221010282.png
Normal file
After Width: | Height: | Size: 190 KiB |
1291
03.网络编程与并发/03.多进程.md
Normal file
BIN
03.网络编程与并发/03.多进程/image-20210725221132043.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
03.网络编程与并发/03.多进程/image-20210725221139272.png
Normal file
After Width: | Height: | Size: 207 KiB |
636
03.网络编程与并发/04.多线程.md
Normal file
@@ -0,0 +1,636 @@
|
||||
# 多线程
|
||||
|
||||
## 操作系统线程理论
|
||||
|
||||
### 进程
|
||||
|
||||
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
|
||||
|
||||
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
|
||||
|
||||
### 线程
|
||||
|
||||
60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端
|
||||
|
||||
1. 是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;
|
||||
2. 是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
|
||||
|
||||
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
|
||||
注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.每一个进程中至少有一个线程。
|
||||
|
||||
### 进程和线程的关系
|
||||
|
||||

|
||||
|
||||
线程与进程的区别可以归纳为以下4点:
|
||||
|
||||
1. 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
|
||||
2. 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
|
||||
3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
|
||||
4. 在多线程操作系统中,进程不是一个可执行的实体。
|
||||
|
||||
### 使用线程的实际场景
|
||||
|
||||
开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
|
||||
|
||||
### 内存中的线程
|
||||
|
||||

|
||||
|
||||
线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
|
||||
|
||||
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())
|
||||
```
|
BIN
03.网络编程与并发/04.多线程/image-20210725221459812.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
03.网络编程与并发/04.多线程/image-20210725221509219.png
Normal file
After Width: | Height: | Size: 15 KiB |
425
03.网络编程与并发/05.多协程.md
Normal file
@@ -0,0 +1,425 @@
|
||||
# 多协程
|
||||
|
||||
## 协程理论
|
||||
|
||||
进程是资源分配的最小单位,线程是CPU调度的最小单位
|
||||
|
||||
无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。
|
||||
|
||||
随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。
|
||||
|
||||
cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长
|
||||
|
||||

|
||||
|
||||
其中第二种情况并不能提升效率,只是为了让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操作自动切换到其它协程(如何实现检测IO,yield、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) # 创建一个协程对象g1,spawn括号内第一个参数是函数名,如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()必须放到被打补丁者的前面,如time,socket模块之前
|
||||
|
||||
或者我们干脆记忆成:要用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()
|
||||
```
|
BIN
03.网络编程与并发/05.多协程/image-20210725221729223.png
Normal file
After Width: | Height: | Size: 23 KiB |