I/O多路復(fù)用

從接觸netty以來,對網(wǎng)絡(luò)編程中的諸多I/O模型存在困惑奢方,直到最近學(xué)習(xí)python搔扁,才漸漸清晰起來。本文主要梳理一下關(guān)于傳統(tǒng)多線程模型蟋字、多路復(fù)用技術(shù)以及select、epoll模式多路復(fù)用的知識點扭勉。

多線程模型

網(wǎng)絡(luò)編程的基本模型是Client-Server模型鹊奖,也就網(wǎng)絡(luò)中兩個進(jìn)程之間相互通信,服務(wù)端提供位置信息涂炎,客戶端向服務(wù)端發(fā)起連接請求忠聚,三次握手成功建立連接之后,雙方就可以通過Socket進(jìn)行通信唱捣。

服務(wù)端:

from socket import *
from threading import Thread


def clientHandler(clientSocket, clientAddress):
    """處理客戶端連接"""
    
    print('與客戶端:%s:%s 建立連接...' % clientAddress)
    
    while True:
        receiveMessage = clientSocket.recv(1024)
        if receiveMessage:
            # 將接受到的信息直接返回
            print(receiveMessage.decode('utf-8'))
        else:
            # 如果客戶端關(guān)閉两蟀,關(guān)閉服務(wù)端連接
            clientSocket.close()
            print('與客戶端:%s:%s 已斷開連接...'% clientAddress)
            break


def main():
    # 創(chuàng)建服務(wù)端
    serverSocket = socket(AF_INET, SOCK_STREAM)

    # 設(shè)置服務(wù)端參數(shù)
    serverSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    serverSocket.bind(('', 8088))

    # 開始監(jiān)聽
    serverSocket.listen(10)

    try:
        while True:
            # 等待客戶端接入,此處會阻塞當(dāng)前線程震缭,直到有新的客戶端接入
            clientSocket, clientAddress = serverSocket.accept()

            # 成功連接后赂毯,會創(chuàng)建新的線程用來處理輸入出
            serverThread = Thread(target=clientHandler, args=(clientSocket, clientAddress))
            serverThread.start()

    finally:
        serverSocket.close()


if __name__ == '__main__':
    main()

客戶端:

from socket import *


def main():
    # 創(chuàng)建scoket
    clientSocket = socket(AF_INET, SOCK_STREAM)
    serverAddr = ('127.0.0.1', 8088)

    # 連接服務(wù)端
    clientSocket.connect(serverAddr)

    sendMessage = input('輸入要發(fā)送的信息:')
    clientSocket.send(bytes(sendMessage, 'utf-8'))

    clientSocket.close()
    print('客戶端已關(guān)閉')


if __name__ == '__main__':
    main()

說明:

  • 服務(wù)端主線程負(fù)責(zé)監(jiān)聽客戶端的連接
  • 接收到客戶端請求后,便為每一個連接創(chuàng)建一個新的線程
  • 處理完成后,關(guān)閉連接党涕,銷毀線程

這就是典型的一請求一應(yīng)答模式烦感,當(dāng)客戶端并發(fā)訪問量增加時,服務(wù)端線程數(shù)與客戶端數(shù)將呈1:1的關(guān)系增加膛堤,這將及其耗費系統(tǒng)的性能手趣。試想有2000個客戶端連接時,服務(wù)端將創(chuàng)建2000個線程用于處理這些連接肥荔,首先要維持這些線程就要耗費大量內(nèi)存绿渣,在做上下文切換的時候可能直接導(dǎo)致系統(tǒng)內(nèi)存耗盡或者當(dāng)前進(jìn)程宕機(jī)。那么能不能只使用少量的線程燕耿,就可以處理這些連接呢中符?

單線程非阻塞

python中可以將socket設(shè)置為非阻塞,看下面代碼:

from socket import *

serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

serverSocket.bind(('', 8088))
serverSocket.listen(10)

# 將服務(wù)端負(fù)責(zé)監(jiān)聽的socket設(shè)置成非阻塞
serverSocket.setblocking(False)

# 保存新建立的連接
g_clientList = []

