IO多路复用 + 基于IO多路复用和socket并发请求 + 协程

 

IO多路复用

模块:   import  select.select()

1. IO多路复用的作用?

  检测监听多个socket是否已经发生了变化   (是否已经连接成功/获取数据/可读/可写等).

操作系统检测socket是否发生变化,有三种模式:

    select: 最多监听1024个socket, 循环检测.

    poll:不限制监听socket个数;循环去检测(水平触发)。

    epoll:不限制监听socket个数;回调方式(边缘触发)。  

在python模块中有2个:   select    epoll

示例 1:

  向百度发送一次请求, 有两种方式. 第一种是通过 requests , 另一种是通过 socket 对象.

socket 发送一次请求 单线程

示例 2:

  若向百度发送3个关键字请求,  将请求封装到函数中, 将关键字放入列表中,通过循环执行请求的函数. 该方法也是单线程的.

虽然能够实现功能,但是如果想要发送的请求是100次,这种单线程的工作,只能是一个一个的处理,因为connect和recv会有阻塞,需要等待事情处理完,才能执行下一个请求.

单线程  发送三次关键字请求

示例 3:

  为了解决上述的问题,有人说可以用多线程来解决,是的多线程的确是可以提高效率,如果线程池中有5个线程,那么就可以5个请求5个请求的处理,可以提高一定的效率.但是线程的工作过程中,connect和recv依然会有阻塞,还是要等待,服务端返回数据,才会执行下一个请求.   将示例2中的循环执行请求,换成创建线程来处理.

import threading

key_list = ['alex','db','sb']
for item in key_list:
    t = threading.Thread(target=get_data,args=(item,))  # get_data函数是发送请求,item为关键字
    t.start()
多线程 3次请求

 

总结: 出现以上的问题都是因为 socket 对象在connect 和 recv 会有阻塞,导致要等待.....

  因此, 想到解除 socket 的阻塞问题,让线程可以做其他的事情,但是解除阻塞以后,

  要考虑,   怎么知道请求是IO操作?    怎么知道连接成功了?  怎么知道结果什么时候返回来了?

  IO多路复用  select.select()   就是帮助我们检测以上问题的.

IO多路复用+socket并发请求

模块:  Twisted

原理:

  1. 解除阻塞  --  非阻塞

  2. 监听什么时候发生变化回来, select.select()

示例1 :

  IO多路复用+socket请求  实现  单线程并发

  http协议:  b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n    重点

import socket
import select

# 创建了3个socket对象

client1 = socket.socket()
client1.setblocking(False) # 百度创建连接: 非阻塞   阻塞的地方需要捕获异常

try:
    client1.connect(('www.baidu.com',80))
except BlockingIOError as e:
    pass


client2 = socket.socket()
client2.setblocking(False) # 百度创建连接: 非阻塞
try:
    client2.connect(('www.sogou.com',80))
except BlockingIOError as e:
    pass


client3 = socket.socket()
client3.setblocking(False) # 百度创建连接: 非阻塞
try:
    client3.connect(('www.oldboyedu.com',80))
except BlockingIOError as e:
    pass



socket_list = [client1,client2,client3]
conn_list = [client1,client2,client3]

