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

相关代码链接:

https://gitee.com/bitwli-luyao/python-network-and-code.git

章节1:并发编程

1、并发编程介绍_串行_并行_并发的区别

串行、并行与并发的区别

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

2、并发编程介绍_进程_线程_协程的区别

进程、线程、协程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
  2. 一个进程由多个线程组成,线程是一个进程中代码的不同执行路线
  3. 进程之间相互独立,但同一进程下的各个线程共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多
  • 进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低
  • 线程(Thread):拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度;线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)
  • 协程(coroutine):拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度;协程切换任务资源很小,效率高

进程是什么?

进程(Process)是一个具有一定独立功能的程序关于某个数据集合的一次运动活动

现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

线程是什么?

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

并发编程解决方案:

多任务的实现有3种方式:

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

协程是什么?

协程(Coroutines),也叫作纤程(Fiber)。是一种在线程中比线程更加轻量级的存在,由程序员自己写程序来管理。

当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充分利用了IO等待的时间,提高了效率。

3、并发编程介绍_同步和异步通信机制的区别

同步和异步介绍

同步和异步强调的是消息通信机制(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. 老板说,找到书了,发给你

4、线程_方法包装创建线程

线程(Thread)特点:

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

线程的创建方式

Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

线程的创建可以通过分为两种方式:

1. 方法包装

2. 类包装

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

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

#encoding=utf-8
from threading import Thread
from time import sleep


def func1(name):
    print(f"线程{name},start")
    for i in range(3):
        print(f"线程:{name},{i}")
        sleep(3)
    print(f"线程{name},end")

if __name__ == '__main__':
    print("主线程,start")
    #创建线程
    t1 = Thread(target=func1,args=("t1",))
    t2 = Thread(target=func1,args=("t2",))
    #启动线程
    t1.start()
    t2.start()
    print("主线程,end")

5、线程_类包装创建线程

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

from threading import Thread
from time import sleep


class MyThread(Thread):
    def __init__(self, name):
        Thread.__init__(self)
        self.name = name

    def run(self):
        print(f"线程{self.name},start")
        for i in range(3):
            print(f"线程:{self.name},{i}")
            sleep(3)
        print(f"线程{self.name},end")

if __name__ == '__main__':
    print("主线程,start")
    # 创建线程
    t1 = MyThread("t1")
    t2 = MyThread("t2")
    # 启动线程
    t1.start()
    t2.start()
    print("主线程,end")

6、线程_join()和守护线程

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")
  • 主线程中执行t1.join(),则主线程会等待t1线程执行完后,再结束
  • 守护线程,主要的特征是它的生命周期。主线程死亡,它也就随之死亡
  • 守护线程最典型的应用就是 GC (垃圾收集器)

7、线程_全局解释器锁GIL问题

全局锁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语言的缺陷

8、线程_线程同步和互斥锁_资源冲突案例

线程同步和互斥锁

线程同步的概念

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

【示例】多线程操作同一个对象(未使用线程同步)

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

没有线程同步机制,两个线程同时操作同一个账户对象,竟然从只有100元的账户,轻松取出80*2=160元,账户余额竟然成为了-60。这么大的问题,显然银行不会答应的。

9、线程_互斥锁典型案例

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

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

互斥锁是什么?

互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。

注意: 互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。

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()  #你老婆取钱
  • acquirerelease方法之间的代码同一时刻只能有一个线程去操作
  • 如果在调用acquire方法的时候 其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。

10、线程_死锁问题和解决方案

死锁

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

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()

死锁的解决方法

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

11、线程_信号量

信号量(Semaphore)

互斥锁使用后,一个资源同时只有一个线程访问。如果某个资源,我们同时想让N个(指定数值)线程访问?这时候,可以使用信号量。

信号量控制同时访问资源的数量。信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。

应用场景

  1. 在读写文件的时候,一般只能只有一个线程在写,而读可以有多个线程同时进行,如果需要限制同时读文件的线程个数,这时候就可以用到信号量了(如果用互斥锁,就是限制同一时刻只能有一个线程读取文件)。
  2. 在做爬虫抓取数据时。

底层原理

信号量底层就是一个内置的计数器。每当资源获取时(调用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
​
'''

12、线程_事件Event对象

事件(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

【示例】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开始吃咯!
'''

13、线程_生产者消费者模式

生产者和消费者模式

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

什么是生产者?

生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

什么是消费者?

消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)

什么是缓冲区?

消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

缓冲区是实现并发的核心,缓冲区的设置有3个好处:

  1. 实现线程的并发协作

有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。

  1. 解耦了生产者和消费者

生产者不需要和消费者直接打交道

  1. 解决忙闲不均,提高效率

生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据

缓冲区和queue对象

从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put()get()操作来向队列中添加或者删除元素。Queue 对象已经包含了必要的锁,所以你可以通过它在多个线程间多安全地共享数据。

【例】生产者消费者模式典型代码

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)
    c = Thread(target=consumer)
    t.start()
    c.start()

