• Python学习第四模块笔记(网络编程基础)


    1、socketserver

    使用socketserver可以实现多并发连接

    socketserver.TCPServer() :TCP

    socketserver.ThreadingTCPServer() :多线程TCP

    socketserver.UDPServer():UDP

    socketserver.ThreadingUDPServer() :多线程UDP

    socketserver.UnixStreamServer():UNIX本机进程间的通讯,使用TCP

    socketserver.UnixDatagramServer():UNIX本机进程间的通讯,使用UDP

    创建socketserver步骤:

    1. 创建一个请求处理类,继承BaseRequestHandler,并重写基类的handle方法(与客户端所有的交互都在handle中完成)
    2. 实例化一个server类,并传递server IP和第一步创建的请求处理类
    3. 使用类的方法:handle_request(),只处理一个请求;server_forever(),处理多个请求

    e.g:

    #服务端
    
    import socketserver
    
    
    class TestServer(socketserver.BaseRequestHandler):
        def handle(self):
            while True:
                data = self.request.recv(1024)    # 接收信息
                print(data)
                self.request.send(b"ok")    # 发送信息
                print(self.client_address)    # 客户端的IP地址和端口号,一个元组
    
    server = socketserver.ThreadingTCPServer(("0.0.0.0", 6699), TestServer)    # 实例化一个类
    server.serve_forever()    # 调用server_forever()方法
    #客户端
    
    import socket
    
    client = socket.socket()
    client.connect(("127.0.0.1", 6699))
    while True:
        data = input("-->")
        client.send(data.encode("utf-8"))
        msg = client.recv(1024)
        print(msg)

    2、paramiko模块

    paramiko是一个基于SSH的用于连接远程服务器并执行相关操作的模块,使用该模块可以在远程服务器上运行命令或上传下载文件(SSHClient用于执行命令,SFTPClient用于上传下载文件)。

    paramiko并不是Python自带的模块,属于第三方模块,使用前必须先使用pip install paramiko安装该模块。

    SSHClient连接远程主机并执行命令

    import paramiko
    
    ssh = paramiko.SSHClient()
    # 创建一个SSH对象
    
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    # 自动将本机加入到know_hosts文件中
    
    ssh.connect(hostname="主机名或IP", port=22, username="用户名", password="密码")
    # 连接远程主机
    
    std_in, std_out, std_err = ssh.exec_command("df")
    # 执行命令并获取结果
    
    res, err = std_out.read(), std_err.read()
    # 读取命令执行正确及错误结果
    result = res if res else err
    # 三元运算,如果命令执行正确输出res,错误输出err
    print(result.decode())
    ssh.close()

    SSHClient使用Transport先建立连接

    import paramiko
    
    transport = paramiko.Transport(("主机名或IP", 端口))
    transport.connect(username="用户名", password="密码")
    
    ssh = paramiko.SSHClient()
    ssh._transport = transport
    
    std_in, std_out, std_err = ssh.exec_command("df")
    res, err = std_out.read(), std_err.read()
    result = res if res else err
    print(result.decode())
    transport.close()

    SSHClient使用密钥文件连接远程主机

    import paramiko
    
    rsa_key = paramiko.RSAKey.from_private_key_file("密钥文件")
    
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname="主机名或IP", port=端口, username="用户名", pkey=rsa_key)
    std_in, std_out, std_err = ssh.exec_command("df")
    res, err = std_out.read(), std_err.read()
    result = res if res else err
    print(result.decode())
    ssh.close()
    #另一种写法
    import paramiko
    
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname="主机名或IP", port=端口, username="用户名", key_filename="密钥文件")
    std_in, std_out, std_err = ssh.exec_command("df")
    res, err = std_out.read(), std_err.read()
    result = res if res else err
    print(result.decode())
    ssh.close()

    SSHClient使用Transoprt先建立连接并使用密钥文件连接远程主机

    import paramiko
    
    rsa_key = paramiko.RSAKey.from_private_key_file("密钥文件")
    
    transport = paramiko.Transport(("主机名或IP", 端口))
    transport.connect(username="用户名", pkey=rsa_key)
    
    ssh = paramiko.SSHClient()
    ssh._transport = transport
    
    std_in, std_out, std_err = ssh.exec_command("df")
    res, err = std_out.read(), std_err.read()
    result = res if res else err
    print(result.decode())
    transport.close()

    SFTPClient连接远程主机并上传下载文件

    import paramiko
    
    transport = paramiko.Transport(("主机名或IP", 端口))
    transport.connect(username="用户名", password="密码")
    sftp = paramiko.SFTPClient.from_transport(transport)
    
    sftp.put("test.text", "/tmp/test.text")    # 上传文件
    sftp.get("/tmp/test.text", "test1.text")    # 下载文件
    
    transport.close()

    SFTPClient使用密钥文件连接远程主机

    import paramiko
    
    rsa_key = paramiko.RSAKey.from_private_key_file("密钥文件")
    
    transport = paramiko.Transport(("主机名或IP", 端口))
    transport.connect(username="用户名", pkey=rsa_key)
    
    sftp = paramiko.SFTPClient.from_transport(transport)
    sftp.put("test.text", "/tmp/test.text")
    sftp.get("/tmp/test.text", "test1.text")
    
    transport.close()

    3、进程与线程

    进程:程序的执行实例称为进程。每个进程都提供给程序执行所需的资源。一个进程有一个虚拟地址空间,可执行代码,对系统对象的开放句柄,一个安全上下文,一个唯一的进程标识符,环境变量,一个优先级类型,最大最小工作集,以及至少一个执行线程。每个进程以一个线程开始,通常称为主线程,可以从它的任何线程创建额外的线程。

    线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程执行不同的任务。CPU通过上下文来在不同的线程中切换执行。

    进程与线程的区别:

    • 线程共享一个内存空间,不同的进程有独立的内存空间
    • 线程直接访问进程的数据,子进程拥有父进程的数据的副本
    • 同一进程的线程之间可以直接相互通信,进程间的通信必须通过一个中间代理实现
    • 新的线程很容易创建,而创建新的进程需要对其父进程进行一次克隆
    • 一个线程可以操作同一进程里的其他线程,进程只能操作子进程
    • 对于主线程的修改有可能会影响其他线程的行为,而对一个父进程的修改不会影响其他子进程

    Python多线程

    线程的两种调用方式

    #直接调用
    
    import time
    import threading
    
    
    def run(n):
        print("线程", n, threading.current_thread())
        time.sleep(1)    # 显示线程同时运行的效果
    
    
    t1 = threading.Thread(target=run, args=("t1",))
    t2 = threading.Thread(target=run, args=("t2",))
    # 实例化线程对象
    t1.start()
    t2.start()
    # 启动线程
    print(t1.getName())    # 查看当前线程名
    print(t2.getName())
    print(threading.current_thread())    # 查看当前线程类型
    print(threading.active_count())    # 查看当前活跃线程个数
    
    #结果
    线程 t1 <Thread(Thread-1, started 25240)>
    线程 t2 <Thread(Thread-2, started 25244)>
    #启动的两个线程
    Thread-1
    Thread-2
    <_MainThread(MainThread, started 25200)>    # 主线程,程序运行时本身就是主线程
    3    #启动两个线程,加上程序本身的线程,一共有三个
    #继承式调用
    
    import time
    import threading
    
    
    class Thread(threading.Thread):
        def __init__(self, n):
            super(Thread, self).__init__()
            self.n = n
    
        def run(self):    # 线程运行的函数函数名必须为run
            print("线程", self.n)
            time.sleep(1)
    
    
    t1 = Thread("t1")
    t2 = Thread("t2")
    t1.start()
    t2.start()

    多线程中的join和守护线程

    #join 等待线程执行完成
    import time
    import threading
    
    
    def run(n):
        print("线程", n)
        time.sleep(1)
    
    
    t_obj = []
    for i in range(10):
        t = threading.Thread(target=run, args=(i,))
        t.start()
        t_obj.append(t)
    
    for t in t_obj:
        t.join()
    # 加上join后等待所有线程执行完毕后打印最后一句话
    
    print("所有线程都执行完了...")
    #守护线程
    import time
    import threading
    
    
    def run(n):
        time.sleep(1)
        print("线程", n)
    
    
    for i in range(10):
        t = threading.Thread(target=run, args=(i,))
        t.setDaemon(True)    # 设置成守护线程,当主线程停止工作后守护线程也同时停止工作
        t.start()
    
    print("主线程停止了,所有守护线程都得停止...")
    
    #结果
    主线程停止了,所有守护线程都得停止...
    #所有的线程在sleep的时候主线程结束了,所以不会打印其他内容

    GIL锁(全局解释器锁)

    GIL锁只存在于CPython解释器中。由于CPython在实现多线程时底层直接调用C语言来实现,此时Python只能被动等待C的返回结果,无法控制多线程的运行,所以在多CPU环境下可能造成多个线程同时访问修改同一份数据,而不是依次访问修改,使最终结果出错。为此CPython加上了GIL锁,使得同一时间只能有一个线程在运行。

    线程锁(互斥锁)

    当多个线程同时访问同一份数据时有可能造成混乱,所以加上线程锁。

    import time
    import threading
    
    n = 0
    t_obj = []
    lock = threading.Lock()    # 生成锁
    
    
    def run(a):
        print("线程", a)
        time.sleep(1)
        lock.acquire()    # 修改数据前加锁
        global n
        n += 1
        lock.release()    # 修改后解锁
    
    
    for i in range(5):
        t = threading.Thread(target=run, args=(i,))
        t.start()
        t_obj.append(t)
    
    for t in t_obj:
        t.join()
    
    print(n)

    递归锁

    包含多个锁的场景

    import threading
    
    n = 0
    t_obj = []
    lock = threading.RLock()
    
    
    def run1():
        lock.acquire()
        global n
        n += 1
        lock.release()
    
    
    def run2():
        lock.acquire()
        run1()
        lock.release()
    
    
    for i in range(5):
        t = threading.Thread(target=run2)
        t.start()
        t_obj.append(t)
    
    for t in t_obj:
        t.join()
    
    print(n)

    信号量

    同时允许一定数量的线程运行。

    import time
    import threading
    
    semaphore = threading.BoundedSemaphore(5)    # 允许同时运行5个线程
    
    
    def run(n):
        semaphore.acquire()
        print("线程", n)
        time.sleep(1)
        semaphore.release()
    
    
    for i in range(20):
        t = threading.Thread(target=run, args=(i,))
        t.start()

    events(事件)

    等待事件发生时执行,用于进程间的交互。

    import time
    import threading
    
    event = threading.Event()
    
    
    def light():
        count = 1
        event.set()    # 设置标志
        while True:
            if 5 < count < 11:
                event.clear()    # 清理标志位
                print("红灯".center(50, "*"))
                count += 1
            elif count > 16:
                event.set()   # 重新设置标志
                print("绿灯".center(50, "*"))
                count = 0
            else:
                print("绿灯".center(50, "*"))
                count += 1
            time.sleep(1)
    
    
    def car():
        while True:
            if event.is_set():    # 如果设置了标志位
                print("绿灯行...")
                time.sleep(1)
            else:
                print("红灯停...")
                event.wait()    # 等待设置标志位
    
    
    light1 = threading.Thread(target=light)
    light1.start()
    car1 = threading.Thread(target=car)
    car1.start()

    队列(queue)

    队列的作用:1.解耦 2.提高效率

    队列中的方法

    #queue.Queue(maxsize=) 先进先出队列,maxsize设置队列大小,默认不限制
    
    #queue.LifoQueue(maxsize=) 后入先出队列
    
    #queue.PriorityQueue(maxsize=) 设置优先级的队列
    
    #Queue.qsize() 队列大小
    >>> q = queue.Queue()
    >>> q.put(1)
    >>> q.qsize()
    1
    
    #Queue.empty() 队列为空返回True
    >>> q = queue.Queue()
    >>> q.empty()
    True
    
    #Queue.full() 队列满返回True 
    >>> q = queue.Queue(1)
    >>> q.put(1)
    >>> q.full()
    True
    
    #Queue.put(item, block=True, timeout=None) 将数据放入队列
    #不加block(默认为1)和timeout,队列满时put会卡住,加block和timeout队列满时抛出异常
    #加block=0同Queue.put_nowait(item)
    >>> q.put(1,block = 0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "C:\Users\MAIMAI\AppData\Local\Programs\Python\Python36\lib\queue.py", line 130, in put
        raise Full
    queue.Full
    
    #Queue.get(block=True, timeout=None) 取出数据
    #加block=0同Queue.get_nowait()
    >>> q.get(block = 0)
    1
    >>> q.get(block = 0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "C:\Users\MAIMAI\AppData\Local\Programs\Python\Python36\lib\queue.py", line 161, in get
        raise Empty
    queue.Empty
    
    #Queue.join() 等待直到队列被消耗完
     
    #Queue.task_done() 任务执行完成

    利用队列实现生产者消费者模型

    import time
    import queue
    import threading
    
    q = queue.Queue()
    
    
    def producer():
        while True:
            print("开始生产了...")
            q.put("物品")
            time.sleep(1)
    
    
    def consumer():
        while True:
            print("开始取走%s..." % q.get())
            time.sleep(1)
    
    
    t1 = threading.Thread(target=producer)
    t1.start()
    t2 = threading.Thread(target=consumer)
    t2.start()

    多线程使用场景

    IO操作不占用CPU,计算占用CPU。Python多线程不适合CPU密集操作的任务,适合用于IO密集型的任务。

    Python多进程

    基本语法

    import os
    from multiprocessing import Process
    
    
    def run(n):
        print("这个例子包含进程的基本语法", n)
        print("主进程ID", os.getppid())
        print("子进程ID", os.getpid())
    
    
    if __name__ == "__main__":
    # 启动进程时必须这么写,意思是判断是否主动执行脚本,如果主动执行该脚本,则执行下面的代码块,如果是从其他地方调用该脚本,则不执行下面的代码块
        p1 = Process(target=run, args=("p1",))
        p2 = Process(target=run, args=("p2",))
        p1.start()
        p2.start()
    
    #结果
    这个例子包含进程的基本语法 p2
    这个例子包含进程的基本语法 p1
    主进程ID 54512
    子进程ID 54564
    主进程ID 54512
    子进程ID 54556
    #程序的执行也必须通过一个进程来启动,所以主进程的ID是相同的,为PyCharm的进程ID

    进程间的数据交互与共享

    由于进程间不共享内存空间,所以进程间需要进行数据交互必须通过第三方来实现。

    1、使用Queue

    from multiprocessing import Process, Queue
    
    
    def run1(q1):
        q1.put("例子")
    
    
    def run2(q2):
        print(q2.get())
    
    
    if __name__ == "__main__":
        q = Queue()
        p1 = Process(target=run1, args=(q,))
        p2 = Process(target=run2, args=(q,))
        p1.start()
        p2.start()
    
    #结果
    例子

    2、使用Pipe(管道)

    from multiprocessing import Process, Pipe
    
    
    def run1(conn1):
        conn1.send("例子")
        conn1.close()
    
    
    def run2(conn2):
        print(conn2.recv())
    
    
    if __name__ == "__main__":
        parent_conn, child_conn = Pipe()
        p1 = Process(target=run1, args=(parent_conn,))
        p2 = Process(target=run2,args=(child_conn,))
        p1.start()
        p2.start()
    
    #结果
    例子

    3、Manager(多进程间的数据共享)

    可传递 list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value和Array等数据类型。

    import os
    from multiprocessing import Process, Manager
    
    
    def run(d1, l1):
        """所以进程共享该函数里面的数据"""
        d1["例子"] = "例子"
        d1["主进程ID"] = os.getppid()
        l1.append(os.getpid())
        print(d1)
        print(l1)
    
    
    if __name__ == "__main__":
        with Manager() as manager:
            d = manager.dict()
            l = manager.list()
            p_list = []
            for i in range(5):
                p = Process(target=run, args=(d, l))
                p.start()
                p_list.append(p)
            for p in p_list:
                p.join()
    
    #结果
    {'例子': '例子', '主进程ID': 62220}
    [62036]
    {'例子': '例子', '主进程ID': 62220}
    [62036, 61996]
    {'例子': '例子', '主进程ID': 62220}
    [62036, 61996, 62344]
    {'例子': '例子', '主进程ID': 62220}
    [62036, 61996, 62344, 62100]
    {'例子': '例子', '主进程ID': 62220}
    [62036, 61996, 62344, 62100, 62460]

    进程加锁

    用于该进程独占屏幕输出,防止与其他进程的输出混淆

    from multiprocessing import Process, Lock
    
    
    def run(l, n):
        l.acquire()    # 加锁
        print("例子", n)
        l.release()    # 解锁
    
    
    if __name__ == "__main__":
        lock = Lock()
        for i in range(5):
            p = Process(target=run, args=(lock, i))
            p.start()

    进程池

    进程池维护一个进程列表,使用时从池中获取一个进程,如果池为空,则等待新的进程加入池中

    from multiprocessing import Pool
    
    
    def run(n):
        print("例子", n)
    
    
    def bar(n):
        print("回调函数", n)
    
    
    if __name__ == "__main__":
        pool = Pool(3)    # 设置池中存3个进程
        for i in range(6):
            pool.apply(func=run, args=(i,))    # 使用串行的方式起进程
             pool.apply_async(func=run, args=(i,), callback=bar(i))
            # 使用并行的方式起进程,callback回调函数,前面的函数执行完成之后执行该函数,由主进程调用
    
         pool.close()
        pool.join()
        # 必须先关闭再join

    4、协程

    协程,又称微线程,是一种用户态的轻量级线程。协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,再切回来时,恢复先前保存的寄存器上下文和栈。线程的切换由操作系统调度,而协程的切换由自己调度。

    优点:

    • 无需线程上下文切换的开销
    • 无需原子操作锁定及同步的开销(原子操作指不会被线程调度机制打断的操作,这种操作一旦开始就一直运行到结束)
    • 方便切换控制流,简化编程模型
    • 高并发+高扩展+低成本(一个CPU可支持上万的协程)

    缺点:

    • 无法利用多核资源:协程的本质是单线程,无法同时使用多个CPU核心,需要和进程配合才能运行在多CPU上
    • 进行阻塞(Blocking)操作时会使整个程序阻塞

    协程需符合以下条件:

    • 必须在一个线程中实现并发
    • 修改共享数据不需要加锁
    • 用户程序中自己保存多个控制流的上下文栈
    • 一个协程遇到IO操作自动切换到其他协程

    greenlet实现上下文切换

    greenlet为第三方模块,使用前需要使用pip install gevent安装(安装gevent同时安装greenlet)

    from greenlet import greenlet
    
    
    def run1():
        print("第一次运行,然后中断...")
        gr2.switch()
        print("run2函数运行后回到这里...")
        gr2.switch()
    
    
    def run2():
        print("run1运行后切到这里....")
        gr1.switch()
        print("run1再次运行后切回这里....")
    
    
    gr1 = greenlet(run1)
    gr2 = greenlet(run2)
    gr1.switch()    # 运行gr1
    
    #结果
    第一次运行,然后中断...
    run1运行后切到这里....
    run2函数运行后回到这里...
    run1再次运行后切回这里....

    gevent实现自动切换

    gevent是一个第三方模块,需要使用pip install gevent安装。使用gevent可轻松实现并发同步或异步编程,gevent内部使用greenlet。

    import gevent
    
    
    def run1():
        print("第一次运行....")
        gevent.sleep(2)    # 模拟IO操作,gevent自动切换
        print("run2运行后再运行...")
    
    
    def run2():
        print("run1第一次运行后运行...")
        gevent.sleep(1)
        print("接着运行...")
    
    
    gevent.joinall([gevent.spawn(run1), gevent.spawn(run2)])
    
    #结果
    第一次运行....
    run1第一次运行后运行...
    接着运行...
    run2运行后再运行...

    gevent并发下载网页

    import gevent
    from gevent import monkey
    from urllib.request import urlopen
    
    monkey.patch_all()    # 识别所有模块中的IO操作
    
    
    def f(url):
        print('GET: %s' % url)
        resp = urlopen(url)
        data = resp.read()
        print('%d bytes received from %s.' % (len(data), url))
    
    
    gevent.joinall([
        gevent.spawn(f, 'https://www.python.org/'),
        gevent.spawn(f, 'https://www.yahoo.com/'),
        gevent.spawn(f, 'https://github.com/'),
    ])
    gevent实现单线程下的并发socket
    #server端
    import socket
    import gevent
    from gevent import monkey
    
    monkey.patch_all()
    
    
    def request(conn):
        while True:
            data = conn.recv(1024)
            print(data)
            conn.send("ok".encode("utf-8"))
            if not data:
                exit("无数据退出...")
    
    
    server = socket.socket()
    server.bind(("0.0.0.0", 9999))
    server.listen(5)
    
    while True:
        client_conn, address = server.accept()
        gevent.spawn(request, client_conn)

    5、事件驱动

    服务器处理请求的模型:

    1. 每收到一个请求,创建新的进程来处理
    2. 每收到一个请求,创建新的线程来处理
    3. 每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO的方式来处理

    第一种方法,由于创建新的进程开销较大,所以会导致服务器性能变差,但实现比较简单

    第二种方法,由于涉及到线程的同步,有可能会面临死锁等问题

    第三种方法,在写应用程序的代码时,逻辑比前两种方法都要复杂

    综合各方面因素,一般普遍认为第三种方法是大多数网络服务器采用的方法。

    事件驱动模型:

    1. 有一个事件队列
    2. 当事件发生时,在队列中增加该事件
    3. 独立的处理线程循环从事件队列中取出事件,并执行调用相应的函数

    事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。下图说明了它们之间的区别(灰色部分为IO操作):

    threading_models

    6、同步IO和异步IO

    情景为Linux环境下的的网络IO

    几个概念:

    • 用户空间和内核空间:现代操作系统普遍采用虚拟存储器。为了保证操作系统内核的安全,用户的进程无法直接操作系统内核。操作系统会将虚拟存储器空间划分为内核空间和用户空间。内核空间供内核使用,用户进程无法访问;用户空间供用户进程使用。
    • 进程切换
    • 进程的阻塞:正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
    • 文件描述符(fd):文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
    • 缓存IO: 又称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。缓存 I/O 的缺点:
      数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

    IO模式

    对于一次IO访问,数据将在内核缓冲区和用户空间进行拷贝,所以当一个IO操作发生时,会经历两个阶段:

    1. 等待数据准备好
    2. 在内核缓冲区和用户空间间进行数据拷贝

    正因为这两个阶段,Linux系统产生了以下五种网络模式方案:

    • 阻塞IO(blocking IO)
    • 非阻塞IO(nonblocking IO)
    • IO多路复用(IO multiplexing)
    • 信号驱动IO(signal driven IO,不常用)
    • 异步IO(asynchronous IO)

    阻塞IO、非阻塞IO、IO多路复用都为同步IO。

    阻塞IO

    720333-20160916171008617-1558216223

    非阻塞IO

    720333-20160916171226852-1916489268

    IO多路复用

    select、poll、epoll

    720333-20160916171333523-650292614

    IO多路复用的特点是通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪状态,select()函数就可以返回。IO多路复用的优势不在对于单个连接能处理得更快,而是在于能处理更多的连接。

    异步IO

    720333-20160916171458461-2052304822

    各种模式的比较

    720333-20160916171648430-240094129

    7、select、poll、epoll

    select
    select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

    select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。

    select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

    另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

    poll
    poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

    poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

    另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

    epoll
    直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

    epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

    epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

    另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

    Python中的select

    # 服务端
    
    import select
    import socket
    import queue
    
    server = socket.socket()
    server.setblocking(False)
    server.bind(("0.0.0.0", 6699))
    server.listen(5)
    
    inputs = [server]    # 从客户端过来的连接列表
    outputs = []    # 需要返回数据给客户端的连接列表
    msg_dict = {}    # 返回给客户端的信息
    
    while True:
        readable, writeable, exceptional = select.select(inputs, outputs, inputs)
    
        for a in readable:
    
            if a is server:    # 自身的连接被激活,说明有新连接
                conn, address = server.accept()
                print("来自%s的新连接..." % address[0])
                conn.setblocking(False)
                inputs.append(conn)
                msg_dict[conn] = queue.Queue()
            else:    # 连接已存在于inputs列表中,说明是已连接上的某个客户端
                data = a.recv(1024)
                if data:
                    print(data.decode())
                    msg_dict[a].put(data)
                    if a not in outputs:    # 不在outputs中,为第一次连接
                        outputs.append(a)
                else:    # 客户端没有发送数据
                    print("客户端%s断开连接..." % a)
                    if a in outputs:
                        outputs.remove(a)
                    inputs.remove(a)
                    del msg_dict[a]
                    a.close()
                    # 从inputs,outputs和msg_dict中删除该连接
    
        for b in writeable:
            if msg_dict[b].empty():    # 要发送的数据为空
                outputs.remove(b)
            else:
                print("发送数据给客户端%s" % b)
                msg = msg_dict[b].get()
                b.send(msg)
    
        for c in exceptional:    # 连接通信过程中出错
            print("与客户端%s的连接出错..." % c)
            if c in outputs:
                outputs.remove(c)
            inputs.remove(c)
            del msg_dict[c]
            c.close()

    selectors模块

    自动识别系统支持select或者epoll,系统支持epoll则使用epoll,不支持则使用select。

    #服务端
    
    import socket
    import selectors
    
    sel = selectors.DefaultSelector()
    
    
    def accept(sock, mask):
        conn, address = sock.accept()
        print("客户端%s连接..." % address[0])
        conn.setblocking(False)
        sel.register(conn, selectors.EVENT_READ, read)    # 登记这个连接
    
    
    def read(conn, mask):
        data = conn.recv(1024)
        if data:
            print(data.decode())
            conn.send("ok".encode("utf-8"))
        else:
            print("客户端断开连接...")
            sel.unregister(conn)    # 注销这个连接
            conn.close()
    
    
    server = socket.socket()
    server.setblocking(False)
    server.bind(("0.0.0.0", 6699))
    server.listen(500)
    sel.register(server, selectors.EVENT_READ, accept)
    
    while True:
        event = sel.select()
        for key, mask in event:
            callback = key.data
            callback(key.fileobj, mask)
  • 相关阅读:
    mac安装protobuf2.4.1时报错./include/gtest/internal/gtest-port.h:428:10: fatal error: 'tr1/tuple' file not found和google/protobuf/message.cc:175:16: error: implicit instantiation of undefined template
    java基础六 [异常处理](阅读Head First Java记录)
    安装和使用iOS的包管理工具CocoaPods
    Node.js的知识点框架整理
    java基础五 [数字与静态](阅读Head First Java记录)
    java基础四 [构造器和垃圾回收](阅读Head First Java记录)
    Appium学习路-安装篇
    Dell笔记本Ubuntu无线网卡驱动安装
    Ubuntu系统使用命令禁用触摸板等输入设备
    linux(ubuntu) 查看系统设备信息
  • 原文地址:https://www.cnblogs.com/yu2006070/p/7856768.html
Copyright © 2020-2023  润新知