while True:
    try:
        # 將socket設(shè)置成非阻塞后缸棵,如果沒有接收到數(shù)據(jù)舟茶,會拋出異常,需要做下異常處理
        clientSocket, clientAddress = serverSocket.accept()
    except:
        pass
    else:
        # 如果沒有報錯 表示成功了建立了一個新的連接
        print('與客戶端:%s:%s 建立連接...' % clientAddress)

        # 將新建立的socket連接設(shè)置成非阻塞堵第,并保存到列表中
        clientSocket.setblocking(False)
        g_clientList.append((clientSocket, clientAddress))

        
    # 此循環(huán)主要用作輪詢列表中的socket吧凉,處理可以接收數(shù)據(jù)的socket
    for clientSocket, clientAddress in g_clientList:
        try:
            # 如果clientsocket中沒有可接收的數(shù)據(jù),此處會拋出異常
            receiveMessage = clientSocket.recv(1024)
        except:
            pass
        else:
            if receiveMessage:
                print(receiveMessage.decode('utf-8'))
            else:
                clientSocket.close()
                
                # 關(guān)閉連接后踏志,將該連接從列表中移除阀捅,不再做輪詢
                g_clientList.remove((clientSocket, clientAddress))
                print('與客戶端:%s:%s 斷開連接...' % clientAddress)

上述代碼實現(xiàn)了單線程的情況下可以處理多個連接,關(guān)鍵點有以下幾點:

  • socket不再阻塞當(dāng)前線程
  • 維護(hù)了一個list针余,用來存放可用的socket連接
  • 輪詢存放socket的集合饲鄙,處理可讀寫的socket連接。

這就是一個簡單版的I/O多路復(fù)用實現(xiàn)圆雁。

多路復(fù)用(Multiplexing)忍级,維基百科上給出的解釋是表示在一個信道上傳輸多路信號或數(shù)據(jù)流的技術(shù)。下圖是維基百科給出的模型圖:

結(jié)合上面的單線程非阻塞例子伪朽,可以理解成多個socket連接復(fù)用一個線程轴咱。

select模式

前面的例子中,使用單線程處理多個socket連接烈涮,可以很大程度地節(jié)約創(chuàng)建線程和線程切換時帶來的系統(tǒng)性能消耗朴肺。但同樣存在一個問題,由于每次都要輪詢所有的socket連接坚洽,這將大量耗費CPU時間戈稿,而且不是所有的socket都處于就緒狀態(tài)(連接、讀讶舰、寫)鞍盗,試想輪詢了2000個連接結(jié)果只有一個scoket在收發(fā)包需了,這無疑浪費了很多CPU性能,那么有沒有方法可以得到這些可用的就緒連接橡疼,只對這些活躍的連接進(jìn)行輪詢呢援所?

這里首先要引入一個概念,叫做文件描述符(file descriptor欣除,fd)住拭,linux中內(nèi)核將所有外部設(shè)備看做一個文件來操作,對一個文件的讀寫操作會調(diào)用內(nèi)核提供的系統(tǒng)命令历帚,返回一個fd滔岳,同樣對socket的操作也會有相應(yīng) 的描述符,描述符是一個數(shù)字挽牢。

所有socket的文件描述符被放入到一個數(shù)組中谱煤,前輩們發(fā)明了一個系統(tǒng)調(diào)用select,select會依次遍歷這個數(shù)組禽拔,如果對應(yīng)的文件描述符處于就緒狀態(tài)刘离,就會返回該描述符。如果遍歷結(jié)束后睹栖,仍沒有一個可用的fd硫惕,它會讓當(dāng)前用戶進(jìn)程睡眠,等到有可用資源的時候再喚醒野来。

下面是select模式多路復(fù)用的簡單實現(xiàn):

from socket import *
from select import *

# 創(chuàng)建服務(wù)端socket恼除,并開啟監(jiān)聽
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('', 8088))
serverSocket.listen(10)

# 存放socket連接
inputSockets = [serverSocket]

while True:
    # select會阻塞等待...
    # select方法接收三個類型為列表的參數(shù):
    # param1: 檢查該list中是否有socket可以接收數(shù)據(jù)
    # param2: 檢查該list中是否有socket可以發(fā)送數(shù)據(jù)
    # param3: 檢查該list中是否有socket發(fā)生異常
    readableList, writeableList, exceptionalList = select(inputSockets, [], [])
    
    # select方法會返回一個元組,包括:可讀的連接列表曼氛、可寫列表豁辉、異常列表

    # 數(shù)據(jù)抵達(dá) 遍歷可寫列表
    for soc in readableList:
      
        # 有新的連接,握手成功后舀患,放入列表中
        if soc == serverSocket:
            client, address = serverSocket.accept()
            print('與客戶端:%s:%s 建立連接...' % address)
            inputSockets.append(client)

        # 有新的數(shù)據(jù)到達(dá)
        else:
            receiveMessage = soc.recv(1024)
            if receiveMessage:
                # 如果有數(shù)據(jù)徽级,則打印該數(shù)據(jù)
                print(receiveMessage.decode('utf-8'))
                
            else:
                # 如果無數(shù)據(jù),從列表中移除該連接聊浅,關(guān)閉連接
                inputSockets.remove(soc)
                soc.close()
                print('與客戶端:%s:%s 斷開連接...' % address)

