Python学习笔记(五)之并发编程 网络通信

Python学习笔记(五)之并发编程 网络通信

第一章 Python 基本概念
第二章 Python 序列 控制语句 函数
第三章 Python 面向编程 异常处理 文件处理
第四章 Python 坦克大战
第五章 Python 并发编程 网络通信
第六章 Python 函数式编程
第七章 Python Linux编程



前言

网络编程中的高并发问题是大型互联网企业必须面对的核心问题,解决高并发可以用多进 程、多线程,python 中还有协程。高并发和网络是相关的,我们会利用学到的并发编程的 知识来编写不同的服务器模型。


一、并发编程

1.介绍

1.1 串行 并行 并发

在这里插入图片描述
串行(serial):一个CPU上,按顺序完成多个任务
并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

1.2 进程 线程 协程

个人理解:主要就是一个进程里面会有多个线程同步进行,线程里面又有协程来跑。
在这里插入图片描述
线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
调度和切换:线程上下文切换比进程上下文切换要快得多。

三者的区别:
进程(Process)拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低。
线程(Thread):拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)。
协程(coroutine):拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高。

进程是什么?
进程(Process)是一个具有一定独立功能的程序关于某个数据集合的一次运行活动
现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
线程是什么?
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
协程是什么?
协程,Coroutines,也叫作纤程(Fiber),是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。**当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。**充分利用了IO等待的时间,提高了效率。

并发编程解决方案:
多任务的实现3种方式:

  • 多进程模式
  • 多线程模式
  • 多进程+多线程模式

启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务
启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务
启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

1.3 同步 异步通信

同步和异步强调的是消息通信机制 (synchronous communication/ asynchronous communication)。
同步(synchronous):A调用B,等待B返回结果后,A继续执行
异步(asynchronous ):A调用B,A继续执行,不等待B返回结果;B有结果了,通知A,A再做处理。
在这里插入图片描述

2.线程Thread

2.1 方法包装

线程(Thread)特点:

  • 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  • 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  • 拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;
  • 调度和切换:线程上下文切换比进程上下文切换要快得多

线程的创建方式
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下只需要使用threading这个高级模块。
线程的创建可以通过分为两种方式:
1. 方法包装
2. 类包装

线程的执行统一通过**start()**方法

线程的创建方式(方法包装)

#encoding=utf-8
#方法方式创建线程
from threading import Thread
from time import sleep
def func1(name):
  for i in range(3):
    print(f"thread:{name} :{i}")
    sleep(1)if __name__ == '__main__':
  print("主线程,start")
  #创建线程
  t1 = Thread(target=func1,args=("t1",))
  t2 = Thread(target=func1,args=("t2",))
  #启动线程
  t1.start()
  t2.start()
  print("主线程,end")'''
运行结果可能会出现换行问题,是因为多个线程抢夺控制台输出的IO流。
比如,如下的输出换行就没有按照预想的显示:
​
主线程,start
thread:t1 :0
thread:t2 :0
主线程,end
thread:t2 :1thread:t1 :1
​
thread:t2 :2
thread:t1 :2
'''

2.2 类包装

#encoding=utf-8
#类的方式创建线程
from threading import Thread
from time import sleep
​
class MyThread(Thread):
  def __init__(self,name):
    Thread.__init__(self)
    self.name =name
  def run(self):
    for i in range(3):
      print(f"thread:{self.name} :{i}")
      sleep(1)if __name__ == '__main__':
  print("主线程,start")
  #创建线程(类的方式)
  t1 = MyThread('t1')
  t2 = MyThread('t2')
  #启动线程
  t1.start()
  t2.start()
  print("主线程,end")

2.3 join() 守护线程

如果需要等待子线程结束后,再结束主线程,可使用join()方法。

#encoding=utf-8
from threading import Thread
from time import sleep
def func1(name):
  for i in range(3):
    print(f"thread:{name} :{i}")
    sleep(1)if __name__ == '__main__':
  print("主线程,start")
  #创建线程
  t1 = Thread(target=func1,args=("t1",))
  t2 = Thread(target=func1,args=("t2",))
  #启动线程
  t1.start()
  t2.start()
  #主线程会等待t1,t2结束后,再往下执行
  t1.join()
  t2.join()
  print("主线程,end")

守护线程:主要的特征是它的生命周期。主线程死亡,它也就随之死亡。在python中,线程通过setDaemon(True|False)来设置是否为守护线程。守护线程作用是为其他线程提供便利服务,守护线程最典型的应用就是 GC (垃圾收集器)。

