网络编程-学习笔记整理

ip地址

IP地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。
用来在标记网络中一台电脑,ip地址在本地局域网上是唯一的。

ip地址分类

每一个IP地址包括两部分:网络地址主机地址
在这里插入图片描述

  • A类IP地址
    一个A类IP地址由1字节的网络地址3字节主机地址组成,网络地址的最高位必须是“0”
    地址范围1.0.0.1-126.255.255.254
    二进制表示为:
    00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110
    可用的A类网络有126个,每个网络能容纳1677214个主机

  • B类IP地址
    一个B类IP地址由2个字节的网络地址2个字节的主机地址组成,网络地址的最高位必须是“10”
    地址范围128.1.0.1-191.255.255.254
    二进制表示为:
    10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110
    可用的B类网络有16384个,每个网络能容纳65534主机

  • C类IP地址
    一个C类IP地址由3字节的网络地址1字节的主机地址组成,网络地址的最高位必须是“110”
    地址范围192.0.1.1-223.255.255.254
    二进制表示为:
    11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110
    C类网络可达2097152个,每个网络能容纳254个主机

  • D类地址
    D类IP地址第一个字节以“1110”开始,它是一个专门保留的地址。
    它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)
    多点广播地址用来一次寻址一组计算机 s
    地址范围 224.0.0.1-239.255.255.254

  • E类IP地址
    以“1111”开始,为将来使用保留
    E类地址保留,仅作实验和开发用

  • 私有ip
    部分IP地址是用于局域网使用,不在公网中使用,属于私有ip
    地址范围
    10.0.0.0~10.255.255.255
    172.16.0.0~172.31.255.255
    192.168.0.0~192.168.255.255
    127.0.0.1可以代表本机IP地址,通常使用http://127.0.0.1测试本机配置的Web服务器

端口

“端口”(port),即设备与外界通讯交流的出口。
端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见。
物理端口又称为接口,可见计算机背板的RJ45网口,交换机路由器集线器等RJ45端口。
端口号是端口的标记,端口号只有整数范围是从0到65535(即 2的16次方)
TCP/IP协议端口分配规则与分类

知名端口(Well Known Ports)
即一些众所周知的端口号,范围从0到1023
HTTP : 80
FTP: 21
通常使用知名端口需要有root权限

动态端口(Dynamic Ports)
顾名思义,是一类动态进行分配的端口,动态分配指当一个系统程序或应用程序需要进行网络通信时,向主机申请一个端口,主机从可用的端口号中分配一个供它使用。
当这个程序关闭时,同时也就释放了所占用的端口号
动态端口的范围是从1024到65535
通过 终端的 netstat -an 命令可以查看端口状态
一台拥有IP地址的主机可以提供许多服务,通过**“IP地址+端口号”来区分不同的服务**。但端口并不一一对应的,例如:你的电脑作为客户机访问一台WWW服务器时,WWW服务器使用“80”端口与你的电脑通信,但你的电脑则可能使用“3457”这样的端口。

Socket(套接字)简介

在本地可以通过进程PID唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的**“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程**了,网络中的进程通信(进程间通信指的是:运行的程序之间的数据共享)就可以利用这个标志与其它进程进行交互。
Socket参考

Socket=(IP地址:端口号)是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号。Socket是进程间通信的一种方式,它能实现不同主机间的进程间通信

参数说明:

  • Address Family
    • AF_INET(用于 Internet 进程间通信,常用)
    • AF_UNIX(用于同一台机器进程间通信)
  • Type(套接字类型)
    • SOCK_STREAM(流式套接字,主要用于 TCP 协议)
    • SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)

创建socket
python中可以通过socket模块提供的socket函数来实现

import socket
socket.socket(AddressFamily, Type)

套接字使用流程 与 文件的使用流程很类似
1 创建套接字
2 使用套接字收/发数据
3 关闭套接字

