PythonでIO多重化を使う(入門編)

20696 ワード

IO操作はCPUを占有せず、IO多重化は、すべてのIO操作を管理する.IO多重化の典型的なシナリオは、socketオブジェクトの内部に変化が発生したか否かを傍受することである
ソケット内部はいつ変化しますか?
  • 接続を確立する
  • 送信メッセージ
  • ソケットインスタンス
    ここではsocketを用いて簡単なEchoサーバの機能を実現する
  • server.py
  • import socket
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    #               
    while True:
        #        
        conn, address = sk.accept()
        #           
        conn.sendall(bytes('hello', encoding='utf8'))
    
        #            
        while True:
    
            # Windows             ,     Windows     
            try:
                recv = conn.recv(1024)
    
                # Linux     recv    ,    Linux     
                if not recv:
                    break
    
                #                   
                if str(recv, encoding='utf-8') == 'exit':
                    break
            except Exception as ex:
                break
    
            #         
            conn.sendall(recv)
    
  • client.py
  • import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 1559))
    #       
    data = sk.recv(1024)
    print(data)
    
    while True:
        i = input("> ")
        #         
        sk.sendall(bytes(i, encoding='utf8'))
    
        #           
        msg = sk.recv(1024)
        print(str(msg, encoding='utf-8'))
    
    sk.close()
    

    以上のsocketコードは、同じ時間に1つのクライアントのリクエストしか処理できません.その後、接続されたクライアントは、最初のクライアントが切断されていない間、前のクライアントのリクエストが切断されるまで待機します.
    select.selext()の最初のパラメータ
    接続の確立
    前述したように、socketが変わるのは接続を確立することです.
    上記のコードでは、接続の確立に関するsocketはskオブジェクト(変数)skオブジェクトがsk.accept()まで実行されたときに、新しいクライアントの接続要求を受けたときにsocket内部が変化し、新しいクライアント側の接続を識別するためにこの変化を傍受する必要がある.
    SOcketを作成し、バインドしてリスニングした後、socketは一般的に変化しません.新しいクライアントが接続されている場合にのみ、socketは変化します.リスニングが必要なのもこの段階の変化です.
    結論:socketが作成され、バインドされ、傍受された後に変化すると、新しいクライアントが接続される
  • server.py
  • import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    while True:
        rList, w, e = select.select([sk,], [], [], 1)
        print(rList)
    

    上のコードはIO多重化におけるselectモデルを導入し、selectを用いる.select()メソッドは、3つの要素を持つ元祖を返します.
    select.select()メソッドの最初のパラメータは,一時的に1つのサービス側のsocketオブジェクトのみを追加し,サービス側のsocketオブジェクトskが変化すれば(新しいクライアント接続)直ちに変化したsocketオブジェクトをrListリストに追加する.
    リスニングされたsocketリストでは,skオブジェクトが変化する--->rList=[sk]
    リスニングされたsocketリストには、socketが変更されていません--->rList=[]
    上記のコードが走る効果:
    []
    []
    []
    ...
    #       []
    # select.sekect()           
    #     ,              ,       
    

    新しい接続要求がないことを説明し、次にクライアント接続が発生した場合を試験する.
  • client.py
  • import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 1559))
    sk.close()
    

    クライアントがサービス側に接続されると、サービス側は次のように表示されます.
    []
    []
    []
    ...
    #        ...
    

    上から分かるように、クライアントが接続されています.サービス側のskオブジェクトの内部に変化が発生し、selectがskオブジェクトの変化を傍受すると、直ちに変化したオブジェクトをrListリスト(appendからリスト)に付与する印刷されたコンテンツから、リストの要素が変化したsocketオブジェクトであることがわかる
    処理rList(サービス側socket)
    rListには変化するすべてのsocketオブジェクトが保存されており,以上のコードではサービス側socketオブジェクトのみが傍受されているが,ここではサービス側socketの変化のみを一時的に議論する.
  • server.py
  • import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    while True:
        #      socket  sk
        rList, w, e = select.select([sk,], [], [], 1)
        print(rList)
        
        #   rList     socket  
        #   rList         socket  
        for s in rList:
            conn, address = s.accept()
            conn.sendall(bytes('hello', encoding='utf8'))
    
  • client.py
  • import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 1559))
    data = sk.recv(1024)
    print(data)
    
    while True:
        input("> ")
    
    sk.close()
    

    3つのクライアント接続を連続して実行する場合、サービス側のエコー:
    []
    []
    []
    []
    []
    []
    []
    []
    []
    

    各クライアントのエコー:
    b'hello'
    >
    

    以上、rListにおけるサービス側socketオブジェクトに対してaccept()メソッドを実行することにより、接続されたクライアントごとにサービス側に要求を受け、「同時に」サービスを提供するという同時接続のような効果を実現した.
    クライアントメッセージの受信
    上述したように、接続の作成はsocketの変化をトリガするだけでなく、クライアントとの接続が確立されると、クライアントからメッセージが送られ、クライアントsocket接続の内部変化を引き起こす
  • server.py
  • import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    inputs = [sk]
    while True:
        rList, w, e = select.select(inputs, [], [], 1)
        print(rList)
    
        for s in rList:
            conn, address = s.accept()
            # conn    socket  
            #     socket         ,       socket                  
            
            #         socket               
            #              socket         
            #       ,                
            inputs.append(conn)
            conn.sendall(bytes('hello', encoding='utf8'))
    

    上記のコードでは、サービス側socketが新しいクライアントのために作成したsocketもリスニングリストに追加したが、クライアントからメッセージが来た場合、selectはクライアントsocket(conn)が変化してrListリストに追加されたことをリスニングした後、forループ処理ではクライアントのsocketにaccept()メソッドはなく、この方法も不要である.これはforサイクルで2種類のsocketを区別する必要がある.
    import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    inputs = [sk]
    while True:
        rList, w, e = select.select(inputs, [], [], 1)
        print("select    socket     >", len(inputs), " |      socket  >", len(rList))
    
        for s in rList:
            #   socket         socket    
            if s == sk:
                conn, address = s.accept()
                # conn    socket  
                #     socket         ,       socket                  
    
                #         socket               
                #              socket         
                #       ,                
                inputs.append(conn)
                conn.sendall(bytes('hello', encoding='utf8'))
            #           socket   
            else:
                #                
                msg = s.recv(1024)
                print(msg)
    
  • client.py
  • import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 1559))
    #       
    data = sk.recv(1024)
    print(data)
    
    while True:
        i = input("> ")
        #         
        sk.sendall(bytes(i, encoding='utf8'))
    
    sk.close()
    

    クライアントを実行し、Ctrl+Cが終了すると、サービス側はインタフェースを表示して狂った印刷メッセージを返します.問題は、サービス側が傍受するクライアントsocket接続です.クライアントがサービス側と接続を切断した場合、サービス側selectがsocketオブジェクトを傍受するリストからクライアントsocketオブジェクトを削除する必要があります.
  • server.py
  • import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    inputs = [sk]
    while True:
        rList, w, e = select.select(inputs, [], [], 1)
        print("select    socket     >", len(inputs), " |      socket  >", len(rList))
    
        for s in rList:
            #   socket         socket    
            if s == sk:
                conn, address = s.accept()
                # conn    socket  
                #     socket         ,       socket                  
    
                #         socket               
                #              socket         
                #       ,                
                inputs.append(conn)
                conn.sendall(bytes('hello', encoding='utf8'))
            #           socket   
            else:
                try:
                    #                
                    msg = s.recv(1024)
    
                    # Linux      
                    if not msg:
                        raise Exception('        ')
                    print(msg)
                except Exception as ex:
                    # Windows      
                    inputs.remove(s)
    

    最新のserverを使用します.pyとclient.pyテストを行う場合、複数のクライアントを順次実行し、複数のクライアントを順次閉じ、サービス側のエコーは以下の通りです.
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 1
    select    socket     > 2  |      socket  > 0
    select    socket     > 2  |      socket  > 0
    select    socket     > 2  |      socket  > 1
    select    socket     > 3  |      socket  > 0
    select    socket     > 3  |      socket  > 0
    select    socket     > 3  |      socket  > 1
    select    socket     > 4  |      socket  > 0
    select    socket     > 4  |      socket  > 0
    select    socket     > 4  |      socket  > 0
    select    socket     > 4  |      socket  > 0
    select    socket     > 4  |      socket  > 1
    b''
    select    socket     > 3  |      socket  > 0
    select    socket     > 3  |      socket  > 0
    select    socket     > 3  |      socket  > 1
    b''
    select    socket     > 2  |      socket  > 0
    select    socket     > 2  |      socket  > 1
    b''
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 0
    select    socket     > 1  |      socket  > 0
    

    特に注意:ここでのサービス側定義では、クライアントから空の値を受信すると、クライアントがサービス側との接続を切断する必要があるとデフォルトで考えられます.サービス側のこのデフォルトルールでは、クライアントを書くときに、クライアントが入力した値が空の場合に注意しなければならない.
    クライアントへのメッセージの返信
  • server.py
  • import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    inputs = [sk]
    while True:
        rList, w, e = select.select(inputs, [], [], 1)
        print("select    socket     >", len(inputs), " |      socket  >", len(rList))
    
        for s in rList:
            #   socket         socket    
            if s == sk:
                conn, address = s.accept()
                # conn    socket  
                #     socket         ,       socket                  
    
                #         socket               
                #              socket         
                #       ,                
                inputs.append(conn)
                conn.sendall(bytes('hello', encoding='utf8'))
            #           socket   
            else:
                try:
                    #                
                    msg = s.recv(1024)
    
                    # Linux      
                    if not msg:
                        raise Exception('        ')
                    print(msg)
                    
                    #         
                    #           ,             
                    s.sendall(msg)
                except Exception as ex:
                    # Windows      
                    inputs.remove(s)
    

    しかし、一般的には読み書き分離が行われ、selectにより読み書き分離(送受信分離)が実現される
    select.selext()の2番目のパラメータ
    rList, wList, e = select.select([], [], [], 1)
    

    select.select()の2番目のパラメータに何の値があるか、wListに何の値があるか
    selectのこの特性により,メッセージに返信するクライアントsocketオブジェクトをselectの2番目のパラメータに割り当てることができる.
    import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    inputs = [sk]
    outputs = []
    while True:
        rList, wList, e = select.select(inputs, outputs, [], 1)
        print("---" * 20)
        print("select    inputs     >", len(inputs), " |      socket  >", len(rList))
        print("select    outputs     >", len(outputs), " |             >", len(wList))
    
        #   rList(         )
        for s in rList:
            #   socket         socket    
            if s == sk:
                conn, address = s.accept()
                # conn    socket  
                #     socket         ,       socket                  
    
                #         socket               
                #              socket         
                #       ,                
                inputs.append(conn)
                conn.sendall(bytes('hello', encoding='utf8'))
            #           socket   
            else:
                try:
                    #                
                    msg = s.recv(1024)
    
                    # Linux      
                    if not msg:
                        raise Exception('        ')
                    else:
                        outputs.append(s)
                        print(msg)
    
                    #         
                    #           ,             
                    # s.sendall(msg)
                except Exception as ex:
                    # Windows      
                    inputs.remove(s)
    
        #   wList(               )
        for s in wList:
    
            #              
            s.sendall(bytes('server response', encoding='utf8'))
    
            #      ,    outputs  socket    
            outputs.remove(s)
    

    wList=サービス側にメッセージを送信するすべてのクライアントであり、メッセージを返信する必要があるクライアントのリストでもある
  • client.py
  • import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 1559))
    #       
    data = sk.recv(1024)
    print(data)
    
    while True:
        i = input("> ")
        #         
        sk.sendall(bytes(i, encoding='utf8'))
    
        #           
        msg = sk.recv(1024)
        print(str(msg, encoding='utf-8'))
    
    sk.close()
    

    プロセスの実行:
  • は、3つのクライアント
  • に順次接続する.
  • 最初のクライアントは、2回のメッセージ
  • をサービスに順次送信する.
    サービス側のエコー:
    ------------------------------------------------------------
    select    inputs     > 1  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 1  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 2  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 2  |      socket  > 1
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 3  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 3  |      socket  > 1
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 1
    select    outputs     > 0  |             > 0
    b'ps'
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 0
    select    outputs     > 1  |             > 1
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 1
    select    outputs     > 0  |             > 0
    b'ps'
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 0
    select    outputs     > 1  |             > 1
    ------------------------------------------------------------
    select    inputs     > 4  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 3  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 3  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 2  |      socket  > 0
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 2  |      socket  > 1
    select    outputs     > 0  |             > 0
    ------------------------------------------------------------
    select    inputs     > 1  |      socket  > 0
    select    outputs     > 0  |             > 0
    

    最初のクライアントのエコー:
    b'hello'
    > ps
    server response
    > ps
    server response
    >
    

    以上のコードは简単な送受信メッセージの分离を実现して、今また少し需要が多くなって、现在すべてサービス端にメッセージを送信するクライアント、サービス端はすべて统一して同じ内容を返事して、今サービス端はEcho Serverの机能を実现して、つまりクライアントはどんな消息を送って、サービス端はクライアントにどんなメッセージを返事します
    import socket
    import select
    
    #   socket  ,  IP  ,  
    sk = socket.socket()
    sk.bind(('127.0.0.1', 1559))
    sk.listen(5)
    
    inputs = [sk]
    outputs = []
    messages = {}
    
    """
    messages = {
        socket_obj1: [msg]
        socket_obj2: [msg]
    }
    """
    
    while True:
        rList, wList, e = select.select(inputs, outputs, [], 1)
        print("---" * 20)
        print("select    inputs     >", len(inputs), " |      socket  >", len(rList))
        print("select    outputs     >", len(outputs), " |             >", len(wList))
    
        #   rList(         )
        for s in rList:
            #   socket         socket    
            if s == sk:
                conn, address = s.accept()
                # conn    socket  
                #     socket         ,       socket                  
    
                #         socket               
                #              socket         
                #       ,                
                inputs.append(conn)
    
                #  messages       key
                messages[conn] = []
    
                conn.sendall(bytes('hello', encoding='utf8'))
            #           socket   
            else:
                try:
                    #                
                    msg = s.recv(1024)
    
                    # Linux      
                    if not msg:
                        raise Exception('        ')
                    else:
                        outputs.append(s)
                        messages[s].append(msg)
    
                    #         
                    #           ,             
                    # s.sendall(msg)
                except Exception as ex:
                    # Windows      
                    inputs.remove(s)
    
                    #          ,         messages         
                    del messages[s]
    
        #   wList(               )
        for s in wList:
    
            #           messages           
            msg = messages[s].pop()
    
            #               
            s.sendall(msg)
    
            #      ,    outputs  socket    
            outputs.remove(s)
    

    サービス側は以上の変更を行い、クライアントは変更する必要はありません.
  • 11動作は、クライアントsocketオブジェクトがメッセージを受信し、メッセージを送信することに関連付けられ、新しいグローバル変数messages
  • を導入する.
  • 440行は、新しいクライアントが接続するときに、そのsocketオブジェクトのためにmessagesに対応するkey
  • を予め作成する.
  • 、254行は、オブジェクトにメッセージ
  • を追加する.
  • ,264行はクライアントで接続を閉じる受信し、そのオブジェクトのメッセージリスト
  • をクリーンアップする.
  • ,270行このオブジェクトを40行目に挿入するメッセージから取り出す
  • .
    まとめ
    IO多重化を用いて,実際には同時効果のような擬似同時化を実現した.内部では、ブロック要求を効率的に処理するためにループが実際に使用されています.
    Pythonにはselectモジュールがあり、select、poll、epollの3つの方法が提供され、システムのselect、poll、epollをそれぞれ呼び出してIO多重化を実現する.
  • Windows Python:提供:select
  • Mac Python:提供:select
  • Linux Python:提供:select、poll、epoll
  • 注意:ネットワークの操作、ファイルの操作、端末の操作などはすべてIOの操作に属して、windowsに対してSocketの操作だけを支持して、その他のシステムはすべてIOの操作を支持して、しかし普通のファイルの操作が自動的に最後に読み取ってすでに変化したかどうかを検出することができません
    selectメソッドの場合:
        11,     22,     33 = select.select(    1,     2,     3,     )
    
     
      :        (     )
       :    
     
    select          ,        ,      。
    1、    1            (accetp read),                  1    
    2、    2         ,                  2    
    3、    3            ,                 3    
    4、          , select     ,           
              = 1 ,               , select    1  ,         ,          ,     。
    

    追加:select poll epollの違い
    IO多重化はシステムカーネルで実現され、システム内部ではforサイクルが維持され、オブジェクトに変化があるかどうかを一つ一つ検出する
    まず明確にしなければならないのは、forサイクルの効率が高くないことです.
    IO多重種別
    実装の原理
    リスニング対象の数
    select
    システム内部でforサイクルを維持
    1024
    poll
    システム内部でforサイクルを維持
    制限なし
    epoll
    ハンドルシーケンスが変化した場合にepollに自動的に通知
    制限なし