#encoding=utf-8
from threading import Thread
from time import sleep
​
class MyThread(Thread):
  def __init__(self,name):
    Thread.__init__(self)
    self.name =name
  def run(self):
    for i in range(3):
      print(f"thread:{self.name} :{i}")
      sleep(1)if __name__ == '__main__':
  print("主线程,start")
  #创建线程(类的方式)
  t1 = MyThread('t1')
  #t1设置为守护线程
  t1.setDaemon(True)#3.10后被废弃,可以直接:t1.daemon=True
  #启动线程
  t1.start()
  print("主线程,end")

2.4 全局解释器锁GIL

在python中,无论你有多少核,在Cpython解释器中永远都是假象。无论你是4核,8核,还是16核…同一时间执行的线程只有一个线程,所以说python中的线程是“含有水分的线程”。
在这里插入图片描述
GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就没有GIL的问题。

2.5 线程同步与互斥锁

线程同步
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
【示例】多线程操作同一个对象(未使用线程同步)

#encoding=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<0:
      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()  #你老婆取钱

可以通过“锁机制”来实现线程同步问题,锁机制有如下要点:

  • 必须使用同一个锁对象
  • 互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题
  • 使用互斥锁的好处确保某段关键代码只能由一个线程从头到尾完整地去执行
  • 使用互斥锁会影响代码的执行效率
  • 同时持有多把锁,容易出现死锁的情况

互斥锁是什么?
互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。
注意: 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。
threading模块中定义了Lock变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。

【示例】多线程操作同一个对象(增加互斥锁,使用线程同步)

#encoding=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<0:
      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()  #你老婆取钱

2.6 死锁问题 和解决方案

在多线程程序中,死锁问题很大一部分是由于一个线程同时获取多个锁造成的。

from threading import Thread, Lock
from time import sleep
​
def fun1():
  lock1.acquire()
  print('fun1拿到菜刀')
  sleep(2)
  lock2.acquire()
  print('fun1拿到锅')
​
  lock2.release()
  print('fun1释放锅')
  lock1.release()
  print('fun1释放菜刀')
​
​
def fun2():
  lock2.acquire()
  print('fun2拿到锅')
  lock1.acquire()
  print('fun2拿到菜刀')
  lock1.release()
  print('fun2释放菜刀')
  lock2.release()
  print('fun2释放锅')
​
​
if __name__ == '__main__':
  lock1 = Lock()
  lock2 = Lock()
​
  t1 = Thread(target=fun1)
  t2 = Thread(target=fun2)
  t1.start()
  t2.start()

死锁的解决方法
死锁是由于“同步块需要同时持有多个锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。

2.7 信号量

互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源,同时想让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):
    p = Thread(target=home, args=(f'tom{i}', se))
    p.start()
'''
执行结果:
tom0进入了房间
tom1进入了房间
******************tom1走出来房间
tom2进入了房间
******************tom0走出来房间
tom3进入了房间
******************tom2走出来房间******************tom3走出来房间
​
tom4进入了房间
tom5进入了房间
******************tom5走出来房间******************tom4走出来房间
​
tom6进入了房间
******************tom6走出来房间
​
Process finished with exit code 0'''

2.8 事件Event对象

事件Event主要用于唤醒正在阻塞等待状态的线程;
Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,event 对象中的信号标志被设置False假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,它将唤醒所有等待个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行。
Event()可以创建一个事件管理标志,该标志(event)默认为False,event对象主要有四种方法可以调用:
在这里插入图片描述
【示例】Event事件对象经典用法