import socket
# 创建tcp_套接字
tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
...
# 使用完后,关闭套接字
tcp_socket.close()
inport socket
# 创建udp套接字
udp_socket = socket.socket(socket.AF_INET,SOCK_DGRAM)
...
# 使用完后,关闭套接字
udp_socket.close()

tcp和udp详细介绍
udp网络程序-发送数据
具体步骤如下:

  • 创建客户端套接字
  • 发送/接收数据

发送数据

  • udp_socket.sendto(发送内容, 对方的ip及port)
import socket
# 创建一个udp套接字 
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 发送数据
# 注意,对方ip及port需要以元组方式传递,ip为str,而port为int
udp_socket.sendto(b'hello',('ip',port))
# 关闭套接字
udp_socket.close()

**注意:**在发送信息时,需要使用bytes类型,否则会报错如下:
在这里插入图片描述
解决方法有2中

  • 解决方法1
    在这里插入图片描述
  • 解决方法2
send_data = 'hello'
dest_addr = ('ip',port)
udp_socket.sendto(send_data.encode('utf-8'),dest_addr)

在这里插入图片描述
需求:实现循环发送消息的udp网络程序

import socket

def main():
	# 创建udp套接字
	udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
	# 循环发送数据
	while True:
		send_data = input('请输入要发送的内容:')
		# 若输入信息为exit,则退出程序
		if send_data == 'exit':
			break
		dest_addr = ('发送目标ip',port)
		udp_socket.sendto(send_data.encode('utf-8'),dest_addr)
	# 关闭套接字
	udp_socket.close()
	
if __name__ == '__main__':
	main()

在这里插入图片描述

接收数据
recv_data = udp_socket.recvfrom(1024) # 1024表示本次接收的最大字节数
recv_data 这个变量中接收到的是一个元组(接收到的数据,(发送方的ip,端口)) 第1个元素是对方发送的数据,第2个元素是对方的ip和端口,由于发送过程中使用bytes类型,因此需要decode解码,才会显示发送信息的原有数据

udp网络程序-发送、接收数据

import socket

def main():
	# 创建udp套接字
	udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
	# 准备接收方ip和port,假设为 192.168.159.130:8080
	dest_ip = input('请输入要发送的目标ip:')
	dest_port = int(input('请输入要发送的目标port:')) # input为str,而port需要int
	while True:
		# 获取要发送的数据
		send_data = input('请输入要发送的数据:')
		# 发送数据 (ip,port)元组形式
		udp_socket.sendto(send_data.encode('utf-8'),(dest_ip,dest_port))
		# 接收返回数据
		recv_data = udp_socket.recvfrom(1024)
		# 输出接收到的数据,注意decode
		print(recv_data[0].decode('gbk'))
		# 输出对方ip和port的元组
		print(recv_data[1])
		# 关闭套接字
	udp_socket.close()

if __name__=='__main__':
	main()

通过网络调试助手测试,使用ip和port为:192.168.236.128:8080
在这里插入图片描述
在这里插入图片描述
绑定固定端口来接收数据
上述程序运行时,会发现port每次都不同,这是因为当程序重新运行时,如果没有固定端口信息,系统默认会随机分配。一般服务性的程序,往往需要一个固定的端口号,避免与其他的网络程序占用同一个端口号。
ip在同一个网络中不允许相同,port在同一台计算机中不允许相同
在这里插入图片描述
端口绑定

import socket 
# 创建套接字
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 绑定本地信息
local_addr = ('',7788) # ip为空,表示本机的任何一个ip
# 绑定一个固定的端口号
udp_socket.bind(local_addr)
# 接收数据
recv_data = udp_socket.recvfrom(1024)
# 输出获取的数据
# [(ip,port)]:接收到的信息
print('[%s]:%s' % (str(recv_data[1]),recv_data[0].decode('gbk')))
# 关闭套接字
udp_socket.close()

在这里插入图片描述
单工、半双工、全双工
单工:一方只能发信息,另一方则只能收信息,单向通信。
半双工:双方都能发信息,同一时间则只能一方发信息
全双工:双方不仅都能发信息,而且能够同时发送