14、进程_方法模式创建进程_windows多进程的一个bug

进程Process

什么是进程

进程(Process):拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度;进程切换需要的资源很最大,效率低。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

进程的优缺点

进程的优点:

  1. 可以使用计算机多核,进行任务的并行执行,提高执行效率
  2. 运行不受其他进程影响,创建方便
  3. 空间独立,数据安全

进程的缺点:

  • 进程的创建和删除消耗的系统资源较多

进程的创建方式(方法模式)

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

进程的创建可以通过分为两种方式:

1. 方法包装

2. 类包装

创建进程后,使用start()启动进程

方法模式创建进程

#方法包装-多进程实现
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')

if __name__=="__main__":
    print('当前进程ID:',os.getpid())
    #创建进程
    p1 = Process(target=func1,args=("p1",))
    p2 = Process(target=func1,args=('p2',))
    p1.start()
    p2.start()

15、进程_类模式创建进程

进程的创建方式(继承Process类)

和使用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')
        print(f'Process:{self.name} end')

if __name__=="__main__":
    #创建进程
    p1=MyProcess('p1')
    p2=MyProcess('p2')
    p1.start()
    p2.start()

16、进程_Queue实现进程通信

Queue实现进程间通信

前面讲解了使用 Queue 模块中的 Queue 类实现线程间通信,但要实现进程间通信,需要使用 multiprocessing 模块中的 Queue 类。

简单的理解 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__':
    p_list=[]
    mq=Queue()
    mq.put('1')
    mq.put('2')
    mq.put('3')
    #循环创建进程
    for i in range(3):
        p = MyProcess('p{}'.format(i),mq)
        p_list.append(p)
    for p in p_list:
        p.start()
    for p in p_list:
        p.join()

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

17、进程_Pipe管道实现进程通信

Pipe实现进程间通信

Pipe 直译过来的意思是“管”或“管道”,和实际生活中的管(管道)是非常类似的。

Pipe方法返回(conn1, conn2)代表一个管道的两个端。

Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个参数是全双工模式,也就是说conn1和conn2均可收发。若duplex为False,conn1只负责接收消息,conn2只负责发送消息。send和recv方法分别是发送和接受消息的方法。例如,在全双工模式下,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞。如果管道已经被关闭,那么recv方法会抛出EOFError。

使用pipe管道实现进程之间的通信

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()

18、进程_Manager管理器实现进程通信

Manager管理器

管理器提供了一种创建共享数据的方法,从而可以在不同进程中共享。

from multiprocessing import Process,current_process,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)

19、进程_进程池管理进程的两种典型案例

进程池(Pool)

Python提供了更好的管理多个进程的方式,就是使用进程池。

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

使用进程池的优点:

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

类/方法

功能

参数

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
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)

20、协程_核心概念

协程是什么

协程,Coroutines,也叫作纤程(Fiber)

协程,全称是“协同程序”,用来实现任务协作。是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。

当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充分利用了IO等待的时间,提高了效率。

一个故事说明进程、线程、协程的关系

乔布斯想开工厂生产手机,费劲力气,制作一条生产线,这个生产线上有很多的器件以及材料。一条生产线就是一个进程。

只有生产线是不够的,所以找五个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这五个工人就是五个线程。

为了提高生产率,想到3种办法:

  1. 一条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式
  2. 多条生产线,每个生产线上多个工人,即多进程多线程
  3. 乔布斯深入一线发现工人不是那么忙,有很多等待时间。于是规定:如果某个员工在等待生产线某个零件生产时 ,不要闲着,干点其他工作。也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,这就是:协程方式。