#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开始吃咯!
'''

2.9 生产者消费者模式

多线程环境经常需要多个线程的并发和协作。这时需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。
在这里插入图片描述
生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)
消费者不能直接使用生产者的数据,它们之间有个**“缓冲区”**。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

缓冲区是实现并发的核心,缓冲区的设置有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()
  t = Thread(target=producer)
  t.start()
  c = Thread(target=consumer)
  c.start()
  c2 = Thread(target=consumer)
  c2.start()
'''
执行结果:
生产:1号,大馒头
获取馒头:大馒头:1号
生产:2号,大馒头
获取馒头:大馒头:2号
生产:3号,大馒头
获取馒头:大馒头:3号
生产:4号,大馒头
获取馒头:大馒头:4号
...
'''

3.进程Process

进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低。对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

进程的优点:
可以使用计算机多核,进行任务的并行执行,提高执行效率
运行不受其他进程影响,创建方便
空间独立,数据安全
进程的缺点:
进程的创建和删除消耗的系统资源较多

Python的标准库提供了个模块:multiprocessing

3.1 方法模式

#coding=utf-8
# 方法包装-多进程实现
from multiprocessing import Process
import os
from time import sleep
​
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()

3.2 类模式

和使用Thread 类创建子线程的方式非常类似,使用 Process 类创建实例化对象,其本质是调用该类的构造方法创建新进程。Process 类的构造方法格式如下:
def init(self,group=None,target=None,name=None,args=(),kwargs={})
其中,各个参数的含义为:
group:该参数未进行实现,不需要传参;
target:为新建进程指定执行任务,也就是指定一个函数;
name:为新建进程设置名称;
args:为 target 参数指定的参数传递非关键字参数;
kwargs:为 target 参数指定的参数传递关键字参数。

# 类的方式-多进程实现
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")
  p2 = MyProcess("p2")
  p1.start()
  p2.start()

3.4 Queue

在这里插入图片描述
使用 Queue 模块中的 Queue 类实现线程间通信,但要实现进程间通信,需要使用 multiprocessing 模块中的 Queue 类。简单的理解 Queue 实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间,各个进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走(先进先出)。

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(2)
    self.mq.put(f"new_data:{self.name}")
    print(f"Process:{self.name} end")
if __name__ == '__main__':
  # 创建进程列表
  t_list = []
  mq = Queue()
  mq.put('1')
  mq.put('2')
  mq.put('3')
  # 循环创建进程
  for i in range(3):
    t = MyProcess('p{}'.format(i),mq)

    t_list.append(t)
  # 等待进程结束
  for t in t_list:
    t.start()

  for t in t_list:
    t.join()

  print(mq.get())
  print(mq.get())
  print(mq.get())

3.5 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--14828发送数据:Hello!
进程2--19300发送数据:你好!
来自进程1:Hello!
来自进程2:你好!
'''

3.6 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(m_list)
    print(m_dict)

3.7 进程池管理进程

Python提供了更好的管理多个进程的方式:使用进程池。
进程池可以提供指定数量的进程给用户使用,即当有新的请求提交到进程池中时,如果池未满,则会创建一个新的进程用来执行该请求;反之,如果池中的进程数已经达到规定最大值,那么该请求就会等待,只要池中有进程空闲下来,该请求就能得到执行。
使用进程池的优点:

  • 提高效率,节省开辟进程和开辟内存空间的时间及销毁进程的时间
  • 节省内存空间

在这里插入图片描述
【示例】进程池使用案例

#coding=utf-8
from multiprocessing import Pool
import os
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()

【示例】使用with管理进程池

#coding=utf-8
from multiprocessing import Pool
import os
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)

4.协程Coroutines

4.1 核心

协程,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上。

4.3 Yeild已淘汰

Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
最初的生成器变形yield/send
引入@asyncio.coroutine和yield from
Python3.5版本后,引入async/await关键字

【示例】不使用协程执行多个任务

#coding=utf-8
import time
​
def func1():
  for i in range(3):
    print(f'北京:第{i}次打印啦')
    time.sleep(1)
  return "func1执行完毕"
def func2():
  for k in range(3):
    print(f'上海:第{k}次打印了' )
    time.sleep(1)
  return "func2执行完毕"
​
def main():
  func1()
  func2()
if __name__ == '__main__':
  start_time = time.time()
  main()
  end_time = time.time()
  print(f"耗时{end_time-start_time}")  #不使用协程,耗时6秒'''
执行结果:
北京:第0次打印啦
北京:第1次打印啦
北京:第2次打印啦
上海:第0次打印了
上海:第1次打印了
上海:第2次打印了
耗时6.042387008666992
'''

【示例】使用yield协程,实现任务切换

#coding=utf-8
import time
def func1():
  for i in range(3):
    print(f'北京:第{i}次打印啦')
    yield # 只要方法包含了yield,就变成一个生成器
    time.sleep(1)
def func2():
  g = func1()  #func1是一个生成器,func1()就不会直接调用,需要通过next()或for循环调用
  print(type(g))
  for k in range(3):
    print(f'上海:第{k}次打印了' )
    next(g)  #继续执行func1的代码
    time.sleep(1)#有了yield,我们实现了两个任务的切换+保存状态
start_time = time.time()
func2()
end_time = time.time()
print(f"耗时{end_time-start_time}")  #耗时5.0秒,效率差别不大'''
执行结果:

上海:第0次打印了
北京:第0次打印啦
上海:第1次打印了
北京:第1次打印啦
上海:第2次打印了
北京:第2次打印啦
耗时5.039638996124268
'''

4.4 syncio异步IO

正常的函数执行时是不会中断的,所以你要写一个能够中断的函数,就需要加async;
async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行;
await 用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序;
asyncio是python3.5之后的协程模块,是python实现并发重要的包,这个包使用事件循环驱动实现并发。

【示例】asyncio异步IO的典型使用方式

#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秒,效率极大提高

二、网络通信

1. 介绍

网络编程就是如何在程序中实现两台计算机的通信。Python语言中,提供了大量的内置模块和第三方模块用于支持各种网络访问。

2. 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 Network Information 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

3. 端口

端口号用来识别计算机中进行通信的应用程序。因此,它也被称为程序地址。一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地进行数据传输。
在这里插入图片描述

4. 网络协议ISO

在这里插入图片描述

5. TCP和UDP的区别

在这里插入图片描述
TCP(Transmission Control Protocol,传输控制协议)。TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。
UDP(User Data Protocol,用户数据报协议)UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP.

TCP和UDP区别

这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。
由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。

在这里插入图片描述
TCP是面向连接的,传输数据安全,稳定,效率相对较低。
UDP是面向无连接的,传输数据不安全,效率较高。

6. 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协议有三次握手,而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使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。

数据包与处理流程
通信传输中的数据单位,一般也称“数据包”。在数据包中包括:包、帧、数据包、段、消息。
网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。
在这里插入图片描述

7. 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对象的内置函数和属性
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8. UDP编程

8.1 服务器接收

UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据。recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。

【示例】UDP接收数据

#coding=utf-8
from socket import *
s = socket(AF_INET, SOCK_DGRAM) #创建套接字
#绑定接收信息端口
s.bind(('127.0.0.1', 8888)) #绑定一个端口,ip地址和端⼝号
print("等待接收数据!")
redata = s.recvfrom(1024) #1024表示本次接收的最⼤字节数
print(redata)
print(f'收到远程信息:{redata[0].decode("gbk")}, from {redata[1]}')
s.close()

8.2 客户端发送

from socket import *
s = socket(AF_INET, SOCK_DGRAM) #创建套接字
addr = ('127.0.0.1', 8888) #准备接收方地址
data = input("请输入:")
#发送数据时,python3需要将字符串转成byte
s.sendto(data.encode('gbk'),addr) #默认的网络助手使用的编码是gbk
s.close()

8.3 持续通信

【示例】UDP接收数据

#coding=utf-8
from socket import *
​
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},from {recv_data[1]}")
  if recv_content == "88":
    print("结束聊天!")
    break
​
s.close()

【示例】UDP发送数据

#coding=utf-8
from socket import *
​
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()

8.4 多线程结合自由通信

服务端、客户端代码几乎一模一样,**注意接收和发送端口对应,**即可。
【示例】UDP实现多线程服务端

# coding=utf-8
from socket import *
from threading import Thread




# 不停接收
def recv_data():
    while True:
        redata = s.recvfrom(1024)
        recv_content = redata[0].decode("gbk")
        print(f'收到信息:{recv_content}, from {redata[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)
    # 绑定接收信息端口
    s.bind(('127.0.0.1', 8888))

    # 创建两个线程
    t1 = Thread(target=send_data)
    t2 = Thread(target=recv_data)
    t2.start()
    t1.start()
    t1.join()
    t2.join()

出现报错:

输入信息:12
输入信息:Exception in thread Thread-2 (recv_data):
Traceback (most recent call last):
File “D:\python22\python3.10\lib\threading.py”, line 1009, in _bootstrap_inner
self.run()
File “D:\python22\python3.10\lib\threading.py”, line 946, in run
self._target(*self._args, **self._kwargs)
File “D:\python22\code\pythonProject1.1\01.py”, line 11, in recv_data
redata = udp_socket.recvfrom(1024)
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。

【示例】UDP实现多线程客户端

# coding=utf-8
from socket import *
from threading import Thread



# 不停接收
def recv_data():
    while True:
        redata = s.recvfrom(1024)
        recv_content = redata[0].decode("gbk")
        print(f'收到信息:{recv_content}, from {redata[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)
    # 绑定接收信息端口
    s.bind(('127.0.0.1', 9999))

    # 创建两个线程
    t1 = Thread(target=send_data)
    t2 = Thread(target=recv_data)
    t2.start()
    t1.start()
    t1.join()
    t2.join()


9.TCP编程

9.1 服务器接收

# coding=utf-8
from socket import *
from threading import Thread

s = socket(AF_INET, SOCK_STREAM)  # 创建UDP类型的套接字
s.bind(("127.0.0.1", 8989))  # 绑定端口,ip可以不写
s.listen(10)
print(("wait"))
client_socket,client_info = s.accept()
recv_data = client_socket.recv(1024)
print(f"收到消息:{recv_data.decode('gbk')},来自:{client_info}")

client_socket.close()
s.close()


9.2 客户端发送

# coding=utf-8
from socket import *
from threading import Thread


c = socket(AF_INET, SOCK_STREAM)  # 创建UDP类型的套接字
c.connect(("127.0.0.1",8989))

c.send("hello".encode("gbk"))
c.close()

wait
收到消息:hello,来自:(‘127.0.0.1’, 3476)

9.3 持续通信

服务器端

# coding=utf-8
from socket import *
from threading import Thread

s = socket(AF_INET, SOCK_STREAM)  # 创建UDP类型的套接字
s.bind(("127.0.0.1", 8989))  # 绑定端口,ip可以不写
s.listen(10)
print(("wait"))
client_socket,client_info = s.accept()
print("建立成功")
while True:
    recv_data = client_socket.recv(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()
s.close()

客户端

# coding=utf-8
from socket import *
from threading import Thread

c = socket(AF_INET, SOCK_STREAM)  # 创建UDP类型的套接字
c.connect(("127.0.0.1",8989))

while True:
    msg = input(">")
    c.send(msg.encode("gbk"))
    if msg == "end":
        break
    recv_data = c.recv(1024)
    print(f"服务端说:{recv_data.decode('gbk')}")

c.close()

9.4 多线程结合自由通信

服务器端:

# coding=utf-8
from socket import *
from threading import Thread


# 不停接收
def recv_data():
    while True:
        recv_data = client_socket.recv(1024)
        recv_content = recv_data.decode('gbk')
        print(f"客户端说:{recv_content},来自:{client_info}")
        if recv_content == "end":
            break


# 不停发送
def send_data():
    while True:
        msg = input(">")
        client_socket.send(msg.encode("gbk"))


if __name__ == '__main__':
    s = socket(AF_INET, SOCK_STREAM)  # 创建UDP类型的套接字
    s.bind(("127.0.0.1", 8899))  # 绑定端口,ip可以不写
    s.listen(10)
    print(("wait"))
    client_socket, client_info = s.accept()
    print("建立成功")
    # 创建两个线程
    t1 = Thread(target=send_data)
    t2 = Thread(target=recv_data)
    t2.start()
    t1.start()
    t1.join()
    t2.join()

客户端

# coding=utf-8
from socket import *
from threading import Thread


# 不停接收
def recv_data():
    while True:
        recv_data = client_socket.recv(1024)
        recv_content = recv_data.decode('gbk')
        print(f"客户端说:{recv_content}")


# 不停发送
def send_data():
    while True:
        msg = input(">")
        client_socket.send(msg.encode("gbk"))
        if msg == "end":
            break


if __name__ == '__main__':
    client_socket = socket(AF_INET,SOCK_STREAM)
    client_socket.connect(("127.0.0.1",8899))

    # 创建两个线程
    t1 = Thread(target=send_data)
    t2 = Thread(target=recv_data)
    t2.start()
    t1.start()
    t1.join()
    t2.join()


    client_socket.close()

9.5 结束通信

服务器端:

# coding=utf-8
from socket import *
from threading import Thread


# 不停接收
def recv_data():
    while True:
        recv_data = client_socket.recv(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__':
    s = socket(AF_INET, SOCK_STREAM)  # 创建UDP类型的套接字
    s.bind(("127.0.0.1", 8899))  # 绑定端口,ip可以不写
    s.listen(10)
    print(("wait"))
    client_socket, client_info = s.accept()
    print("建立成功")
    # 创建两个线程
    t1 = Thread(target=send_data)
    t2 = Thread(target=recv_data)
    t2.start()
    t1.start()
    t1.join()
    t2.join()

客户端

# coding=utf-8
from socket import *
from threading import Thread


# 不停接收
def recv_data():
    while True:
        recv_data = client_socket.recv(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":
            break


if __name__ == '__main__':
    client_socket = socket(AF_INET,SOCK_STREAM)
    client_socket.connect(("127.0.0.1",8899))

    # 创建两个线程
    t1 = Thread(target=send_data)
    t2 = Thread(target=recv_data)
    t2.start()
    t1.start()
    t1.join()
    t2.join()


    client_socket.close()

总结

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值