udp聊天器
需求:
在一个电脑中编写1个程序,有2个功能

  • 获取键盘数据,并将其发送给对方
  • 接收数据并显示
  • 并且可以任意选择以上的2个不同功能调用
import socket

# 发送数据
def send_msg(udp_socket):
	dest_ip = input('请输入发送ip地址:')
	dest_port = int(input('请输入发送port:'))
	send_data = input('请输入发送的信息')
	# 发送数据
	udp_socket.sendto(send_data.encoding('utf-8'),(dest_ip,dest_port))

# 接收数据
def recv_msg(udp_socket):
	# 接收数据最大长度1024
	recv_data = udp_socket.recvfrom(1024)
	# 输出接收到的数据
	print('[%s]:%s' % (str(recv_data[1]),recv_data[0].decode('gbk')))
		
def main():
	# 创建套接字
	udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
	# 绑定本地信息
	udp_socket.bind('',7788)
	while True:
		# 进行功能选择
		option = input('请选择功能:1.发送数据 2.接收数据 0.退出')
		if option == '1':
			send_msg(udp_socket)
		elif option == '2':
			recv_msg(udp_socket)
		elif option == '0':
			break
		else:
			print('没有这个功能,请重新选择')
	# 关闭套接字
	upd_socket.close()

if __name__ =='__main__':
	main()

在这里插入图片描述

TCP(Transmission Control Protocol,TCP)

TCP协议,传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过创建连接、数据传送、终止连接三个步骤。
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据

TCP特点
通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输。
双方间的数据传输都可以通过这一个连接进行。
完成数据交换后,双方必须断开此连接,以释放系统资源。
这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议。

可靠传输

  • TCP采用发送应答机制
    TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功
  • 超时重传
    发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。
    TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
  • 错误校验
    TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
  • 流量控制和阻塞管理
    流量控制用来避免主机发送得过快而使接收方来不及完全收下。

udp通信模型
udp通信模型中,在通信开始之前,不需要建立相关的链接,只需要发送数据即可
在这里插入图片描述
TCP通信模型
udp通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据
在这里插入图片描述
TCP与UDP的不同点
面向连接(确认有创建三次握手,连接已创建才作传输。)

  • 有序数据传输
  • 重发丢失的数据包
  • 舍弃重复的数据包
  • 无差错的数据传输
  • 阻塞/流量控制

tcp客户端收、发数据构建

import socket
# 创建tcp套接字
tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 获取服务器ip port
server_ip = input('请输入服务器ip:')
server_port = int(input('请输入服务器port:'))
# 连接服务器
tcp_socket.connect((server_ip,server_port))
# 获取发送内容
send_data = input('请输入要发送的数据:')
# 发送数据
tcp_socket.send(send_data.encode('utf-8'))
# 接收数据
recv_data = tcp_socket.recv(1024)
print('接收到的数据为:%s' % recv_data.decode('gbk'))
# 关闭套接字
tcp_socket.close()

在这里插入图片描述

注意:tcp接收数据与upd不同的中无服务器地址,只有接收内容,因此只需要对内容进行解码
udp

 recv_data = udp_socket.recvfrom(1024)
 # [(ip,port)]:内容
 print('[%s]:%s' % (str(recv_data[1]),recv_data[0].decode('gbk/utf-8')))

tcp

recv_data = tcp_socket.recv(1024)
# 只有内容
print('接收到的数据为:%s' % recv_data.decode('gbk/utf-8'))

tcp服务器
流程如下:

  • socket创建一个套接字

  • bind绑定ip和port

  • listen使套接字变为可以被动链接 listen参数解释

  • accept等待客户端的链接

  • recv/send接收发送数据

