Python 网络与并发编程
并发编程
并发编程介绍
串行-并行-并发的区别
- 串行(serial)):一个CPU上,按顺序完成多个任务
- 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
- 并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
进程-线程-协程的区别
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低
线程(Thread):拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑GL的情况下)
协程(coroutine):拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高
进程是什么?
**进程(Process)**是一个具有一定独立功能的程序关于某个数据集合的一次运行活动
线程是什么?
**线程(Thread)**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
并发编程解决方案:
多任务的实现有3种方式:
- 多进程模式
- 多线程模式
- 多进程+多线程模式
协程是什么?
协程,Coroutines,也叫作纤程(Fiber),是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。
当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充
分利用了IO等待的时间,提高了效率。
同步和异步通信机制的区别
同步和异步强调的是消息通信机制 (synchronous communication/asynchronous communication)。
同步(synchronous):A调用B,等待B返回结果后,A继续执行
异步(asynchronous ):A调用B,A继续执行,不等待B返回结果;B有结果了,通知A,A再做处理。
同步方式通信:
1 高淇买一本书《Python实战笔记》。
2 书店老板说:等三分钟啊,我帮你查查。
3 高淇等一小时
4 老板说,找到书了,发给你
异步方式通信:
1 高淇买一本电子书《Python实战笔记》。
2 书店老板说:我查一下,有结果了告诉你。
3 高淇刷抖音一小时
4 老板说,找到书了,发给你
线程
线程(Thread)特点:
1 **线程(Thread)**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
2 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
3 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
4 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
5 调度和切换:线程上下文切换比进程上下文切换要快得多。
线程的创建方式
Python的标准库提供了两个模块: _thread
和 threading
, _thread
是低级模块, threading
是高级模块,对 _thread
进行了封装。绝大多数情况下,我们只需要使用 threading
这个高级模块。
线程的创建可以通过分为两种方式:
- 方法包装
- 类包装
线程的执行统一通过 start()
方法
方法包装创建线程
#coding=utf-8
""""
方法包装建立线程
"""
from threading import Thread
from time import sleep
def function1(name):
print(f"线程{name}, start") # format
for i in range(3):
print(f"线程:{name}, {i}")
sleep(1)
print(f"线程{name}, end") # format
if __name__ == "__main__":
print("主线程,start")
# 创建线程
t1 = Thread(target=function1, args=("t1",))
t2 = Thread(target=function1, args=("t2",))
# 启动线程
t1.start()
t2.start()
print("主线程,end")
"""
运行结果可能会出现换行问题,是因为多个线程抢夺控制台输出的IO流。
主线程,start
线程t1, start
线程:t1, 0
线程t2, start
线程:t2, 0
主线程,end
线程:t2, 1线程:t1, 1
线程:t2, 2
线程:t1, 2
线程t1, end线程t2, end
"""
类包装创建线程
from time import sleep
from threading import Thread
"""
类包装创建线程
"""
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
# 重写run方法
def run(self):
print(f"线程{self.name}, start") # format
for i in range(3):
print(f"线程:{self.name}, {i}")
sleep(1)
print(f"线程{self.name}, end") # format
if __name__ == '__main__':
print("主线程,start")
# 创建线程
t1 = MyThread("t1")
t2 = MyThread("t2")
# 启动线程
t1.start()
t2.start()
print("主线程,end")
join()和守护线程
join()
之前的代码,主线程不会等待子线程结束。
如果需要等待子线程结束后,再结束主线程,可使用join()方法。
from threading import Thread
from time import sleep
def function1(name):
print(f"线程{name}, start") # format
for i in range(3):
print(f"线程:{name}, {i}")
sleep(1)
print(f"线程{name}, end") # format
if __name__ == "__main__":
print("主线程,start")
# 创建线程
t1 = Thread(target=function1, args=("t1",))
t2 = Thread(target=function1, args=("t2",))
# 启动线程
t1.start()
t2.start()
# 主线程会等待t1,t2结束后,再往下执行
t1.join()
t2.join()
print("主线程,end")
守护线程
在行为上还有一种叫守护线程,主要的特征是它的生命周期。主线程死亡,它也就随之死亡。在python中,线程通过 setDaemon(True|False)
来设置是否为守护线程。
守护线程的作用:
守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。
from time import sleep
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
# 重写run方法
def run(self):
print(f"线程{self.name}, start") # format
for i in range(3):
print(f"线程:{self.name}, {i}")
sleep(1)
print(f"线程{self.name}, end") # format
if __name__ == '__main__':
print("主线程,start")
# 创建线程(类的方式)
t1 = MyThread("t1")
# t1设置为守护线程
t1.setDaemon(True)
# 启动线程
t1.start()
print("主线程,end")
全局解释器锁GIL问题
在python中,无论你有多少核,在Cpython解释器中永远都是假象。无论你是4核,8核,还是16核…不好意思,同一时间执行的线程只有一个线程,它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是“含有水分的线程”。
Python GIL(Global Interpreter Lock)
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
⚠️GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。
线程同步和互斥锁资源冲突案例
线程同步的概念
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
# coding=utf-8
"""
未使用线程同步和互斥锁的情况
"""
from threading import Thread
from time import sleep
class Account:
def __init__(self, money, name):
self.money = money
self.name = name
# 模拟提款操作
class Drawing(Thread):
def __init__(self, drawingNum, account):
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
if self.account.money < self.drawingNum:
return
sleep(1) # 判断完后阻塞
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
print(f"账户{self.account.name},余额{self.account.money}")
print(f"账户{self.account.name},总共取了{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100, "gaoqi")
draw1 = Drawing(80, a1) # 定义取钱线程对象;
draw2 = Drawing(80, a1) # 定义取钱线程对象;
draw1.start() # 你取钱
draw2.start() # 你老婆取钱
"""
账户gaoqi,余额20账户gaoqi,余额-60
账户gaoqi,总共取了80
账户gaoqi,总共取了80
"""
互斥锁典型案例
我们可以通过“锁机制”来实现线程同步问题,锁机制有如下几个要点:
- 必须使用同一个锁对象
- 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
- 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
- 使用互斥锁会影响代码的执行效率
- 同时持有多把锁,容易出现死锁的情况
互斥锁是什么?
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意: 互斥锁是**多个线程一起去抢**,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁
threading
模块中定义了 Lock
变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
# coding=utf-8
"""
互斥锁典型案例
"""
from threading import Thread, Lock
from time import sleep
class Account:
def __init__(self, money, name):
self.money = money
self.name = name
# 模拟提款操作
class Drawing(Thread):
def __init__(self, drawingNum, account):
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
lock1.acquire()
if self.account.money < self.drawingNum:
print("账户余额不足")
return
sleep(1) # 判断完后阻塞
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
lock1.release()
print(f"账户{self.account.name},余额{self.account.money}")
print(f"账户{self.account.name},总共取了{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(100, "gaoqi")
lock1 = Lock()
draw1 = Drawing(80, a1) # 定义取钱线程对象;
draw2 = Drawing(80, a1) # 定义取钱线程对象;
draw1.start() # 你取钱
draw2.start() # 你老婆取钱
死锁问题和解决方案
死锁
在多线程程序中,死锁问题很大一部分是由于一个线程同时获取多个锁造成的。
死锁的解决方法
死锁是由于“同步块需要同时持有多个锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。
信号量
互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源,我们同时想让N个(定数值)线程访问?这时候,可以使用信号量。信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。
应用场景
- 在读写文件的时候,一般只能只有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,这时候就可以用到信号量了(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)。
- 在做爬虫抓取数据时。
底层原理
信号量底层就是一个内置的计数器。每当资源获取时(调用acquire)计数器-1,资源释放时(调用release)计数器+1。
# coding=utf-8
"""
信号量的使用案例
"""
from threading import Thread, Lock
from time import sleep
from multiprocessing import Semaphore
"""
一个房间一次只允许两个人通过
若不使用信号量,会造成所有人都进入这个房子
若只允许一人通过可以用锁-Lock()
"""
def home(name, se):
se.acquire()
print(f"{name}进入房间")
sleep(3)
print(f"***{name}走出房间")
se.release()
if __name__ == '__main__':
se = Semaphore(2) # 信号量对象
for i in range(7):
t = Thread(target=home, args=(f"tom{i}", se))
t.start()
"""
tom0进入房间
tom1进入房间
***tom0走出房间***tom1走出房间
tom3进入房间
tom2进入房间
***tom3走出房间***tom2走出房间
tom4进入房间
tom5进入房间
***tom5走出房间
***tom4走出房间
tom6进入房间
***tom6走出房间
"""
事件Event对象
事件Event主要用于唤醒正在阻塞等待状态的线程。
原理
Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行
Event()
可以创建一个事件管理标志,该标志(event)默认为False,event对象主要有四种方法可以调用:
方法名 | 说明 |
---|---|
event.wait(timeout=None) | 调用该方法的线程会被阻塞,如果设置了timeout参数,超时后,线程会停止阻塞继续执行 |
event.set() | 将event的标志设置为True,调用wait方法的所有线程将被唤醒 |
event.clear() | 将event的标志设置为False,调用wait方法的所有线程将被阻塞 |
event.is_set() | 判断event的标志是否为True |
# coding:utf-8
"""
小伙伴们,围着吃火锅,当菜上齐了,请客的主人说:开吃!
于是小伙伴一起动筷子,这种场景如何实现
"""
import threading
import time
def chihuoguo(name):
# 等待事件,进入等待阻塞状态
print(f'{name}已经启动')
print(f'小伙伴{name}已经进入就餐状态!')
time.sleep(1)
event.wait()
# 收到事件后进入运行状态
print(f'{name}收到通知了.')
print(f'小伙伴{name}开始吃咯!')
if __name__ == '__main__':
event = threading.Event()
# 创建新线程
thread1 = threading.Thread(target=chihuoguo, args=("tom",))
thread2 = threading.Thread(target=chihuoguo, args=("cherry",))
# 开启线程
thread1.start()
thread2.start()
time.sleep(10)
# 发送事件通知
print('---->>>主线程通知小伙伴开吃咯!')
event.set()
"""
tom已经启动
小伙伴tom已经进入就餐状态!
cherry已经启动
小伙伴cherry已经进入就餐状态!
---->>>主线程通知小伙伴开吃咯!
tom收到通知了.
小伙伴tom开始吃咯!
cherry收到通知了.
小伙伴cherry开始吃咯!
"""
生产者-消费者模式
多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
什么是生产者?
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
什么是消费者?
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)
什么是缓冲区?
消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
缓冲区是实现并发的核心,缓冲区的设置有3个好处:
实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
解耦了生产者和消费者
生产者不需要和消费者直接打交道。
解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据。
缓冲区和queue对象
从一个线程向另一个线程发送数据最安全的方式可能就是使用queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put()
和 get()
操作来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。
# coding=utf-8
from queue import Queue
from threading import Thread
from time import sleep
def producer():
num = 1
while True:
if queue.qsize() < 5:
print(f"生产{num}号,大馒头")
queue.put(f"大馒头: {num}号")
num += 1
else:
print("馒头框满了,等待来人消费")
sleep(1)
def consumer():
while True:
print(f"获取馒头:{queue.get()}")
sleep(1)
if __name__ == '__main__':
queue = Queue()
t1 = Thread(target=producer)
t2 = Thread(target=consumer)
t1.start()
t2.start()
"""
生产1号,大馒头
获取馒头:大馒头: 1号
生产2号,大馒头
获取馒头:大馒头: 2号
生产3号,大馒头
获取馒头:大馒头: 3号
生产4号,大馒头
获取馒头:大馒头: 4号
生产5号,大馒头
获取馒头:大馒头: 5号
"""
进程
进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
进程的优缺点
进程的优点:
- 可以使用计算机多核,进行任务的并行执行,提高执行效率
- 运行不受其他进程影响,创建方便
- 空间独立,数据安全
进程的缺点:
进程的创建和删除消耗的系统资源较多
进程的创建方式(方法模式)
Python的标准库提供了个模块: multiprocessing
进程的创建可以通过分为两种方式:
- 方法包装
- 类包装
创建进程后,使用start()启动进程
方法模式创建进程
# coding=utf-8
# 方法包装-多进程实现
from multiprocessing import Process
from time import sleep
import os
def func1(name):
print("当前进程ID:", os.getpid())
print("父进程ID:", os.getppid())
print(f"Process:{name} start")
sleep(3)
print(f"Process:{name} end")
'''
这是一个关于windows上多进程实现的bug。
在windows上,子进程会自动import启动它的这个文件,而在import的时候是会自动执行这些语句的。
如果不加__main__限制的话,就会无限递归创建子进程,进而报错。
于是import的时候使用 __name__ =="__main__" 保护起来就可以了。
'''
if __name__ == '__main__':
print("当前进程ID:", os.getpid())
# 创建进程
p1 = Process(target=func1, args=('p1',))
p2 = Process(target=func1, args=('p2',))
p1.start()
p2.start()
"""
当前进程ID: 8132
当前进程ID: 2100
当前进程ID: 4468
父进程ID: 8132
Process:p1 start
父进程ID: 8132
Process:p2 start
Process:p1 end
Process:p2 end
"""
windows:多进程的一个bug
这是一个关于windows上多进程实现的bug。
在windows上,子进程会自动import启动它的这个文件,而在import的时候是会自动执行这些语句的。
如果不加__main__限制的话,就会无限递归创建子进程,进而报错。
于是import的时候使用 __name__ =="__main__" 保护起来就可以了。
类模式创建进程
和使用Thread 类创建子线程的方式非常类似,使用 Process 类创建实例化对象,其本质是调用该类的构造方法创建新进程。Process类的构造方法格式如下:
def __init__(self,group=None,target=None,name=None,args=(),kwargs={})
其中,各个参数的含义为:
group
:该参数未进行实现,不需要传参;
target
:为新建进程指定执行任务,也就是指定一个函数;
name
:为新建进程设置名称;
args
:为 target 参数指定的参数传递非关键字参数;
kwargs
:为 target 参数指定的参数传递关键字参数。
# coding=utf-8
from multiprocessing import Process
from time import sleep
class MyProcess(Process):
def __init__(self, name):
Process.__init__(self)
self.name = name
def run(self):
print(f"process:{self.name},start")
sleep(3)
print(f"process:{self.name},end")
if __name__ == '__main__':
# 创建进程
p1 = MyProcess("p1")
p1.start()
Queue实现进程通信
前面讲解了使用 Queue
模块中的 Queue
类实现线程间通信,但要实现进程间通信,需要使用 multiprocessing
模块中的 Queue
类。简单的理解 Queue
实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间,各个进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走。
# coding=utf-8
from multiprocessing import Process, Queue
from time import sleep
class MyProcess(Process):
def __init__(self, name, mq):
Process.__init__(self)
self.name = name
self.mq = mq
def run(self):
print(f"process:{self.name},start")
print(f"get data:{self.mq.get()}")
sleep(3)
print(f"process:{self.name},end")
if __name__ == '__main__':
mq = Queue()
mq.put("1")
mq.put("2")
mq.put("3")
# 进程列表
p_list = []
for i in range(3):
p = MyProcess(f"p{i}", mq)
p_list.append(p)
for p in p_list:
p.start()
Pipe管道实现进程通信
Pipe 直译过来的意思是“管”或“管道”,和实际生活中的管(管道)是非常类似的。
Pipe方法返回(conn1, conn2)
代表一个管道的两个端。
Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个参数是全双工模式,也就是说
conn1
和conn2
均可收发。若duplex为False,conn1
只负责接收消息,conn2
只负责发送消息。send和recv方法分别是发送和接受消息的方法。例如,在全双工模式下,可以调用conn1.send
发送消息,conn1.recv
接收消息。如果没有消息可接收,recv方法会一直阻塞。如果管道已经被关闭,那么recv方法会抛出EOFError。
# coding=utf-8
import multiprocessing
from time import sleep
def func1(conn1):
sub_info = "Hello!"
print(f"进程1--{multiprocessing.current_process().pid}发送数据:{sub_info}")
sleep(1)
conn1.send(sub_info)
print(f"来自进程2:{conn1.recv()}")
sleep(1)
def func2(conn2):
sub_info = "你好!"
print(f"进程2--{multiprocessing.current_process().pid}发送数据:{sub_info}")
sleep(1)
conn2.send(sub_info)
print(f"来自进程1:{conn2.recv()}")
sleep(1)
if __name__ == '__main__':
# 创建管道
conn1, conn2 = multiprocessing.Pipe()
# 创建子进程
process1 = multiprocessing.Process(target=func1, args=(conn1,))
process2 = multiprocessing.Process(target=func2, args=(conn2,))
# 启动子进程
process1.start()
process2.start()
"""
进程1--22424发送数据:Hello!
进程2--17168发送数据:你好!
来自进程1:Hello!来自进程2:你好!
"""
Manager管理器实现进程通信
管理器提供了一种创建共享数据的方法,从而可以在不同进程中共享。
# coding=utf-8
from multiprocessing import Process, current_process
from multiprocessing import Manager
def func(name, m_list, m_dict):
m_dict['name'] = '尚学堂'
m_list.append('你好')
if __name__ == '__main__':
with Manager() as mgr:
m_list = mgr.list()
m_dict = mgr.dict()
m_list.append('Hello!!')
# 两个进程不能直接互相使用对象,需要互相传递
p1 = Process(target=func, args=('p1', m_list, m_dict))
p1.start()
p1.join() # 等p1进程结束,主进程继续执行
print(f"主进程:{m_list}")
print(f"主进程:{m_dict}")
进程池(Pool)管理进程的两种典型案例
Python提供了更好的管理多个进程的方式,就是使用进程池。
进程池可以提供指定数量的进程给用户使用,即当有新的请求提交到进程池中时,如果池未满,则会创建一个新的进程用来执行该请求;反之,如果池中的进程数已经达到规定最大值,那么该请求就会等待,只要池中有进程空闲下来,该请求就能得到执行。
使用进程池的优点:
- 提高效率,节省开辟进程和开辟内存空间的时间及销毁进程的时间
- 节省内存空间
类/方法 | 功能 | 参数 |
---|---|---|
Pool(processes) | 创建进程池对象 | processes表示进程池中有多少进程 |
pool.apply_async(func,args,kwds) | 异步执行;将事件放入到进程池队列 | func 事件函数 args 以元组形式给func传参kwds 以字典形式给func传参 返回值:返回一个代表进程池事件的对象,通过返回值的get方法可以得到事件函数的返回值 |
pool.apply(func,args,kwds) | 同步执行;将事件放入到进程池队列 | func 事件函数 args 以元组形式给func传参kwds 以字典形式给func传参 |
pool.close() | 关闭进程池 | |
pool.join() | 回收进程池 | |
pool.map(func,iter) | 类似于python的map函数,将要做的事件放入进程 | func 要执行的函数 iter 迭代对象 |
# coding=utf-8
import os
from multiprocessing import Pool
from time import sleep
def func1(name):
print(f"打印当前进程的id:{os.getpid()},{name}")
sleep(2)
return name
def func2(args):
print(args)
if __name__ == '__main__':
pool = Pool(5)
pool.apply_async(func=func1, args=("sxt1",), callback=func2)
pool.apply_async(func=func1, args=("sxt2",), callback=func2)
pool.apply_async(func=func1, args=("sxt3",), callback=func2)
pool.apply_async(func=func1, args=("sxt4",))
pool.apply_async(func=func1, args=("sxt5",))
pool.apply_async(func=func1, args=("sxt6",))
pool.apply_async(func=func1, args=("sxt7",))
pool.apply_async(func=func1, args=("sxt8",))
pool.close()
pool.join()
"""
打印当前进程的id:26676,sxt1
打印当前进程的id:15944,sxt2
打印当前进程的id:1352,sxt3
打印当前进程的id:27176,sxt4
打印当前进程的id:22592,sxt5
打印当前进程的id:26676,sxt6
sxt1
打印当前进程的id:22592,sxt7
打印当前进程的id:27176,sxt8
sxt2
sxt3
"""
使用with管理进程池
# coding=utf-8
import os
from multiprocessing import Pool
from time import sleep
def func1(name):
print(f"打印当前进程的id:{os.getpid()},{name}")
sleep(2)
return name
if __name__ == '__main__':
with Pool(5) as pool:
args = pool.map(func1, ("sxt1,", "sxt2,", "sxt3,", "sxt4,", "sxt5,", "sxt6,", "sxt7,", "sxt8,"))
for a in args:
print(a)
协程
核心概念面试重点
协程,Coroutines,也叫作纤程(Fiber)协程,全称是“协同程序”,用来实现任务协作。是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。
当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充分利用了IO等待的时间,提高了效率。
协程的核心(控制流的让出和恢复)
- 每个协程有自己的执行栈,可以保存自己的执行现场
- 可以由用户程序按需创建协程(比如:遇到io操作)
- 协程“主动让出(yield)”执行权时候,会保存执行现场(保存中断时的寄存器上下文和栈),然后切换到其他协程
- 协程恢复执行(resume)时,根据之前保存的执行现场恢复到中断前的状态,继续执行,这样就通过协程实现了轻量的由用户态调度的多任务模型
协程和多线程比较
比如,有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
- 在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。
- 多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。
- 协程版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。
协程的优点
- 由于自身带有上下文和栈,无需线程上下文切换的开销,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;
- 无需原子操作的锁定及同步的开销
- 方便切换控制流,简化编程模型
- 单线程内就可以实现并发的效果,最大限度地利用cpu,且可扩展性高,成本低(注:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理)
asyncio协程是写爬虫比较好的方式。比多线程和多进程都好.
开辟新的线程和进程是非常耗时的。
协程的缺点
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。
- 当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
yield方式实现(已淘汰,了解即可)
Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
- 最初的生成器变形
yield/send
- 引入
@asyncio.coroutine
和yield from
- Python3.5版本后,引入
async/await
关键字
asyncio异步IO实现协程(重点)
- 正常的函数执行时是不会中断的,所以你要写一个能够中断的函数,就需要加
async
async
用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5)
)消失后,也就是5秒到了再回来执行await
用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。asyncio
是python3.5之后的协程模块,是python实现并发重要的包,这个包使用事件循环驱动实现并发。
# coding=utf-8
import asyncio
import time
async def func1(): # async表示方法是异步的
for i in range(3):
print(f'北京:第{i}次打印啦')
await asyncio.sleep(1)
return "func1执行完毕"
async def func2():
for k in range(3):
print(f'上海:第{k}次打印了')
await asyncio.sleep(1)
return "func2执行完毕"
async def main():
res = await asyncio.gather(func1(), func2())
# await异步执行func1方法
# 返回值为函数的返回值列表,本例为["func1执行完毕", "func2执行完毕"]
print(res)
if __name__ == '__main__':
start_time = time.time()
asyncio.run(main())
end_time = time.time()
print(f"耗时{end_time - start_time}") # 耗时3秒,效率极大提高
"""
北京:第0次打印啦
上海:第0次打印了
北京:第1次打印啦
上海:第1次打印了
北京:第2次打印啦
上海:第2次打印了
['func1执行完毕', 'func2执行完毕']
耗时3.0080811977386475
"""
网络通信
基本概念
IP地址
IP是Internet Protocol Address,即"互联网协议地址"。
用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址。
路由器是连接两个或多个网络的网络设备。
IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如 192.168.0.1
实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于 2001:0db8:85a3:0042:1000:8a2e:0370:7334
公有地址
公有地址(Public address)由Inter NIC(Internet NetworkInformation Center互联网信息中心)负责。这些IP地址分配给注册并向Inter NIC提出申请的组织机构。通过它直接访问互联网。
私有地址
私有地址(Private address)属于非注册地址,专门为组织机构内部使用。
以下列出留用的内部私有地址
A类 10.0.0.0–10.255.255.255
B类 172.16.0.0–172.31.255.255
C类 192.168.0.0–192.168.255.255
端口port
端口号用来识别计算机中进行通信的应用程序。因此,它也被称为程序地址。
一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地进行数据传输。
端口分配
端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。 端口的表示是一个16位的二进制整数,对应十进制的0-65535。操作系统中一共提供了0~65535可用端口范围。
按端口号分类:
**公认端口(Well Known Ports):**从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务
的协议。例如:80端口实际上总是HTTP通讯
**注册端口(Registered Ports):**从1024到65535。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些 端 口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
网络通信协议
国际标准化组织(ISO,即International Organization forStandardization)定义了网络通信协议的基本框架,被称为OSI(Open System Interconnect,即开放系统互联)模型。
OSI模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。
OSI七层协议模型如图所示:
网络通信协议的分层-ISO分层模型
虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用最多的网络通信协议是TCP/IP网络通信协议。
TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层,网络接口层(物理+数据链路层)。
把用户应用程序作为最高层,把物理通信线路作为最低层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。
ISO模型与TCP/IP模型的对应关系如图所示。
TCP和UDP传输数据的区别
TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:
TCP 用于在传输层有必要实现可靠传输的情况;
UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。
TCP 和 UDP 应该根据应用的目的按需使用。
TCP
TCP(Transmission Control Protocol,传输控制协议)。TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。
UDP
UDP(User Data Protocol,用户数据报协议)
UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
TCP和UDP区别
这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
总结
TCP是面向连接的,传输数据安全,稳定,效率相对较低。
UDP是面向无连接的,传输数据不安全,效率较高。
TCP建立连接的三次握手
TCP是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂, 只简单的描述下这三次对话的简单过程:
1)主机A向主机B发出连接请求:“我想给你发数据,可以吗?”,这是第一次对话;
2)主机B向主机A发送同意连接和要求同步 (同步就是两台主机一个在发送,一个在接收,协调工作)的数据包 :“可以,你什么时 候发?”,这是第二次对话;
3)主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”, 这是第三次握手。三次“对话”的目的是使数据包 的发送和接收同步, 经过三次“对话”之后,主机A才向主机B正式发送数据。
- 第一步,客户端发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
- 第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)
- 第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。然后才开始通信的第二步:数据处理。
这就是所说的TCP的三次握手(Three-way Handshake)。
为什么TCP协议有三次握手,而UDP协议没有?
因为三次握手的目的是在client端和server端建立可靠的连接。保证双方发送的数据对方都能接受到,这也是TCP协议的被
称为可靠的数据传输协议的原因。而UDP就不一样,UDP不提供可靠的传输模式,发送端并不需要得到接收端的状态,因此
UDP协议就用不着使用三次握手。
TCP断开连接的四次挥手
TCP建立连接要进行3次握手,而断开连接要进行4次:
第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;
第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;
第三次: 由B 端再提出反方向的关闭请求,将FIN置1;
第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束。
由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据
正式传输前就有了交互, 为数据正式传输打下了可靠的基础。
数据包与处理流程
什么是数据包
通信传输中的数据单位,一般也称“数据包”。在数据包中包括:
包、帧、数据包、段、消息。
网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规
范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所
要处理的数据。包首部就像协议的脸。
数据包处理流程
套接字编程实战
socket编程介绍
TCP协议和UDP协议是传输层的两种协议。Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。
Socket编程封装了常见的TCP、UDP操作,可以实现非常方便的网络编程。
socket()函数介绍
在Python语言标准库中,通过使用socket模块提供的socket对象,可以在计算机网络中建立可以互相通信的服务器与客户端。在服务
器端需要建立一个socket对象,并等待客户端的连接。客户端使用socket对象与服务器端进行连接,一旦连接成功,客户端和服务器
端就可以进行通信了。
⚠️上图中,我们可以看出socket通讯中,发送和接收数据,都是通过操作系统控制网卡来进行。因此,我们在使用之后,必须关闭socket。
在Python 中,通常用一个Socket表示“打开了一个网络连接”,语法格式如下:
socket.socket([family[, type[, proto]]])
family
: 套接字家族可以使 AF_UNIX
或者 AF_INET
;
AF 表示ADDRESS FAMILY 地址族
AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型;而AF_UNIX 则是 Unix 系统本地通信。
type
: 套接字类型可以根据是面向连接的还是非连接分为 SOCK_STREAM
或 SOCK_DGRAM
;
protocol
: 一般不填,默认为0。
Socket主要分为面向连接的Socket和无连接的Socket。
无连接Socket的主要协议是用户数据报协议,也就是常说的UDP,UDP Socket的名字是 SOCK_DGRAM
。创建套接字UDP/IP套接字,可以调用 socket.socket()
。示例代码如下 :
udpSocket=socket.socket (AF_INET,SOCK_DGRAM)
socket对象的内置函数和属性
UDP编程介绍
UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
创建Socket时, SOCK_DGRAM
指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用 listen()
方法,而是直接接收来自任何客户端的数据。 recvfrom()
方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用 sendto()
就可以把数据用UDP发给客户端。
UDP编程的实现
UDP接收数据
# coding=utf-8
from socket import *
# 最简化的UDP服务端代码
s = socket(AF_INET, SOCK_DGRAM) # 创建UDP类型的套接字
s.bind(("127.0.0.1", 8888)) # 绑定端口,IP可以不写
print("等待接收的数据!")
recv_data = s.recvfrom(1024) # 1024表示本次接受的最大字节数
print(f"收到的远程信息:{recv_data[0].decode('gbk')}, form{recv_data[1]}")
s.close()
UDP发送数据
# coding=utf-8
from socket import *
# 最简化的UDP客户端发送消息代码
s = socket(AF_INET, SOCK_DGRAM) # 创建UDP类型的套接字
addr = ("127.0.0.1", 8888)
data = input("请输入:")
s.sendto(data.encode("gbk"), addr)
s.close()
UDP持续通信
# coding=utf-8
from socket import *
# UDP服务端持续接收消息代码
s = socket(AF_INET, SOCK_DGRAM) # 创建UDP类型的套接字
s.bind(("127.0.0.1", 8888)) # 绑定端口,IP可以不写
print("等待接收的数据!")
while True:
recv_data = s.recvfrom(1024) # 1024表示本次接受的最大字节数
recv_content = recv_data[0].decode('gbk')
print(f"收到的远程信息:{recv_content}, form{recv_data[1]}")
if recv_content == "88":
print("结束聊天")
break
s.close()
# coding=utf-8
from socket import *
# UDP客户端持续发送消息代码
s = socket(AF_INET, SOCK_DGRAM) # 创建UDP类型的套接字
addr = ("127.0.0.1", 8888)
while True:
data = input("请输入:")
s.sendto(data.encode("gbk"), addr)
if data == "88":
print("结束聊天")
break
s.close()
结合多线程实现UDP双向自由通信
UDP 不同于 TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因为其提供服务而
称为服务器端。
如下服务端、客户端代码几乎一模一样,注意接收和发送端口对应,即可。
UDP实现多线程服务端
# coding=utf-8
from socket import *
# UDP实现多线程服务端代码
from threading import Thread
def recv_data():
while True:
recv_data = s.recvfrom(1024) # 1024表示本次接受的最大字节数
recv_content = recv_data[0].decode('gbk')
print(f"收到的远程信息:{recv_content}, form{recv_data[1]}")
if recv_content == "88":
print("结束聊天")
break
def send_data():
addr = ("127.0.0.1", 9999)
while True:
data = input("请输入:")
s.sendto(data.encode("gbk"), addr)
if data == "88":
print("结束聊天")
break
if __name__ == '__main__':
s = socket(AF_INET, SOCK_DGRAM) # 创建UDP类型的套接字
s.bind(("127.0.0.1", 8888)) # 绑定端口,IP可以不写
# 创建两个线程
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
UDP实现多线程客户端
# coding=utf-8
from socket import *
# UDP实现多线程客户端代码
from threading import Thread
def recv_data():
while True:
recv_data = s.recvfrom(1024) # 1024表示本次接受的最大字节数
recv_content = recv_data[0].decode('gbk')
print(f"收到的远程信息:{recv_content}, form{recv_data[1]}")
if recv_content == "88":
print("结束聊天")
break
def send_data():
addr = ("127.0.0.1", 8888)
while True:
data = input("请输入:")
s.sendto(data.encode("gbk"), addr)
if data == "88":
print("结束聊天")
break
if __name__ == '__main__':
s = socket(AF_INET, SOCK_DGRAM) # 创建UDP类型的套接字
s.bind(("127.0.0.1", 9999)) # 绑定端口,IP可以不写
# 创建两个线程
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
TCP编程介绍
面向连接的Socket使用的协议是TCP协议。TCP的Socket名称是SOCK_STREAM
。创建套接字TCP套接字,可以调用 socket.socket()
。示例代码如下:
tcpSocket=socket.socket(AF_INET,SOCK_STREAM)
TCP编程的实现
在Python语言中创建Socket服务端程序,需要使用socket模块中的socket类。创建Socket服务器程序的步骤如下:
(1) 创建Socket对象。
(2) 绑定端口号。
(3) 监听端口号。
(4) 等待客户端Socket的连接。
(5) 读取客户端发送过来的数据。
(6) 向客户端发送数据。
(7) 关闭客户端Socket连接。
(8) 关闭服务端Socket连接。
TCP服务器端接收数据
# coding=utf-8
from socket import *
server_socket = socket(AF_INET, SOCK_STREAM) # 创建TCP类型的套接字
server_socket.bind("127.0.0.1", 8899) # 本机监听8899端口
server_socket.listen(5)
print("等待接收连接!")
client_socket, client_info = server_socket.accept()
recv_data = client_socket.recv(1024) # 最大接收1024字节
print(f"收到信息,{recv_data.decode('gbk')},来自:{client_info}")
client_socket.close()
server_socket.close()
TCP客户端发送数据到服务端
# coding=utf-8
from socket import *
client_socket = socket(AF_INET, SOCK_STREAM) # 创建TCP类型的套接字
client_socket.connect(("127.0.0.1", 8899))
client_socket.send("hello".encode('gbk'))
client_socket.close()
TCP双向持续通信
TCP:双向通信Socket之服务器端
# coding=utf-8
from socket import *
server_socket = socket(AF_INET, SOCK_STREAM) # 创建TCP类型的套接字
server_socket.bind(("127.0.0.1", 8899)) # 本机监听8899端口
server_socket.listen(5)
print("等待接收连接!")
client_socket, client_info = server_socket.accept()
print("一个客户端建立成功")
while True:
recv_data = client_socket.recv(1024) # 最大接收1024字节
recv_content = recv_data.decode('gbk')
print(f"客户端说:{recv_content},来自:{client_info}")
if recv_content == "end":
break
msg = input(">")
client_socket.send(msg.encode('gbk'))
client_socket.close()
server_socket.close()
TCP:双向通信Socke之客户端
# coding=utf-8
from socket import *
client_socket = socket(AF_INET, SOCK_STREAM) # 创建TCP类型的套接字
client_socket.connect(("127.0.0.1", 8899))
while True:
# 给服务端发消息
msg = input(">")
client_socket.send(msg.encode('gbk'))
if msg == "end":
break
# 接收服务端消息
recv_data = client_socket.recv(1024) # 最大接收1024字节
print(f"服务端说:{recv_data.decode('gbk')}")
client_socket.close()
结合多线程实现TCP双向传送(自由聊天)
TCP服务端结合多线程实现自由收发信息
# coding=utf-8
'''
双向通信Socket之服务器端
读取客户端发送的数据,将内容输出到控制台
将控制台输入的信息发送给客户器端
'''
from socket import *
from threading import Thread
def recv_data():
while True:
# 读取客户端的消息
recv_data = client_socket.recv(1024) # 最大接收1024字节
recv_content = recv_data.decode('gbk')
# 将消息输出到控制台
print(f"客户端说:{recv_content},来自:{client_info}")
if recv_content == "end":
print("结束接收消息!")
break
def send_data():
while True:
# 获取控制台信息
msg = input(">")
client_socket.send(msg.encode('gbk'))
if msg == "end":
print("结束发送消息!")
break
if __name__ == '__main__':
# 创建Socket对象
server_socket = socket(AF_INET, SOCK_STREAM) # 创建TCP类型的套接字
# 绑定端口
server_socket.bind(("127.0.0.1", 8899)) # 本机监听8899端口
# 监听客户端的连接
server_socket.listen(5)
print("等待接收连接!")
# 接收客户端连接
client_socket, client_info = server_socket.accept()
print("一个客户端建立成功")
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
client_socket.close()
server_socket.close()
TCP客户端结合多线程实现自由收发信息
# coding=utf-8
'''
双向通信Socket之客户端
将控制台输入的信息发送给服务器端
读取服务器端的数据,将内容输出到控制台
'''
from socket import *
from threading import Thread
def recv_data():
while True:
# 接收服务端消息
recv_data = client_socket.recv(1024) # 最大接收1024字节
recv_content = recv_data.decode('gbk')
print(f"服务端说:{recv_content}")
if recv_content == "end":
print("结束接收消息!")
break
def send_data():
while True:
# 给服务端发消息
msg = input(">")
client_socket.send(msg.encode('gbk'))
if msg == "end":
print("结束发送消息!")
break
if __name__ == '__main__':
client_socket = socket(AF_INET, SOCK_STREAM) # 创建TCP类型的套接字
client_socket.connect(("127.0.0.1", 8899))
t1 = Thread(target=recv_data)
t2 = Thread(target=send_data)
t1.start()
t2.start()
t1.join()
t2.join()
client_socket.close()