协程的核心(控制流的让出和恢复)

  1. 每个协程有自己的执行栈,可以保存自己的执行现场
  2. 可以由用户程序按需创建协程(比如:遇到io操作)
  3. 协程“主动让出(yield)”执行权时候,会保存执行现场(保存中断时的寄存器上下文和栈),然后切换到其他协程
  4. 协程恢复执行(resume)时,根据之前保存的执行现场恢复到中断前的状态,继续执行,这样就通过协程实现了轻量的由用户态调度的多任务模型

协程和多线程比较

比如,有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。

  1. 在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。
  2. 多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。
  3. 协程版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。

协程的优点

  1. 由于自身带有上下文和栈,无需线程上下文切换的开销,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;
  2. 无需原子操作的锁定及同步的开销;
  3. 方便切换控制流,简化编程模型
  4. 单线程内就可以实现并发的效果,最大限度地利用cpu,且可扩展性高,成本低(注:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理)

asyncio协程是写爬虫比较好的方式。比多线程和多进程都好. 开辟新的线程和进程是非常耗时的。

协程的缺点

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。
  2. 当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。

21、协程_yield方式实现

使用yield实现协程(已淘汰,了解)

Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:

  1. 最初的生成器变形yield/send
  2. 引入@asyncio.coroutineyield from
  3. 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
'''

22、协程_syncio异步IO实现协程

asyncio实现协程(重点)

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

【示例】不使用asncio的任务切换

#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
'''

使用asyncio,整体执行完,耗时3秒,效率极大提高。

【示例】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秒,效率极大提高

章节2:网络通信

1、网络编程学习要点和整体介绍

网络编程

互联网时代,现在基本上所有的程序都是网络程序,很少有单机版的程序了。

网络编程就是如何在程序中实现两台计算机的通信。

Python语言中,提供了大量的内置模块和第三方模块用于支持各种网络访问,而且Python语言在网络通信方面的优点特别突出,远远领先其他语言。

通过本章,可以学到:

  1. 了解TCP和UDP
  2. 掌握编写UDP Socket客户端应用
  3. 掌握编写UDP Socket服务器端应用
  4. 掌握编写TCP Socket客户端应用
  5. 掌握编写TCP Socket服务器端应用

2、IP地址_私有地址等概念

基本概念

IP地址

IP是Internet Protocol Address,即"互联网协议地址"。

用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址。

路由器是连接两个或多个网络的网络设备。

IP地址就像是我们的家庭住址一样,如果你要写信给一个人,你就要知道他(她)的地址,这样邮递员才能把信送到。计算机发送信息就好比是邮递员,它必须知道唯一的“家庭地址”才能不至于把信送错人家。只不过我们的地址是用文字来表示的,计算机的地址用二进制数字表示。 IP地址被用来给Internet上的电脑一个编号。大家日常见到的情况是每台联网的PC上都需要有IP地址,才能正常通信。我们可以把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,而Internet中的路由器,就相当于电信局的“程控式交换机”。

类别

最大网络数

IP地址范围

单个网段最大主机数

私有IP地址范围

A

126(2^7-2)

1.0.0.1-127.255.255.254

16777214

10.0.0.0-10.255.255.255

B

16384(2^14)

128.0.0.1-191.255.255.254

65534

172.16.0.0-172.31.255.255

C

2097152(2^21)

192.0.0.1-223.255.255.254

254

192.168.0.0-192.168.255.255

目前主流使用的IP地址是IPV4,但是随着网络规模的不断扩大,IPV4面临着枯竭的危险,所以推出了IPV6。

IPV4,采用32位地址长度,只有大约43亿个地址,它只有4段数字,每一段最大不超过255。随着互联网的发展,IP地址不够用了,在2019年11月25日IPv4位地址分配完毕。

IPv6采用128位地址长度,几乎可以不受限制地提供地址。按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可分配1000多个地址。

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

注意事项

  • 127.0.0.1 本机地址
  • 192.168.0.0--192.168.255.255为私有地址,属于非注册地址,专门为组织机构内部使用

3、端口的概念

端口port

端口号用来识别计算机中进行通信的应用程序。因此,它也被称为程序地址。

一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地进行数据传输。

总结

  • IP地址好比每个人的地址(门牌号),端口好比是房间号。必须同时指定IP地址和端口号才能够正确的发送数据。
  • IP地址好比为电话号码,而端口号就好比为分机号。

端口分配

端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。 端口的表示是一个16位的二进制整数,对应十进制的0-65535。