import socket
# 创建socket
tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 绑定本地信息
tcp_server_socket.bind(('',port))
# 将socket由主动变为被动,使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
# listen(n)传入的值, n表示的是服务器拒绝(超过限制数量的)连接之前,操作系统可以挂起的最大连接数量。n也可以看作是"排队的数量"
tcp_server_socket.listen(128)
# accept()产生新的套接字,并为其服务,返回为一个套接字对象和地址端口信息的元组
client_socket,client_addr = tcp_server_socket.accept()
# 接收客户端发送请求
recv_data = client_socket.recv(1024) 
print('接收到的数据是',recv_data.decode('gbk'))
# 客户端发送数据
client_socket.send('Thank you !'.encode('gbk'))
# 关闭套接字
client_socket.close()
tcp_server_socket.close()

在这里插入图片描述
在这里插入图片描述
tcp注意点

  • tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
  • tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机
  • tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
  • 当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
  • 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
  • listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
  • 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信
  • 关闭accept返回的套接字意味着这个客户端已经服务完毕
  • 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
    案例:循环为多个客户端服务,并且每个客户端服务多次,并判断是否为该客户服务结束
import socket
def main():
	# 创建套接字
	tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	# 绑定本地信息
	tcp_server_socket.bind(('',tcp_server_port))
	# listen由主动变被动
	tcp_server_socket.listen(128)
	# accept等待客户端连接
	while True:
		print('---等待新客户端链接---')
		client_socket,client_addr = tcp_server_socket.accept()
		print('---%s 已链接---' % str(client_addr))
		while True:
			# 接收客户端发送的请求
			recv_data = client_socket.recv(1024)
			print('接收到的数据为:' recv_data.decode('gbk'))
			# 判断客户端是否下线
			if recv_data:
				# 发送数据给服务器
				client_socket.send('Thank you '.encode('gbk'))
			else:
				break
		# 关闭客户端
		client_socket.close()
		print('服务结束')
	tcp_server_socket.close()

if __name__=='__main__':
	main()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
案例:文件下载器

# 客户端
import socket
def main():
	# 创建client套接字
	tcp_client_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	# 获取服务器ip port
	tcp_server_ip = input('请输入服务器ip:')
	tcp_server_port = int(input('请输入服务器port:'))
	# 连接服务器
	tcp_client.connect((tcp_server_ip,tcp_server_port))
	# 获取下载文件名
	download_filename = input('请输入要下载的文件名:')
	# 将下载文件名发送给服务器
	tcp_client_socket.send(download_filename.encode('gbk'))
	# 接收下载文件数据
	recv_data = tcp_client_socket.recv(1024*1024) # 1024为1K 
	# 写入下载文件数据
	with open('[附件]'+download_filename,'wb') as f:
		f.write(recv_data)
	# 关闭套接字
	tcp_client_socket.close()

if __name__=='__name__':
	main()
# 服务器端
import socket

def get_file_content(file_name):
	"""获取文件内容"""
	trywith open(file_name,'rb') as f:
			content = f.read()
		return content
	exception:
		print('没有所要下载的文件:%s' % file_name)