通過select調(diào)用可以讓用戶程序直接獲取可用的socket連接灰追,相比較于用程序直接輪詢所有socket連接,socket模式在系統(tǒng)內(nèi)核層面實現(xiàn)狗超,效率極高,而且select基本上所有平臺朴下。

但是...

  • select單個線程可監(jiān)視的fd數(shù)量存在限制努咐,一般是1024
  • 'If a file descriptor being monitored by select() is closed in another thread, the result is unspecified',如果別的線程關(guān)閉了正在被select監(jiān)聽的fd殴胧,結(jié)果將是不可預(yù)測的...
  • select采用輪詢的方法渗稍,效率極低

那佩迟,怎么辦!

epoll模式

先來看代碼:

import socket
import select

# 創(chuàng)建server
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('', 8088))
server.listen(10)

# windows不支持
epoll = select.epoll()

# 將創(chuàng)建的套接字添加到epoll的事件監(jiān)聽中
epoll.register(server.fileno(), select.EPOLLIN | select.EPOLLET)

clients = {}
addresses = {}

while True:

    # 用來檢測套接字可讀寫的狀態(tài)竿屹,處于可用狀態(tài)的socket會通知epoll
    epoll_list = epoll.poll()

    # 對事件進(jìn)行判斷
    for fd, event in epoll_list:

        # 如果負(fù)責(zé)監(jiān)聽的socket被激活
        if fd == server.fileno():
            client, address = server.accept()
            print('與客戶端:%s:%s 建立連接...' % address)

            # 將socket信息和address信息保存在字典中
            clients[client.fileno()] = client
            addresses[client.fileno()] = address

            # 向epoll注冊新接入的連接
            epoll.register(client.fileno(), select.EPOLLIN | select.EPOLLET)
        
        # 可接收數(shù)據(jù)的事件报强,處理對應(yīng)連接
        elif event == select.EPOLLIN:
            message = clients[fd].recv(1024)

            if message:
                print(message.decode('utf-8'))
            else:
                # 從epoll中解除注冊
                epoll.unregister(fd)
                clients[fd].close()
                print('與客戶端:%s:%s 斷開連接...' % addresses[fd])

不同于select,epoll采用事件回調(diào)的方式拱燃。socket一開始會向epoll注冊事件秉溉,如果socket變?yōu)榭捎脿顟B(tài),則會觸發(fā)事件回調(diào)碗誉,被epoll獲取召嘶。同時epoll也解決了select單個線程所能監(jiān)視的fd數(shù)量有限的問題。

無論服務(wù)端有多少個連接哮缺,epoll關(guān)心的只是那些活躍的連接弄跌,所以epoll的效率較之select也要高出很多。(打個比方尝苇,比如說考勤铛只,是每天把大家集中在一起點一下名,看誰沒來糠溜,還是直接打卡簽到快淳玩?)

總結(jié)

無論是select,還是epoll诵冒,I/O多路復(fù)用的關(guān)鍵維護(hù)了一張fd表凯肋。它把多個socket連接的阻塞,轉(zhuǎn)移到單線程如何從眾多連接中篩選出可用狀態(tài)的fd上汽馋。至于如何使用這張fd表侮东,是單線程還是交給其他的線程去處理,由具體的實際需要決定了豹芯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悄雅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子铁蹈,更是在濱河造成了極大的恐慌宽闲,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件容诬,死亡現(xiàn)場離奇詭異,居然都是意外死亡览徒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門习蓬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纽什,“玉大人,你說我怎么就攤上這事躲叼。” “怎么了枫慷?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長流礁。 經(jīng)常有香客問我涕俗,道長,這世上最難降的妖魔是什么神帅? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮找御,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘栖疑。我一直安慰自己滔驶,他們只是感情好遇革,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布揭糕。 她就那樣靜靜地躺著,像睡著了一般揪漩。 火紅的嫁衣襯著肌膚如雪吏口。 梳的紋絲不亂的頭發(fā)上奄容,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天昂勒,我揣著相機(jī)與錄音舟铜,去河邊找鬼叁怪。 笑死深滚,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的血柳。 我是一名探鬼主播生兆,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鸦难!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起击敌,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拴事,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后刃宵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡哮针,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年坦袍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片键闺。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辛燥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情挎塌,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布待锈,位于F島的核電站嘴高,受9級特大地震影響和屎,放射性物質(zhì)發(fā)生泄漏春瞬。R本人自食惡果不足惜柴信,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一随常、第九天 我趴在偏房一處隱蔽的房頂上張望萄涯。 院中可真熱鬧绪氛,春花似錦涝影、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逗旁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間红伦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工昙读, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留膨桥,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓沮稚,卻偏偏與公主長得像册舞,于是被迫代替她去往敵國和親蕴掏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容