python网络通信和线程相关
文章目录
网络通信
网络通信概念
网络是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。
- 使用网络能够将多方链接在一起,进行数据传递
- 网络编程就是让不同的电脑能够进行数据传递,及进程间通信
网络通信的方式
- 直连通信:两台设备之间通过网线链接的直接通信;
- 集线器通信:多台设备以广播方式进行数据传播,容易出现网络拥堵;
- 交换机通信:克服集线器的缺点,可多播可单播,企业中常用来完成多台设备的局域网搭建;
- 路由器链接多个网络
名词解释:
- MAC地址:在设备与设备之间数据通信时用来标记收发双方(网卡的序列号)
- IP地址:在逻辑上标记一台电脑,用来指引数据包的收发方向(相当于电脑的序列号)
- 网络掩码:用来区分ip地址的网络号和主机号
- 默认网关:当需要发送的数据包的目的ip不在本网段内时,就会发送给默认的一台电脑,成为网关
- 集线器:已过时,用来连接多态电脑,缺点:每次收发数据都进行广播,网络会变的拥堵
- 交换机:集线器的升级版,有学习功能知道需要发送给哪台设备,根据需要进行单播、广播
- 路由器:连接多个不同的网段,让他们之间可以进行收发数据,每次收到数据后,ip不变,但是MAC地址会变化
- DNS:用来解析出IP(类似电话簿)
- http服务器:提供浏览器能够访问到的数据
IP和端口
IP
ip地址:用来在网络中标记一台电脑,在同一网络中IP地址是唯一的。
**ip地址的分类:**每一个IP地址包括两部分:网络地址和主机地址。IP地址通常由点分十进制(例如:192.168.1.1)的方式来表示,IP地址要和子网掩码(用来区分网络位和主机位)配合使用。
注意事项
- 每一个字节都为0的地址(“0.0.0.0”)对应于当前主机。
- IP地址中的每一个字节都为1的IP地址(“255.255.255.255”)是当前子网的广播地址。
- IP地址中凡是以“1111”开头的E类IP地址都保留用于将来和实验使用。
- IP地址中不能以十进制“127”作为开头,该类地址中数字127.0.0.1到127.255.255.255用于回路测试,如:127.0.0.1可以代表本机IP地址,用
http://127.0.0.1
就可以测试本机中配置的Web服务器 - 网络ID的第一个8位组也不能全置为“0”,全“0”表示本地网络。
端口
端口就像一个房子的门,是出入房间的必经之地。一个程序收发网络数据就需要这一个端口。
端口号用来标记每个端口,只有整数。
知名端口是众所周知的端口号,范围从0到1023,可以理解为系统的一些常用的功能使用的号码。
动态端口的范围是从1024到65535,之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。
动态分配是指当一个系统程序或应用程序程序需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用,当这个程序关闭时,同时也就释放了所占用的端口号。
socket概念
socket(简称 套接字
) 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:
它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的。
创建socket
在 Python 中 使用socket 模块的函数 socket 就可以完成:
import socket
socket.socket(AddressFamily, Type)
Copy
函数 socket.socket 创建一个 socket,该函数带有两个参数:
- Address Family:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET
- Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)
创建一个tcp socket(tcp套接字)
import socket
# 创建tcp的套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# ...这里是使用套接字的功能(省略)...
# 不用的时候,关闭套接字
s.close()
Copy
创建一个udp socket(udp套接字)
import socket
# 创建udp的套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# ...这里是使用套接字的功能(省略)...
# 不用的时候,关闭套接字
s.close()
Copy
套接字使用流程 与 文件的使用流程很类似
- 创建套接字
- 使用套接字收/发数据
- 关闭套接字
TCP、UDP协议
UDP
UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议。在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中,“写信”。
TCP
1)TCP采用发送应答机制
TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功
2)超时重传
发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。
TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
3)错误校验
TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
- 流量控制和阻塞管理
流量控制用来避免主机发送得过快而使接收方来不及完全收下。
TCP文件下载案例:
TCP服务器端:
from socket import *
def get_file_content(file_name):
"""获取文件的内容"""
try:
with open(file_name, "rb") as f:
content = f.read()
return content
except:
print("没有下载的文件:%s" % file_name)
def main():
# 创建socket
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 本地信息
address = ('', 7890)
# 绑定本地信息
tcp_server_socket.bind(address)
# 将主动套接字变为被动套接字
tcp_server_socket.listen(128)
while True:
# 等待客户端的链接,即为这个客户端发送文件
client_socket, clientAddr = tcp_server_socket.accept()
# 接收对方发送过来的数据
recv_data = client_socket.recv(1024) # 接收1024个字节
file_name = recv_data.decode("utf-8")
print("对方请求下载的文件名为:%s" % file_name)
file_content = get_file_content(file_name)
# 发送文件的数据给客户端
# 因为获取打开文件时是以rb方式打开,所以file_content中的数据已经是二进制的格式,因此不需要encode编码
if file_content:
client_socket.send(file_content)
# 关闭这个套接字
client_socket.close()
# 关闭监听套接字
tcp_server_socket.close()
if __name__ == '__main__':
main()
TCP客户端:
from socket import *
def main():
# 创建socket
tcp_client_socket = socket(AF_INET, SOCK_STREAM)
# 目的信息
server_ip = input("请输入服务器ip:")
server_port = int(input("请输入服务器port:"))
# 链接服务器
tcp_client_socket.connect((server_ip, server_port))
# 输入需要下载的文件名
file_name = input("请输入要下载的文件名:")
# 发送文件下载请求
tcp_client_socket.send(file_name.encode("utf-8"))
# 接收对方发送过来的数据,最大接收1024个字节(1K)
recv_data = tcp_client_socket.recv(1024)
# print('接收到的数据为:', recv_data.decode('utf-8'))
# 如果接收到数据再创建文件,否则不创建
if recv_data:
with open("[接收]"+file_name, "wb") as f:
f.write(recv_data)
# 关闭套接字
tcp_client_socket.close()
if __name__ == "__main__":
main()
TCP与UDP的区别
- 面向连接(确认有创建三方交握,连接已创建才作传输。)
- 有序数据传输
- 重发丢失的数据包
- 舍弃重复的数据包
- 无差错的数据传输
- 阻塞/流量控制
多任务
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。
我们前面编写的所有的Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?
有两种解决方案:
一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
总结一下就是,多任务的实现有3种方式:
- 多进程模式;
- 多线程模式;
- 多进程+多线程模式。
注意:
- 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
- 并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的。
线程
是[操作系统]能够进行运算[调度]的最小单位。它被包含在[进程]之中,是[进程]中的实际运作单位。一条线程指的是[进程]中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
线程锁的经典卖票案例:
import threading
import time
ticket = 20
lock = threading.Lock()
def sell_ticket():
global ticket
while True:
lock.acquire()
if ticket > 0:
time.sleep(0.5)
ticket -= 1
lock.release()
print('{}卖了一张票,还剩{}'.format(threading.current_thread().name, ticket))
else:
print('{}票卖完了'.format(threading.current_thread().name))
lock.release()
break
for i in range(5):
t = threading.Thread(target=sell_ticket, name='thread-{}'.format(i + 1))
t.start()
进程
进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。
进程的状态
工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态。
- 就绪态:运行的条件都已经满足,正在等在cpu执行。
- 执行态:cpu正在执行其功能。
- 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态。
进程和线程的区别
- 一个程序至少有一个进程,一个进程至少有一个线程.
- 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
- 线线程不能够独立执行,必须依存在进程中
- 可以将进程理解为工厂中的一条流水线,而其中的线程就是这个流水线上的工人
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
进程间通信:
from multiprocessing import Queue
q=Queue(3) #初始化一个Queue对象,最多可接收三条put消息
q.put("消息1")
q.put("消息2")
print(q.full()) #False
q.put("消息3")
print(q.full()) #True
#因为消息列队已满下面的try都会抛出异常,第一个try会等待2秒后再抛出异常,第二个Try会立刻抛出异常
try:
q.put("消息4",True,2)
except:
print("消息列队已满,现有消息数量:%s"%q.qsize())
try:
q.put_nowait("消息4")
except:
print("消息列队已满,现有消息数量:%s"%q.qsize())
#推荐的方式,先判断消息列队是否已满,再写入
if not q.full():
q.put_nowait("消息4")
#读取消息时,先判断消息列队是否为空,再读取
if not q.empty():
for i in range(q.qsize()):
print(q.get_nowait())
进程池
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务,请看下面的实例:
from multiprocessing import Pool
import os, time, random
def worker(msg):
t_start = time.time()
print("%s开始执行,进程号为%d" % (msg,os.getpid()))
# random.random()随机生成0~1之间的浮点数
time.sleep(random.random()*2)
t_stop = time.time()
print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start))
po = Pool(3) # 定义一个进程池,最大进程数3
for i in range(0,10):
# Pool().apply_async(要调用的目标,(传递给目标的参数元祖,))
# 每次循环将会用空闲出来的子进程去调用目标
po.apply_async(worker,(i,))
print("----start----")
po.close() # 关闭进程池,关闭后po不再接收新的请求
po.join() # 等待po中所有子进程执行完成,必须放在close语句之后
print("-----end-----")
Copy
运行效果:
----start----
0开始执行,进程号为21466
1开始执行,进程号为21468
2开始执行,进程号为21467
0 执行完毕,耗时1.01
3开始执行,进程号为21466
2 执行完毕,耗时1.24
4开始执行,进程号为21467
3 执行完毕,耗时0.56
5开始执行,进程号为21466
1 执行完毕,耗时1.68
6开始执行,进程号为21468
4 执行完毕,耗时0.67
7开始执行,进程号为21467
5 执行完毕,耗时0.83
8开始执行,进程号为21466
6 执行完毕,耗时0.75
9开始执行,进程号为21468
7 执行完毕,耗时1.03
8 执行完毕,耗时1.05
9 执行完毕,耗时1.69
-----end-----