def main():
	# 创建server套接字
	tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	# 绑定本地信息
	tcp_server_socket.bind(('',port))
	# listen主动变为被动
	tcp_server_socket.listen(128)
	# 等待客户端链接
	while True:
		client_socket,client_addr = tcp_server_socket.accept()
		print('---%s 已连入---' % str(client_addr))
		while True:
			# 接收对方发送的数据
			recv_data = client_socket.recv(1024)
			# 判断是否该客户端已离线
			if recv_data:
				# 获取文件名
				file_name = recv_data.decode('utf-8')
				print('客户端[%s]要下载的文件名为:%s' % (str(client_addr),file_name)
				# 获取下载文件内容
				file_content = get_file_content(file_name)
				if file_content:
					# 发送文件给客户端
					# 由于获取文件内容时,采用二进制格式获取,因此不需要encode
					client_socket.send(file_content)
				else:
					print('该文件为空白文件')
				# 关闭客户端套接字
				client_socket.close()
				print('下载完成')
			else:
				break
		# 关闭监听套接字
		tcp_server_socket.close()

if __name__=='__main__':
	main()

多任务

操作系统可以同时运行多个任务。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。即 时间片轮转。真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
并发:指的是任务数多余cpu核数
通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的

进程是资源分配的最小单位,线程则是CPU调度的最小单位

线程参考

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
python提供了threading模块,可以实现一个多线程的程序
threading 提供了两种调用方式:

  • 直接调用
# 线程例子
import threading
import time
def sing():
	for i in range(3):
		print('%d 正在唱歌...' % i)
		time.sleep(1)

def dance():
	for i in range(3):
		print('%d 正在跳舞...' % i)
		time.sleep(1)

if __name__=='__main__':
	# 创建线程
	t1 = threading.Thread(target = sing)
	t2 = threading.Thread(target = dance)
	# 开启线程
	# 注意:当调用start()时,才会真正的创建线程,并且开始执行
	t1.start()
	t2.start()
	

当调用Thread()只是创建一个对象,而当实例对象调用start()时,才会真正的创建线程,并且开始执行
enumerate() 获取当前运行程序中所有线程

import threading
from time import sleep,ctime
def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)
def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)
if __name__ == '__main__':
    print('---开始---:%s'%ctime())
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()
    while True:
        length = len(threading.enumerate())
        print('当前运行的线程数为:%d'%length)
        if length<=1:
            break
        sleep(0.5)
  • 继承调用
import threading
import time
# 必须继承threading.Thread类
class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
            print(msg)
if __name__ == '__main__':
    t = MyThread()
	t.start()

python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
通过类创建线程时,当通过类创建的实例对象调用start方法时,实质上是调用重写的run(),而其他封装在类里的方法不会被调用,若要实现调用,只需在run()方法中调用封装在类中的其他方法即可。

多线程-共享全局变量
python中的值是通过引用(地址值)传递的,不可变类型(数字、元组、字符串、bool)的值一旦被修改后会创建一个内存空间并且生成新的地址值,可变类型(列表、字典)的值会在原内存空间中被修改

import threading 
import time
# 定义一个全局变量
g_num = 100 

def test1():
	global g_num # 声明全局变量
	g_num += 1
	print('---in test1 g_num=%d' % g_num)
	
def test2():
	print('---in test2 g_num=%d' % g_num)
	
def main():
	t1 = threading.Thread(target=test1)
	t2 = threading.Thread(target=test2)
	t1.start()
	time.sleep(1)
	t2.start()
	time.sleep(2)
	print('---in main thread g_num=%d' % g_num)

if __name__=='__main__':
	main()

在这里插入图片描述

  • 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
  • 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
# 资源竞争
import threading 
import time
# 定义一个全局变量
g_num = 0

def test1(num):
	global g_num 
	for i in range(num):
		g_num += 1
	print('---in test1 g_num=%d' % g_num)
	
def test2(num):
	global g_num 
	for i in range(num):
		g_num += 1
	print('---in test2 g_num=%d' % g_num)
	
def main():
	t1 = threading.Thread(target=test1,args=(100,)) # 传入参数
	t2 = threading.Thread(target=test2,args=(100,))
	t1.start()
	t2.start()
	# 等待2个线程执行完毕
	time.sleep(5)
	print('---in main thread g_num=%d' % g_num)

if __name__=='__main__':
	main()

图中第二个应该为test2
当num = 100000时候
图中第二个应该为test2
如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确,通过线程同步可以解决以上问题
同步
同步就是协同步调,按预定的先后次序进行运行。
互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()

注意:

  • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
# 互斥锁解决资源竞争
import threading 
import time
# 定义一个全局变量
g_num = 0

def test1(num):
	global g_num 
	mutex.acquire() # 上锁
	for i in range(num):
		
		g_num += 1
	mutex.release() # 解锁
	print('---in test1 g_num=%d' % g_num)
	
def test2(num):
	global g_num 
	mutex.acquire() # 上锁
	for i in range(num):
		g_num += 1
	mutex.release() # 解锁
	print('---in test2 g_num=%d' % g_num)
	