while True:
    rlist,wlist,elist = select.select(socket_list,conn_list,[],0.005)  # 进行检测,rlist放返回数据的socket对象
                                                        # wlist放连接成功的socket对象,elist放出现异常的对象
    # wlist中表示已经连接成功的socket对象
    for sk in wlist:
        if sk == client1:
            sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
        elif sk==client2:
            sk.sendall(b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n')
        else:
            sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.oldboyedu.com\r\n\r\n')
        conn_list.remove(sk)
    for sk in rlist:
        chunk_list = []
        while True:
            try:
                chunk = sk.recv(8096)
                if not chunk:
                    break
                chunk_list.append(chunk)
            except BlockingIOError as e:
                break
        body = b''.join(chunk_list)
        # print(body.decode('utf-8'))
        print('------------>',body)
        sk.close()
        socket_list.remove(sk)
    if not socket_list:
        break

示例2 : 

  单线程的并发   --  高级版本    (实际上是对Twisted模块的原理解析,以后的工作中,会用该模块来实现基于IO复用的单线程并发)

import socket
import select

class Req(object):      # 进行封装
    def __init__(self,sk,func):
        self.sock = sk
        self.func = func

    def fileno(self):
        return self.sock.fileno()


class Nb(object):     # 进行封装

    def __init__(self):
        self.conn_list = []
        self.socket_list = []

    def add(self,url,func):
        client = socket.socket()
        client.setblocking(False)  # 非阻塞
        try:
            client.connect((url, 80))
        except BlockingIOError as e:
            pass
        obj = Req(client,func)
        self.conn_list.append(obj)
        self.socket_list.append(obj)

    def run(self):

        while True:
            rlist,wlist,elist = select.select(self.socket_list,self.conn_list,[],0.005)
            # wlist中表示已经连接成功的req对象
            for sk in wlist:
                # 发生变换的req对象
                sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
                self.conn_list.remove(sk)
            for sk in rlist:
                chunk_list = []
                while True:
                    try:
                        chunk = sk.sock.recv(8096)
                        if not chunk:
                            break
                        chunk_list.append(chunk)
                    except BlockingIOError as e:
                        break
                body = b''.join(chunk_list)
                # print(body.decode('utf-8'))
                sk.func(body)
                sk.sock.close()
                self.socket_list.remove(sk)
            if not self.socket_list:
                break


def baidu_repsonse(body):
    print('百度下载结果:',body)

def sogou_repsonse(body):
    print('搜狗下载结果:', body)

def oldboyedu_repsonse(body):
    print('老男孩下载结果:', body)


t1 = Nb()
t1.add('www.baidu.com',baidu_repsonse)    # 函数的回调
t1.add('www.sogou.com',sogou_repsonse)
t1.add('www.oldboyedu.com',oldboyedu_repsonse)
t1.run()
单线程高级版本 -- 类似Twisted原理

示例3 :  

  利用 Twisted 模块

from twisted.web.client import getPage, defer
from twisted.internet import reactor

def all_done(arg):
    reactor.stop()

def callback(contents):
    print(contents)

    deferred_list = []
url_list = ['http://www.bing.com', 'http://www.baidu.com', ]
for url in url_list:
    deferred = getPage(bytes(url, encoding='utf8'))
    deferred.addCallback(callback)
    deferred_list.append(deferred)

    dlist = defer.DeferredList(deferred_list)
    dlist.addBoth(all_done)

reactor.run()

总结:

  1. socket 默认是否是阻塞的?阻塞体现在哪里?

    默认是阻塞的,体现在 connect 和 recv上

  2. 如何让socket编程非阻塞?

    sk = socket.socket()

    sk.setblocking(False)     #设置非阻塞,  需要捕获异常,否则会报错

    try:  .........

    except  BlockingIOError as e :

       break

  3. IO多路复用的作用?
    检测多个socket是否发生变化。
    操作系统检测socket是否发生变化,有三种模式:
      select:最多1024个socket;循环去检测。
      poll:不限制监听socket个数;循环去检测(水平触发)。
      epoll:不限制监听socket个数;回调方式(边缘触发)。
    Python模块:
      select.select
      select.epoll      但是 windows不支持, linux 支持.

  4. 提高并发的方案?

    多进程

    多线程

    异步非阻塞模块(Twisted)   scrapy框架(单线程完成并发)

  5. 什么是异步非阻塞?

    非阻塞: 不等待,

      比如创建socket对某个地址进行connect. 获取接收数据recv 时默认都会等待(连接成功或接收到数据),才执行后续操作.

      如果设置setblocking(False),以上两个过程就不再等待了,但是会报 BlockingIOError 的错误,只要捕获即可.

    异步:通知, 执行完成之后自动执行回调函数或自动执行某些操作(通知).

      比如做爬虫中向某个地址baidu.com发送请求,当请求执行完成之后自动执行回调函数.

  6. 什么是同步阻塞?

    阻塞: 等

    同步: 按照顺序执行 

key_list = ['alex','db','sb']
            for item in key_list:
                ret = requests.get('https://www.baidu.com/s?wd=%s' %item)
                print(ret.text)

协程

1. 什么是协程?

  进程: 操作系统中存在的

  线程: 操作系统中存在的

  协程: 微线程,是由程序员创造出来的一个不是真实存在的东西.

2. 协程的作用及编写?

  作用:对一个线程进行分片,使得线程在代码块之间来回进行切换,而不是原来的逐行执行.

需要导入模块  import  greenlet

该模块需要在cmd安装:  pip3 install greenlet

示例 1 :

  单纯的协程是无用的.

 

import greenlet

def f1():
    print(11)
    gr2.switch()
    print(22)
    gr2.switch()

def f2():
    print(33)
    gr1.switch()
    print(44)

# 协程 gr1
gr1 = greenlet.greenlet(f1)   # 只是创建协程并未运行
# 协程 gr2
gr2 = greenlet.greenlet(f2)
gr1.switch()  # 运行f1

# 11
# 33
# 22
# 44
单纯的协程是无用的

示例 2: ♥♥

 单纯的协程是无用的,但是当   协程 +  遇到IO就切换   ==>>  实现单线程的并发

需要安装模块  pip3 install gevent

导入     from gevent import monkey

       monkey.patch_all()        #非常重要,以后代码中遇到IO都会自动执行greenlet的switch进行切换

协程_IO切换示例:

from gevent import monkey
monkey.patch_all() # 以后代码中遇到IO都会自动执行greenlet的switch进行切换   重点
import requests
import gevent


def get_page1(url):
    ret = requests.get(url)
    print(url,ret.content)

def get_page2(url):
    ret = requests.get(url)
    print(url,ret.content)

def get_page3(url):
    ret = requests.get(url)
    print(url,ret.content)

gevent.joinall([
    gevent.spawn(get_page1, 'https://www.python.org/'), # 协程1
    gevent.spawn(get_page2, 'https://www.yahoo.com/'),  # 协程2
    gevent.spawn(get_page3, 'https://github.com/'),     # 协程3
])

示例3 : 

  手动实现协程, 基于yield 生成器

def f1():
    print(11)
    yield
    print(22)
    yield
    print(33)

def f2():
    print(55)
    yield
    print(66)
    yield
    print(77)

v1 = f1()
v2 = f2()

next(v1) # v1.send(None)
next(v2) # v1.send(None)
next(v1) # v1.send(None)
next(v2) # v1.send(None)
next(v1) # v1.send(None)
next(v2) # v1.send(None)   # 报错可以抛出异常处理
手动实现协程

 总结:

  1.什么是协程?

     协程也可以称为“微线程”,就是开发者控制线程执行流程,控制先执行某段代码然后再切换到另外函执行代码...来回切换。

   2. 协程可以提高并发吗?

    协程本身是无法实现并发的,(甚至性能会降低)

    协程 + IO切换,就可以实现并发,提高性能.

  3. 进程,线程,协程的区别?

    重点

  4. 单线程的并发(2种)

    1) 协程 + IO切换: gevent

    2) 基于事件循环的 异步非阻塞框架: Twisted 

 

posted @ 2018-09-13 16:32  葡萄想柠檬  Views(538)  Comments(0)    收藏  举报
目录代码