操作系统中一共提供了0~65535可用端口范围。

按端口号分类:

公认端口(Well Known Ports):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。

注册端口(Registered Ports):从1024到65535。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。

4、网络协议ISO分层模型

网络通信协议

国际标准化组织(ISO,即International Organization for Standardization)定义了网络通信协议的基本框架,被称为OSI(Open System Interconnect,即开放系统互联)模型。要制定通讯规则,内容会很多,比如要考虑A电脑如何找到B电脑,A电脑在发送信息给B电脑时是否需要B电脑进行反馈,A电脑传送给B电脑的数据格式又是怎样的?内容太多太杂,所以OSI模型将这些通讯标准进行层次划分,每一层次解决一个类别的问题,这样就使得标准的制定没那么复杂。OSI模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。

OSI七层协议模型如图所示:

网络协议的分层

虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用最多的网络通信协议是TCP/IP网络通信协议。

TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层,网络接口层(物理+数据链路层)。

把用户应用程序作为最高层,把物理通信线路作为最低层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。

ISO模型与TCP/IP模型的对应关系如图所示。

5、TCP和UDP传输数据的区别

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是面向无连接的,传输数据不安全,效率较高。

6、TCP的三次握手和四次挥手_数据包的概念

TCP建立连接的三次握手

TCP是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂, 只简单的描述下这三次对话的简单过程:

1)主机A向主机B发出连接请求:“我想给你发数据,可以吗?”,这是第一次对话;

2)主机B向主机A发送同意连接和要求同步 (同步就是两台主机一个在发送,一个在接收,协调工作)的数据包 :“可以,你什么时候发?”,这是第二次对话;

3)主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”, 这是第三次握手。

三次“对话”的目的是使数据包的发送和接收同步, 经过三次“对话”之后,主机A才向主机B正式发送数据。

  1. 第一步,客户端发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
  2. 第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)
  3. 第三步,客户端也返回一个确认报文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使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。

数据包与处理流程

什么是数据包

通信传输中的数据单位,一般也称“数据包”。在数据包中包括:包、帧、数据包、段、消息。

网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。

7、socket套接字编程底层原理和TCP_UDP的关系

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_STREAMSOCK_DGRAMprotocol: 一般不填,默认为0。

Socket主要分为面向连接的Socket和无连接的Socket。

无连接Socket的主要协议是用户数据报协议,也就是常说的UDP,UDP Socket的名字是SOCK_DGRAM。创建套接字UDP/IP套接字,可以调用socket.socket()。示例代码如下:

udpSocket=socket.socket (AF_INET,SOCK_DGRAM)

socket对象的内置函数和属性

在Python语言中socket对象中,提供如表所示的内置函数。

函数

功能

服务器端套接字函数

s.bind()

绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。

s.listen()

开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。

s.accept()

被动接受TCP客户端连接,(阻塞式)等待连接的到来

客户端套接字

s.connect()

主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

s.connect_ex()

connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数

s.recv()

接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。

s.send()

发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。

s.sendall()

完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

s.recvfrom()

接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

s.sendto()

发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。

s.close()

关闭套接字

s.getpeername()

返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

s.getsockname()

返回套接字自己的地址。通常是一个元组(ipaddr,port)

s.setsockopt(level,optname,value)

设置给定套接字选项的值。

s.getsockopt(level,optname[.buflen])

返回套接字选项的值。

s.settimeout(timeout)

设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())

s.gettimeout()

返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。

s.fileno()

返回套接字的文件描述符。

s.setblocking(flag)

如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。

s.makefile()

创建一个与该套接字相关连的文件

8、UDP编程1_服务器接收数据经典案例

UDP编程介绍

UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。

创建Socket时,SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样,但是不需要调用listen()方法,而是直接接收来自任何客户端的数据。recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。

UDP编程的实现

【示例】UDP接收数据

from socket import *
#最简化的UDP服务端接收信息代码
s=socket(AF_INET,SOCK_DGRAM) #创建UDP类型的套接字
#绑定接收信息端口
s.bind(('127.0.0.1',8888)) #绑定一个端口号,ip地址和端口号,ip地址可以不写
print('等待接收数据!')
redata=s.recvfrom(1024) #1024表示本次接收的最大字节数
print(redata)
print(f"收到的远程信息:{redata[0].decode('gbk')},from{redata[1]}")
s.close()