if __name__=='__main__':
	# 创建锁 默认是未上锁
	mutex = threading.Lock()
	t1 = threading.Thread(target=test1,args=(1000000,)) # 传入参数
	t2 = threading.Thread(target=test2,args=(1000000,))
	t1.start()
	t2.start()
	# 等待2个线程执行完毕
	time.sleep(3)
	print('---in main thread g_num=%d' % g_num)

在这里插入图片描述
在这里插入图片描述
锁的好处:

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
    由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

死锁

  • 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。
  • 解决死锁问题 银行家算法

案例:实现多任务udp聊天器
在这里插入图片描述
进程
一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元
进程的状态
在这里插入图片描述

  • 就绪态:运行的条件都已经慢去,正在等在cpu执行
  • 执行态:cpu正在执行其功能
  • 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态

进程的创建-multiprocessing
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动

import multiprocessing
import time 
def test1():
	while True:
		print('---1---')
		time.sleep(1)
def test2():
	while True:
		print('---2---')
		time.sleep(1)
def main():
	p1 = multiprocessing.Process(target=test1)
	p2 = multiprocessing.Process(target=test2)
	p1.start()
	p2.start()

if __name__=='__main__':
	main()

在这里插入图片描述
进程pid
linux 终端中 ps 查看进程,ps -aux.
kill +pid 关闭相应进程
在这里插入图片描述
在这里插入图片描述
Process语法结构如下:
Process([group [, target [, name [, args [, kwargs]]]]])

  • target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
  • args:给target指定的函数传递的参数,以元组的方式传递
  • kwargs:给target指定的函数传递命名参数
  • name:给进程设定一个名字,可以不设定
  • group:指定进程组,大多数情况下用不到

Process创建的实例对象的常用方法:

  • start():启动子进程实例(创建子进程)
  • is_alive():判断进程子进程是否还在活着
  • join([timeout]):是否等待子进程执行结束,或等待多少秒
  • terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:

  • name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
  • pid:当前进程的pid(进程号)

在这里插入图片描述
在这里插入图片描述
进程间不共享全局变量

import multiprocessing
import os
import time

# 定义一个全局变量
num = [11,22]

def test1():
	print('in process1 --pid = %d,name=%s,nums=%s' % (os.getpid(),nums))
	for i in range(3):
		num.append(i)
		print('--in process1 --pid = %d,name=%s,nums=%s' % (os.getpid(),nums))
def test2():
	print('in process2 --pid = %d,name=%s,nums=%s' % (os.getpid(),nums))

def main():
	p1 = multiprocessing.Process(target=test1)
	p1.start()
	p1.join() # 等待子进程1执行结束
	p2 = multiprocessing.Process(target=test2)
	p2.start()

if __name__=='__main__':
	main()

在这里插入图片描述
进程、线程对比
进程是资源分配的最小单位,线程则是CPU调度的最小单位

  • 进程是系统进行资源分配和调度的一个独立单位.
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
  • ·一个程序至少有一个进程,一个进程至少有一个线程.
  • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
  • 线程不能够独立执行,必须依存在进程中

可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

进程间通信-Queue
Queue的使用
可以使用multiprocessing模块的Queue实现多进程之间的数据传递,Queue本身是一个消息列队程序
在这里插入图片描述

在这里插入图片描述
案例:模拟从网上 下载数据

import multiprocessing

def download_fromweb(q):
	"""模拟从网上下载的数据"""
	data = [11,2233,44]
	# 向队列中写入数据
	for d in data:
		q.put(d)
		print('%d 下载成功' % d)
	print('数据下载完成,并成功写入队列')

def analysis_data(q):
	"""处理数据"""
	temp = list()
	# 从队列中获取数据
	while True:
		data = q.get()
		temp.append(data)
		print('%d 已被成功写入' % data)
		# 判断是否数据已全部从队列取出
		if q.empty():
			break
	print('数据写入成功,写入数据为%d ' % temp)

def main():
	# 创建一个队列
	q = multiprocessing.Queue()
	# 创建多进程来下载和处理数据
	p1 = multiprocessing.Process(target=download_fromweb,args=(q,)) # 元组只有一个元素时,需要加,
	p2 = multiprocessing.Process(target=analysis,args=(q,))
	# 启动进程
	p1.start()
	# 等待下载完成,即等待p1结束
	p1.join()
	p2.start()

if __name__=='__main__':
	main()	

在这里插入图片描述
注意: 初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头)

  • Queue.qsize():返回当前队列包含的消息数量
  • Queue.empty():如果队列为空,返回True,反之False
  • Queue.full():如果队列满了,返回True,反之False
  • Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True
    • 如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常
    • 如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常,相当于Queue.get_nowait()
  • Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True
    • 如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常
    • 如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常,相当于Queue.put_nowait(item)

进程池Pool
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果数量较为庞大,手动创建就很费时费力,此时就可以用multiprocessing模块提供的Pool方法
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务

import multiprocessing
import time
import random
import os 

def test(msg):
	t_start = time.time()
	print('%s 开始执行,pid = %d' %(msg,os.getpid())) 
	time.sleep(random.random()*2)  # 休眠随机时间,0-2秒不等
	t_stop = time.time()
	print(msg,'执行完毕,耗时%.2f' % (t_stop-t_start))
	
po = multiprocessing.Pool(3)
for i in range(0,10)
	po.apply_async(test,(i,))
print('---start---')
po.close()
po.join()
print('---end---')	

在这里插入图片描述
multiprocessing.Pool常用函数

  • apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表
  • close():关闭Pool,使其不再接受新的任务
  • terminate():不管任务是否完成,立即终止
  • join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用
  • multiprocessing.Manager().Queue() :进程池中的Queue

案例:多文件下载器

import os 
import multiprocessing

def copy_file(q,file_name,old_folder_name,new_folder_name):
	"""copy功能函数"""
	print('正在从%s copy文件至 %s ' % (old_folder_name,new_folder_name))
	# 读取源文件中文件内容
	old_f = open(old_folder_name+'/'+file_name,'rb')
	content = old_f.read()
	old_f.close()

	# 写入目标文件
	new_f = open(new_folder+'/'+file_name+'[复件]''wb')
	new_f.write(content)
	new_f.close()

	# copy完毕后,将文件名添加入队列中
	q.put(file_name)

def main():
	# 获取要copy文件名字
	old_folder_name = input('请输入要copy的文件名:')
	# 创建一个新目录,名字为'copy文件名+[复件]'
	# 避免重复创建报错,捕获异常
	try:
		new_folder_name = old_folder_name+'[复件]'
		os.mkdir(new_folder_name) 
	except:
		pass
	# 获取要copy目录下所有文件的文件名,os.listdir(路径/文件名),返回指定路径/目录下的所有文件名列表
	file_names = os.listdir(old_folder_name)
	# 创建进程池和进程队列
	po = multiprocessing.Pool(5)	
	q = multiprocessing.Manager().Queue()
	# 向队列中添加copy文件任务
	for file_name in file_names:
		po.apply_ansyc(copy_file,args=(q,file_name,old_folder_name,new_folder_name))
	# copy结束,关闭进程池
	po.close()
	# 判断文件是否全部copy
	# 获取源文件中文件数量
	all_file_num = len(old_folder_name)
	# 初始化变量计算copy到目标文件中的文件数量
	count = 0
	while True:
		file_name = q.get()
		print('%s 已成功copy ' % file_name)
		# 计数器+1
		count += 1 
		# 输出当前copy进度,以百分数显示,-r表示回到行首
		print('--\r当前copy进度为: %.2f%%'%(count*100/all_file_num),end='')
		# 全部已copy则退出循环
		if count>=all_file_num:
			break
	print('')
	print('copy完成')

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值