9、UDP编程2_客户端发送数据经典案例

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()

10、UDP编程3_持续通信

【示例】UDP接收数据

from socket import *
#UDP服务端持续接收信息
s=socket(AF_INET,SOCK_DGRAM) #创建UDP类型的套接字
#绑定接收信息端口
s.bind(('127.0.0.1',8888)) #绑定一个端口号,ip地址和端口号,ip地址可以不写
print('等待接收数据!')
while True:
    redata=s.recvfrom(1024) #1024表示本次接收的最大字节数
    print(redata)
    recv_content=redata[0].decode('gbk')
    print(f"收到的远程信息:{recv_content},from{redata[1]}")
    if recv_content=='888':
        print('结束聊天!')
        break
s.close()

【示例】UDP发送数据

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=='888':
        print('结束聊天!')
        break
s.close()

11、UDP编程4_多线程结合自由通信

结合多线程实现UDP双向自由通信

UDP 不同于 TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因为其提供服务而称为服务器端。

如下服务端和客户端代码几乎相同,要注意接收端口号和发送端口号相对应

【示例】UDP实现多线程服务端(接收端口号是8888)

from socket import *
from threading import Thread
def recv_data():
    while True:
        redata=s.recvfrom(1024) #1024表示本次接收的最大字节数
        print(redata)
        recv_content=redata[0].decode('gbk')
        print(f"收到的远程信息:{recv_content},from{redata[1]}")
        if recv_content=='888':
            print('结束聊天!')
            break

def send_data():
    addr=("127.0.0.1",9999)
    while True:
        data=input('请输入:')
        s.sendto(data.encode('gbk'),addr)
        if data=='888':
            print('结束聊天!')
            break

if __name__=='__main__':
    s=socket(AF_INET,SOCK_DGRAM)
    s.bind(('127.0.0.1',8888))
    #创建两个线程
    t1 = Thread(target=recv_data)
    t2=Thread(target=send_data)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

【示例】UDP实现多线程客户端(发送方端口号是8888)

from socket import *
from threading import Thread
def recv_data():
    while True:
        redata=s.recvfrom(1024) #1024表示本次接收的最大字节数
        print(redata)
        recv_content=redate[0].decode('gbk')
        print(f"收到的远程信息:{recv_content},from{redata[1]}")
        if recv_content=='888':
            print('结束聊天!')
            break

def send_data():
    addr=("127.0.0.1",8888)
    while True:
        data=input('请输入:')
        s.sendto(data.encode('gbk'),addr)
        if data=='888':
            print('结束聊天!')
            break

if __name__=='__main__':
    s=socket(AF_INET,SOCK_DGRAM)
    s.bind(('127.0.0.1',9999))
    #创建两个线程
    t1 = Thread(target=recv_data)
    t2=Thread(target=send_data)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

12、TCP编程1_服务器端简化代码

from socket import *
server_socket  = socket(AF_INET,SOCK_STREAM) #建立TCP套接字
server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
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()

13、TCP编程2_客户端经简化代码

from socket import *
client_socket=socket(AF_INET,SOCK_STREAM)
client_socket.connect(('127.0.0.1',8899))
client_socket.send("hello".encode("gbk"))
client_socket.close()

14、TCP编程3_服务端和客户端持续通信

服务端:

from socket import *
server_socket  = socket(AF_INET,SOCK_STREAM) #建立TCP套接字
server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
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':
        print('结束连接!')
        break
    msg = input('>')
    client_socket.send(msg.encode('gbk'))
client_socket.close()
server_socket.close()

客户端:

from socket import *
client_socket=socket(AF_INET,SOCK_STREAM)
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)
    recv_content=recv_data.decode('gbk')
    print(f"服务端说:{recv_content}")
    
client_socket.close()

15、TCP编程4_结合多线程实现自由通信

服务端:

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 msg=='end':
            print('结束接收消息')
            break
    
def send_data():
    while True:
        if recv_content=='end':
            print('结束发送消息')
            break
        msg = input('>')
        client_socket.send(msg.encode('gbk'))

if "__name__"=="__main__":
    server_socket  = socket(AF_INET,SOCK_STREAM) #建立TCP套接字
    server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    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()

客户端:

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':
            print('结束发送消息')
            break

if "__name__"=="__main__":
    client_socket=socket(AF_INET,SOCK_STREAM)
    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()

16、TCP编程5_结束双方通信

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